From 96a000e8ab23062a79b5323aa61576c886998d3c Mon Sep 17 00:00:00 2001 From: Steve Ebersole Date: Wed, 6 Sep 2023 09:29:57 -0500 Subject: [PATCH] HHH-17164 - Proper, first-class soft-delete support https://hibernate.atlassian.net/browse/HHH-17164 --- .../org/hibernate/annotations/SoftDelete.java | 98 ++++ .../boot/model/internal/BinderHelper.java | 41 ++ .../boot/model/internal/CollectionBinder.java | 38 ++ .../boot/model/internal/EntityBinder.java | 51 ++- .../boot/model/internal/SoftDeleteHelper.java | 219 +++++++++ .../hibernate/internal/util/StringHelper.java | 17 + .../CollectionBatchLoaderArrayParam.java | 6 + .../CollectionBatchLoaderInPredicate.java | 7 + .../internal/CollectionLoaderSingleKey.java | 11 +- .../CollectionLoaderSubSelectFetch.java | 6 + .../EntityBatchLoaderInPredicate.java | 115 +---- .../org/hibernate/mapping/Collection.java | 14 +- .../java/org/hibernate/mapping/RootClass.java | 13 +- .../org/hibernate/mapping/SoftDeletable.java | 15 + .../UnsupportedMappingException.java | 4 + .../metamodel/mapping/EntityMappingType.java | 15 +- .../mapping/PluralAttributeMapping.java | 16 +- .../mapping/SoftDeletableModelPart.java | 21 + .../metamodel/mapping/SoftDeleteMapping.java | 129 ++++++ .../metamodel/mapping/TableDetails.java | 2 +- .../internal/MappingModelCreationHelper.java | 3 +- .../internal/PluralAttributeMappingImpl.java | 139 +++++- .../internal/SoftDeleteMappingImpl.java | 227 +++++++++ .../internal/ToOneAttributeMapping.java | 57 ++- .../domain/internal/MappingMetamodelImpl.java | 4 + .../AbstractCollectionPersister.java | 67 +-- .../collection/BasicCollectionPersister.java | 109 +++++ .../entity/AbstractEntityPersister.java | 90 +++- .../persister/entity/EntityPersister.java | 13 +- .../mutation/AbstractDeleteCoordinator.java | 326 +++++++++++++ .../entity/mutation/DeleteCoordinator.java | 416 +---------------- .../mutation/DeleteCoordinatorSoft.java | 175 +++++++ .../mutation/DeleteCoordinatorStandard.java | 169 +++++++ .../entity/mutation/EntityTableMapping.java | 2 +- .../entity/mutation/InsertCoordinator.java | 1 + .../AnonymousTupleEntityValuedModelPart.java | 11 + .../sqm/internal/AbstractDeleteQueryPlan.java | 226 +++++++++ .../internal/MultiTableDeleteQueryPlan.java | 1 - .../query/sqm/internal/QuerySqmImpl.java | 15 +- .../sqm/internal/SimpleDeleteQueryPlan.java | 201 +------- .../sqm/internal/SoftDeleteQueryPlan.java | 71 +++ .../MultiTableSqmMutationConverter.java | 2 +- .../internal/MutationQueryLogging.java | 26 ++ .../internal/cte/CteDeleteHandler.java | 15 +- .../internal/cte/CteMutationStrategy.java | 22 +- .../internal/cte/CteSoftDeleteHandler.java | 94 ++++ .../internal/inline/InlineDeleteHandler.java | 89 +++- .../internal/inline/InlineUpdateHandler.java | 15 +- .../AbstractDeleteExecutionDelegate.java | 98 ++++ .../GlobalTemporaryTableStrategy.java | 5 + .../LocalTemporaryTableMutationStrategy.java | 6 +- .../LocalTemporaryTableStrategy.java | 5 + .../temptable/PersistentTableStrategy.java | 6 +- .../RestrictedDeleteExecutionDelegate.java | 122 +++-- .../SoftDeleteExecutionDelegate.java | 432 ++++++++++++++++++ .../temptable/TableBasedDeleteHandler.java | 17 +- .../temptable/UpdateExecutionDelegate.java | 23 +- .../sql/ast/spi/AbstractSqlAstTranslator.java | 2 +- .../ast/tree/expression/ColumnReference.java | 9 + .../sql/model/ast/ColumnWriteFragment.java | 2 +- ...bstractRestrictedTableMutationBuilder.java | 5 + .../RestrictedTableMutationBuilder.java | 2 + .../builder/TableDeleteBuilderSkipped.java | 4 + .../model/ast/builder/TableInsertBuilder.java | 1 + .../builder/TableUpdateBuilderSkipped.java | 4 + .../builder/TableUpdateBuilderStandard.java | 1 + .../schema/internal/ColumnDefinitions.java | 2 +- .../type/BooleanAsBooleanConverter.java | 51 +++ .../hibernate/type/CharBooleanConverter.java | 8 +- .../type/NumericBooleanConverter.java | 5 +- .../type/StandardBooleanConverter.java | 15 + .../org/hibernate/type/StandardConverter.java | 21 + .../hibernate/type/TrueFalseConverter.java | 1 + .../org/hibernate/type/YesNoConverter.java | 1 + .../batch/FailingAddToBatchTest.java | 4 +- .../contributed/EntityHidingTests.java | 2 +- .../converted/converter/QueryTest.java | 1 - .../CollectionOfSoftDeleteTests.java | 170 +++++++ .../orm/test/softdelete/MappingTests.java | 125 +++++ .../orm/test/softdelete/MappingVerifier.java | 27 ++ .../softdelete/ReverseYesNoConverter.java | 32 ++ .../softdelete/SimpleSoftDeleteTests.java | 274 +++++++++++ .../orm/test/softdelete/ToOneTests.java | 162 +++++++ .../orm/test/softdelete/ValidationTests.java | 83 ++++ .../collections/CollectionOwned.java | 32 ++ .../collections/CollectionOwner.java | 103 +++++ .../collections/CollectionOwner2.java | 81 ++++ .../collections/FetchLoadableTests.java | 129 ++++++ .../collections/InvalidCollectionOwner.java | 37 ++ .../softdelete/collections/MappingTests.java | 45 ++ .../softdelete/collections/UsageTests.java | 174 +++++++ .../collections/ValidationTests.java | 33 ++ .../converter/ConvertedSoftDeleteTests.java | 63 +++ .../test/softdelete/converter/TheEntity.java | 49 ++ .../softdelete/converter/package-info.java | 13 + .../reversed/ReversedSoftDeleteTests.java | 63 +++ .../converter/reversed/TheEntity.java | 49 ++ .../orm/test/softdelete/package-info.java | 13 + .../orm/test/softdelete/pkg/AnEntity.java | 55 +++ .../pkg/PackageLevelSoftDeleteTests.java | 45 ++ .../orm/test/softdelete/pkg/package-info.java | 18 + .../orm/test/softdelete/pkg2/AnEntity.java | 45 ++ .../pkg2/CustomTrueFalseConverter.java | 39 ++ .../pkg2/PackageLevelSoftDeleteTests2.java | 33 ++ .../test/softdelete/pkg2/package-info.java | 17 + .../test/softdelete/secondary/JoinedRoot.java | 55 +++ .../test/softdelete/secondary/JoinedSub.java | 31 ++ .../JoinedSubclassSoftDeleteTests.java | 205 +++++++++ .../softdelete/secondary/MappingTests.java | 53 +++ .../testing/orm/junit/NotImplementedYet.java | 6 - .../orm/junit/NotImplementedYetExtension.java | 43 +- 111 files changed, 5771 insertions(+), 910 deletions(-) create mode 100644 hibernate-core/src/main/java/org/hibernate/annotations/SoftDelete.java create mode 100644 hibernate-core/src/main/java/org/hibernate/boot/model/internal/SoftDeleteHelper.java create mode 100644 hibernate-core/src/main/java/org/hibernate/mapping/SoftDeletable.java create mode 100644 hibernate-core/src/main/java/org/hibernate/metamodel/mapping/SoftDeletableModelPart.java create mode 100644 hibernate-core/src/main/java/org/hibernate/metamodel/mapping/SoftDeleteMapping.java create mode 100644 hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/SoftDeleteMappingImpl.java create mode 100644 hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/AbstractDeleteCoordinator.java create mode 100644 hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/DeleteCoordinatorSoft.java create mode 100644 hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/DeleteCoordinatorStandard.java create mode 100644 hibernate-core/src/main/java/org/hibernate/query/sqm/internal/AbstractDeleteQueryPlan.java create mode 100644 hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SoftDeleteQueryPlan.java create mode 100644 hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/MutationQueryLogging.java create mode 100644 hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteSoftDeleteHandler.java create mode 100644 hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/AbstractDeleteExecutionDelegate.java create mode 100644 hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/SoftDeleteExecutionDelegate.java create mode 100644 hibernate-core/src/main/java/org/hibernate/type/BooleanAsBooleanConverter.java create mode 100644 hibernate-core/src/main/java/org/hibernate/type/StandardBooleanConverter.java create mode 100644 hibernate-core/src/main/java/org/hibernate/type/StandardConverter.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/CollectionOfSoftDeleteTests.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/MappingTests.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/MappingVerifier.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/ReverseYesNoConverter.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/SimpleSoftDeleteTests.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/ToOneTests.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/ValidationTests.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/collections/CollectionOwned.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/collections/CollectionOwner.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/collections/CollectionOwner2.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/collections/FetchLoadableTests.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/collections/InvalidCollectionOwner.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/collections/MappingTests.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/collections/UsageTests.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/collections/ValidationTests.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/converter/ConvertedSoftDeleteTests.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/converter/TheEntity.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/converter/package-info.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/converter/reversed/ReversedSoftDeleteTests.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/converter/reversed/TheEntity.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/package-info.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/pkg/AnEntity.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/pkg/PackageLevelSoftDeleteTests.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/pkg/package-info.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/pkg2/AnEntity.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/pkg2/CustomTrueFalseConverter.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/pkg2/PackageLevelSoftDeleteTests2.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/pkg2/package-info.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/secondary/JoinedRoot.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/secondary/JoinedSub.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/secondary/JoinedSubclassSoftDeleteTests.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/secondary/MappingTests.java diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/SoftDelete.java b/hibernate-core/src/main/java/org/hibernate/annotations/SoftDelete.java new file mode 100644 index 0000000000..66acbba324 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/annotations/SoftDelete.java @@ -0,0 +1,98 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.annotations; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import org.hibernate.dialect.Dialect; +import org.hibernate.type.BooleanAsBooleanConverter; + +import jakarta.persistence.AttributeConverter; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PACKAGE; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Describes a soft-delete indicator mapping. + *

+ * Soft deletes handle "deletions" from a database table by setting a column in + * the table to indicate deletion. + *

+ * May be defined at various levels

+ * + * @since 6.4 + * @author Steve Ebersole + */ +@Target({PACKAGE, TYPE, FIELD, METHOD, ANNOTATION_TYPE}) +@Retention(RUNTIME) +@Documented +public @interface SoftDelete { + /** + * (Optional) The name of the column. + *

+ * Defaults to {@code deleted}. + */ + String columnName() default "deleted"; + + /** + * (Optional) The Java type used for values when dealing with JDBC. + * This type should match the "relational type" of the specified + * {@linkplain #converter() converter}. + *

+ * By default, Hibernate will inspect the {@linkplain #converter() converter} + * and determine the proper type from its signature. + * + * @apiNote Sometimes useful since {@linkplain #converter() converter} + * signatures are not required to be parameterized. + */ + Class jdbcType() default void.class; + + /** + * (Optional) Conversion to apply to determine the appropriate value to + * store in the database. The "domain representation" can be:

+ *
{@code true}
+ *
Indicates that the row is considered deleted
+ * + *
{@code false}
+ *
Indicates that the row is considered NOT deleted
+ *
+ *

+ * By default, values are stored as booleans in the database according to + * the {@linkplain Dialect#getPreferredSqlTypeCodeForBoolean() dialect} + * and {@linkplain org.hibernate.cfg.MappingSettings#PREFERRED_BOOLEAN_JDBC_TYPE settings} + * + * @apiNote The converter should never return {@code null} + */ + Class> converter() default BooleanAsBooleanConverter.class; +} diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/BinderHelper.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/BinderHelper.java index e504718b55..a551f4c2cc 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/BinderHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/BinderHelper.java @@ -36,12 +36,15 @@ import org.hibernate.annotations.OnDeleteAction; import org.hibernate.annotations.SqlFragmentAlias; import org.hibernate.annotations.common.reflection.XAnnotatedElement; import org.hibernate.annotations.common.reflection.XClass; +import org.hibernate.annotations.common.reflection.XPackage; import org.hibernate.annotations.common.reflection.XProperty; +import org.hibernate.boot.registry.classloading.spi.ClassLoaderService; import org.hibernate.boot.spi.InFlightMetadataCollector; import org.hibernate.boot.spi.MetadataBuildingContext; import org.hibernate.boot.spi.PropertyData; import org.hibernate.dialect.DatabaseVersion; import org.hibernate.dialect.Dialect; +import org.hibernate.internal.util.StringHelper; import org.hibernate.mapping.Any; import org.hibernate.mapping.AttributeContainer; import org.hibernate.mapping.BasicValue; @@ -69,6 +72,7 @@ import static org.hibernate.boot.model.internal.AnnotatedColumn.buildColumnOrFor import static org.hibernate.boot.model.internal.HCANNHelper.findAnnotation; import static org.hibernate.internal.util.StringHelper.isEmpty; import static org.hibernate.internal.util.StringHelper.isNotEmpty; +import static org.hibernate.internal.util.StringHelper.qualifier; import static org.hibernate.internal.util.StringHelper.qualify; import static org.hibernate.property.access.spi.BuiltInPropertyAccessStrategies.EMBEDDED; import static org.hibernate.property.access.spi.BuiltInPropertyAccessStrategies.NOOP; @@ -1120,4 +1124,41 @@ public class BinderHelper { return false; } + /** + * Extract an annotation from the package-info for the package the given class is defined in + * + * @param annotationType The type of annotation to return + * @param xClass The class in the package + * @param context The processing context + * + * @return The annotation or {@code null} + */ + public static A extractFromPackage( + Class annotationType, + XClass xClass, + MetadataBuildingContext context) { + +// todo (soft-delete) : or if we want caching of this per package +// + +// final SoftDelete fromPackage = context.getMetadataCollector().resolvePackageAnnotation( packageName, SoftDelete.class ); +// + +// where context.getMetadataCollector() can cache some of this - either the annotations themselves +// or even just the XPackage resolutions + + final String declaringClassName = xClass.getName(); + final String packageName = qualifier( declaringClassName ); + if ( isNotEmpty( packageName ) ) { + final ClassLoaderService classLoaderService = context.getBootstrapContext() + .getServiceRegistry() + .getService( ClassLoaderService.class ); + assert classLoaderService != null; + final Package declaringClassPackage = classLoaderService.packageForNameOrNull( packageName ); + if ( declaringClassPackage != null ) { + // will be null when there is no `package-info.class` + final XPackage xPackage = context.getBootstrapContext().getReflectionManager().toXPackage( declaringClassPackage ); + return xPackage.getAnnotation( annotationType ); + } + } + return null; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/CollectionBinder.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/CollectionBinder.java index e8f55b3c41..ed8a1ec214 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/CollectionBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/CollectionBinder.java @@ -71,6 +71,7 @@ import org.hibernate.annotations.SQLOrder; import org.hibernate.annotations.SQLRestriction; import org.hibernate.annotations.SQLSelect; import org.hibernate.annotations.SQLUpdate; +import org.hibernate.annotations.SoftDelete; import org.hibernate.annotations.SortComparator; import org.hibernate.annotations.SortNatural; import org.hibernate.annotations.Synchronize; @@ -94,6 +95,7 @@ import org.hibernate.internal.CoreMessageLogger; import org.hibernate.internal.util.collections.CollectionHelper; import org.hibernate.mapping.Any; import org.hibernate.mapping.Backref; +import org.hibernate.mapping.BasicValue; import org.hibernate.mapping.CheckConstraint; import org.hibernate.mapping.Collection; import org.hibernate.mapping.Column; @@ -110,6 +112,7 @@ import org.hibernate.mapping.SimpleValue; import org.hibernate.mapping.Table; import org.hibernate.mapping.Value; import org.hibernate.metamodel.CollectionClassification; +import org.hibernate.metamodel.UnsupportedMappingException; import org.hibernate.metamodel.spi.EmbeddableInstantiator; import org.hibernate.persister.collection.CollectionPersister; import org.hibernate.resource.beans.spi.ManagedBean; @@ -170,6 +173,7 @@ import static org.hibernate.boot.model.internal.BinderHelper.toAliasTableMap; import static org.hibernate.boot.model.internal.EmbeddableBinder.fillEmbeddable; import static org.hibernate.boot.model.internal.GeneratorBinder.buildGenerators; import static org.hibernate.boot.model.internal.PropertyHolderBuilder.buildPropertyHolder; +import static org.hibernate.boot.model.internal.BinderHelper.extractFromPackage; import static org.hibernate.boot.model.source.internal.hbm.ModelBinder.useEntityWhereClauseForCollections; import static org.hibernate.engine.spi.ExecuteUpdateResultCheckStyle.fromResultCheckStyle; import static org.hibernate.internal.util.StringHelper.getNonEmptyOrConjunctionIfBothNonEmpty; @@ -423,6 +427,13 @@ public abstract class CollectionBinder { + annotationName( oneToMany, manyToMany, elementCollection )); } + if ( oneToMany != null && property.isAnnotationPresent( SoftDelete.class ) ) { + throw new UnsupportedMappingException( + "@SoftDelete cannot be applied to @OneToMany - " + + property.getDeclaringClass().getName() + "." + property.getName() + ); + } + if ( property.isAnnotationPresent( OrderColumn.class ) && manyToMany != null && !manyToMany.mappedBy().isEmpty() ) { throw new AnnotationException("Collection '" + getPath( propertyHolder, inferredData ) + @@ -2475,6 +2486,7 @@ public abstract class CollectionBinder { final Table collectionTable = tableBinder.bind(); collection.setCollectionTable( collectionTable ); handleCheckConstraints( collectionTable ); + processSoftDeletes(); } private void handleCheckConstraints(Table collectionTable) { @@ -2500,6 +2512,31 @@ public abstract class CollectionBinder { : new CheckConstraint( name, constraint ) ); } + private void processSoftDeletes() { + assert collection.getCollectionTable() != null; + + final SoftDelete softDelete = extractSoftDelete( property, propertyHolder, buildingContext ); + if ( softDelete == null ) { + return; + } + + SoftDeleteHelper.bindSoftDeleteIndicator( + softDelete, + collection, + collection.getCollectionTable(), + buildingContext + ); + } + + private static SoftDelete extractSoftDelete(XProperty property, PropertyHolder propertyHolder, MetadataBuildingContext context) { + final SoftDelete fromProperty = property.getAnnotation( SoftDelete.class ); + if ( fromProperty != null ) { + return fromProperty; + } + + return extractFromPackage( SoftDelete.class, property.getDeclaringClass(), context ); + } + private void handleUnownedManyToMany( XClass elementType, PersistentClass collectionEntity, @@ -2528,6 +2565,7 @@ public abstract class CollectionBinder { // this is a ToOne with a @JoinTable or a regular property : otherSidePropertyValue.getTable(); collection.setCollectionTable( table ); + processSoftDeletes(); if ( property.isAnnotationPresent( Checks.class ) || property.isAnnotationPresent( Check.class ) ) { diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/EntityBinder.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/EntityBinder.java index 13d246e530..527a30c39c 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/EntityBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/EntityBinder.java @@ -50,13 +50,14 @@ import org.hibernate.annotations.SQLDeleteAll; import org.hibernate.annotations.SQLDeletes; import org.hibernate.annotations.SQLInsert; import org.hibernate.annotations.SQLInserts; +import org.hibernate.annotations.SQLRestriction; import org.hibernate.annotations.SQLSelect; import org.hibernate.annotations.SQLUpdate; import org.hibernate.annotations.SQLUpdates; -import org.hibernate.annotations.SQLRestriction; import org.hibernate.annotations.SecondaryRow; import org.hibernate.annotations.SecondaryRows; import org.hibernate.annotations.SelectBeforeUpdate; +import org.hibernate.annotations.SoftDelete; import org.hibernate.annotations.Subselect; import org.hibernate.annotations.Synchronize; import org.hibernate.annotations.Tables; @@ -146,6 +147,7 @@ import static org.hibernate.boot.model.internal.PropertyBinder.addElementsOfClas import static org.hibernate.boot.model.internal.PropertyBinder.hasIdAnnotation; import static org.hibernate.boot.model.internal.PropertyBinder.processElementAnnotations; import static org.hibernate.boot.model.internal.PropertyHolderBuilder.buildPropertyHolder; +import static org.hibernate.boot.model.internal.BinderHelper.extractFromPackage; import static org.hibernate.engine.spi.ExecuteUpdateResultCheckStyle.fromResultCheckStyle; import static org.hibernate.internal.util.StringHelper.isEmpty; import static org.hibernate.internal.util.StringHelper.isNotEmpty; @@ -214,6 +216,7 @@ public class EntityBinder { final InheritanceState inheritanceState = inheritanceStates.get( clazzToProcess ); final PersistentClass superEntity = getSuperEntity( clazzToProcess, inheritanceStates, context, inheritanceState ); detectedAttributeOverrideProblem( clazzToProcess, superEntity ); + final PersistentClass persistentClass = makePersistentClass( inheritanceState, superEntity, context ); final EntityBinder entityBinder = new EntityBinder( clazzToProcess, persistentClass, context ); entityBinder.bindEntity(); @@ -233,6 +236,7 @@ public class EntityBinder { final InFlightMetadataCollector collector = context.getMetadataCollector(); if ( persistentClass instanceof RootClass ) { collector.addSecondPass( new CreateKeySecondPass( (RootClass) persistentClass ) ); + bindSoftDelete( clazzToProcess, (RootClass) persistentClass, inheritanceState, context ); } if ( persistentClass instanceof Subclass) { assert superEntity != null; @@ -248,6 +252,51 @@ public class EntityBinder { entityBinder.callTypeBinders( persistentClass ); } + private static void bindSoftDelete( + XClass xClass, + RootClass rootClass, + InheritanceState inheritanceState, + MetadataBuildingContext context) { + // todo (soft-delete) : do we assume all package-level registrations are already available? + // or should this be a "second pass"? + + final SoftDelete softDelete = extractSoftDelete( xClass, rootClass, inheritanceState, context ); + if ( softDelete == null ) { + return; + } + + SoftDeleteHelper.bindSoftDeleteIndicator( + softDelete, + rootClass, + rootClass.getRootTable(), + context + ); + } + + private static SoftDelete extractSoftDelete( + XClass xClass, + RootClass rootClass, + InheritanceState inheritanceState, + MetadataBuildingContext context) { + final SoftDelete fromClass = xClass.getAnnotation( SoftDelete.class ); + if ( fromClass != null ) { + return fromClass; + } + + MappedSuperclass mappedSuperclass = rootClass.getSuperMappedSuperclass(); + while ( mappedSuperclass != null ) { + // todo (soft-delete) : use XClass for MappedSuperclass? for the time being, just use the Java type + final SoftDelete fromMappedSuperclass = mappedSuperclass.getMappedClass().getAnnotation( SoftDelete.class ); + if ( fromMappedSuperclass != null ) { + return fromMappedSuperclass; + } + + mappedSuperclass = mappedSuperclass.getSuperMappedSuperclass(); + } + + return extractFromPackage( SoftDelete.class, xClass, context ); + } + private void handleCheckConstraints() { if ( annotatedClass.isAnnotationPresent( Checks.class ) ) { // if we have more than one of them they are not overrideable :-/ diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/SoftDeleteHelper.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/SoftDeleteHelper.java new file mode 100644 index 0000000000..60b2f5df2c --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/SoftDeleteHelper.java @@ -0,0 +1,219 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.boot.model.internal; + +import org.hibernate.annotations.SoftDelete; +import org.hibernate.boot.model.convert.internal.ClassBasedConverterDescriptor; +import org.hibernate.boot.model.naming.Identifier; +import org.hibernate.boot.model.naming.PhysicalNamingStrategy; +import org.hibernate.boot.model.relational.Database; +import org.hibernate.boot.spi.MetadataBuildingContext; +import org.hibernate.dialect.Dialect; +import org.hibernate.mapping.BasicValue; +import org.hibernate.mapping.Column; +import org.hibernate.mapping.SoftDeletable; +import org.hibernate.mapping.Table; +import org.hibernate.metamodel.mapping.SoftDeletableModelPart; +import org.hibernate.metamodel.mapping.SoftDeleteMapping; +import org.hibernate.metamodel.mapping.internal.MappingModelCreationProcess; +import org.hibernate.metamodel.mapping.internal.SoftDeleteMappingImpl; +import org.hibernate.sql.ast.spi.SqlExpressionResolver; +import org.hibernate.sql.ast.tree.expression.ColumnReference; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.JdbcLiteral; +import org.hibernate.sql.ast.tree.from.TableReference; +import org.hibernate.sql.ast.tree.predicate.ComparisonPredicate; +import org.hibernate.sql.ast.tree.predicate.Predicate; +import org.hibernate.sql.ast.tree.update.Assignment; +import org.hibernate.type.descriptor.converter.spi.BasicValueConverter; +import org.hibernate.type.descriptor.jdbc.JdbcLiteralFormatter; + +import static org.hibernate.internal.util.StringHelper.coalesce; +import static org.hibernate.query.sqm.ComparisonOperator.EQUAL; + +/** + * Helper for dealing with {@link org.hibernate.annotations.SoftDelete} + * + * @author Steve Ebersole + */ +public class SoftDeleteHelper { + + public static final String DEFAULT_COLUMN_NAME = "deleted"; + + /** + * Creates and binds the column and value for modeling the soft-delete in the database + * + * @param softDeleteConfig The SoftDelete annotation + * @param target The thing which is to be soft-deleted + * @param table The table to which the soft-delete should be applied + * @param context The processing context for access to needed info and services + */ + public static void bindSoftDeleteIndicator( + SoftDelete softDeleteConfig, + SoftDeletable target, + Table table, + MetadataBuildingContext context) { + final BasicValue softDeleteIndicatorValue = createSoftDeleteIndicatorValue( softDeleteConfig, table, context ); + final Column softDeleteIndicatorColumn = createSoftDeleteIndicatorColumn( + softDeleteConfig, + softDeleteIndicatorValue, + context + ); + table.addColumn( softDeleteIndicatorColumn ); + target.enableSoftDelete( softDeleteIndicatorColumn ); + } + + public static BasicValue createSoftDeleteIndicatorValue( + SoftDelete softDelete, + Table table, + MetadataBuildingContext context) { + final ClassBasedConverterDescriptor converterDescriptor = new ClassBasedConverterDescriptor( + softDelete.converter(), + context.getBootstrapContext().getClassmateContext() + ); + + final BasicValue softDeleteIndicatorValue = new BasicValue( context, table ); + softDeleteIndicatorValue.setJpaAttributeConverterDescriptor( converterDescriptor ); + softDeleteIndicatorValue.setImplicitJavaTypeAccess( (typeConfiguration) -> { + return converterDescriptor.getRelationalValueResolvedType().getErasedType(); + } ); + return softDeleteIndicatorValue; + } + + public static Column createSoftDeleteIndicatorColumn( + SoftDelete softDeleteConfig, + BasicValue softDeleteIndicatorValue, + MetadataBuildingContext context) { + final Column softDeleteColumn = new Column(); + + applyColumnName( softDeleteColumn, softDeleteConfig, context ); + + softDeleteColumn.setLength( 1 ); + softDeleteColumn.setNullable( false ); + softDeleteColumn.setUnique( false ); + softDeleteColumn.setComment( "Soft-delete indicator" ); + + softDeleteColumn.setValue( softDeleteIndicatorValue ); + softDeleteIndicatorValue.addColumn( softDeleteColumn ); + + return softDeleteColumn; + } + + private static void applyColumnName( + Column softDeleteColumn, + SoftDelete softDeleteConfig, + MetadataBuildingContext context) { + final Database database = context.getMetadataCollector().getDatabase(); + final PhysicalNamingStrategy namingStrategy = context.getBuildingOptions().getPhysicalNamingStrategy(); + final String logicalColumnName = softDeleteConfig == null + ? DEFAULT_COLUMN_NAME + : coalesce( DEFAULT_COLUMN_NAME, softDeleteConfig.columnName() ); + final Identifier physicalColumnName = namingStrategy.toPhysicalColumnName( + database.toIdentifier( logicalColumnName ), + database.getJdbcEnvironment() + ); + softDeleteColumn.setName( physicalColumnName.render( database.getDialect() ) ); + } + + public static SoftDeleteMappingImpl resolveSoftDeleteMapping( + SoftDeletableModelPart softDeletableModelPart, + SoftDeletable bootMapping, + String tableName, + MappingModelCreationProcess creationProcess) { + return resolveSoftDeleteMapping( + softDeletableModelPart, + bootMapping, + tableName, + creationProcess.getCreationContext().getDialect() + ); + } + + public static SoftDeleteMappingImpl resolveSoftDeleteMapping( + SoftDeletableModelPart softDeletableModelPart, + SoftDeletable bootMapping, + String tableName, + Dialect dialect) { + final Column softDeleteColumn = bootMapping.getSoftDeleteColumn(); + if ( softDeleteColumn == null ) { + return null; + } + + final BasicValue columnValue = (BasicValue) softDeleteColumn.getValue(); + final BasicValue.Resolution resolution = columnValue.resolve(); + //noinspection unchecked + final BasicValueConverter converter = (BasicValueConverter) resolution.getValueConverter(); + //noinspection unchecked + final JdbcLiteralFormatter literalFormatter = resolution.getJdbcMapping().getJdbcLiteralFormatter(); + + final Object deletedLiteralValue = converter.toRelationalValue( true ); + final Object nonDeletedLiteralValue = converter.toRelationalValue( false ); + + return new SoftDeleteMappingImpl( + softDeletableModelPart, + softDeleteColumn.getName(), + tableName, + deletedLiteralValue, + literalFormatter.toJdbcLiteral( deletedLiteralValue, dialect, null ), + nonDeletedLiteralValue, + literalFormatter.toJdbcLiteral( nonDeletedLiteralValue, dialect, null ), + resolution.getJdbcMapping() + ); + } + + /** + * Create a SQL AST Predicate for restricting matches to non-deleted rows + * + * @param tableReference The table reference for the table containing the soft-delete column + * @param softDeleteMapping The soft-delete mapping + */ + public static Predicate createNonSoftDeletedRestriction( + TableReference tableReference, + SoftDeleteMapping softDeleteMapping) { + final ColumnReference softDeleteColumn = new ColumnReference( tableReference, softDeleteMapping ); + final JdbcLiteral notDeletedLiteral = new JdbcLiteral<>( + softDeleteMapping.getNonDeletedLiteralValue(), + softDeleteMapping.getJdbcMapping() + ); + return new ComparisonPredicate( softDeleteColumn, EQUAL, notDeletedLiteral ); + } + + /** + * Create a SQL AST Predicate for restricting matches to non-deleted rows + * + * @param tableReference The table reference for the table containing the soft-delete column + * @param softDeleteMapping The soft-delete mapping + */ + public static Predicate createNonSoftDeletedRestriction( + TableReference tableReference, + SoftDeleteMapping softDeleteMapping, + SqlExpressionResolver expressionResolver) { + final Expression softDeleteColumn = expressionResolver.resolveSqlExpression( tableReference, softDeleteMapping ); + final JdbcLiteral notDeletedLiteral = new JdbcLiteral<>( + softDeleteMapping.getNonDeletedLiteralValue(), + softDeleteMapping.getJdbcMapping() + ); + return new ComparisonPredicate( softDeleteColumn, EQUAL, notDeletedLiteral ); + } + + /** + * Create a SQL AST Assignment for setting the soft-delete column to its + * deleted indicate value + * + * @param tableReference The table reference for the table containing the soft-delete column + * @param softDeleteMapping The soft-delete mapping + */ + public static Assignment createSoftDeleteAssignment( + TableReference tableReference, + SoftDeleteMapping softDeleteMapping) { + final ColumnReference softDeleteColumn = new ColumnReference( tableReference, softDeleteMapping ); + final JdbcLiteral softDeleteIndicator = new JdbcLiteral<>( + softDeleteMapping.getDeletedLiteralValue(), + softDeleteMapping.getJdbcMapping() + ); + return new Assignment( softDeleteColumn, softDeleteIndicator ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/internal/util/StringHelper.java b/hibernate-core/src/main/java/org/hibernate/internal/util/StringHelper.java index f989186fca..68a0bba2fe 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/util/StringHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/util/StringHelper.java @@ -20,6 +20,7 @@ import org.hibernate.dialect.Dialect; import org.hibernate.internal.util.collections.ArrayHelper; import org.hibernate.loader.internal.AliasConstantsHelper; +import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; public final class StringHelper { @@ -854,6 +855,22 @@ public final class StringHelper { return buffer.toString(); } + public static String coalesce(@NonNull String fallbackValue, @NonNull String... values) { + for ( int i = 0; i < values.length; i++ ) { + if ( isNotEmpty( values[i] ) ) { + return values[i]; + } + } + return fallbackValue; + } + + public static String coalesce(@NonNull String fallbackValue, String value) { + if ( isNotEmpty( value ) ) { + return value; + } + return fallbackValue; + } + public interface Renderer { String render(T value); } diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CollectionBatchLoaderArrayParam.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CollectionBatchLoaderArrayParam.java index 3c0e5cee3e..fb578add53 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CollectionBatchLoaderArrayParam.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CollectionBatchLoaderArrayParam.java @@ -25,6 +25,8 @@ import org.hibernate.metamodel.mapping.ValuedModelPart; import org.hibernate.metamodel.mapping.internal.IdClassEmbeddable; import org.hibernate.query.spi.QueryOptions; import org.hibernate.sql.ast.tree.expression.JdbcParameter; +import org.hibernate.sql.ast.tree.from.TableGroup; +import org.hibernate.sql.ast.tree.select.QuerySpec; import org.hibernate.sql.ast.tree.select.SelectStatement; import org.hibernate.sql.exec.internal.JdbcParameterBindingImpl; import org.hibernate.sql.exec.internal.JdbcParameterBindingsImpl; @@ -95,6 +97,10 @@ public class CollectionBatchLoaderArrayParam getSessionFactory() ); + final QuerySpec querySpec = sqlSelect.getQueryPart().getFirstQuerySpec(); + final TableGroup tableGroup = querySpec.getFromClause().getRoots().get( 0 ); + attributeMapping.applySoftDeleteRestrictions( tableGroup, querySpec::applyPredicate ); + jdbcSelectOperation = getSessionFactory().getJdbcServices() .getJdbcEnvironment() .getSqlAstTranslatorFactory() diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CollectionBatchLoaderInPredicate.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CollectionBatchLoaderInPredicate.java index b4ab77d88b..428b814cbd 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CollectionBatchLoaderInPredicate.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CollectionBatchLoaderInPredicate.java @@ -16,6 +16,8 @@ import org.hibernate.loader.ast.spi.CollectionBatchLoader; import org.hibernate.loader.ast.spi.SqlArrayMultiKeyLoader; import org.hibernate.metamodel.mapping.PluralAttributeMapping; import org.hibernate.query.spi.QueryOptions; +import org.hibernate.sql.ast.tree.from.TableGroup; +import org.hibernate.sql.ast.tree.select.QuerySpec; import org.hibernate.sql.ast.tree.select.SelectStatement; import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParameterBindings; @@ -71,6 +73,11 @@ public class CollectionBatchLoaderInPredicate jdbcParametersBuilder::add, sessionFactory ); + + final QuerySpec querySpec = sqlAst.getQueryPart().getFirstQuerySpec(); + final TableGroup tableGroup = querySpec.getFromClause().getRoots().get( 0 ); + attributeMapping.applySoftDeleteRestrictions( tableGroup, querySpec::applyPredicate ); + this.jdbcParameters = jdbcParametersBuilder.build(); assert this.jdbcParameters.size() == this.sqlBatchSize * this.keyColumnCount; diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CollectionLoaderSingleKey.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CollectionLoaderSingleKey.java index d185f6829a..98d89e4cbd 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CollectionLoaderSingleKey.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CollectionLoaderSingleKey.java @@ -8,7 +8,6 @@ package org.hibernate.loader.ast.internal; import org.hibernate.LockOptions; import org.hibernate.collection.spi.PersistentCollection; -import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment; import org.hibernate.engine.jdbc.spi.JdbcServices; import org.hibernate.engine.spi.CollectionKey; import org.hibernate.engine.spi.EntityKey; @@ -19,7 +18,9 @@ import org.hibernate.engine.spi.SubselectFetch; import org.hibernate.loader.ast.spi.CollectionLoader; import org.hibernate.metamodel.mapping.PluralAttributeMapping; import org.hibernate.query.spi.QueryOptions; -import org.hibernate.sql.ast.SqlAstTranslatorFactory; +import org.hibernate.sql.ast.tree.from.FromClause; +import org.hibernate.sql.ast.tree.from.TableGroup; +import org.hibernate.sql.ast.tree.select.QuerySpec; import org.hibernate.sql.ast.tree.select.SelectStatement; import org.hibernate.sql.exec.internal.BaseExecutionContext; import org.hibernate.sql.exec.internal.JdbcParameterBindingsImpl; @@ -64,6 +65,12 @@ public class CollectionLoaderSingleKey implements CollectionLoader { jdbcParametersBuilder::add, sessionFactory ); + + final QuerySpec querySpec = sqlAst.getQueryPart().getFirstQuerySpec(); + final FromClause fromClause = querySpec.getFromClause(); + final TableGroup tableGroup = fromClause.getRoots().get( 0 ); + attributeMapping.applySoftDeleteRestrictions( tableGroup, querySpec::applyPredicate ); + this.jdbcParameters = jdbcParametersBuilder.build(); this.jdbcSelect = sessionFactory.getJdbcServices() .getJdbcEnvironment() diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CollectionLoaderSubSelectFetch.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CollectionLoaderSubSelectFetch.java index b983f95adb..67118de200 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CollectionLoaderSubSelectFetch.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CollectionLoaderSubSelectFetch.java @@ -26,6 +26,8 @@ import org.hibernate.loader.ast.spi.CollectionLoader; import org.hibernate.metamodel.mapping.PluralAttributeMapping; import org.hibernate.query.spi.QueryOptions; import org.hibernate.sql.ast.SqlAstTranslatorFactory; +import org.hibernate.sql.ast.tree.from.TableGroup; +import org.hibernate.sql.ast.tree.select.QuerySpec; import org.hibernate.sql.ast.tree.select.SelectStatement; import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; import org.hibernate.sql.results.graph.DomainResult; @@ -61,6 +63,10 @@ public class CollectionLoaderSubSelectFetch implements CollectionLoader { jdbcParameter -> {}, session.getFactory() ); + + final QuerySpec querySpec = sqlAst.getQueryPart().getFirstQuerySpec(); + final TableGroup tableGroup = querySpec.getFromClause().getRoots().get( 0 ); + attributeMapping.applySoftDeleteRestrictions( tableGroup, querySpec::applyPredicate ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/EntityBatchLoaderInPredicate.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/EntityBatchLoaderInPredicate.java index a2acb837cb..6f2ea798cb 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/EntityBatchLoaderInPredicate.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/EntityBatchLoaderInPredicate.java @@ -77,7 +77,6 @@ public class EntityBatchLoaderInPredicate final EntityIdentifierMapping identifierMapping = getLoadable().getIdentifierMapping(); final int expectedNumberOfParameters = identifierMapping.getJdbcTypeCount() * sqlBatchSize; - final JdbcParametersList.Builder jdbcParametersBuilder = JdbcParametersList.newBuilder( expectedNumberOfParameters ); sqlAst = LoaderSelectBuilder.createSelect( getLoadable(), @@ -177,7 +176,7 @@ public class EntityBatchLoaderInPredicate getLoadable().getEntityName(), pkValue, startIndex, - startIndex + ( sqlBatchSize -1) + startIndex + ( sqlBatchSize - 1 ) ); } }, @@ -187,120 +186,8 @@ public class EntityBatchLoaderInPredicate }, session ); - - - -// int numberOfIdsLeft = idsToInitialize.length; -// int start = 0; -// while ( numberOfIdsLeft > 0 ) { -// if ( MULTI_KEY_LOAD_DEBUG_ENABLED ) { -// MULTI_KEY_LOAD_LOGGER.debugf( "Processing batch-fetch chunk (`%s#%s`) %s - %s", getLoadable().getEntityName(), pkValue, start, start + ( sqlBatchSize -1) ); -// } -// initializeChunk( idsToInitialize, start, pkValue, entityInstance, lockOptions, readOnly, session ); -// -// start += sqlBatchSize; -// numberOfIdsLeft -= sqlBatchSize; -// } } -// private void initializeChunk( -// Object[] idsToInitialize, -// int start, -// Object pkValue, -// Object entityInstance, -// LockOptions lockOptions, -// Boolean readOnly, -// SharedSessionContractImplementor session) { -// initializeChunk( -// idsToInitialize, -// getLoadable(), -// start, -// sqlBatchSize, -// jdbcParameters, -// sqlAst, -// jdbcSelectOperation, -// pkValue, -// entityInstance, -// lockOptions, -// readOnly, -// session -// ); -// } - -// private static void initializeChunk( -// Object[] idsToInitialize, -// EntityMappingType entityMapping, -// int startIndex, -// int numberOfKeys, -// List jdbcParameters, -// SelectStatement sqlAst, -// JdbcOperationQuerySelect jdbcSelectOperation, -// Object pkValue, -// Object entityInstance, -// LockOptions lockOptions, -// Boolean readOnly, -// SharedSessionContractImplementor session) { -// final BatchFetchQueue batchFetchQueue = session.getPersistenceContext().getBatchFetchQueue(); -// -// final int numberOfJdbcParameters = entityMapping.getIdentifierMapping().getJdbcTypeCount() * numberOfKeys; -// final JdbcParameterBindings jdbcParameterBindings = new JdbcParameterBindingsImpl( numberOfJdbcParameters ); -// -// final List entityKeys = arrayList( numberOfKeys ); -// int bindCount = 0; -// for ( int i = 0; i < numberOfKeys; i++ ) { -// final int idPosition = i + startIndex; -// final Object value; -// if ( idPosition >= idsToInitialize.length ) { -// value = null; -// } -// else { -// value = idsToInitialize[idPosition]; -// } -// if ( value != null ) { -// entityKeys.add( session.generateEntityKey( value, entityMapping.getEntityPersister() ) ); -// } -// bindCount += jdbcParameterBindings.registerParametersForEachJdbcValue( -// value, -// bindCount, -// entityMapping.getIdentifierMapping(), -// jdbcParameters, -// session -// ); -// } -// assert bindCount == jdbcParameters.size(); -// -// if ( entityKeys.isEmpty() ) { -// // there are no non-null keys in the chunk -// return; -// } -// -// // Create a SubselectFetch.RegistrationHandler for handling any subselect fetches we encounter here -// final SubselectFetch.RegistrationHandler subSelectFetchableKeysHandler = SubselectFetch.createRegistrationHandler( -// batchFetchQueue, -// sqlAst, -// jdbcParameters, -// jdbcParameterBindings -// ); -// -// session.getJdbcServices().getJdbcSelectExecutor().list( -// jdbcSelectOperation, -// jdbcParameterBindings, -// new SingleIdExecutionContext( -// pkValue, -// entityInstance, -// entityMapping.getRootEntityDescriptor(), -// readOnly, -// lockOptions, -// subSelectFetchableKeysHandler, -// session -// ), -// RowTransformerStandardImpl.instance(), -// ListResultsConsumer.UniqueSemantic.FILTER -// ); -// -// entityKeys.forEach( batchFetchQueue::removeBatchLoadableEntityKey ); -// } - @Override public String toString() { return String.format( diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/Collection.java b/hibernate-core/src/main/java/org/hibernate/mapping/Collection.java index e6752c7e17..2cd411d3c0 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/Collection.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/Collection.java @@ -41,7 +41,7 @@ import org.hibernate.usertype.UserCollectionType; * * @author Gavin King */ -public abstract class Collection implements Fetchable, Value, Filterable { +public abstract class Collection implements Fetchable, Value, Filterable, SoftDeletable { public static final String DEFAULT_ELEMENT_COLUMN_NAME = "elt"; public static final String DEFAULT_KEY_COLUMN_NAME = "id"; @@ -99,6 +99,8 @@ public abstract class Collection implements Fetchable, Value, Filterable { private boolean customDeleteAllCallable; private ExecuteUpdateResultCheckStyle deleteAllCheckStyle; + private Column softDeleteColumn; + private String loaderName; /** @@ -827,4 +829,14 @@ public abstract class Collection implements Fetchable, Value, Filterable { public boolean isColumnUpdateable(int index) { return false; } + + @Override + public void enableSoftDelete(Column indicatorColumn) { + this.softDeleteColumn = indicatorColumn; + } + + @Override + public Column getSoftDeleteColumn() { + return softDeleteColumn; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/RootClass.java b/hibernate-core/src/main/java/org/hibernate/mapping/RootClass.java index 1bc08f4721..ac3dbf008a 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/RootClass.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/RootClass.java @@ -28,7 +28,7 @@ import static org.hibernate.internal.util.StringHelper.nullIfEmpty; * * @author Gavin King */ -public class RootClass extends PersistentClass implements TableOwner { +public class RootClass extends PersistentClass implements TableOwner, SoftDeletable { private static final CoreMessageLogger LOG = CoreLogging.messageLogger( RootClass.class ); @Deprecated(since = "6.2") @Remove @@ -58,6 +58,7 @@ public class RootClass extends PersistentClass implements TableOwner { private int nextSubclassId; private Property declaredIdentifierProperty; private Property declaredVersion; + private Column softDeleteColumn; public RootClass(MetadataBuildingContext buildingContext) { super( buildingContext ); @@ -401,6 +402,16 @@ public class RootClass extends PersistentClass implements TableOwner { return tables; } + @Override + public void enableSoftDelete(Column indicatorColumn) { + this.softDeleteColumn = indicatorColumn; + } + + @Override + public Column getSoftDeleteColumn() { + return softDeleteColumn; + } + @Override public Object accept(PersistentClassVisitor mv) { return mv.accept( this ); diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/SoftDeletable.java b/hibernate-core/src/main/java/org/hibernate/mapping/SoftDeletable.java new file mode 100644 index 0000000000..9064be0b6b --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/mapping/SoftDeletable.java @@ -0,0 +1,15 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.mapping; + +/** + * @author Steve Ebersole + */ +public interface SoftDeletable { + void enableSoftDelete(Column indicatorColumn); + Column getSoftDeleteColumn(); +} diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/UnsupportedMappingException.java b/hibernate-core/src/main/java/org/hibernate/metamodel/UnsupportedMappingException.java index a854141220..5d7402207e 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/UnsupportedMappingException.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/UnsupportedMappingException.java @@ -9,6 +9,10 @@ package org.hibernate.metamodel; import org.hibernate.HibernateException; import org.hibernate.metamodel.mapping.NonTransientException; +/** + * Indicated a problem with a mapping. Usually this is a problem with a combination + * of mapping constructs. + */ public class UnsupportedMappingException extends HibernateException implements NonTransientException { public UnsupportedMappingException(String message) { super( message ); diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/EntityMappingType.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/EntityMappingType.java index ab0f4a4dec..a2395e5559 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/EntityMappingType.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/EntityMappingType.java @@ -56,7 +56,8 @@ import static org.hibernate.bytecode.enhance.spi.LazyPropertyInitializer.UNFETCH * @author Steve Ebersole */ public interface EntityMappingType - extends ManagedMappingType, EntityValuedModelPart, Loadable, Restrictable, Discriminable { + extends ManagedMappingType, EntityValuedModelPart, Loadable, Restrictable, Discriminable, + SoftDeletableModelPart { /** * The entity name. @@ -356,6 +357,18 @@ public interface EntityMappingType */ EntityRowIdMapping getRowIdMapping(); + /** + * Mapping for soft-delete support, or {@code null} if soft-delete not defined + */ + default SoftDeleteMapping getSoftDeleteMapping() { + return null; + } + + @Override + default TableDetails getSoftDeleteTableDetails() { + return getIdentifierTableDetails(); + } + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Attribute mappings diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/PluralAttributeMapping.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/PluralAttributeMapping.java index 2c85664dbd..11f39d6cb9 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/PluralAttributeMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/PluralAttributeMapping.java @@ -33,7 +33,7 @@ import org.hibernate.sql.results.graph.basic.BasicResult; * @author Steve Ebersole */ public interface PluralAttributeMapping - extends AttributeMapping, TableGroupJoinProducer, FetchableContainer, Loadable, Restrictable { + extends AttributeMapping, TableGroupJoinProducer, FetchableContainer, Loadable, Restrictable, SoftDeletableModelPart { CollectionPersister getCollectionDescriptor(); @@ -44,6 +44,13 @@ public interface PluralAttributeMapping @Override CollectionMappingType getMappedType(); + @FunctionalInterface + interface PredicateConsumer { + void applyPredicate(Predicate predicate); + } + + void applySoftDeleteRestrictions(TableGroup tableGroup, PredicateConsumer predicateConsumer); + interface IndexMetadata { CollectionPart getIndexDescriptor(); int getListIndexBase(); @@ -56,6 +63,13 @@ public interface PluralAttributeMapping CollectionIdentifierDescriptor getIdentifierDescriptor(); + /** + * Mapping for soft-delete support, or {@code null} if soft-delete not defined + */ + default SoftDeleteMapping getSoftDeleteMapping() { + return null; + } + OrderByFragment getOrderByFragment(); OrderByFragment getManyToManyOrderByFragment(); diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/SoftDeletableModelPart.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/SoftDeletableModelPart.java new file mode 100644 index 0000000000..9ed8d4442e --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/SoftDeletableModelPart.java @@ -0,0 +1,21 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.metamodel.mapping; + +/** + * Model part which can be soft-deleted + * + * @author Steve Ebersole + */ +public interface SoftDeletableModelPart extends ModelPartContainer { + /** + * Get the mapping of the soft-delete indicator + */ + SoftDeleteMapping getSoftDeleteMapping(); + + TableDetails getSoftDeleteTableDetails(); +} diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/SoftDeleteMapping.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/SoftDeleteMapping.java new file mode 100644 index 0000000000..e87cf8bc69 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/SoftDeleteMapping.java @@ -0,0 +1,129 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.metamodel.mapping; + +/** + * + * Metadata about the indicator column for entities and collections enabled + * for soft delete + * + * @see org.hibernate.annotations.SoftDelete + * + * @author Steve Ebersole + */ +public interface SoftDeleteMapping extends SelectableMapping, VirtualModelPart, SqlExpressible { + /** + * The name of the soft-delete indicator column. + */ + String getColumnName(); + + /** + * The name of the table which holds the {@linkplain #getColumnName() indicator column} + */ + String getTableName(); + + /** + * The SQL literal value which indicates a deleted row + */ + Object getDeletedLiteralValue(); + + /** + * The String representation of the SQL literal value which indicates a deleted row + */ + String getDeletedLiteralText(); + + /** + * The SQL literal value which indicates a non-deleted row + * + * @apiNote The inverse of {@linkplain #getDeletedLiteralValue()} + */ + Object getNonDeletedLiteralValue(); + + /** + * The String representation of the SQL literal value which indicates a non-deleted row + */ + String getNonDeletedLiteralText(); + + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // SelectableMapping + + @Override + default String getSelectionExpression() { + return getColumnName(); + } + + @Override + default String getSelectableName() { + return getColumnName(); + } + + @Override + default String getWriteExpression() { + return getNonDeletedLiteralText(); + } + + @Override + default String getContainingTableExpression() { + return getTableName(); + } + + @Override + default String getCustomReadExpression() { + return null; + } + + @Override + default String getCustomWriteExpression() { + return null; + } + + @Override + default boolean isFormula() { + return false; + } + + @Override + default boolean isNullable() { + return false; + } + + @Override + default boolean isInsertable() { + return true; + } + + @Override + default boolean isUpdateable() { + return true; + } + + @Override + default boolean isPartitioned() { + return false; + } + + @Override + default String getColumnDefinition() { + return null; + } + + @Override + default Long getLength() { + return null; + } + + @Override + default Integer getPrecision() { + return null; + } + + @Override + default Integer getScale() { + return null; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/TableDetails.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/TableDetails.java index 023cb5ca4c..54021b69fa 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/TableDetails.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/TableDetails.java @@ -60,7 +60,7 @@ public interface TableDetails { /** * Details about a column within the key group */ - interface KeyColumn { + interface KeyColumn extends SelectableMapping { /** * The name of the column */ diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/MappingModelCreationHelper.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/MappingModelCreationHelper.java index 3b5801c8a2..6c9db66831 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/MappingModelCreationHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/MappingModelCreationHelper.java @@ -678,7 +678,8 @@ public class MappingModelCreationHelper { style, cascadeStyle, declaringType, - collectionDescriptor + collectionDescriptor, + creationProcess ) ); creationProcess.registerInitializationCallback( diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/PluralAttributeMappingImpl.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/PluralAttributeMappingImpl.java index 2d15434cfa..406a1d807e 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/PluralAttributeMappingImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/PluralAttributeMappingImpl.java @@ -10,6 +10,7 @@ import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Supplier; +import org.hibernate.boot.model.internal.SoftDeleteHelper; import org.hibernate.cache.MutableCacheKeyBuilder; import org.hibernate.engine.FetchStyle; import org.hibernate.engine.FetchTiming; @@ -36,11 +37,14 @@ import org.hibernate.metamodel.mapping.ModelPart; import org.hibernate.metamodel.mapping.ModelPartContainer; import org.hibernate.metamodel.mapping.PluralAttributeMapping; import org.hibernate.metamodel.mapping.SelectableMapping; +import org.hibernate.metamodel.mapping.SoftDeleteMapping; +import org.hibernate.metamodel.mapping.TableDetails; import org.hibernate.metamodel.mapping.ordering.OrderByFragment; import org.hibernate.metamodel.mapping.ordering.OrderByFragmentTranslator; import org.hibernate.metamodel.mapping.ordering.TranslationContext; import org.hibernate.metamodel.model.domain.NavigableRole; import org.hibernate.persister.collection.CollectionPersister; +import org.hibernate.persister.collection.mutation.CollectionMutationTarget; import org.hibernate.persister.entity.Joinable; import org.hibernate.property.access.spi.PropertyAccess; import org.hibernate.spi.NavigablePath; @@ -71,6 +75,9 @@ import org.hibernate.sql.results.graph.collection.internal.SelectEagerCollection import org.jboss.logging.Logger; +import static org.hibernate.boot.model.internal.SoftDeleteHelper.createNonSoftDeletedRestriction; +import static org.hibernate.boot.model.internal.SoftDeleteHelper.resolveSoftDeleteMapping; + /** * @author Steve Ebersole */ @@ -106,6 +113,8 @@ public class PluralAttributeMappingImpl private final CollectionIdentifierDescriptor identifierDescriptor; private final FetchTiming fetchTiming; private final FetchStyle fetchStyle; + private final SoftDeleteMapping softDeleteMapping; + private Boolean hasSoftDelete; private final String bidirectionalAttributeName; @@ -136,7 +145,8 @@ public class PluralAttributeMappingImpl FetchStyle fetchStyle, CascadeStyle cascadeStyle, ManagedMappingType declaringType, - CollectionPersister collectionDescriptor) { + CollectionPersister collectionDescriptor, + MappingModelCreationProcess creationProcess) { super( attributeName, fetchableIndex, declaringType ); this.propertyAccess = propertyAccess; this.attributeMetadata = attributeMetadata; @@ -193,9 +203,12 @@ public class PluralAttributeMappingImpl } }; + softDeleteMapping = resolveSoftDeleteMapping( this, bootDescriptor, getSeparateCollectionTable(), creationProcess.getCreationContext().getDialect() ); + injectAttributeMapping( elementDescriptor, indexDescriptor, collectionDescriptor, this ); } + /** * For Hibernate Reactive */ @@ -210,6 +223,8 @@ public class PluralAttributeMappingImpl this.identifierDescriptor = original.identifierDescriptor; this.fetchTiming = original.fetchTiming; this.fetchStyle = original.fetchStyle; + this.softDeleteMapping = original.softDeleteMapping; + this.hasSoftDelete = original.hasSoftDelete; this.collectionDescriptor = original.collectionDescriptor; this.referencedPropertyName = original.referencedPropertyName; this.mapKeyPropertyName = original.mapKeyPropertyName; @@ -335,6 +350,16 @@ public class PluralAttributeMappingImpl return identifierDescriptor; } + @Override + public SoftDeleteMapping getSoftDeleteMapping() { + return softDeleteMapping; + } + + @Override + public TableDetails getSoftDeleteTableDetails() { + return ( (CollectionMutationTarget) getCollectionDescriptor() ).getCollectionTableMapping(); + } + @Override public OrderByFragment getOrderByFragment() { return orderByFragment; @@ -401,6 +426,39 @@ public class PluralAttributeMappingImpl return false; } + @Override + public void applySoftDeleteRestrictions(TableGroup tableGroup, PredicateConsumer predicateConsumer) { + if ( !hasSoftDelete() ) { + // short-circuit + return; + } + + if ( getCollectionDescriptor().isOneToMany() + || getCollectionDescriptor().isManyToMany() ) { + // see if the associated entity has soft-delete defined + final EntityCollectionPart elementDescriptor = (EntityCollectionPart) getElementDescriptor(); + final EntityMappingType associatedEntityDescriptor = elementDescriptor.getAssociatedEntityMappingType(); + final SoftDeleteMapping softDeleteMapping = associatedEntityDescriptor.getSoftDeleteMapping(); + if ( softDeleteMapping != null ) { + final Predicate softDeleteRestriction = SoftDeleteHelper.createNonSoftDeletedRestriction( + tableGroup.resolveTableReference( associatedEntityDescriptor.getSoftDeleteTableDetails().getTableName() ), + softDeleteMapping + ); + predicateConsumer.applyPredicate( softDeleteRestriction ); + } + } + + // apply the collection's soft-delete mapping, if one + final SoftDeleteMapping softDeleteMapping = getSoftDeleteMapping(); + if ( softDeleteMapping != null ) { + final Predicate softDeleteRestriction = SoftDeleteHelper.createNonSoftDeletedRestriction( + tableGroup.resolveTableReference( getSoftDeleteTableDetails().getTableName() ), + softDeleteMapping + ); + predicateConsumer.applyPredicate( softDeleteRestriction ); + } + } + @Override public DomainResult createDomainResult( NavigablePath navigablePath, @@ -643,6 +701,7 @@ public class PluralAttributeMappingImpl boolean addsPredicate, SqlAstCreationState creationState) { final PredicateCollector predicateCollector = new PredicateCollector(); + final TableGroup tableGroup = createRootTableGroupJoin( navigablePath, lhs, @@ -672,6 +731,12 @@ public class PluralAttributeMappingImpl creationState ); + applySoftDeleteRestriction( + predicateCollector::applyPredicate, + tableGroup, + creationState + ); + return new TableGroupJoin( navigablePath, determineSqlJoinType( lhs, requestedJoinType, fetched ), @@ -680,6 +745,78 @@ public class PluralAttributeMappingImpl ); } + private boolean hasSoftDelete() { + // NOTE : this needs to be done lazily because the associated entity mapping (if one) + // does not know its SoftDeleteMapping yet when this is created + if ( hasSoftDelete == null ) { + if ( softDeleteMapping != null ) { + hasSoftDelete = true; + } + else { + if ( getElementDescriptor() instanceof EntityCollectionPart ) { + final EntityMappingType associatedEntityMapping = ( (EntityCollectionPart) getElementDescriptor() ).getAssociatedEntityMappingType(); + hasSoftDelete = associatedEntityMapping.getSoftDeleteMapping() != null; + } + else { + hasSoftDelete = false; + } + } + } + + return hasSoftDelete; + } + + private void applySoftDeleteRestriction( + Consumer predicateConsumer, + TableGroup tableGroup, + SqlAstCreationState creationState) { + if ( !hasSoftDelete() ) { + // short circuit + return; + } + + if ( getElementDescriptor() instanceof EntityCollectionPart ) { + final EntityMappingType entityMappingType = ( (EntityCollectionPart) getElementDescriptor() ).getAssociatedEntityMappingType(); + final SoftDeleteMapping softDeleteMapping = entityMappingType.getSoftDeleteMapping(); + if ( softDeleteMapping != null ) { + final TableDetails softDeleteTable = entityMappingType.getSoftDeleteTableDetails(); + predicateConsumer.accept( createNonSoftDeletedRestriction( + tableGroup.resolveTableReference( softDeleteTable.getTableName() ), + softDeleteMapping, + creationState.getSqlExpressionResolver() + ) ); + } + } + + final SoftDeleteMapping softDeleteMapping = getSoftDeleteMapping(); + if ( softDeleteMapping != null ) { + final TableDetails softDeleteTable = getSoftDeleteTableDetails(); + predicateConsumer.accept( createNonSoftDeletedRestriction( + tableGroup.resolveTableReference( softDeleteTable.getTableName() ), + softDeleteMapping, + creationState.getSqlExpressionResolver() + ) ); + } + } + + public SqlAstJoinType determineSqlJoinType(TableGroup lhs, SqlAstJoinType requestedJoinType, boolean fetched) { + if ( hasSoftDelete() ) { + return SqlAstJoinType.LEFT; + } + + if ( requestedJoinType == null ) { + if ( fetched ) { + return getDefaultSqlAstJoinType( lhs ); + } + else { + return SqlAstJoinType.INNER; + } + } + else { + return requestedJoinType; + } + } + @Override public TableGroup createRootTableGroupJoin( NavigablePath navigablePath, diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/SoftDeleteMappingImpl.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/SoftDeleteMappingImpl.java new file mode 100644 index 0000000000..e19bb38ee6 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/SoftDeleteMappingImpl.java @@ -0,0 +1,227 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.metamodel.mapping.internal; + +import java.util.function.BiConsumer; + +import org.hibernate.cache.MutableCacheKeyBuilder; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.internal.util.IndexedConsumer; +import org.hibernate.metamodel.mapping.EntityMappingType; +import org.hibernate.metamodel.mapping.JdbcMapping; +import org.hibernate.metamodel.mapping.MappingType; +import org.hibernate.metamodel.mapping.SoftDeletableModelPart; +import org.hibernate.metamodel.mapping.SoftDeleteMapping; +import org.hibernate.metamodel.mapping.TableDetails; +import org.hibernate.metamodel.model.domain.NavigableRole; +import org.hibernate.spi.NavigablePath; +import org.hibernate.sql.ast.spi.SqlExpressionResolver; +import org.hibernate.sql.ast.spi.SqlSelection; +import org.hibernate.sql.ast.tree.from.TableGroup; +import org.hibernate.sql.ast.tree.from.TableReference; +import org.hibernate.sql.results.graph.DomainResult; +import org.hibernate.sql.results.graph.DomainResultCreationState; +import org.hibernate.sql.results.graph.basic.BasicResult; +import org.hibernate.type.descriptor.java.JavaType; + +/** + * SoftDeleteMapping implementation + * + * @author Steve Ebersole + */ +public class SoftDeleteMappingImpl implements SoftDeleteMapping { + public static final String ROLE_NAME = "{soft-delete}"; + + private final SoftDeletableModelPart softDeletable; + private final String columnName; + private final String tableName; + private final Object deletedLiteralValue; + private final String deletedLiteralText; + private final Object nonDeletedLiteralValue; + private final String nonDeletedLiteralText; + private final JdbcMapping jdbcMapping; + + private final NavigableRole navigableRole; + + public SoftDeleteMappingImpl( + SoftDeletableModelPart softDeletable, + String columnName, + String tableName, + Object deletedLiteralValue, + String deletedLiteralText, + Object nonDeletedLiteralValue, + String nonDeletedLiteralText, + JdbcMapping jdbcMapping) { + this.softDeletable = softDeletable; + this.columnName = columnName; + this.tableName = tableName; + this.deletedLiteralValue = deletedLiteralValue; + this.deletedLiteralText = deletedLiteralText; + this.nonDeletedLiteralValue = nonDeletedLiteralValue; + this.nonDeletedLiteralText = nonDeletedLiteralText; + this.jdbcMapping = jdbcMapping; + + this.navigableRole = softDeletable.getNavigableRole().append( ROLE_NAME ); + } + + @Override + public String getColumnName() { + return columnName; + } + + @Override + public String getTableName() { + return tableName; + } + + @Override + public Object getDeletedLiteralValue() { + return deletedLiteralValue; + } + + @Override + public String getDeletedLiteralText() { + return deletedLiteralText; + } + + @Override + public Object getNonDeletedLiteralValue() { + return nonDeletedLiteralValue; + } + + @Override + public String getNonDeletedLiteralText() { + return nonDeletedLiteralText; + } + + @Override + public JdbcMapping getJdbcMapping() { + return jdbcMapping; + } + + + @Override + public String getPartName() { + return ROLE_NAME; + } + + @Override + public NavigableRole getNavigableRole() { + return navigableRole; + } + + @Override + public int forEachJdbcType(int offset, IndexedConsumer action) { + action.accept( offset, jdbcMapping ); + return 1; + } + + @Override + public Object disassemble(Object value, SharedSessionContractImplementor session) { + return value; + } + + @Override + public void addToCacheKey(MutableCacheKeyBuilder cacheKey, Object value, SharedSessionContractImplementor session) { + } + + @Override + public int forEachDisassembledJdbcValue( + Object value, + int offset, + X x, + Y y, + JdbcValuesBiConsumer valuesConsumer, + SharedSessionContractImplementor session) { + valuesConsumer.consume( offset, x, y, value, getJdbcMapping() ); + return 1; + } + + @Override + public MappingType getPartMappingType() { + return jdbcMapping; + } + + @Override + public JavaType getJavaType() { + return jdbcMapping.getMappedJavaType(); + } + + @Override + public boolean hasPartitionedSelectionMapping() { + return false; + } + + @Override + public DomainResult createDomainResult( + NavigablePath navigablePath, + TableGroup tableGroup, + String resultVariable, + DomainResultCreationState creationState) { + final SqlSelection sqlSelection = resolveSqlSelection( navigablePath, tableGroup, creationState ); + return new BasicResult<>( sqlSelection.getValuesArrayPosition(), resultVariable, getJdbcMapping() ); + } + + private SqlSelection resolveSqlSelection( + NavigablePath navigablePath, + TableGroup tableGroup, + DomainResultCreationState creationState) { + final TableDetails indicatorTable = softDeletable.getSoftDeleteTableDetails(); + final TableReference tableReference = tableGroup.resolveTableReference( + navigablePath.getRealParent(), + indicatorTable.getTableName() + ); + final SqlExpressionResolver expressionResolver = creationState.getSqlAstCreationState().getSqlExpressionResolver(); + final SqlSelection sqlSelection = expressionResolver.resolveSqlSelection( + expressionResolver.resolveSqlExpression( tableReference, this ), + getJavaType(), + null, + creationState.getSqlAstCreationState().getCreationContext().getMappingMetamodel().getTypeConfiguration() + ); + return sqlSelection; + } + + @Override + public void applySqlSelections( + NavigablePath navigablePath, + TableGroup tableGroup, + DomainResultCreationState creationState) { + resolveSqlSelection( navigablePath, tableGroup, creationState ); + } + + @Override + public void applySqlSelections( + NavigablePath navigablePath, + TableGroup tableGroup, + DomainResultCreationState creationState, + BiConsumer selectionConsumer) { + final SqlSelection sqlSelection = resolveSqlSelection( navigablePath, tableGroup, creationState ); + selectionConsumer.accept( sqlSelection, getJdbcMapping() ); + } + + @Override + public int breakDownJdbcValues( + Object domainValue, + int offset, + X x, + Y y, + JdbcValueBiConsumer valueConsumer, + SharedSessionContractImplementor session) { + valueConsumer.consume( offset, x, y, disassemble( domainValue, session ), this ); + return 1; + } + + @Override + public EntityMappingType findContainingEntityMapping() { + return softDeletable.findContainingEntityMapping(); + } + + @Override + public String toString() { + return "SoftDeleteMappingImpl(" + tableName + "." + columnName + ")"; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ToOneAttributeMapping.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ToOneAttributeMapping.java index 2211076926..35c866e409 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ToOneAttributeMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ToOneAttributeMapping.java @@ -9,6 +9,7 @@ package org.hibernate.metamodel.mapping.internal; import java.util.Arrays; import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Set; import java.util.function.BiConsumer; import java.util.function.Consumer; @@ -34,6 +35,7 @@ import org.hibernate.mapping.Property; import org.hibernate.mapping.Selectable; import org.hibernate.mapping.ToOne; import org.hibernate.mapping.Value; +import org.hibernate.metamodel.UnsupportedMappingException; import org.hibernate.metamodel.mapping.AssociationKey; import org.hibernate.metamodel.mapping.AttributeMapping; import org.hibernate.metamodel.mapping.AttributeMetadata; @@ -52,6 +54,7 @@ import org.hibernate.metamodel.mapping.PluralAttributeMapping; import org.hibernate.metamodel.mapping.SelectableConsumer; import org.hibernate.metamodel.mapping.SelectableMapping; import org.hibernate.metamodel.mapping.SelectablePath; +import org.hibernate.metamodel.mapping.SoftDeleteMapping; import org.hibernate.metamodel.mapping.ValuedModelPart; import org.hibernate.metamodel.mapping.VirtualModelPart; import org.hibernate.metamodel.model.domain.NavigableRole; @@ -102,6 +105,8 @@ import org.hibernate.type.EmbeddedComponentType; import org.hibernate.type.EntityType; import org.hibernate.type.Type; +import static org.hibernate.boot.model.internal.SoftDeleteHelper.createNonSoftDeletedRestriction; + /** * @author Steve Ebersole */ @@ -413,6 +418,18 @@ public class ToOneAttributeMapping isInternalLoadNullable = isNullable(); } + if ( entityMappingType.getSoftDeleteMapping() != null ) { + // cannot be lazy + if ( getTiming() == FetchTiming.DELAYED ) { + throw new UnsupportedMappingException( String.format( + Locale.ROOT, + "To-one attribute (%s.%s) cannot be mapped as LAZY as its associated entity is defined with @SoftDelete", + declaringType.getPartName(), + getAttributeName() + ) ); + } + } + if ( referencedPropertyName == null ) { final Set targetKeyPropertyNames = new HashSet<>( 2 ); targetKeyPropertyNames.add( EntityIdentifierMapping.ID_ROLE_NAME ); @@ -1526,7 +1543,8 @@ public class ToOneAttributeMapping ); } } - else if ( notFoundAction != null ) { + else if ( notFoundAction != null + || getAssociatedEntityMappingType().getSoftDeleteMapping() != null ) { // For the target side only add keyResult when a not-found action is present keyResult = foreignKeyDescriptor.createTargetDomainResult( fetchablePath, @@ -1609,7 +1627,9 @@ public class ToOneAttributeMapping final boolean selectByUniqueKey = isSelectByUniqueKey( side ); // Consider all associations annotated with @NotFound as EAGER - if ( fetchTiming == FetchTiming.IMMEDIATE || hasNotFoundAction() ) { + if ( fetchTiming == FetchTiming.IMMEDIATE + || hasNotFoundAction() + || getAssociatedEntityMappingType().getSoftDeleteMapping() != null ) { return buildEntityFetchSelect( fetchParent, this, @@ -1978,6 +1998,20 @@ public class ToOneAttributeMapping if ( getAssociatedEntityMappingType().getSuperMappingType() != null && !creationState.supportsEntityNameUsage() ) { getAssociatedEntityMappingType().applyDiscriminator( null, null, tableGroup, creationState ); } + + final SoftDeleteMapping softDeleteMapping = getAssociatedEntityMappingType().getSoftDeleteMapping(); + if ( softDeleteMapping != null ) { + // add the restriction + final TableReference tableReference = lazyTableGroup.resolveTableReference( + navigablePath, + getAssociatedEntityMappingType().getSoftDeleteTableDetails().getTableName() + ); + join.applyPredicate( createNonSoftDeletedRestriction( + tableReference, + softDeleteMapping, + creationState.getSqlExpressionResolver() + ) ); + } } ); @@ -2014,11 +2048,15 @@ public class ToOneAttributeMapping creationState.getSqlAliasBaseGenerator() ); + final SoftDeleteMapping softDeleteMapping = getAssociatedEntityMappingType().getSoftDeleteMapping(); + final boolean canUseInnerJoin; if ( ! lhs.canUseInnerJoins() ) { canUseInnerJoin = false; } - else if ( isNullable || hasNotFoundAction() ) { + else if ( isNullable + || hasNotFoundAction() + || softDeleteMapping != null ) { canUseInnerJoin = false; } else { @@ -2089,6 +2127,19 @@ public class ToOneAttributeMapping ) ) ); + + if ( fetched && softDeleteMapping != null ) { + // add the restriction + final TableReference tableReference = lazyTableGroup.resolveTableReference( + navigablePath, + getAssociatedEntityMappingType().getSoftDeleteTableDetails().getTableName() + ); + predicateConsumer.accept( createNonSoftDeletedRestriction( + tableReference, + softDeleteMapping, + creationState.getSqlExpressionResolver() + ) ); + } } if ( requestedJoinType != null && realParentTableGroup instanceof CorrelatedTableGroup ) { diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/MappingMetamodelImpl.java b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/MappingMetamodelImpl.java index 34b489b971..d3c7f9868f 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/MappingMetamodelImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/MappingMetamodelImpl.java @@ -201,6 +201,10 @@ public class MappingMetamodelImpl extends QueryParameterBindingTypeResolverImpl registerEntityNameResolvers( persister, entityNameResolvers ); } + for ( EntityPersister persister : entityPersisterMap.values() ) { + persister.prepareLoaders(); + } + collectionPersisterMap.values().forEach( CollectionPersister::postInstantiate ); registerEmbeddableMappingType( bootModel ); diff --git a/hibernate-core/src/main/java/org/hibernate/persister/collection/AbstractCollectionPersister.java b/hibernate-core/src/main/java/org/hibernate/persister/collection/AbstractCollectionPersister.java index f8c4224c72..d675135993 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/collection/AbstractCollectionPersister.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/collection/AbstractCollectionPersister.java @@ -6,6 +6,18 @@ */ package org.hibernate.persister.collection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; + import org.hibernate.AssertionFailure; import org.hibernate.FetchMode; import org.hibernate.Filter; @@ -120,18 +132,6 @@ import org.hibernate.type.CompositeType; import org.hibernate.type.EntityType; import org.hibernate.type.Type; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.Map; -import java.util.Set; -import java.util.function.Consumer; - import static org.hibernate.internal.util.collections.CollectionHelper.arrayList; import static org.hibernate.sql.model.ModelMutationLogging.MODEL_MUTATION_LOGGER; @@ -1775,11 +1775,35 @@ public abstract class AbstractCollectionPersister ParameterUsage.RESTRICT, keyColumnCount ); - final java.util.List keyRestrictionBindings = arrayList( keyColumnCount ); - fkDescriptor.getKeyPart().forEachSelectable( parameterBinders ); - for ( ColumnValueParameter columnValueParameter : parameterBinders ) { + final java.util.List restrictionBindings = arrayList( keyColumnCount ); + applyKeyRestrictions( tableReference, parameterBinders, restrictionBindings ); + + //noinspection unchecked,rawtypes + return (RestrictedTableMutation) new TableDeleteStandard( + tableReference, + this, + "one-shot delete for " + getRolePath(), + restrictionBindings, + Collections.emptyList(), + parameterBinders, + sqlWhereString + ); + } + + protected void applyKeyRestrictions( + MutatingTableReference tableReference, + ColumnValueParameterList parameterList, + java.util.List restrictionBindings) { + + final ForeignKeyDescriptor fkDescriptor = getAttributeMapping().getKeyDescriptor(); + assert fkDescriptor != null; + + final int keyColumnCount = fkDescriptor.getJdbcTypeCount(); + + fkDescriptor.getKeyPart().forEachSelectable( parameterList ); + for ( ColumnValueParameter columnValueParameter : parameterList ) { final ColumnReference columnReference = columnValueParameter.getColumnReference(); - keyRestrictionBindings.add( + restrictionBindings.add( new ColumnValueBinding( columnReference, new ColumnWriteFragment( @@ -1790,17 +1814,6 @@ public abstract class AbstractCollectionPersister ) ); } - - //noinspection unchecked,rawtypes - return (RestrictedTableMutation) new TableDeleteStandard( - tableReference, - this, - "one-shot delete for " + getRolePath(), - keyRestrictionBindings, - Collections.emptyList(), - parameterBinders, - sqlWhereString - ); } diff --git a/hibernate-core/src/main/java/org/hibernate/persister/collection/BasicCollectionPersister.java b/hibernate-core/src/main/java/org/hibernate/persister/collection/BasicCollectionPersister.java index 8c7c729eec..8b0f0cc3f0 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/collection/BasicCollectionPersister.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/collection/BasicCollectionPersister.java @@ -6,6 +6,9 @@ */ package org.hibernate.persister.collection; +import java.util.Collections; +import java.util.List; + import org.hibernate.HibernateException; import org.hibernate.Internal; import org.hibernate.MappingException; @@ -25,6 +28,7 @@ import org.hibernate.metamodel.mapping.CollectionIdentifierDescriptor; import org.hibernate.metamodel.mapping.CollectionPart; import org.hibernate.metamodel.mapping.ForeignKeyDescriptor; import org.hibernate.metamodel.mapping.PluralAttributeMapping; +import org.hibernate.metamodel.mapping.SoftDeleteMapping; import org.hibernate.metamodel.spi.RuntimeModelCreationContext; import org.hibernate.persister.collection.mutation.DeleteRowsCoordinator; import org.hibernate.persister.collection.mutation.DeleteRowsCoordinatorNoOp; @@ -42,7 +46,11 @@ import org.hibernate.persister.collection.mutation.UpdateRowsCoordinatorNoOp; import org.hibernate.persister.collection.mutation.UpdateRowsCoordinatorStandard; import org.hibernate.persister.spi.PersisterCreationContext; import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.tree.expression.ColumnReference; import org.hibernate.sql.ast.tree.from.TableGroup; +import org.hibernate.sql.model.ast.ColumnValueBinding; +import org.hibernate.sql.model.ast.ColumnValueParameterList; +import org.hibernate.sql.model.ast.ColumnWriteFragment; import org.hibernate.sql.model.ast.MutatingTableReference; import org.hibernate.sql.model.ast.RestrictedTableMutation; import org.hibernate.sql.model.ast.TableInsert; @@ -50,8 +58,10 @@ import org.hibernate.sql.model.ast.TableMutation; import org.hibernate.sql.model.ast.builder.CollectionRowDeleteBuilder; import org.hibernate.sql.model.ast.builder.TableInsertBuilderStandard; import org.hibernate.sql.model.ast.builder.TableUpdateBuilderStandard; +import org.hibernate.sql.model.internal.TableUpdateStandard; import org.hibernate.sql.model.jdbc.JdbcMutationOperation; +import static org.hibernate.internal.util.collections.CollectionHelper.arrayList; import static org.hibernate.sql.model.ModelMutationLogging.MODEL_MUTATION_LOGGER; /** @@ -205,6 +215,51 @@ public class BasicCollectionPersister extends AbstractCollectionPersister { } + @Override + public RestrictedTableMutation generateDeleteAllAst(MutatingTableReference tableReference) { + assert getAttributeMapping() != null; + + final SoftDeleteMapping softDeleteMapping = getAttributeMapping().getSoftDeleteMapping(); + if ( softDeleteMapping == null ) { + return super.generateDeleteAllAst( tableReference ); + } + + final ForeignKeyDescriptor fkDescriptor = getAttributeMapping().getKeyDescriptor(); + assert fkDescriptor != null; + + final int keyColumnCount = fkDescriptor.getJdbcTypeCount(); + final ColumnValueParameterList parameterBinders = new ColumnValueParameterList( + tableReference, + ParameterUsage.RESTRICT, + keyColumnCount + ); + final java.util.List restrictionBindings = arrayList( keyColumnCount ); + applyKeyRestrictions( tableReference, parameterBinders, restrictionBindings ); + + final ColumnReference softDeleteColumn = new ColumnReference( tableReference, softDeleteMapping ); + final ColumnWriteFragment nonDeletedLiteral = new ColumnWriteFragment( + softDeleteMapping.getNonDeletedLiteralText(), + Collections.emptyList(), + softDeleteMapping.getJdbcMapping() + ); + final ColumnWriteFragment deletedLiteral = new ColumnWriteFragment( + softDeleteMapping.getDeletedLiteralText(), + Collections.emptyList(), + softDeleteMapping.getJdbcMapping() + ); + restrictionBindings.add( new ColumnValueBinding( softDeleteColumn, nonDeletedLiteral ) ); + final List valueBindings = List.of( new ColumnValueBinding( softDeleteColumn, deletedLiteral ) ); + + return new TableUpdateStandard( + tableReference, + this, + "soft-delete removal", + valueBindings, + restrictionBindings, + null + ); + } + protected RowMutationOperations buildRowMutationOperations() { final OperationProducer insertRowOperationProducer; final RowMutationOperations.Values insertRowValues; @@ -295,6 +350,11 @@ public class BasicCollectionPersister extends AbstractCollectionPersister { } attributeMapping.getElementDescriptor().forEachInsertable( insertBuilder ); + + final SoftDeleteMapping softDeleteMapping = getAttributeMapping().getSoftDeleteMapping(); + if ( softDeleteMapping != null ) { + insertBuilder.addValueColumn( softDeleteMapping ); + } } private JdbcMutationOperation buildGeneratedInsertRowOperation(MutatingTableReference tableReference) { @@ -597,6 +657,11 @@ public class BasicCollectionPersister extends AbstractCollectionPersister { final PluralAttributeMapping pluralAttribute = getAttributeMapping(); assert pluralAttribute != null; + final SoftDeleteMapping softDeleteMapping = pluralAttribute.getSoftDeleteMapping(); + if ( softDeleteMapping != null ) { + return generateSoftDeleteRowsAst( tableReference ); + } + final ForeignKeyDescriptor fkDescriptor = pluralAttribute.getKeyDescriptor(); assert fkDescriptor != null; @@ -627,6 +692,50 @@ public class BasicCollectionPersister extends AbstractCollectionPersister { return (RestrictedTableMutation) deleteBuilder.buildMutation(); } + protected RestrictedTableMutation generateSoftDeleteRowsAst(MutatingTableReference tableReference) { + final SoftDeleteMapping softDeleteMapping = getAttributeMapping().getSoftDeleteMapping(); + assert softDeleteMapping != null; + + final ForeignKeyDescriptor fkDescriptor = getAttributeMapping().getKeyDescriptor(); + assert fkDescriptor != null; + + final TableUpdateBuilderStandard updateBuilder = new TableUpdateBuilderStandard<>( + this, + tableReference, + getFactory(), + sqlWhereString + ); + + if ( getAttributeMapping().getIdentifierDescriptor() != null ) { + updateBuilder.addKeyRestrictionsLeniently( getAttributeMapping().getIdentifierDescriptor() ); + } + else { + updateBuilder.addKeyRestrictionsLeniently( getAttributeMapping().getKeyDescriptor().getKeyPart() ); + + if ( hasIndex() && !indexContainsFormula ) { + assert getAttributeMapping().getIndexDescriptor() != null; + updateBuilder.addKeyRestrictionsLeniently( getAttributeMapping().getIndexDescriptor() ); + } + else { + updateBuilder.addKeyRestrictions( getAttributeMapping().getElementDescriptor() ); + } + } + + updateBuilder.addLiteralRestriction( + softDeleteMapping.getColumnName(), + softDeleteMapping.getNonDeletedLiteralText(), + softDeleteMapping.getJdbcMapping() + ); + + updateBuilder.addValueColumn( + softDeleteMapping.getColumnName(), + softDeleteMapping.getDeletedLiteralText(), + softDeleteMapping.getJdbcMapping() + ); + + return updateBuilder.buildMutation(); + } + private void applyDeleteRowRestrictions( PersistentCollection collection, Object keyValue, diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java index d5bd3b593f..7103b8e1ca 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java @@ -46,6 +46,7 @@ import org.hibernate.Remove; import org.hibernate.StaleObjectStateException; import org.hibernate.StaleStateException; import org.hibernate.boot.Metadata; +import org.hibernate.boot.model.internal.SoftDeleteHelper; import org.hibernate.boot.model.relational.SqlStringGenerationContext; import org.hibernate.boot.spi.MetadataImplementor; import org.hibernate.boot.spi.SessionFactoryOptions; @@ -147,11 +148,13 @@ import org.hibernate.mapping.Formula; import org.hibernate.mapping.Join; import org.hibernate.mapping.PersistentClass; import org.hibernate.mapping.Property; +import org.hibernate.mapping.RootClass; import org.hibernate.mapping.Selectable; import org.hibernate.mapping.Subclass; import org.hibernate.mapping.Table; import org.hibernate.mapping.Value; import org.hibernate.metadata.ClassMetadata; +import org.hibernate.metamodel.UnsupportedMappingException; import org.hibernate.metamodel.mapping.Association; import org.hibernate.metamodel.mapping.AttributeMapping; import org.hibernate.metamodel.mapping.AttributeMappingsList; @@ -179,6 +182,8 @@ import org.hibernate.metamodel.mapping.PluralAttributeMapping; import org.hibernate.metamodel.mapping.SelectableConsumer; import org.hibernate.metamodel.mapping.SelectableMapping; import org.hibernate.metamodel.mapping.SingularAttributeMapping; +import org.hibernate.metamodel.mapping.SoftDeleteMapping; +import org.hibernate.metamodel.mapping.TableDetails; import org.hibernate.metamodel.mapping.VirtualModelPart; import org.hibernate.metamodel.mapping.internal.BasicEntityIdentifierMappingImpl; import org.hibernate.metamodel.mapping.internal.CompoundNaturalIdMapping; @@ -203,6 +208,8 @@ import org.hibernate.metamodel.spi.MappingMetamodelImplementor; import org.hibernate.metamodel.spi.RuntimeModelCreationContext; import org.hibernate.persister.collection.CollectionPersister; import org.hibernate.persister.entity.mutation.DeleteCoordinator; +import org.hibernate.persister.entity.mutation.DeleteCoordinatorSoft; +import org.hibernate.persister.entity.mutation.DeleteCoordinatorStandard; import org.hibernate.persister.entity.mutation.EntityMutationTarget; import org.hibernate.persister.entity.mutation.EntityTableMapping; import org.hibernate.persister.entity.mutation.InsertCoordinator; @@ -262,6 +269,7 @@ import org.hibernate.sql.exec.spi.JdbcParametersList; import org.hibernate.sql.model.MutationOperation; import org.hibernate.sql.model.MutationOperationGroup; import org.hibernate.sql.model.ast.builder.MutationGroupBuilder; +import org.hibernate.sql.model.ast.builder.TableInsertBuilder; import org.hibernate.sql.results.graph.DomainResult; import org.hibernate.sql.results.graph.DomainResultCreationState; import org.hibernate.sql.results.graph.Fetch; @@ -438,6 +446,7 @@ public abstract class AbstractEntityPersister private EntityVersionMapping versionMapping; private EntityRowIdMapping rowIdMapping; private EntityDiscriminatorMapping discriminatorMapping; + private SoftDeleteMapping softDeleteMapping; private AttributeMappingsList attributeMappings; protected AttributeMappingsMap declaredAttributeMappings = AttributeMappingsMap.builder().build(); @@ -3092,10 +3101,22 @@ public abstract class AbstractEntityPersister getFactory() ); - if ( additionalPredicateCollectorAccess != null && needsDiscriminator() ) { - final String alias = tableGroup.getPrimaryTableReference().getIdentificationVariable(); - final Predicate discriminatorPredicate = createDiscriminatorPredicate( alias, tableGroup, creationState ); - additionalPredicateCollectorAccess.get().accept( discriminatorPredicate ); + if ( additionalPredicateCollectorAccess != null ) { + if ( needsDiscriminator() ) { + final String alias = tableGroup.getPrimaryTableReference().getIdentificationVariable(); + final Predicate discriminatorPredicate = createDiscriminatorPredicate( alias, tableGroup, creationState ); + additionalPredicateCollectorAccess.get().accept( discriminatorPredicate ); + } + + if ( softDeleteMapping != null ) { + final TableReference tableReference = tableGroup.resolveTableReference( getSoftDeleteTableDetails().getTableName() ); + final Predicate softDeletePredicate = SoftDeleteHelper.createNonSoftDeletedRestriction( + tableReference, + softDeleteMapping, + creationState.getSqlExpressionResolver() + ); + additionalPredicateCollectorAccess.get().accept( softDeletePredicate ); + } } return tableGroup; @@ -3363,6 +3384,15 @@ public abstract class AbstractEntityPersister initPropertyPaths( mapping ); } + @Override + public void prepareLoaders() { + // Hibernate Reactive needs to override the loaders + singleIdLoader = buildSingleIdEntityLoader(); + multiIdLoader = buildMultiIdLoader(); + + lazyLoadPlanByFetchGroup = getLazyLoadPlanByFetchGroup(); + } + private void doLateInit() { if ( isIdentifierAssignedByInsert() ) { final OnExecutionGenerator generator = (OnExecutionGenerator) getGenerator(); @@ -3386,7 +3416,6 @@ public abstract class AbstractEntityPersister } //select SQL - lazyLoadPlanByFetchGroup = getLazyLoadPlanByFetchGroup(); sqlVersionSelectString = generateSelectVersionString(); logStaticSQL(); @@ -3617,12 +3646,24 @@ public abstract class AbstractEntityPersister } protected DeleteCoordinator buildDeleteCoordinator() { - return new DeleteCoordinator( this, factory ); + if ( softDeleteMapping == null ) { + return new DeleteCoordinatorStandard( this, factory ); + } + else { + return new DeleteCoordinatorSoft( this, factory ); + } } public void addDiscriminatorToInsertGroup(MutationGroupBuilder insertGroupBuilder) { } + public void addSoftDeleteToInsertGroup(MutationGroupBuilder insertGroupBuilder) { + if ( softDeleteMapping != null ) { + final TableInsertBuilder insertBuilder = insertGroupBuilder.getTableDetailsBuilder( getIdentifierTableName() ); + insertBuilder.addValueColumn( softDeleteMapping ); + } + } + protected String substituteBrackets(String sql) { return new SQLQueryParser( sql, null, getFactory() ).process(); } @@ -3630,9 +3671,6 @@ public abstract class AbstractEntityPersister @Override public final void postInstantiate() throws MappingException { doLateInit(); - // Hibernate Reactive needs to override the loaders - singleIdLoader = buildSingleIdEntityLoader(); - multiIdLoader = buildMultiIdLoader(); } /** @@ -4883,6 +4921,7 @@ public abstract class AbstractEntityPersister naturalIdMapping = superMappingType.getNaturalIdMapping(); versionMapping = superMappingType.getVersionMapping(); rowIdMapping = superMappingType.getRowIdMapping(); + softDeleteMapping = superMappingType.getSoftDeleteMapping(); } else { prepareMappingModel( creationProcess, bootEntityDescriptor ); @@ -5068,6 +5107,27 @@ public abstract class AbstractEntityPersister } discriminatorMapping = generateDiscriminatorMapping( bootEntityDescriptor, creationProcess ); + softDeleteMapping = resolveSoftDeleteMapping( this, bootEntityDescriptor, getIdentifierTableName(), creationProcess ); + + if ( softDeleteMapping != null ) { + if ( bootEntityDescriptor.getRootClass().getCustomSQLDelete() != null ) { + throw new UnsupportedMappingException( "Entity may not define both @SoftDelete and @SQLDelete" ); + } + } + } + + private static SoftDeleteMapping resolveSoftDeleteMapping( + AbstractEntityPersister persister, + PersistentClass bootEntityDescriptor, + String identifierTableName, + MappingModelCreationProcess creationProcess) { + final RootClass rootClass = bootEntityDescriptor.getRootClass(); + return SoftDeleteHelper.resolveSoftDeleteMapping( + persister, + rootClass, + identifierTableName, + creationProcess.getCreationContext().getJdbcServices().getDialect() + ); } private void postProcessAttributeMappings(MappingModelCreationProcess creationProcess, PersistentClass bootEntityDescriptor) { @@ -5795,6 +5855,16 @@ public abstract class AbstractEntityPersister return discriminatorMapping; } + @Override + public SoftDeleteMapping getSoftDeleteMapping() { + return softDeleteMapping; + } + + @Override + public TableDetails getSoftDeleteTableDetails() { + return getIdentifierTableDetails(); + } + @Override public AttributeMappingsList getAttributeMappings() { if ( attributeMappings == null ) { @@ -6307,7 +6377,7 @@ public abstract class AbstractEntityPersister /** * Generate the SQL that deletes a row by id (and version) * - * @deprecated No longer used. See {@link DeleteCoordinator} + * @deprecated No longer used. See {@link DeleteCoordinatorStandard} */ @Deprecated(forRemoval = true) @Remove diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/EntityPersister.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/EntityPersister.java index 72fd57cbf3..c01d6cade5 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/EntityPersister.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/EntityPersister.java @@ -42,6 +42,7 @@ import org.hibernate.metadata.ClassMetadata; import org.hibernate.metamodel.mapping.AttributeMapping; import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.mapping.internal.InFlightEntityMappingType; +import org.hibernate.metamodel.mapping.internal.MappingModelCreationProcess; import org.hibernate.metamodel.spi.EntityRepresentationStrategy; import org.hibernate.persister.walking.spi.AttributeSource; import org.hibernate.query.sqm.mutation.spi.SqmMultiTableInsertStrategy; @@ -119,6 +120,17 @@ public interface EntityPersister extends EntityMappingType, RootTableGroupProduc */ void postInstantiate() throws MappingException; + /** + * Prepare loaders associated with the persister. Distinct "phase" + * in building the persister after {@linkplain InFlightEntityMappingType#prepareMappingModel} + * and {@linkplain #postInstantiate()} have occurred. + *

+ * The distinct phase is used to ensure that all {@linkplain org.hibernate.metamodel.mapping.TableDetails} + * are available across the entire model + */ + default void prepareLoaders() { + } + /** * Return the {@link org.hibernate.SessionFactory} to which this persister * belongs. @@ -1110,5 +1122,4 @@ public interface EntityPersister extends EntityMappingType, RootTableGroupProduc */ @Incubating Iterable uniqueKeyEntries(); - } diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/AbstractDeleteCoordinator.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/AbstractDeleteCoordinator.java new file mode 100644 index 0000000000..0baee8c610 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/AbstractDeleteCoordinator.java @@ -0,0 +1,326 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.persister.entity.mutation; + +import org.hibernate.engine.OptimisticLockStyle; +import org.hibernate.engine.jdbc.batch.internal.BasicBatchKey; +import org.hibernate.engine.jdbc.mutation.JdbcValueBindings; +import org.hibernate.engine.jdbc.mutation.MutationExecutor; +import org.hibernate.engine.jdbc.mutation.ParameterUsage; +import org.hibernate.engine.jdbc.mutation.group.PreparedStatementDetails; +import org.hibernate.engine.spi.EntityEntry; +import org.hibernate.engine.spi.PersistenceContext; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.metamodel.mapping.AttributeMapping; +import org.hibernate.metamodel.mapping.EntityRowIdMapping; +import org.hibernate.metamodel.mapping.EntityVersionMapping; +import org.hibernate.persister.entity.AbstractEntityPersister; +import org.hibernate.sql.model.MutationOperation; +import org.hibernate.sql.model.MutationOperationGroup; + +import static org.hibernate.engine.jdbc.mutation.internal.ModelMutationHelper.identifiedResultsCheck; + +/** + * Template support for DeleteCoordinator implementations. Mainly + * centers around delegation via {@linkplain #generateOperationGroup}. + * + * @author Steve Ebersole + */ +public abstract class AbstractDeleteCoordinator + extends AbstractMutationCoordinator + implements DeleteCoordinator { + private final BasicBatchKey batchKey; + private final MutationOperationGroup staticOperationGroup; + + private MutationOperationGroup noVersionDeleteGroup; + + public AbstractDeleteCoordinator( + AbstractEntityPersister entityPersister, + SessionFactoryImplementor factory) { + super( entityPersister, factory ); + + this.batchKey = new BasicBatchKey( entityPersister.getEntityName() + "#DELETE" ); + this.staticOperationGroup = generateOperationGroup( "", null, true, null ); + if ( !entityPersister.isVersioned() ) { + noVersionDeleteGroup = staticOperationGroup; + } + } + + @Override + public MutationOperationGroup getStaticDeleteGroup() { + return staticOperationGroup; + } + + @Override + public BasicBatchKey getBatchKey() { + return batchKey; + } + + protected abstract MutationOperationGroup generateOperationGroup( + Object rowId, + Object[] loadedState, + boolean applyVersion, + SharedSessionContractImplementor session); + + @Override + public void coordinateDelete( + Object entity, + Object id, + Object version, + SharedSessionContractImplementor session) { + boolean isImpliedOptimisticLocking = entityPersister().optimisticLockStyle().isAllOrDirty(); + + final PersistenceContext persistenceContext = session.getPersistenceContextInternal(); + final EntityEntry entry = persistenceContext.getEntry( entity ); + final Object[] loadedState = entry != null && isImpliedOptimisticLocking ? entry.getLoadedState() : null; + final Object rowId = entry != null ? entry.getRowId() : null; + + if ( ( isImpliedOptimisticLocking && loadedState != null ) || ( rowId == null && entityPersister().hasRowId() ) ) { + doDynamicDelete( entity, id, rowId, loadedState, session ); + } + else { + doStaticDelete( entity, id, rowId, entry == null ? null : entry.getLoadedState(), version, session ); + } + } + + protected void doDynamicDelete( + Object entity, + Object id, + Object rowId, + Object[] loadedState, + SharedSessionContractImplementor session) { + final MutationOperationGroup operationGroup = generateOperationGroup( null, loadedState, true, session ); + final MutationExecutor mutationExecutor = executor( session, operationGroup ); + + for ( int i = 0; i < operationGroup.getNumberOfOperations(); i++ ) { + final MutationOperation mutation = operationGroup.getOperation( i ); + if ( mutation != null ) { + final String tableName = mutation.getTableDetails().getTableName(); + mutationExecutor.getPreparedStatementDetails( tableName ); + } + } + + applyDynamicDeleteTableDetails( + id, + rowId, + loadedState, + mutationExecutor, + operationGroup, + session + ); + + try { + mutationExecutor.execute( + entity, + null, + null, + (statementDetails, affectedRowCount, batchPosition) -> identifiedResultsCheck( + statementDetails, + affectedRowCount, + batchPosition, + entityPersister(), + id, + factory() + ), + session + ); + } + finally { + mutationExecutor.release(); + } + } + + protected void applyDynamicDeleteTableDetails( + Object id, + Object rowId, + Object[] loadedState, + MutationExecutor mutationExecutor, + MutationOperationGroup operationGroup, + SharedSessionContractImplementor session) { + applyLocking( null, loadedState, mutationExecutor, session ); + applyId( id, null, mutationExecutor, operationGroup, session ); + } + + protected void applyLocking( + Object version, + Object[] loadedState, + MutationExecutor mutationExecutor, + SharedSessionContractImplementor session) { + final JdbcValueBindings jdbcValueBindings = mutationExecutor.getJdbcValueBindings(); + final OptimisticLockStyle optimisticLockStyle = entityPersister().optimisticLockStyle(); + switch ( optimisticLockStyle ) { + case VERSION: + applyVersionLocking( version, jdbcValueBindings ); + break; + case ALL: + case DIRTY: + applyAllOrDirtyLocking( loadedState, session, jdbcValueBindings ); + break; + } + } + + private void applyAllOrDirtyLocking( + Object[] loadedState, + SharedSessionContractImplementor session, + JdbcValueBindings jdbcValueBindings) { + if ( loadedState != null ) { + final AbstractEntityPersister persister = entityPersister(); + final boolean[] versionability = persister.getPropertyVersionability(); + for ( int attributeIndex = 0; attributeIndex < versionability.length; attributeIndex++ ) { + final AttributeMapping attribute; + // only makes sense to lock on singular attributes which are not excluded from optimistic locking + if ( versionability[attributeIndex] && !( attribute = persister.getAttributeMapping( attributeIndex ) ).isPluralAttributeMapping() ) { + final Object loadedValue = loadedState[attributeIndex]; + if ( loadedValue != null ) { + final String mutationTableName = persister.getAttributeMutationTableName( attributeIndex ); + attribute.breakDownJdbcValues( + loadedValue, + 0, + jdbcValueBindings, + mutationTableName, + (valueIndex, bindings, tableName, jdbcValue, jdbcValueMapping) -> { + if ( jdbcValue == null ) { + // presumably the SQL was generated with `is null` + return; + } + bindings.bindValue( + jdbcValue, + tableName, + jdbcValueMapping.getSelectionExpression(), + ParameterUsage.RESTRICT + ); + }, + session + ); + } + } + } + } + } + + private void applyVersionLocking( + Object version, + JdbcValueBindings jdbcValueBindings) { + final AbstractEntityPersister persister = entityPersister(); + final EntityVersionMapping versionMapping = persister.getVersionMapping(); + if ( version != null && versionMapping != null ) { + jdbcValueBindings.bindValue( + version, + persister.physicalTableNameForMutation( versionMapping ), + versionMapping.getSelectionExpression(), + ParameterUsage.RESTRICT + ); + } + } + + protected void applyId( + Object id, + Object rowId, + MutationExecutor mutationExecutor, + MutationOperationGroup operationGroup, + SharedSessionContractImplementor session) { + final JdbcValueBindings jdbcValueBindings = mutationExecutor.getJdbcValueBindings(); + final EntityRowIdMapping rowIdMapping = entityPersister().getRowIdMapping(); + + for ( int position = 0; position < operationGroup.getNumberOfOperations(); position++ ) { + final MutationOperation jdbcMutation = operationGroup.getOperation( position ); + final EntityTableMapping tableDetails = (EntityTableMapping) jdbcMutation.getTableDetails(); + breakDownKeyJdbcValues( id, rowId, session, jdbcValueBindings, tableDetails ); + final PreparedStatementDetails statementDetails = mutationExecutor.getPreparedStatementDetails( tableDetails.getTableName() ); + if ( statementDetails != null ) { + // force creation of the PreparedStatement + //noinspection resource + statementDetails.resolveStatement(); + } + } + } + + protected void doStaticDelete( + Object entity, + Object id, + Object rowId, + Object[] loadedState, + Object version, + SharedSessionContractImplementor session) { + final boolean applyVersion; + final MutationOperationGroup operationGroupToUse; + if ( entity == null ) { + applyVersion = false; + operationGroupToUse = resolveNoVersionDeleteGroup( session ); + } + else { + applyVersion = true; + operationGroupToUse = staticOperationGroup; + } + + final MutationExecutor mutationExecutor = executor( session, operationGroupToUse ); + for ( int position = 0; position < staticOperationGroup.getNumberOfOperations(); position++ ) { + final MutationOperation mutation = staticOperationGroup.getOperation( position ); + if ( mutation != null ) { + mutationExecutor.getPreparedStatementDetails( mutation.getTableDetails().getTableName() ); + } + } + + applyStaticDeleteTableDetails( + id, + rowId, + loadedState, + version, + applyVersion, + mutationExecutor, + session + ); + + mutationExecutor.execute( + entity, + null, + null, + (statementDetails, affectedRowCount, batchPosition) -> identifiedResultsCheck( + statementDetails, + affectedRowCount, + batchPosition, + entityPersister(), + id, + factory() + ), + session + ); + + mutationExecutor.release(); + } + + protected void applyStaticDeleteTableDetails( + Object id, + Object rowId, + Object[] loadedState, + Object version, + boolean applyVersion, + MutationExecutor mutationExecutor, + SharedSessionContractImplementor session) { + if ( applyVersion ) { + applyLocking( version, null, mutationExecutor, session ); + } + + final JdbcValueBindings jdbcValueBindings = mutationExecutor.getJdbcValueBindings(); + bindPartitionColumnValueBindings( loadedState, session, jdbcValueBindings ); + + applyId( id, rowId, mutationExecutor, staticOperationGroup, session ); + } + + private MutationExecutor executor(SharedSessionContractImplementor session, MutationOperationGroup group) { + return mutationExecutorService.createExecutor( resolveBatchKeyAccess( false, session ), group, session ); + } + + protected MutationOperationGroup resolveNoVersionDeleteGroup(SharedSessionContractImplementor session) { + if ( noVersionDeleteGroup == null ) { + noVersionDeleteGroup = generateOperationGroup( "", null, false, session ); + } + + return noVersionDeleteGroup; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/DeleteCoordinator.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/DeleteCoordinator.java index 487a4d4ffb..72b1678030 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/DeleteCoordinator.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/DeleteCoordinator.java @@ -6,32 +6,8 @@ */ package org.hibernate.persister.entity.mutation; -import org.hibernate.engine.OptimisticLockStyle; -import org.hibernate.engine.jdbc.batch.internal.BasicBatchKey; -import org.hibernate.engine.jdbc.mutation.JdbcValueBindings; -import org.hibernate.engine.jdbc.mutation.MutationExecutor; -import org.hibernate.engine.jdbc.mutation.ParameterUsage; -import org.hibernate.engine.jdbc.mutation.group.PreparedStatementDetails; -import org.hibernate.engine.spi.EntityEntry; -import org.hibernate.engine.spi.PersistenceContext; -import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SharedSessionContractImplementor; -import org.hibernate.metamodel.mapping.AttributeMapping; -import org.hibernate.metamodel.mapping.AttributeMappingsList; -import org.hibernate.metamodel.mapping.EntityVersionMapping; -import org.hibernate.metamodel.mapping.SelectableMapping; -import org.hibernate.persister.entity.AbstractEntityPersister; -import org.hibernate.sql.model.MutationOperation; import org.hibernate.sql.model.MutationOperationGroup; -import org.hibernate.sql.model.MutationType; -import org.hibernate.sql.model.ast.ColumnValueBindingList; -import org.hibernate.sql.model.ast.builder.MutationGroupBuilder; -import org.hibernate.sql.model.ast.builder.RestrictedTableMutationBuilder; -import org.hibernate.sql.model.ast.builder.TableDeleteBuilder; -import org.hibernate.sql.model.ast.builder.TableDeleteBuilderSkipped; -import org.hibernate.sql.model.ast.builder.TableDeleteBuilderStandard; - -import static org.hibernate.engine.jdbc.mutation.internal.ModelMutationHelper.identifiedResultsCheck; /** * Coordinates the deleting of an entity. @@ -40,389 +16,19 @@ import static org.hibernate.engine.jdbc.mutation.internal.ModelMutationHelper.id * * @author Steve Ebersole */ -public class DeleteCoordinator extends AbstractMutationCoordinator { - private final MutationOperationGroup staticOperationGroup; - private final BasicBatchKey batchKey; +public interface DeleteCoordinator { + /** + * The operation group used to perform the deletion unless some form + * of dynamic delete is necessary + */ + MutationOperationGroup getStaticDeleteGroup(); - private MutationOperationGroup noVersionDeleteGroup; - - public DeleteCoordinator(AbstractEntityPersister entityPersister, SessionFactoryImplementor factory) { - super( entityPersister, factory ); - - this.staticOperationGroup = generateOperationGroup( "", null, true, null ); - this.batchKey = new BasicBatchKey( entityPersister.getEntityName() + "#DELETE" ); - - if ( !entityPersister.isVersioned() ) { - noVersionDeleteGroup = staticOperationGroup; - } - } - - public MutationOperationGroup getStaticDeleteGroup() { - return staticOperationGroup; - } - - @SuppressWarnings("unused") - public BasicBatchKey getBatchKey() { - return batchKey; - } - - public void coordinateDelete( + /** + * Perform the deletions + */ + void coordinateDelete( Object entity, Object id, Object version, - SharedSessionContractImplementor session) { - - boolean isImpliedOptimisticLocking = entityPersister().optimisticLockStyle().isAllOrDirty(); - - final PersistenceContext persistenceContext = session.getPersistenceContextInternal(); - final EntityEntry entry = persistenceContext.getEntry( entity ); - final Object[] loadedState = entry != null ? entry.getLoadedState() : null; - final Object rowId = entry != null ? entry.getRowId() : null; - - if ( ( isImpliedOptimisticLocking && loadedState != null ) || ( rowId == null && entityPersister().hasRowId() ) ) { - doDynamicDelete( entity, id, loadedState, session ); - } - else { - doStaticDelete( entity, id, rowId, loadedState, version, session ); - } - } - - protected void doDynamicDelete( - Object entity, - Object id, - Object[] loadedState, - SharedSessionContractImplementor session) { - final MutationOperationGroup operationGroup = generateOperationGroup( null, loadedState, true, session ); - - final MutationExecutor mutationExecutor = executor( session, operationGroup ); - - for ( int i = 0; i < operationGroup.getNumberOfOperations(); i++ ) { - final MutationOperation mutation = operationGroup.getOperation( i ); - if ( mutation != null ) { - final String tableName = mutation.getTableDetails().getTableName(); - mutationExecutor.getPreparedStatementDetails( tableName ); - } - } - - applyLocking( null, loadedState, mutationExecutor, session ); - - applyId( id, null, mutationExecutor, operationGroup, session ); - - try { - mutationExecutor.execute( - entity, - null, - null, - (statementDetails, affectedRowCount, batchPosition) -> identifiedResultsCheck( - statementDetails, - affectedRowCount, - batchPosition, - entityPersister(), - id, - factory() - ), - session - ); - } - finally { - mutationExecutor.release(); - } - } - - private MutationExecutor executor(SharedSessionContractImplementor session, MutationOperationGroup group) { - return mutationExecutorService - .createExecutor( resolveBatchKeyAccess( false, session ), group, session ); - } - - protected void applyLocking( - Object version, - Object[] loadedState, - MutationExecutor mutationExecutor, - SharedSessionContractImplementor session) { - final JdbcValueBindings jdbcValueBindings = mutationExecutor.getJdbcValueBindings(); - final OptimisticLockStyle optimisticLockStyle = entityPersister().optimisticLockStyle(); - switch ( optimisticLockStyle ) { - case VERSION: - applyVersionLocking( version, session, jdbcValueBindings ); - break; - case ALL: - case DIRTY: - applyAllOrDirtyLocking( loadedState, session, jdbcValueBindings ); - break; - } - } - - private void applyAllOrDirtyLocking( - Object[] loadedState, - SharedSessionContractImplementor session, - JdbcValueBindings jdbcValueBindings) { - if ( loadedState != null ) { - final AbstractEntityPersister persister = entityPersister(); - final boolean[] versionability = persister.getPropertyVersionability(); - for ( int attributeIndex = 0; attributeIndex < versionability.length; attributeIndex++ ) { - final AttributeMapping attribute; - // only makes sense to lock on singular attributes which are not excluded from optimistic locking - if ( versionability[attributeIndex] && !( attribute = persister.getAttributeMapping( attributeIndex ) ).isPluralAttributeMapping() ) { - final Object loadedValue = loadedState[attributeIndex]; - if ( loadedValue != null ) { - final String mutationTableName = persister.getAttributeMutationTableName( attributeIndex ); - attribute.breakDownJdbcValues( - loadedValue, - 0, - jdbcValueBindings, - mutationTableName, - (valueIndex, bindings, tableName, jdbcValue, jdbcValueMapping) -> { - if ( jdbcValue == null ) { - // presumably the SQL was generated with `is null` - return; - } - bindings.bindValue( - jdbcValue, - tableName, - jdbcValueMapping.getSelectionExpression(), - ParameterUsage.RESTRICT - ); - }, - session - ); - } - } - } - } - } - - private void applyVersionLocking( - Object version, - SharedSessionContractImplementor session, - JdbcValueBindings jdbcValueBindings) { - final AbstractEntityPersister persister = entityPersister(); - final EntityVersionMapping versionMapping = persister.getVersionMapping(); - if ( version != null && versionMapping != null ) { - jdbcValueBindings.bindValue( - version, - persister.physicalTableNameForMutation( versionMapping ), - versionMapping.getSelectionExpression(), - ParameterUsage.RESTRICT - ); - } - } - - protected void applyId( - Object id, - Object rowId, - MutationExecutor mutationExecutor, - MutationOperationGroup operationGroup, - SharedSessionContractImplementor session) { - final JdbcValueBindings jdbcValueBindings = mutationExecutor.getJdbcValueBindings(); - for ( int position = 0; position < operationGroup.getNumberOfOperations(); position++ ) { - final MutationOperation jdbcMutation = operationGroup.getOperation( position ); - final EntityTableMapping tableDetails = (EntityTableMapping) jdbcMutation.getTableDetails(); - breakDownKeyJdbcValues( id, rowId, session, jdbcValueBindings, tableDetails ); - final PreparedStatementDetails statementDetails = mutationExecutor.getPreparedStatementDetails( tableDetails.getTableName() ); - if ( statementDetails != null ) { - // force creation of the PreparedStatement - //noinspection resource - statementDetails.resolveStatement(); - } - } - } - - protected void doStaticDelete( - Object entity, - Object id, - Object rowId, - Object[] loadedState, - Object version, - SharedSessionContractImplementor session) { - - final boolean applyVersion; - final MutationOperationGroup operationGroupToUse; - if ( entity == null ) { - applyVersion = false; - operationGroupToUse = resolveNoVersionDeleteGroup( session ); - } - else { - applyVersion = true; - operationGroupToUse = staticOperationGroup; - } - - final MutationExecutor mutationExecutor = executor( session, operationGroupToUse ); - - for ( int position = 0; position < staticOperationGroup.getNumberOfOperations(); position++ ) { - final MutationOperation mutation = staticOperationGroup.getOperation( position ); - if ( mutation != null ) { - mutationExecutor.getPreparedStatementDetails( mutation.getTableDetails().getTableName() ); - } - } - - if ( applyVersion ) { - applyLocking( version, null, mutationExecutor, session ); - } - final JdbcValueBindings jdbcValueBindings = mutationExecutor.getJdbcValueBindings(); - - bindPartitionColumnValueBindings( loadedState, session, jdbcValueBindings ); - - applyId( id, rowId, mutationExecutor, staticOperationGroup, session ); - - mutationExecutor.execute( - entity, - null, - null, - (statementDetails, affectedRowCount, batchPosition) -> identifiedResultsCheck( - statementDetails, - affectedRowCount, - batchPosition, - entityPersister(), - id, - factory() - ), - session - ); - - mutationExecutor.release(); - } - - protected MutationOperationGroup resolveNoVersionDeleteGroup(SharedSessionContractImplementor session) { - if ( noVersionDeleteGroup == null ) { - noVersionDeleteGroup = generateOperationGroup( "", null, false, session ); - } - - return noVersionDeleteGroup; - } - - protected MutationOperationGroup generateOperationGroup( - Object rowId, - Object[] loadedState, - boolean applyVersion, - SharedSessionContractImplementor session) { - final MutationGroupBuilder deleteGroupBuilder = new MutationGroupBuilder( MutationType.DELETE, entityPersister() ); - - entityPersister().forEachMutableTableReverse( (tableMapping) -> { - final TableDeleteBuilder tableDeleteBuilder = tableMapping.isCascadeDeleteEnabled() - ? new TableDeleteBuilderSkipped( tableMapping ) - : new TableDeleteBuilderStandard( entityPersister(), tableMapping, factory() ); - deleteGroupBuilder.addTableDetailsBuilder( tableDeleteBuilder ); - } ); - - applyTableDeleteDetails( deleteGroupBuilder, rowId, loadedState, applyVersion, session ); - - return createOperationGroup( null, deleteGroupBuilder.buildMutationGroup() ); - } - - private void applyTableDeleteDetails( - MutationGroupBuilder deleteGroupBuilder, - Object rowId, - Object[] loadedState, - boolean applyVersion, - SharedSessionContractImplementor session) { - // first, the table key column(s) - deleteGroupBuilder.forEachTableMutationBuilder( (builder) -> { - final EntityTableMapping tableMapping = (EntityTableMapping) builder.getMutatingTable().getTableMapping(); - final TableDeleteBuilder tableDeleteBuilder = (TableDeleteBuilder) builder; - applyKeyRestriction( rowId, entityPersister(), tableDeleteBuilder, tableMapping ); - } ); - - if ( applyVersion ) { - // apply any optimistic locking - applyOptimisticLocking( deleteGroupBuilder, loadedState, session ); - final AbstractEntityPersister persister = entityPersister(); - if ( persister.hasPartitionedSelectionMapping() ) { - final AttributeMappingsList attributeMappings = persister.getAttributeMappings(); - for ( int m = 0; m < attributeMappings.size(); m++ ) { - final AttributeMapping attributeMapping = attributeMappings.get( m ); - final int jdbcTypeCount = attributeMapping.getJdbcTypeCount(); - for ( int i = 0; i < jdbcTypeCount; i++ ) { - final SelectableMapping selectableMapping = attributeMapping.getSelectable( i ); - if ( selectableMapping.isPartitioned() ) { - final String tableNameForMutation = - persister.physicalTableNameForMutation( selectableMapping ); - final RestrictedTableMutationBuilder rootTableMutationBuilder = - deleteGroupBuilder.findTableDetailsBuilder( tableNameForMutation ); - rootTableMutationBuilder.addKeyRestrictionLeniently( selectableMapping ); - } - } - } - } - } - // todo (6.2) : apply where + where-fragments - } - - protected void applyOptimisticLocking( - MutationGroupBuilder mutationGroupBuilder, - Object[] loadedState, - SharedSessionContractImplementor session) { - final OptimisticLockStyle optimisticLockStyle = entityPersister().optimisticLockStyle(); - if ( optimisticLockStyle.isVersion() && entityPersister().getVersionMapping() != null ) { - applyVersionBasedOptLocking( mutationGroupBuilder ); - } - else if ( loadedState != null && entityPersister().optimisticLockStyle().isAllOrDirty() ) { - applyNonVersionOptLocking( - optimisticLockStyle, - mutationGroupBuilder, - loadedState, - session - ); - } - } - - protected void applyVersionBasedOptLocking(MutationGroupBuilder mutationGroupBuilder) { - assert entityPersister().optimisticLockStyle() == OptimisticLockStyle.VERSION; - assert entityPersister().getVersionMapping() != null; - - final String tableNameForMutation = entityPersister().physicalTableNameForMutation( entityPersister().getVersionMapping() ); - final RestrictedTableMutationBuilder rootTableMutationBuilder = mutationGroupBuilder.findTableDetailsBuilder( tableNameForMutation ); - rootTableMutationBuilder.addOptimisticLockRestriction( entityPersister().getVersionMapping() ); - } - - protected void applyNonVersionOptLocking( - OptimisticLockStyle lockStyle, - MutationGroupBuilder mutationGroupBuilder, - Object[] loadedState, - SharedSessionContractImplementor session) { - final AbstractEntityPersister persister = entityPersister(); - assert loadedState != null; - assert lockStyle.isAllOrDirty(); - assert persister.optimisticLockStyle().isAllOrDirty(); - assert session != null; - - final boolean[] versionability = persister.getPropertyVersionability(); - for ( int attributeIndex = 0; attributeIndex < versionability.length; attributeIndex++ ) { - final AttributeMapping attribute; - // only makes sense to lock on singular attributes which are not excluded from optimistic locking - if ( versionability[attributeIndex] && !( attribute = persister.getAttributeMapping( attributeIndex ) ).isPluralAttributeMapping() ) { - breakDownJdbcValues( mutationGroupBuilder, session, attribute, loadedState[attributeIndex] ); - } - } - } - - private void breakDownJdbcValues( - MutationGroupBuilder mutationGroupBuilder, - SharedSessionContractImplementor session, - AttributeMapping attribute, - Object loadedValue) { - final RestrictedTableMutationBuilder tableMutationBuilder = - mutationGroupBuilder.findTableDetailsBuilder( attribute.getContainingTableExpression() ); - if ( tableMutationBuilder != null ) { - final ColumnValueBindingList optimisticLockBindings = tableMutationBuilder.getOptimisticLockBindings(); - if ( optimisticLockBindings != null ) { - attribute.breakDownJdbcValues( - loadedValue, - (valueIndex, value, jdbcValueMapping) -> { - if ( !tableMutationBuilder.getKeyRestrictionBindings() - .containsColumn( - jdbcValueMapping.getSelectableName(), - jdbcValueMapping.getJdbcMapping() - ) ) { - optimisticLockBindings.consume( valueIndex, value, jdbcValueMapping ); - } - } - , - session - ); - } - } - // else there is no actual delete statement for that table, - // generally indicates we have an on-delete=cascade situation - } - + SharedSessionContractImplementor session); } diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/DeleteCoordinatorSoft.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/DeleteCoordinatorSoft.java new file mode 100644 index 0000000000..31f265fd5e --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/DeleteCoordinatorSoft.java @@ -0,0 +1,175 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.persister.entity.mutation; + +import org.hibernate.engine.OptimisticLockStyle; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.metamodel.mapping.AttributeMapping; +import org.hibernate.metamodel.mapping.AttributeMappingsList; +import org.hibernate.metamodel.mapping.SelectableMapping; +import org.hibernate.metamodel.mapping.SoftDeleteMapping; +import org.hibernate.persister.entity.AbstractEntityPersister; +import org.hibernate.sql.model.MutationOperation; +import org.hibernate.sql.model.MutationOperationGroup; +import org.hibernate.sql.model.MutationType; +import org.hibernate.sql.model.ast.ColumnValueBindingList; +import org.hibernate.sql.model.ast.RestrictedTableMutation; +import org.hibernate.sql.model.ast.builder.TableUpdateBuilder; +import org.hibernate.sql.model.ast.builder.TableUpdateBuilderStandard; +import org.hibernate.sql.model.internal.MutationGroupSingle; +import org.hibernate.sql.model.internal.MutationOperationGroupFactory; + +/** + * DeleteCoordinator for soft-deletes + * + * @author Steve Ebersole + */ +public class DeleteCoordinatorSoft extends AbstractDeleteCoordinator { + public DeleteCoordinatorSoft(AbstractEntityPersister entityPersister, SessionFactoryImplementor factory) { + super( entityPersister, factory ); + } + + @Override + protected MutationOperationGroup generateOperationGroup( + Object rowId, + Object[] loadedState, + boolean applyVersion, + SharedSessionContractImplementor session) { + final EntityTableMapping rootTableMapping = entityPersister().getIdentifierTableMapping(); + final TableUpdateBuilderStandard tableUpdateBuilder = new TableUpdateBuilderStandard<>( + entityPersister(), + rootTableMapping, + factory() + ); + + applyKeyRestriction( rowId, entityPersister(), tableUpdateBuilder, rootTableMapping ); + applySoftDelete( entityPersister().getSoftDeleteMapping(), tableUpdateBuilder ); + applyPartitionKeyRestriction( tableUpdateBuilder ); + applyOptimisticLocking( tableUpdateBuilder, loadedState, session ); + + final RestrictedTableMutation tableMutation = tableUpdateBuilder.buildMutation(); + final MutationGroupSingle mutationGroup = new MutationGroupSingle( + MutationType.DELETE, + entityPersister(), + tableMutation + ); + + final MutationOperation mutationOperation = tableMutation.createMutationOperation( null, factory() ); + return MutationOperationGroupFactory.singleOperation( mutationGroup, mutationOperation ); + } + + private void applyPartitionKeyRestriction(TableUpdateBuilder tableUpdateBuilder) { + final AbstractEntityPersister persister = entityPersister(); + if ( persister.hasPartitionedSelectionMapping() ) { + final AttributeMappingsList attributeMappings = persister.getAttributeMappings(); + for ( int m = 0; m < attributeMappings.size(); m++ ) { + final AttributeMapping attributeMapping = attributeMappings.get( m ); + final int jdbcTypeCount = attributeMapping.getJdbcTypeCount(); + for ( int i = 0; i < jdbcTypeCount; i++ ) { + final SelectableMapping selectableMapping = attributeMapping.getSelectable( i ); + if ( selectableMapping.isPartitioned() ) { + tableUpdateBuilder.addKeyRestrictionLeniently( selectableMapping ); + } + } + } + } + } + + private void applySoftDelete( + SoftDeleteMapping softDeleteMapping, + TableUpdateBuilderStandard tableUpdateBuilder) { + tableUpdateBuilder.addLiteralRestriction( + softDeleteMapping.getSelectionExpression(), + softDeleteMapping.getNonDeletedLiteralText(), + softDeleteMapping.getJdbcMapping() + ); + tableUpdateBuilder.addValueColumn( + softDeleteMapping.getSelectionExpression(), + softDeleteMapping.getDeletedLiteralText(), + softDeleteMapping.getJdbcMapping() + ); + } + + protected void applyOptimisticLocking( + TableUpdateBuilderStandard tableUpdateBuilder, + Object[] loadedState, + SharedSessionContractImplementor session) { + final OptimisticLockStyle optimisticLockStyle = entityPersister().optimisticLockStyle(); + if ( optimisticLockStyle.isVersion() && entityPersister().getVersionMapping() != null ) { + applyVersionBasedOptLocking( tableUpdateBuilder ); + } + else if ( loadedState != null && entityPersister().optimisticLockStyle().isAllOrDirty() ) { + applyNonVersionOptLocking( + optimisticLockStyle, + tableUpdateBuilder, + loadedState, + session + ); + } + } + + protected void applyVersionBasedOptLocking(TableUpdateBuilderStandard tableUpdateBuilder) { + assert entityPersister().optimisticLockStyle() == OptimisticLockStyle.VERSION; + assert entityPersister().getVersionMapping() != null; + + tableUpdateBuilder.addOptimisticLockRestriction( entityPersister().getVersionMapping() ); + } + + protected void applyNonVersionOptLocking( + OptimisticLockStyle lockStyle, + TableUpdateBuilderStandard tableUpdateBuilder, + Object[] loadedState, + SharedSessionContractImplementor session) { + final AbstractEntityPersister persister = entityPersister(); + assert loadedState != null; + assert lockStyle.isAllOrDirty(); + assert persister.optimisticLockStyle().isAllOrDirty(); + assert session != null; + + final boolean[] versionability = persister.getPropertyVersionability(); + for ( int attributeIndex = 0; attributeIndex < versionability.length; attributeIndex++ ) { + final AttributeMapping attribute; + // only makes sense to lock on singular attributes which are not excluded from optimistic locking + if ( versionability[attributeIndex] + && !( attribute = persister.getAttributeMapping( attributeIndex ) ).isPluralAttributeMapping() ) { + breakDownJdbcValues( tableUpdateBuilder, session, attribute, loadedState[attributeIndex] ); + } + } + } + + private void breakDownJdbcValues( + TableUpdateBuilderStandard tableUpdateBuilder, + SharedSessionContractImplementor session, + AttributeMapping attribute, + Object loadedValue) { + if ( !tableUpdateBuilder.getMutatingTable() + .getTableName() + .equals( attribute.getContainingTableExpression() ) ) { + // it is not on the root table, skip it + return; + } + + final ColumnValueBindingList optimisticLockBindings = tableUpdateBuilder.getOptimisticLockBindings(); + if ( optimisticLockBindings != null ) { + attribute.breakDownJdbcValues( + loadedValue, + (valueIndex, value, jdbcValueMapping) -> { + if ( !tableUpdateBuilder.getKeyRestrictionBindings() + .containsColumn( + jdbcValueMapping.getSelectableName(), + jdbcValueMapping.getJdbcMapping() + ) ) { + optimisticLockBindings.consume( valueIndex, value, jdbcValueMapping ); + } + } + , + session + ); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/DeleteCoordinatorStandard.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/DeleteCoordinatorStandard.java new file mode 100644 index 0000000000..8154c0330c --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/DeleteCoordinatorStandard.java @@ -0,0 +1,169 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.persister.entity.mutation; + +import org.hibernate.engine.OptimisticLockStyle; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.metamodel.mapping.AttributeMapping; +import org.hibernate.metamodel.mapping.AttributeMappingsList; +import org.hibernate.metamodel.mapping.SelectableMapping; +import org.hibernate.persister.entity.AbstractEntityPersister; +import org.hibernate.sql.model.MutationOperationGroup; +import org.hibernate.sql.model.MutationType; +import org.hibernate.sql.model.ast.ColumnValueBindingList; +import org.hibernate.sql.model.ast.builder.MutationGroupBuilder; +import org.hibernate.sql.model.ast.builder.RestrictedTableMutationBuilder; +import org.hibernate.sql.model.ast.builder.TableDeleteBuilder; +import org.hibernate.sql.model.ast.builder.TableDeleteBuilderSkipped; +import org.hibernate.sql.model.ast.builder.TableDeleteBuilderStandard; + +/** + * Coordinates standard deleting of an entity. + * + * @author Steve Ebersole + */ +public class DeleteCoordinatorStandard extends AbstractDeleteCoordinator { + + public DeleteCoordinatorStandard(AbstractEntityPersister entityPersister, SessionFactoryImplementor factory) { + super( entityPersister, factory ); + } + + @Override + protected MutationOperationGroup generateOperationGroup( + Object rowId, + Object[] loadedState, + boolean applyVersion, + SharedSessionContractImplementor session) { + final MutationGroupBuilder deleteGroupBuilder = new MutationGroupBuilder( MutationType.DELETE, entityPersister() ); + + entityPersister().forEachMutableTableReverse( (tableMapping) -> { + final TableDeleteBuilder tableDeleteBuilder = tableMapping.isCascadeDeleteEnabled() + ? new TableDeleteBuilderSkipped( tableMapping ) + : new TableDeleteBuilderStandard( entityPersister(), tableMapping, factory() ); + deleteGroupBuilder.addTableDetailsBuilder( tableDeleteBuilder ); + } ); + + applyTableDeleteDetails( deleteGroupBuilder, rowId, loadedState, applyVersion, session ); + + return createOperationGroup( null, deleteGroupBuilder.buildMutationGroup() ); + } + + private void applyTableDeleteDetails( + MutationGroupBuilder deleteGroupBuilder, + Object rowId, + Object[] loadedState, + boolean applyVersion, + SharedSessionContractImplementor session) { + // first, the table key column(s) + deleteGroupBuilder.forEachTableMutationBuilder( (builder) -> { + final EntityTableMapping tableMapping = (EntityTableMapping) builder.getMutatingTable().getTableMapping(); + final TableDeleteBuilder tableDeleteBuilder = (TableDeleteBuilder) builder; + applyKeyRestriction( rowId, entityPersister(), tableDeleteBuilder, tableMapping ); + } ); + + if ( applyVersion ) { + // apply any optimistic locking + applyOptimisticLocking( deleteGroupBuilder, loadedState, session ); + final AbstractEntityPersister persister = entityPersister(); + if ( persister.hasPartitionedSelectionMapping() ) { + final AttributeMappingsList attributeMappings = persister.getAttributeMappings(); + for ( int m = 0; m < attributeMappings.size(); m++ ) { + final AttributeMapping attributeMapping = attributeMappings.get( m ); + final int jdbcTypeCount = attributeMapping.getJdbcTypeCount(); + for ( int i = 0; i < jdbcTypeCount; i++ ) { + final SelectableMapping selectableMapping = attributeMapping.getSelectable( i ); + if ( selectableMapping.isPartitioned() ) { + final String tableNameForMutation = + persister.physicalTableNameForMutation( selectableMapping ); + final RestrictedTableMutationBuilder rootTableMutationBuilder = + deleteGroupBuilder.findTableDetailsBuilder( tableNameForMutation ); + rootTableMutationBuilder.addKeyRestrictionLeniently( selectableMapping ); + } + } + } + } + } + } + + protected void applyOptimisticLocking( + MutationGroupBuilder mutationGroupBuilder, + Object[] loadedState, + SharedSessionContractImplementor session) { + final OptimisticLockStyle optimisticLockStyle = entityPersister().optimisticLockStyle(); + if ( optimisticLockStyle.isVersion() && entityPersister().getVersionMapping() != null ) { + applyVersionBasedOptLocking( mutationGroupBuilder ); + } + else if ( loadedState != null && entityPersister().optimisticLockStyle().isAllOrDirty() ) { + applyNonVersionOptLocking( + optimisticLockStyle, + mutationGroupBuilder, + loadedState, + session + ); + } + } + + protected void applyVersionBasedOptLocking(MutationGroupBuilder mutationGroupBuilder) { + assert entityPersister().optimisticLockStyle() == OptimisticLockStyle.VERSION; + assert entityPersister().getVersionMapping() != null; + + final String tableNameForMutation = entityPersister().physicalTableNameForMutation( entityPersister().getVersionMapping() ); + final RestrictedTableMutationBuilder rootTableMutationBuilder = mutationGroupBuilder.findTableDetailsBuilder( tableNameForMutation ); + rootTableMutationBuilder.addOptimisticLockRestriction( entityPersister().getVersionMapping() ); + } + + protected void applyNonVersionOptLocking( + OptimisticLockStyle lockStyle, + MutationGroupBuilder mutationGroupBuilder, + Object[] loadedState, + SharedSessionContractImplementor session) { + final AbstractEntityPersister persister = entityPersister(); + assert loadedState != null; + assert lockStyle.isAllOrDirty(); + assert persister.optimisticLockStyle().isAllOrDirty(); + assert session != null; + + final boolean[] versionability = persister.getPropertyVersionability(); + for ( int attributeIndex = 0; attributeIndex < versionability.length; attributeIndex++ ) { + final AttributeMapping attribute; + // only makes sense to lock on singular attributes which are not excluded from optimistic locking + if ( versionability[attributeIndex] && !( attribute = persister.getAttributeMapping( attributeIndex ) ).isPluralAttributeMapping() ) { + breakDownJdbcValues( mutationGroupBuilder, session, attribute, loadedState[attributeIndex] ); + } + } + } + + private void breakDownJdbcValues( + MutationGroupBuilder mutationGroupBuilder, + SharedSessionContractImplementor session, + AttributeMapping attribute, + Object loadedValue) { + final RestrictedTableMutationBuilder tableMutationBuilder = + mutationGroupBuilder.findTableDetailsBuilder( attribute.getContainingTableExpression() ); + if ( tableMutationBuilder != null ) { + final ColumnValueBindingList optimisticLockBindings = tableMutationBuilder.getOptimisticLockBindings(); + if ( optimisticLockBindings != null ) { + attribute.breakDownJdbcValues( + loadedValue, + (valueIndex, value, jdbcValueMapping) -> { + if ( !tableMutationBuilder.getKeyRestrictionBindings() + .containsColumn( + jdbcValueMapping.getSelectableName(), + jdbcValueMapping.getJdbcMapping() + ) ) { + optimisticLockBindings.consume( valueIndex, value, jdbcValueMapping ); + } + } + , + session + ); + } + } + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/EntityTableMapping.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/EntityTableMapping.java index e139a1eb6a..24d8599f03 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/EntityTableMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/EntityTableMapping.java @@ -285,7 +285,7 @@ public class EntityTableMapping implements TableMapping { } } - public static class KeyColumn implements SelectableMapping, TableDetails.KeyColumn { + public static class KeyColumn implements TableDetails.KeyColumn { private final String tableName; private final String columnName; private final String writeExpression; diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/InsertCoordinator.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/InsertCoordinator.java index 1f139cf0ac..49fe80b90e 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/InsertCoordinator.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/InsertCoordinator.java @@ -390,6 +390,7 @@ public class InsertCoordinator extends AbstractMutationCoordinator { // add the discriminator entityPersister().addDiscriminatorToInsertGroup( insertGroupBuilder ); + entityPersister().addSoftDeleteToInsertGroup( insertGroupBuilder ); // add the keys final InsertGeneratedIdentifierDelegate identityDelegate = entityPersister().getIdentityInsertDelegate(); diff --git a/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleEntityValuedModelPart.java b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleEntityValuedModelPart.java index 543bddf501..797033d5fe 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleEntityValuedModelPart.java +++ b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleEntityValuedModelPart.java @@ -40,6 +40,7 @@ import org.hibernate.metamodel.mapping.NaturalIdMapping; import org.hibernate.metamodel.mapping.PluralAttributeMapping; import org.hibernate.metamodel.mapping.SelectableConsumer; import org.hibernate.metamodel.mapping.SelectableMapping; +import org.hibernate.metamodel.mapping.SoftDeleteMapping; import org.hibernate.metamodel.mapping.TableDetails; import org.hibernate.metamodel.mapping.ValuedModelPart; import org.hibernate.metamodel.mapping.internal.OneToManyCollectionPart; @@ -681,6 +682,16 @@ public class AnonymousTupleEntityValuedModelPart return delegate.getEntityMappingType().getRowIdMapping(); } + @Override + public SoftDeleteMapping getSoftDeleteMapping() { + return delegate.getEntityMappingType().getSoftDeleteMapping(); + } + + @Override + public TableDetails getSoftDeleteTableDetails() { + return delegate.getEntityMappingType().getSoftDeleteTableDetails(); + } + @Override public void visitConstraintOrderedTables(ConstraintOrderedTableConsumer consumer) { delegate.getEntityMappingType().visitConstraintOrderedTables( consumer ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/AbstractDeleteQueryPlan.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/AbstractDeleteQueryPlan.java new file mode 100644 index 0000000000..fbc1a96072 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/AbstractDeleteQueryPlan.java @@ -0,0 +1,226 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.query.sqm.internal; + +import java.util.List; +import java.util.Map; + +import org.hibernate.action.internal.BulkOperationCleanupAction; +import org.hibernate.dialect.DmlTargetColumnQualifierSupport; +import org.hibernate.engine.jdbc.spi.JdbcServices; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.internal.util.MutableObject; +import org.hibernate.metamodel.mapping.EntityMappingType; +import org.hibernate.metamodel.mapping.ForeignKeyDescriptor; +import org.hibernate.metamodel.mapping.MappingModelExpressible; +import org.hibernate.metamodel.mapping.internal.MappingModelCreationHelper; +import org.hibernate.query.spi.DomainQueryExecutionContext; +import org.hibernate.query.spi.NonSelectQueryPlan; +import org.hibernate.query.spi.QueryParameterImplementor; +import org.hibernate.query.sqm.mutation.internal.SqmMutationStrategyHelper; +import org.hibernate.query.sqm.spi.SqmParameterMappingModelResolutionAccess; +import org.hibernate.query.sqm.sql.SqmTranslation; +import org.hibernate.query.sqm.sql.SqmTranslator; +import org.hibernate.query.sqm.tree.delete.SqmDeleteStatement; +import org.hibernate.query.sqm.tree.expression.SqmParameter; +import org.hibernate.spi.NavigablePath; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.tree.AbstractUpdateOrDeleteStatement; +import org.hibernate.sql.ast.tree.delete.DeleteStatement; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.from.MutatingTableReferenceGroupWrapper; +import org.hibernate.sql.ast.tree.from.NamedTableReference; +import org.hibernate.sql.ast.tree.from.TableGroup; +import org.hibernate.sql.ast.tree.predicate.InSubQueryPredicate; +import org.hibernate.sql.ast.tree.predicate.Predicate; +import org.hibernate.sql.ast.tree.select.QuerySpec; +import org.hibernate.sql.exec.spi.JdbcOperationQueryMutation; +import org.hibernate.sql.exec.spi.JdbcParameterBindings; +import org.hibernate.sql.exec.spi.JdbcParametersList; +import org.hibernate.sql.results.internal.SqlSelectionImpl; + +/** + * @author Steve Ebersole + */ +public abstract class AbstractDeleteQueryPlan + implements NonSelectQueryPlan { + private final EntityMappingType entityDescriptor; + private final SqmDeleteStatement sqmDelete; + private final DomainParameterXref domainParameterXref; + + private O jdbcOperation; + + private SqmTranslation sqmInterpretation; + private Map, Map, List>> jdbcParamsXref; + + public AbstractDeleteQueryPlan( + EntityMappingType entityDescriptor, + SqmDeleteStatement sqmDelete, + DomainParameterXref domainParameterXref) { + assert entityDescriptor.getEntityName().equals( sqmDelete.getTarget().getEntityName() ); + + this.entityDescriptor = entityDescriptor; + this.sqmDelete = sqmDelete; + this.domainParameterXref = domainParameterXref; + } + + public EntityMappingType getEntityDescriptor() { + return entityDescriptor; + } + + @Override + public int executeUpdate(DomainQueryExecutionContext executionContext) { + BulkOperationCleanupAction.schedule( executionContext.getSession(), sqmDelete ); + + final SharedSessionContractImplementor session = executionContext.getSession(); + final SessionFactoryImplementor factory = session.getFactory(); + final JdbcServices jdbcServices = factory.getJdbcServices(); + SqlAstTranslator sqlAstTranslator = null; + if ( jdbcOperation == null ) { + sqlAstTranslator = createTranslator( executionContext ); + } + + final JdbcParameterBindings jdbcParameterBindings = SqmUtil.createJdbcParameterBindings( + executionContext.getQueryParameterBindings(), + domainParameterXref, + jdbcParamsXref, + factory.getRuntimeMetamodels().getMappingMetamodel(), + sqmInterpretation.getFromClauseAccess()::findTableGroup, + new SqmParameterMappingModelResolutionAccess() { + @Override @SuppressWarnings("unchecked") + public MappingModelExpressible getResolvedMappingModelType(SqmParameter parameter) { + return (MappingModelExpressible) sqmInterpretation.getSqmParameterMappingModelTypeResolutions().get(parameter); + } + }, + session + ); + + if ( jdbcOperation != null + && ! jdbcOperation.isCompatibleWith( jdbcParameterBindings, executionContext.getQueryOptions() ) ) { + sqlAstTranslator = createTranslator( executionContext ); + } + + if ( sqlAstTranslator != null ) { + jdbcOperation = sqlAstTranslator.translate( jdbcParameterBindings, executionContext.getQueryOptions() ); + } + + final boolean missingRestriction = sqmInterpretation.getSqlAst().getRestriction() == null; + if ( missingRestriction ) { + assert domainParameterXref.getSqmParameterCount() == 0; + assert jdbcParamsXref.isEmpty(); + } + + final SqmJdbcExecutionContextAdapter executionContextAdapter = SqmJdbcExecutionContextAdapter.usingLockingAndPaging( executionContext ); + + SqmMutationStrategyHelper.cleanUpCollectionTables( + entityDescriptor, + (tableReference, attributeMapping) -> { + final TableGroup collectionTableGroup = new MutatingTableReferenceGroupWrapper( + new NavigablePath( attributeMapping.getRootPathName() ), + attributeMapping, + (NamedTableReference) tableReference + ); + + final MutableObject additionalPredicate = new MutableObject<>(); + attributeMapping.applyBaseRestrictions( + p -> additionalPredicate.set( Predicate.combinePredicates( additionalPredicate.get(), p ) ), + collectionTableGroup, + factory.getJdbcServices().getDialect().getDmlTargetColumnQualifierSupport() == DmlTargetColumnQualifierSupport.TABLE_ALIAS, + executionContext.getSession().getLoadQueryInfluencers().getEnabledFilters(), + null, + null + ); + + if ( missingRestriction ) { + return additionalPredicate.get(); + } + + final ForeignKeyDescriptor fkDescriptor = attributeMapping.getKeyDescriptor(); + final Expression fkColumnExpression = MappingModelCreationHelper.buildColumnReferenceExpression( + collectionTableGroup, + fkDescriptor.getKeyPart(), + null, + factory + ); + + final QuerySpec matchingIdSubQuery = new QuerySpec( false ); + + final MutatingTableReferenceGroupWrapper tableGroup = new MutatingTableReferenceGroupWrapper( + new NavigablePath( attributeMapping.getRootPathName() ), + attributeMapping, + sqmInterpretation.getSqlAst().getTargetTable() + ); + final Expression fkTargetColumnExpression = MappingModelCreationHelper.buildColumnReferenceExpression( + tableGroup, + fkDescriptor.getTargetPart(), + sqmInterpretation.getSqlExpressionResolver(), + factory + ); + matchingIdSubQuery.getSelectClause().addSqlSelection( new SqlSelectionImpl( 0, fkTargetColumnExpression ) ); + + matchingIdSubQuery.getFromClause().addRoot( + tableGroup + ); + + matchingIdSubQuery.applyPredicate( SqmMutationStrategyHelper.getIdSubqueryPredicate( + sqmInterpretation.getSqlAst().getRestriction(), + entityDescriptor, + tableGroup, + session + ) ); + + return Predicate.combinePredicates( + additionalPredicate.get(), + new InSubQueryPredicate( fkColumnExpression, matchingIdSubQuery, false ) + ); + }, + ( missingRestriction ? JdbcParameterBindings.NO_BINDINGS : jdbcParameterBindings ), + executionContextAdapter + ); + + return jdbcServices.getJdbcMutationExecutor().execute( + jdbcOperation, + jdbcParameterBindings, + sql -> session + .getJdbcCoordinator() + .getStatementPreparer() + .prepareStatement( sql ), + (integer, preparedStatement) -> {}, + executionContextAdapter + ); + } + + protected SqlAstTranslator createTranslator(DomainQueryExecutionContext executionContext) { + final SessionFactoryImplementor factory = executionContext.getSession().getFactory(); + final SqmTranslator translator = factory.getQueryEngine().getSqmTranslatorFactory().createSimpleDeleteTranslator( + sqmDelete, + executionContext.getQueryOptions(), + domainParameterXref, + executionContext.getQueryParameterBindings(), + executionContext.getSession().getLoadQueryInfluencers(), + factory + ); + sqmInterpretation = translator.translate(); + + this.jdbcParamsXref = SqmUtil.generateJdbcParamsXref( + domainParameterXref, + sqmInterpretation::getJdbcParamsBySqmParam + ); + + final S ast = buildAst( sqmInterpretation, executionContext ); + return createTranslator( ast, executionContext ); + } + + protected abstract S buildAst( + SqmTranslation sqmInterpretation, + DomainQueryExecutionContext executionContext); + + protected abstract SqlAstTranslator createTranslator( + S ast, + DomainQueryExecutionContext executionContext); +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/MultiTableDeleteQueryPlan.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/MultiTableDeleteQueryPlan.java index 7155660e61..d641da765f 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/MultiTableDeleteQueryPlan.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/MultiTableDeleteQueryPlan.java @@ -11,7 +11,6 @@ import org.hibernate.query.spi.DomainQueryExecutionContext; import org.hibernate.query.spi.NonSelectQueryPlan; import org.hibernate.query.sqm.mutation.spi.SqmMultiTableMutationStrategy; import org.hibernate.query.sqm.tree.delete.SqmDeleteStatement; -import org.hibernate.sql.exec.spi.ExecutionContext; /** * @author Steve Ebersole diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/QuerySqmImpl.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/QuerySqmImpl.java index 438af1b75d..8ede8265dd 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/QuerySqmImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/QuerySqmImpl.java @@ -759,12 +759,17 @@ public class QuerySqmImpl private NonSelectQueryPlan buildConcreteDeleteQueryPlan(@SuppressWarnings("rawtypes") SqmDeleteStatement sqmDelete) { final EntityDomainType entityDomainType = sqmDelete.getTarget().getModel(); final String entityNameToDelete = entityDomainType.getHibernateEntityName(); - final EntityPersister persister = - getSessionFactory().getMappingMetamodel().getEntityDescriptor( entityNameToDelete ); + final EntityPersister persister = getSessionFactory().getMappingMetamodel().getEntityDescriptor( entityNameToDelete ); final SqmMultiTableMutationStrategy multiTableStrategy = persister.getSqmMultiTableMutationStrategy(); - return multiTableStrategy == null - ? new SimpleDeleteQueryPlan( persister, sqmDelete, domainParameterXref ) - : new MultiTableDeleteQueryPlan( sqmDelete, domainParameterXref, multiTableStrategy ); + if ( multiTableStrategy != null ) { + // NOTE : MultiTableDeleteQueryPlan and SqmMultiTableMutationStrategy already handle soft-deletes internally + return new MultiTableDeleteQueryPlan( sqmDelete, domainParameterXref, multiTableStrategy ); + } + else { + return persister.getSoftDeleteMapping() != null + ? new SoftDeleteQueryPlan( persister, sqmDelete, domainParameterXref ) + : new SimpleDeleteQueryPlan( persister, sqmDelete, domainParameterXref ); + } } private NonSelectQueryPlan buildAggregatedDeleteQueryPlan(@SuppressWarnings("rawtypes") SqmDeleteStatement[] concreteSqmStatements) { diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SimpleDeleteQueryPlan.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SimpleDeleteQueryPlan.java index 65ea018103..122b2d9b8d 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SimpleDeleteQueryPlan.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SimpleDeleteQueryPlan.java @@ -6,207 +6,42 @@ */ package org.hibernate.query.sqm.internal; -import java.util.List; -import java.util.Map; - -import org.hibernate.action.internal.BulkOperationCleanupAction; -import org.hibernate.dialect.DmlTargetColumnQualifierSupport; -import org.hibernate.engine.jdbc.spi.JdbcServices; import org.hibernate.engine.spi.SessionFactoryImplementor; -import org.hibernate.engine.spi.SharedSessionContractImplementor; -import org.hibernate.internal.util.MutableObject; import org.hibernate.metamodel.mapping.EntityMappingType; -import org.hibernate.metamodel.mapping.ForeignKeyDescriptor; -import org.hibernate.metamodel.mapping.MappingModelExpressible; -import org.hibernate.metamodel.mapping.internal.MappingModelCreationHelper; import org.hibernate.query.spi.DomainQueryExecutionContext; -import org.hibernate.query.spi.NonSelectQueryPlan; -import org.hibernate.query.spi.QueryParameterImplementor; -import org.hibernate.query.sqm.mutation.internal.SqmMutationStrategyHelper; -import org.hibernate.query.sqm.spi.SqmParameterMappingModelResolutionAccess; import org.hibernate.query.sqm.sql.SqmTranslation; import org.hibernate.query.sqm.tree.delete.SqmDeleteStatement; -import org.hibernate.query.sqm.tree.expression.SqmParameter; -import org.hibernate.spi.NavigablePath; import org.hibernate.sql.ast.SqlAstTranslator; import org.hibernate.sql.ast.tree.delete.DeleteStatement; -import org.hibernate.sql.ast.tree.expression.Expression; -import org.hibernate.sql.ast.tree.from.MutatingTableReferenceGroupWrapper; -import org.hibernate.sql.ast.tree.from.NamedTableReference; -import org.hibernate.sql.ast.tree.from.TableGroup; -import org.hibernate.sql.ast.tree.predicate.InSubQueryPredicate; -import org.hibernate.sql.ast.tree.predicate.Predicate; -import org.hibernate.sql.ast.tree.select.QuerySpec; import org.hibernate.sql.exec.spi.JdbcOperationQueryDelete; -import org.hibernate.sql.exec.spi.JdbcParameterBindings; -import org.hibernate.sql.exec.spi.JdbcParametersList; -import org.hibernate.sql.results.internal.SqlSelectionImpl; /** * @author Steve Ebersole */ -public class SimpleDeleteQueryPlan implements NonSelectQueryPlan { - private final EntityMappingType entityDescriptor; - private final SqmDeleteStatement sqmDelete; - private final DomainParameterXref domainParameterXref; - - private JdbcOperationQueryDelete jdbcDelete; - private SqmTranslation sqmInterpretation; - private Map, Map, List>> jdbcParamsXref; - +public class SimpleDeleteQueryPlan extends AbstractDeleteQueryPlan { public SimpleDeleteQueryPlan( EntityMappingType entityDescriptor, SqmDeleteStatement sqmDelete, DomainParameterXref domainParameterXref) { - assert entityDescriptor.getEntityName().equals( sqmDelete.getTarget().getEntityName() ); - - this.entityDescriptor = entityDescriptor; - this.sqmDelete = sqmDelete; - this.domainParameterXref = domainParameterXref; - } - - protected SqlAstTranslator createDeleteTranslator(DomainQueryExecutionContext executionContext) { - final SessionFactoryImplementor factory = executionContext.getSession().getFactory(); - - sqmInterpretation = - factory.getQueryEngine().getSqmTranslatorFactory(). - createSimpleDeleteTranslator( - sqmDelete, - executionContext.getQueryOptions(), - domainParameterXref, - executionContext.getQueryParameterBindings(), - executionContext.getSession().getLoadQueryInfluencers(), - factory - ) - .translate(); - - this.jdbcParamsXref = SqmUtil.generateJdbcParamsXref( - domainParameterXref, - sqmInterpretation::getJdbcParamsBySqmParam - ); - - return factory.getJdbcServices().getJdbcEnvironment().getSqlAstTranslatorFactory() - .buildDeleteTranslator( factory, sqmInterpretation.getSqlAst() ); + super( entityDescriptor, sqmDelete, domainParameterXref ); } @Override - public int executeUpdate(DomainQueryExecutionContext executionContext) { - BulkOperationCleanupAction.schedule( executionContext.getSession(), sqmDelete ); - final SharedSessionContractImplementor session = executionContext.getSession(); - final SessionFactoryImplementor factory = session.getFactory(); - final JdbcServices jdbcServices = factory.getJdbcServices(); - SqlAstTranslator deleteTranslator = null; - if ( jdbcDelete == null ) { - deleteTranslator = createDeleteTranslator( executionContext ); - } - - final JdbcParameterBindings jdbcParameterBindings = SqmUtil.createJdbcParameterBindings( - executionContext.getQueryParameterBindings(), - domainParameterXref, - jdbcParamsXref, - factory.getRuntimeMetamodels().getMappingMetamodel(), - sqmInterpretation.getFromClauseAccess()::findTableGroup, - new SqmParameterMappingModelResolutionAccess() { - @Override @SuppressWarnings("unchecked") - public MappingModelExpressible getResolvedMappingModelType(SqmParameter parameter) { - return (MappingModelExpressible) sqmInterpretation.getSqmParameterMappingModelTypeResolutions().get(parameter); - } - }, - session - ); - - if ( jdbcDelete != null - && ! jdbcDelete.isCompatibleWith( jdbcParameterBindings, executionContext.getQueryOptions() ) ) { - deleteTranslator = createDeleteTranslator( executionContext ); - } - - if ( deleteTranslator != null ) { - jdbcDelete = deleteTranslator.translate( jdbcParameterBindings, executionContext.getQueryOptions() ); - } - - final boolean missingRestriction = sqmInterpretation.getSqlAst().getRestriction() == null; - if ( missingRestriction ) { - assert domainParameterXref.getSqmParameterCount() == 0; - assert jdbcParamsXref.isEmpty(); - } - - final SqmJdbcExecutionContextAdapter executionContextAdapter = SqmJdbcExecutionContextAdapter.usingLockingAndPaging( executionContext ); - - SqmMutationStrategyHelper.cleanUpCollectionTables( - entityDescriptor, - (tableReference, attributeMapping) -> { - final TableGroup collectionTableGroup = new MutatingTableReferenceGroupWrapper( - new NavigablePath( attributeMapping.getRootPathName() ), - attributeMapping, - (NamedTableReference) tableReference - ); - - final MutableObject additionalPredicate = new MutableObject<>(); - attributeMapping.applyBaseRestrictions( - p -> additionalPredicate.set( Predicate.combinePredicates( additionalPredicate.get(), p ) ), - collectionTableGroup, - factory.getJdbcServices().getDialect().getDmlTargetColumnQualifierSupport() == DmlTargetColumnQualifierSupport.TABLE_ALIAS, - executionContext.getSession().getLoadQueryInfluencers().getEnabledFilters(), - null, - null - ); - - if ( missingRestriction ) { - return additionalPredicate.get(); - } - - final ForeignKeyDescriptor fkDescriptor = attributeMapping.getKeyDescriptor(); - final Expression fkColumnExpression = MappingModelCreationHelper.buildColumnReferenceExpression( - collectionTableGroup, - fkDescriptor.getKeyPart(), - null, - factory - ); - - final QuerySpec matchingIdSubQuery = new QuerySpec( false ); - - final MutatingTableReferenceGroupWrapper tableGroup = new MutatingTableReferenceGroupWrapper( - new NavigablePath( attributeMapping.getRootPathName() ), - attributeMapping, - sqmInterpretation.getSqlAst().getTargetTable() - ); - final Expression fkTargetColumnExpression = MappingModelCreationHelper.buildColumnReferenceExpression( - tableGroup, - fkDescriptor.getTargetPart(), - sqmInterpretation.getSqlExpressionResolver(), - factory - ); - matchingIdSubQuery.getSelectClause().addSqlSelection( new SqlSelectionImpl( 0, fkTargetColumnExpression ) ); - - matchingIdSubQuery.getFromClause().addRoot( - tableGroup - ); - - matchingIdSubQuery.applyPredicate( SqmMutationStrategyHelper.getIdSubqueryPredicate( - sqmInterpretation.getSqlAst().getRestriction(), - entityDescriptor, - tableGroup, - session - ) ); - - return Predicate.combinePredicates( - additionalPredicate.get(), - new InSubQueryPredicate( fkColumnExpression, matchingIdSubQuery, false ) - ); - }, - ( missingRestriction ? JdbcParameterBindings.NO_BINDINGS : jdbcParameterBindings ), - executionContextAdapter - ); - - return jdbcServices.getJdbcMutationExecutor().execute( - jdbcDelete, - jdbcParameterBindings, - sql -> session - .getJdbcCoordinator() - .getStatementPreparer() - .prepareStatement( sql ), - (integer, preparedStatement) -> {}, - executionContextAdapter - ); + protected DeleteStatement buildAst( + SqmTranslation sqmInterpretation, + DomainQueryExecutionContext executionContext) { + return sqmInterpretation.getSqlAst(); } + + @Override + protected SqlAstTranslator createTranslator( + DeleteStatement ast, + DomainQueryExecutionContext executionContext) { + final SessionFactoryImplementor factory = executionContext.getSession().getFactory(); + return factory.getJdbcServices() + .getJdbcEnvironment() + .getSqlAstTranslatorFactory() + .buildDeleteTranslator( factory, ast ); + } + } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SoftDeleteQueryPlan.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SoftDeleteQueryPlan.java new file mode 100644 index 0000000000..0e6b3d70c8 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SoftDeleteQueryPlan.java @@ -0,0 +1,71 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.query.sqm.internal; + +import java.util.Collections; + +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.metamodel.mapping.EntityMappingType; +import org.hibernate.metamodel.mapping.SoftDeleteMapping; +import org.hibernate.query.spi.DomainQueryExecutionContext; +import org.hibernate.query.sqm.sql.SqmTranslation; +import org.hibernate.query.sqm.tree.delete.SqmDeleteStatement; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.tree.delete.DeleteStatement; +import org.hibernate.sql.ast.tree.expression.ColumnReference; +import org.hibernate.sql.ast.tree.expression.JdbcLiteral; +import org.hibernate.sql.ast.tree.from.NamedTableReference; +import org.hibernate.sql.ast.tree.update.Assignment; +import org.hibernate.sql.ast.tree.update.UpdateStatement; +import org.hibernate.sql.exec.spi.JdbcOperationQueryUpdate; + +/** + * NonSelectQueryPlan for handling DELETE queries against an entity with soft-delete + * + * @author Steve Ebersole + */ +public class SoftDeleteQueryPlan extends AbstractDeleteQueryPlan { + public SoftDeleteQueryPlan( + EntityMappingType entityDescriptor, + SqmDeleteStatement sqmDelete, + DomainParameterXref domainParameterXref) { + super( entityDescriptor, sqmDelete, domainParameterXref ); + assert entityDescriptor.getSoftDeleteMapping() != null; + } + + @Override + protected UpdateStatement buildAst( + SqmTranslation sqmInterpretation, + DomainQueryExecutionContext executionContext) { + final DeleteStatement sqlDeleteAst = sqmInterpretation.getSqlAst(); + final NamedTableReference targetTable = sqlDeleteAst.getTargetTable(); + final SoftDeleteMapping columnMapping = getEntityDescriptor().getSoftDeleteMapping(); + final ColumnReference columnReference = new ColumnReference( targetTable, columnMapping ); + //noinspection rawtypes,unchecked + final JdbcLiteral jdbcLiteral = new JdbcLiteral( columnMapping.getDeletedLiteralValue(), columnMapping.getJdbcMapping() ); + final Assignment assignment = new Assignment( columnReference, jdbcLiteral ); + + return new UpdateStatement( + targetTable, + Collections.singletonList( assignment ), + sqlDeleteAst.getRestriction() + ); + } + + @Override + protected SqlAstTranslator createTranslator( + UpdateStatement sqlUpdateAst, + DomainQueryExecutionContext executionContext) { + final SharedSessionContractImplementor session = executionContext.getSession(); + final SessionFactoryImplementor factory = session.getFactory(); + return factory.getJdbcServices() + .getJdbcEnvironment() + .getSqlAstTranslatorFactory() + .buildUpdateTranslator( factory, sqlUpdateAst ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/MultiTableSqmMutationConverter.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/MultiTableSqmMutationConverter.java index 4a153e21a4..6e58861064 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/MultiTableSqmMutationConverter.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/MultiTableSqmMutationConverter.java @@ -185,7 +185,7 @@ public class MultiTableSqmMutationConverter extends BaseSqmToSqlAstConverter, List> parameterResolutions, SessionFactoryImplementor factory) { - final TableGroup updatingTableGroup = sqmConverter.getMutatingTableGroup(); + final TableGroup mutatingTableGroup = sqmConverter.getMutatingTableGroup(); final SelectStatement idSelectStatement = (SelectStatement) idSelectCte.getCteDefinition(); sqmConverter.getProcessingStateStack().push( new SqlAstQueryPartProcessingStateImpl( @@ -73,14 +73,13 @@ public class CteDeleteHandler extends AbstractCteMutationHandler implements Dele ) ); SqmMutationStrategyHelper.visitCollectionTables( - (EntityMappingType) updatingTableGroup.getModelPart(), + (EntityMappingType) mutatingTableGroup.getModelPart(), pluralAttribute -> { if ( pluralAttribute.getSeparateCollectionTable() != null ) { // Ensure that the FK target columns are available final boolean useFkTarget = !pluralAttribute.getKeyDescriptor() .getTargetPart().isEntityIdentifierMapping(); if ( useFkTarget ) { - final TableGroup mutatingTableGroup = sqmConverter.getMutatingTableGroup(); pluralAttribute.getKeyDescriptor().getTargetPart().applySqlSelections( mutatingTableGroup.getNavigablePath(), mutatingTableGroup, @@ -141,6 +140,14 @@ public class CteDeleteHandler extends AbstractCteMutationHandler implements Dele sqmConverter.getProcessingStateStack().pop(); + applyDmlOperations( statement, idSelectCte, factory, mutatingTableGroup ); + } + + protected void applyDmlOperations( + CteContainer statement, + CteStatement idSelectCte, + SessionFactoryImplementor factory, + TableGroup updatingTableGroup) { getEntityDescriptor().visitConstraintOrderedTables( (tableExpression, tableColumnsVisitationSupplier) -> { final String cteTableName = getCteTableName( tableExpression ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteMutationStrategy.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteMutationStrategy.java index 1a31b0f776..9f2a73270d 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteMutationStrategy.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteMutationStrategy.java @@ -94,7 +94,27 @@ public class CteMutationStrategy implements SqmMultiTableMutationStrategy { DomainParameterXref domainParameterXref, DomainQueryExecutionContext context) { checkMatch( sqmDelete ); - return new CteDeleteHandler( idCteTable, sqmDelete, domainParameterXref, this, sessionFactory ).execute( context ); + + final CteDeleteHandler deleteHandler; + if ( rootDescriptor.getSoftDeleteMapping() != null ) { + deleteHandler = new CteSoftDeleteHandler( + idCteTable, + sqmDelete, + domainParameterXref, + this, + sessionFactory + ); + } + else { + deleteHandler = new CteDeleteHandler( + idCteTable, + sqmDelete, + domainParameterXref, + this, + sessionFactory + ); + } + return deleteHandler.execute( context ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteSoftDeleteHandler.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteSoftDeleteHandler.java new file mode 100644 index 0000000000..aacdca0504 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteSoftDeleteHandler.java @@ -0,0 +1,94 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.query.sqm.mutation.internal.cte; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.metamodel.mapping.EntityMappingType; +import org.hibernate.metamodel.mapping.SoftDeleteMapping; +import org.hibernate.metamodel.mapping.TableDetails; +import org.hibernate.query.sqm.internal.DomainParameterXref; +import org.hibernate.query.sqm.tree.delete.SqmDeleteStatement; +import org.hibernate.sql.ast.tree.MutationStatement; +import org.hibernate.sql.ast.tree.cte.CteContainer; +import org.hibernate.sql.ast.tree.cte.CteStatement; +import org.hibernate.sql.ast.tree.cte.CteTable; +import org.hibernate.sql.ast.tree.expression.ColumnReference; +import org.hibernate.sql.ast.tree.expression.JdbcLiteral; +import org.hibernate.sql.ast.tree.from.NamedTableReference; +import org.hibernate.sql.ast.tree.from.TableGroup; +import org.hibernate.sql.ast.tree.from.TableReference; +import org.hibernate.sql.ast.tree.update.Assignment; +import org.hibernate.sql.ast.tree.update.UpdateStatement; + +/** + * Specialized CteDeleteHandler for soft-delete handling + * + * @author Steve Ebersole + */ +public class CteSoftDeleteHandler extends CteDeleteHandler { + protected CteSoftDeleteHandler( + CteTable cteTable, + SqmDeleteStatement sqmDeleteStatement, + DomainParameterXref domainParameterXref, + CteMutationStrategy strategy, + SessionFactoryImplementor sessionFactory) { + super( cteTable, sqmDeleteStatement, domainParameterXref, strategy, sessionFactory ); + } + + protected void applyDmlOperations( + CteContainer statement, + CteStatement idSelectCte, + SessionFactoryImplementor factory, + TableGroup updatingTableGroup) { + final SoftDeleteMapping softDeleteMapping = getEntityDescriptor().getSoftDeleteMapping(); + final TableDetails softDeleteTable = getEntityDescriptor().getSoftDeleteTableDetails(); + final CteTable dmlResultCte = new CteTable( + getCteTableName( softDeleteTable.getTableName() ), + idSelectCte.getCteTable().getCteColumns() + ); + final TableReference updatingTableReference = updatingTableGroup.getTableReference( + updatingTableGroup.getNavigablePath(), + softDeleteTable.getTableName(), + true + ); + final NamedTableReference dmlTableReference = resolveUnionTableReference( + updatingTableReference, + softDeleteTable.getTableName() + ); + + final List columnReferences = new ArrayList<>( idSelectCte.getCteTable().getCteColumns().size() ); + final TableDetails.KeyDetails keyDetails = softDeleteTable.getKeyDetails(); + keyDetails.forEachKeyColumn( (position, selectable) -> columnReferences.add( + new ColumnReference( dmlTableReference, selectable ) + ) ); + + final ColumnReference softDeleteColumnReference = new ColumnReference( + dmlTableReference, + softDeleteMapping + ); + final JdbcLiteral deletedIndicator = new JdbcLiteral<>( + softDeleteMapping.getDeletedLiteralValue(), + softDeleteMapping.getJdbcMapping() + ); + final Assignment assignment = new Assignment( + softDeleteColumnReference, + deletedIndicator + ); + + final MutationStatement dmlStatement = new UpdateStatement( + dmlTableReference, + Collections.singletonList( assignment ), + createIdSubQueryPredicate( columnReferences, idSelectCte, factory ), + columnReferences + ); + statement.addCteStatement( new CteStatement( dmlResultCte, dmlStatement ) ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/inline/InlineDeleteHandler.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/inline/InlineDeleteHandler.java index 267ef0a22e..2dd29f18cd 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/inline/InlineDeleteHandler.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/inline/InlineDeleteHandler.java @@ -7,16 +7,18 @@ package org.hibernate.query.sqm.mutation.internal.inline; import java.sql.PreparedStatement; +import java.util.Collections; import java.util.List; import java.util.function.Consumer; import java.util.function.Supplier; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.internal.util.MutableInteger; -import org.hibernate.metamodel.mapping.EntityIdentifierMapping; import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.mapping.ModelPart; import org.hibernate.metamodel.mapping.SelectableConsumer; +import org.hibernate.metamodel.mapping.SoftDeleteMapping; +import org.hibernate.metamodel.mapping.TableDetails; import org.hibernate.query.spi.DomainQueryExecutionContext; import org.hibernate.query.sqm.internal.DomainParameterXref; import org.hibernate.query.sqm.internal.SqmJdbcExecutionContextAdapter; @@ -28,12 +30,18 @@ import org.hibernate.sql.ast.SqlAstTranslatorFactory; import org.hibernate.sql.ast.tree.delete.DeleteStatement; import org.hibernate.sql.ast.tree.from.NamedTableReference; import org.hibernate.sql.ast.tree.predicate.Predicate; +import org.hibernate.sql.ast.tree.update.Assignment; +import org.hibernate.sql.ast.tree.update.UpdateStatement; import org.hibernate.sql.exec.internal.JdbcParameterBindingsImpl; import org.hibernate.sql.exec.spi.JdbcMutationExecutor; import org.hibernate.sql.exec.spi.JdbcOperationQueryDelete; +import org.hibernate.sql.exec.spi.JdbcOperationQueryUpdate; import org.hibernate.sql.exec.spi.JdbcParameterBindings; import org.hibernate.sql.exec.spi.StatementCreatorHelper; +import static org.hibernate.boot.model.internal.SoftDeleteHelper.createNonSoftDeletedRestriction; +import static org.hibernate.boot.model.internal.SoftDeleteHelper.createSoftDeleteAssignment; + /** * DeleteHandler for the in-line strategy * @@ -128,9 +136,18 @@ public class InlineDeleteHandler implements DeleteHandler { } ); - entityDescriptor.visitConstraintOrderedTables( - (tableExpression, tableKeyColumnsVisitationSupplier) -> { - executeDelete( + final SoftDeleteMapping softDeleteMapping = entityDescriptor.getSoftDeleteMapping(); + if ( softDeleteMapping != null ) { + performSoftDelete( + entityDescriptor, + idsAndFks, + jdbcParameterBindings, + executionContext + ); + } + else { + entityDescriptor.visitConstraintOrderedTables( + (tableExpression, tableKeyColumnsVisitationSupplier) -> executeDelete( tableExpression, entityDescriptor, tableKeyColumnsVisitationSupplier, @@ -139,13 +156,71 @@ public class InlineDeleteHandler implements DeleteHandler { null, jdbcParameterBindings, executionContext - ); - } - ); + ) + ); + } return idsAndFks.size(); } + /** + * Perform a soft-delete, which just needs to update the root table + */ + private void performSoftDelete( + EntityMappingType entityDescriptor, + List idsAndFks, + JdbcParameterBindings jdbcParameterBindings, + DomainQueryExecutionContext executionContext) { + final TableDetails softDeleteTable = entityDescriptor.getSoftDeleteTableDetails(); + final SoftDeleteMapping softDeleteMapping = entityDescriptor.getSoftDeleteMapping(); + assert softDeleteMapping != null; + + final NamedTableReference targetTableReference = new NamedTableReference( + softDeleteTable.getTableName(), + DeleteStatement.DEFAULT_ALIAS + ); + + final SqmJdbcExecutionContextAdapter executionContextAdapter = SqmJdbcExecutionContextAdapter.omittingLockingAndPaging( executionContext ); + + final Predicate matchingIdsPredicate = matchingIdsPredicateProducer.produceRestriction( + idsAndFks, + entityDescriptor, + 0, + entityDescriptor.getIdentifierMapping(), + targetTableReference, + null, + executionContextAdapter + ); + + final Predicate predicate = Predicate.combinePredicates( + matchingIdsPredicate, + createNonSoftDeletedRestriction( targetTableReference, softDeleteMapping ) + ); + + final Assignment softDeleteAssignment = createSoftDeleteAssignment( + targetTableReference, + softDeleteMapping + ); + + final UpdateStatement updateStatement = new UpdateStatement( + targetTableReference, + Collections.singletonList( softDeleteAssignment ), + predicate + ); + + final JdbcOperationQueryUpdate jdbcOperation = sqlAstTranslatorFactory + .buildUpdateTranslator( sessionFactory, updateStatement ) + .translate( jdbcParameterBindings, executionContext.getQueryOptions() ); + + jdbcMutationExecutor.execute( + jdbcOperation, + jdbcParameterBindings, + this::prepareQueryStatement, + (integer, preparedStatement) -> {}, + executionContextAdapter + ); + } + private void executeDelete( String targetTableExpression, EntityMappingType entityDescriptor, diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/inline/InlineUpdateHandler.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/inline/InlineUpdateHandler.java index 1204e147be..56152440ae 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/inline/InlineUpdateHandler.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/inline/InlineUpdateHandler.java @@ -18,6 +18,7 @@ import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Supplier; +import org.hibernate.boot.model.internal.SoftDeleteHelper; import org.hibernate.engine.jdbc.spi.JdbcServices; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.internal.util.collections.CollectionHelper; @@ -28,6 +29,7 @@ import org.hibernate.metamodel.mapping.BasicValuedModelPart; import org.hibernate.metamodel.mapping.EntityIdentifierMapping; import org.hibernate.metamodel.mapping.MappingModelExpressible; import org.hibernate.metamodel.mapping.SelectableConsumer; +import org.hibernate.metamodel.mapping.SoftDeleteMapping; import org.hibernate.persister.entity.AbstractEntityPersister; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.persister.entity.Joinable; @@ -166,7 +168,7 @@ public class InlineUpdateHandler implements UpdateHandler { final TableGroup updatingTableGroup = converterDelegate.getMutatingTableGroup(); - final TableReference hierarchyRootTableReference = updatingTableGroup.resolveTableReference( + final NamedTableReference hierarchyRootTableReference = (NamedTableReference) updatingTableGroup.resolveTableReference( updatingTableGroup.getNavigablePath(), hierarchyRootTableName ); @@ -207,7 +209,16 @@ public class InlineUpdateHandler implements UpdateHandler { final Predicate providedPredicate; final SqmWhereClause whereClause = sqmUpdate.getWhereClause(); if ( whereClause == null || whereClause.getPredicate() == null ) { - providedPredicate = null; + final SoftDeleteMapping softDeleteMapping = entityDescriptor.getSoftDeleteMapping(); + if ( softDeleteMapping != null ) { + providedPredicate = SoftDeleteHelper.createNonSoftDeletedRestriction( + hierarchyRootTableReference, + softDeleteMapping + ); + } + else { + providedPredicate = null; + } } else { providedPredicate = converterDelegate.visitWhereClause( diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/AbstractDeleteExecutionDelegate.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/AbstractDeleteExecutionDelegate.java new file mode 100644 index 0000000000..5697406d80 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/AbstractDeleteExecutionDelegate.java @@ -0,0 +1,98 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.query.sqm.mutation.internal.temptable; + +import java.util.function.Function; + +import org.hibernate.dialect.temptable.TemporaryTable; +import org.hibernate.engine.spi.LoadQueryInfluencers; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.metamodel.mapping.EntityMappingType; +import org.hibernate.query.spi.QueryOptions; +import org.hibernate.query.spi.QueryParameterBindings; +import org.hibernate.query.sqm.internal.DomainParameterXref; +import org.hibernate.query.sqm.mutation.internal.MultiTableSqmMutationConverter; +import org.hibernate.query.sqm.tree.delete.SqmDeleteStatement; + +/** + * @author Steve Ebersole + */ +public abstract class AbstractDeleteExecutionDelegate implements TableBasedDeleteHandler.ExecutionDelegate { + private final EntityMappingType entityDescriptor; + private final TemporaryTable idTable; + private final AfterUseAction afterUseAction; + private final SqmDeleteStatement sqmDelete; + private final DomainParameterXref domainParameterXref; + private final SessionFactoryImplementor sessionFactory; + private final Function sessionUidAccess; + + private final MultiTableSqmMutationConverter converter; + + public AbstractDeleteExecutionDelegate( + EntityMappingType entityDescriptor, + TemporaryTable idTable, + AfterUseAction afterUseAction, + SqmDeleteStatement sqmDelete, + DomainParameterXref domainParameterXref, + QueryOptions queryOptions, + LoadQueryInfluencers loadQueryInfluencers, + QueryParameterBindings queryParameterBindings, + Function sessionUidAccess, + SessionFactoryImplementor sessionFactory) { + this.entityDescriptor = entityDescriptor; + this.idTable = idTable; + this.afterUseAction = afterUseAction; + this.sqmDelete = sqmDelete; + this.domainParameterXref = domainParameterXref; + this.sessionFactory = sessionFactory; + this.sessionUidAccess = sessionUidAccess; + + this.converter = new MultiTableSqmMutationConverter( + entityDescriptor, + getSqmDelete(), + getSqmDelete().getTarget(), + getDomainParameterXref(), + queryOptions, + loadQueryInfluencers, + queryParameterBindings, + getSessionFactory() + ); + } + + public EntityMappingType getEntityDescriptor() { + return entityDescriptor; + } + + public TemporaryTable getIdTable() { + return idTable; + } + + public AfterUseAction getAfterUseAction() { + return afterUseAction; + } + + public SqmDeleteStatement getSqmDelete() { + return sqmDelete; + } + + public DomainParameterXref getDomainParameterXref() { + return domainParameterXref; + } + + public SessionFactoryImplementor getSessionFactory() { + return sessionFactory; + } + + public Function getSessionUidAccess() { + return sessionUidAccess; + } + + public MultiTableSqmMutationConverter getConverter() { + return converter; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/GlobalTemporaryTableStrategy.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/GlobalTemporaryTableStrategy.java index 5d51c8120e..14e5b217d2 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/GlobalTemporaryTableStrategy.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/GlobalTemporaryTableStrategy.java @@ -15,6 +15,7 @@ import org.hibernate.engine.config.spi.ConfigurationService; import org.hibernate.engine.config.spi.StandardConverters; import org.hibernate.engine.jdbc.connections.spi.JdbcConnectionAccess; import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.mapping.internal.MappingModelCreationProcess; import org.jboss.logging.Logger; @@ -52,6 +53,10 @@ public class GlobalTemporaryTableStrategy { } } + public EntityMappingType getEntityDescriptor() { + return temporaryTable.getEntityDescriptor(); + } + public void prepare( MappingModelCreationProcess mappingModelCreationProcess, JdbcConnectionAccess connectionAccess) { diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/LocalTemporaryTableMutationStrategy.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/LocalTemporaryTableMutationStrategy.java index 5593c48349..9892b1a1bf 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/LocalTemporaryTableMutationStrategy.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/LocalTemporaryTableMutationStrategy.java @@ -8,6 +8,7 @@ package org.hibernate.query.sqm.mutation.internal.temptable; import org.hibernate.dialect.temptable.TemporaryTable; import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.query.spi.DomainQueryExecutionContext; import org.hibernate.query.sqm.internal.DomainParameterXref; import org.hibernate.query.sqm.mutation.spi.SqmMultiTableMutationStrategy; @@ -51,7 +52,7 @@ public class LocalTemporaryTableMutationStrategy extends LocalTemporaryTableStra SqmDeleteStatement sqmDelete, DomainParameterXref domainParameterXref, DomainQueryExecutionContext context) { - return new TableBasedDeleteHandler( + final TableBasedDeleteHandler deleteHandler = new TableBasedDeleteHandler( sqmDelete, domainParameterXref, getTemporaryTable(), @@ -62,7 +63,8 @@ public class LocalTemporaryTableMutationStrategy extends LocalTemporaryTableStra throw new UnsupportedOperationException( "Unexpected call to access Session uid" ); }, getSessionFactory() - ).execute( context ); + ); + return deleteHandler.execute( context ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/LocalTemporaryTableStrategy.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/LocalTemporaryTableStrategy.java index 54ec08ece3..40e74f33fe 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/LocalTemporaryTableStrategy.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/LocalTemporaryTableStrategy.java @@ -11,6 +11,7 @@ import org.hibernate.engine.config.spi.ConfigurationService; import org.hibernate.engine.config.spi.StandardConverters; import org.hibernate.engine.jdbc.connections.spi.JdbcConnectionAccess; import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.mapping.internal.MappingModelCreationProcess; /** @@ -56,6 +57,10 @@ public class LocalTemporaryTableStrategy { return temporaryTable; } + public EntityMappingType getEntityDescriptor() { + return getTemporaryTable().getEntityDescriptor(); + } + public boolean isDropIdTables() { return dropIdTables; } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/PersistentTableStrategy.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/PersistentTableStrategy.java index ac8aad8ca8..481894fab5 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/PersistentTableStrategy.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/PersistentTableStrategy.java @@ -15,6 +15,7 @@ import org.hibernate.engine.config.spi.ConfigurationService; import org.hibernate.engine.config.spi.StandardConverters; import org.hibernate.engine.jdbc.connections.spi.JdbcConnectionAccess; import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.mapping.internal.MappingModelCreationProcess; import org.jboss.logging.Logger; @@ -40,7 +41,6 @@ public abstract class PersistentTableStrategy { public static final String CATALOG = "hibernate.hql.bulk_id_strategy.persistent.catalog"; private final TemporaryTable temporaryTable; - private final SessionFactoryImplementor sessionFactory; private boolean prepared; @@ -58,6 +58,10 @@ public abstract class PersistentTableStrategy { } } + public EntityMappingType getEntityDescriptor() { + return getTemporaryTable().getEntityDescriptor(); + } + public void prepare( MappingModelCreationProcess mappingModelCreationProcess, JdbcConnectionAccess connectionAccess) { diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/RestrictedDeleteExecutionDelegate.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/RestrictedDeleteExecutionDelegate.java index 245082a0f6..a8c7d1f029 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/RestrictedDeleteExecutionDelegate.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/RestrictedDeleteExecutionDelegate.java @@ -36,7 +36,6 @@ import org.hibernate.query.spi.QueryParameterBindings; import org.hibernate.query.sqm.internal.DomainParameterXref; import org.hibernate.query.sqm.internal.SqmJdbcExecutionContextAdapter; import org.hibernate.query.sqm.internal.SqmUtil; -import org.hibernate.query.sqm.mutation.internal.MultiTableSqmMutationConverter; import org.hibernate.query.sqm.mutation.internal.SqmMutationStrategyHelper; import org.hibernate.query.sqm.mutation.internal.TableKeyExpressionCollector; import org.hibernate.query.sqm.spi.SqmParameterMappingModelResolutionAccess; @@ -62,62 +61,45 @@ import org.hibernate.sql.exec.spi.ExecutionContext; import org.hibernate.sql.exec.spi.JdbcOperationQueryDelete; import org.hibernate.sql.exec.spi.JdbcParameterBindings; -import org.jboss.logging.Logger; +import static org.hibernate.query.sqm.mutation.internal.MutationQueryLogging.MUTATION_QUERY_LOGGER; /** * @author Steve Ebersole */ -public class RestrictedDeleteExecutionDelegate implements TableBasedDeleteHandler.ExecutionDelegate { - private static final Logger log = Logger.getLogger( RestrictedDeleteExecutionDelegate.class ); - - private final EntityMappingType entityDescriptor; - private final TemporaryTable idTable; - private final AfterUseAction afterUseAction; - private final SqmDeleteStatement sqmDelete; - private final DomainParameterXref domainParameterXref; - private final SessionFactoryImplementor sessionFactory; - - private final Function sessionUidAccess; - private final MultiTableSqmMutationConverter converter; - +public class RestrictedDeleteExecutionDelegate extends AbstractDeleteExecutionDelegate { public RestrictedDeleteExecutionDelegate( EntityMappingType entityDescriptor, TemporaryTable idTable, AfterUseAction afterUseAction, SqmDeleteStatement sqmDelete, DomainParameterXref domainParameterXref, - Function sessionUidAccess, QueryOptions queryOptions, LoadQueryInfluencers loadQueryInfluencers, QueryParameterBindings queryParameterBindings, + Function sessionUidAccess, SessionFactoryImplementor sessionFactory) { - this.entityDescriptor = entityDescriptor; - this.idTable = idTable; - this.afterUseAction = afterUseAction; - this.sqmDelete = sqmDelete; - this.domainParameterXref = domainParameterXref; - this.sessionUidAccess = sessionUidAccess; - this.sessionFactory = sessionFactory; - this.converter = new MultiTableSqmMutationConverter( + super( entityDescriptor, + idTable, + afterUseAction, sqmDelete, - sqmDelete.getTarget(), domainParameterXref, queryOptions, loadQueryInfluencers, queryParameterBindings, + sessionUidAccess, sessionFactory ); } @Override public int execute(DomainQueryExecutionContext executionContext) { - final EntityPersister entityDescriptor = sessionFactory.getRuntimeMetamodels() + final EntityPersister entityDescriptor = getSessionFactory().getRuntimeMetamodels() .getMappingMetamodel() - .getEntityDescriptor( sqmDelete.getTarget().getEntityName() ); + .getEntityDescriptor( getSqmDelete().getTarget().getEntityName() ); final String hierarchyRootTableName = ( (Joinable) entityDescriptor ).getTableName(); - final TableGroup deletingTableGroup = converter.getMutatingTableGroup(); + final TableGroup deletingTableGroup = getConverter().getMutatingTableGroup(); final TableReference hierarchyRootTableReference = deletingTableGroup.resolveTableReference( deletingTableGroup.getNavigablePath(), @@ -128,7 +110,7 @@ public class RestrictedDeleteExecutionDelegate implements TableBasedDeleteHandle final Map, List>> parameterResolutions; final Map, MappingModelExpressible> paramTypeResolutions; - if ( domainParameterXref.getSqmParameterCount() == 0 ) { + if ( getDomainParameterXref().getSqmParameterCount() == 0 ) { parameterResolutions = Collections.emptyMap(); paramTypeResolutions = Collections.emptyMap(); } @@ -143,8 +125,8 @@ public class RestrictedDeleteExecutionDelegate implements TableBasedDeleteHandle // table it comes from. if all of the referenced columns (if any at all) are from the root table // we can perform all of the deletes without using an id-table final MutableBoolean needsIdTableWrapper = new MutableBoolean( false ); - final Predicate specifiedRestriction = converter.visitWhereClause( - sqmDelete.getWhereClause(), + final Predicate specifiedRestriction = getConverter().visitWhereClause( + getSqmDelete().getWhereClause(), columnReference -> { if ( ! hierarchyRootTableReference.getIdentificationVariable().equals( columnReference.getQualifier() ) ) { needsIdTableWrapper.setValue( true ); @@ -169,10 +151,10 @@ public class RestrictedDeleteExecutionDelegate implements TableBasedDeleteHandle true, executionContext.getSession().getLoadQueryInfluencers().getEnabledFilters(), null, - converter + getConverter() ); - converter.pruneTableGroupJoins(); + getConverter().pruneTableGroupJoins(); // We need an id table if we want to delete from an intermediate table to avoid FK violations // The intermediate table has a FK to the root table, so we can't delete from the root table first @@ -198,7 +180,7 @@ public class RestrictedDeleteExecutionDelegate implements TableBasedDeleteHandle deletingTableGroup, parameterResolutions, paramTypeResolutions, - converter.getSqlExpressionResolver(), + getConverter().getSqlExpressionResolver(), executionContextAdapter ); } @@ -211,9 +193,9 @@ public class RestrictedDeleteExecutionDelegate implements TableBasedDeleteHandle Map, MappingModelExpressible> paramTypeResolutions, SqlExpressionResolver sqlExpressionResolver, ExecutionContext executionContext) { - assert entityDescriptor == entityDescriptor.getRootEntityDescriptor(); + assert getEntityDescriptor() == getEntityDescriptor().getRootEntityDescriptor(); - final EntityPersister rootEntityPersister = entityDescriptor.getEntityPersister(); + final EntityPersister rootEntityPersister = getEntityDescriptor().getEntityPersister(); final String rootTableName = ( (Joinable) rootEntityPersister ).getTableName(); final NamedTableReference rootTableReference = (NamedTableReference) tableGroup.resolveTableReference( tableGroup.getNavigablePath(), @@ -226,17 +208,17 @@ public class RestrictedDeleteExecutionDelegate implements TableBasedDeleteHandle suppliedPredicate, rootEntityPersister, sqlExpressionResolver, - sessionFactory + getSessionFactory() ); final JdbcParameterBindings jdbcParameterBindings = SqmUtil.createJdbcParameterBindings( executionContext.getQueryParameterBindings(), - domainParameterXref, + getDomainParameterXref(), SqmUtil.generateJdbcParamsXref( - domainParameterXref, + getDomainParameterXref(), () -> restrictionSqmParameterResolutions ), - sessionFactory.getRuntimeMetamodels().getMappingMetamodel(), + getSessionFactory().getRuntimeMetamodels().getMappingMetamodel(), navigablePath -> tableGroup, new SqmParameterMappingModelResolutionAccess() { @Override @SuppressWarnings("unchecked") @@ -248,7 +230,7 @@ public class RestrictedDeleteExecutionDelegate implements TableBasedDeleteHandle ); SqmMutationStrategyHelper.cleanUpCollectionTables( - entityDescriptor, + getEntityDescriptor(), (tableReference, attributeMapping) -> { // No need for a predicate if there is no supplied predicate i.e. this is a full cleanup if ( suppliedPredicate == null ) { @@ -267,7 +249,7 @@ public class RestrictedDeleteExecutionDelegate implements TableBasedDeleteHandle suppliedPredicate, rootEntityPersister, sqlExpressionResolver, - sessionFactory + getSessionFactory() ); } return new InSubQueryPredicate( @@ -279,7 +261,7 @@ public class RestrictedDeleteExecutionDelegate implements TableBasedDeleteHandle ), fkDescriptor, null, - sessionFactory + getSessionFactory() ), idSelectFkSubQuery, false @@ -292,7 +274,7 @@ public class RestrictedDeleteExecutionDelegate implements TableBasedDeleteHandle if ( rootTableReference instanceof UnionTableReference ) { final MutableInteger rows = new MutableInteger(); - entityDescriptor.visitConstraintOrderedTables( + getEntityDescriptor().visitConstraintOrderedTables( (tableExpression, tableKeyColumnVisitationSupplier) -> { final NamedTableReference tableReference = new NamedTableReference( tableExpression, @@ -322,7 +304,7 @@ public class RestrictedDeleteExecutionDelegate implements TableBasedDeleteHandle return rows.get(); } else { - entityDescriptor.visitConstraintOrderedTables( + getEntityDescriptor().visitConstraintOrderedTables( (tableExpression, tableKeyColumnVisitationSupplier) -> { if ( !tableExpression.equals( rootTableName ) ) { final NamedTableReference tableReference = (NamedTableReference) tableGroup.getTableReference( @@ -381,7 +363,7 @@ public class RestrictedDeleteExecutionDelegate implements TableBasedDeleteHandle JdbcParameterBindings jdbcParameterBindings, ExecutionContext executionContext) { assert targetTableReference != null; - log.tracef( "deleteFromNonRootTable - %s", targetTableReference.getTableExpression() ); + MUTATION_QUERY_LOGGER.tracef( "deleteFromNonRootTable - %s", targetTableReference.getTableExpression() ); final NamedTableReference deleteTableReference = new NamedTableReference( targetTableReference.getTableExpression(), @@ -423,7 +405,7 @@ public class RestrictedDeleteExecutionDelegate implements TableBasedDeleteHandle deletingTableColumnRefsExpression = deletingTableColumnRefs.get( 0 ); } else { - deletingTableColumnRefsExpression = new SqlTuple( deletingTableColumnRefs, entityDescriptor.getIdentifierMapping() ); + deletingTableColumnRefsExpression = new SqlTuple( deletingTableColumnRefs, getEntityDescriptor().getIdentifierMapping() ); } tableDeletePredicate = new InSubQueryPredicate( @@ -439,7 +421,7 @@ public class RestrictedDeleteExecutionDelegate implements TableBasedDeleteHandle jdbcParameterBindings, executionContext ); - log.debugf( "deleteFromNonRootTable - `%s` : %s rows", targetTableReference, rows ); + MUTATION_QUERY_LOGGER.debugf( "deleteFromNonRootTable - `%s` : %s rows", targetTableReference, rows ); return rows; } @@ -477,12 +459,12 @@ public class RestrictedDeleteExecutionDelegate implements TableBasedDeleteHandle ExecutionContext executionContext) { final JdbcParameterBindings jdbcParameterBindings = SqmUtil.createJdbcParameterBindings( executionContext.getQueryParameterBindings(), - domainParameterXref, + getDomainParameterXref(), SqmUtil.generateJdbcParamsXref( - domainParameterXref, + getDomainParameterXref(), () -> restrictionSqmParameterResolutions ), - sessionFactory.getRuntimeMetamodels().getMappingMetamodel(), + getSessionFactory().getRuntimeMetamodels().getMappingMetamodel(), navigablePath -> deletingTableGroup, new SqmParameterMappingModelResolutionAccess() { @Override @SuppressWarnings("unchecked") @@ -494,7 +476,7 @@ public class RestrictedDeleteExecutionDelegate implements TableBasedDeleteHandle ); ExecuteWithTemporaryTableHelper.performBeforeTemporaryTableUseActions( - idTable, + getIdTable(), executionContext ); @@ -503,9 +485,9 @@ public class RestrictedDeleteExecutionDelegate implements TableBasedDeleteHandle } finally { ExecuteWithTemporaryTableHelper.performAfterTemporaryTableUseActions( - idTable, - sessionUidAccess, - afterUseAction, + getIdTable(), + getSessionUidAccess(), + getAfterUseAction(), executionContext ); } @@ -516,23 +498,23 @@ public class RestrictedDeleteExecutionDelegate implements TableBasedDeleteHandle ExecutionContext executionContext, JdbcParameterBindings jdbcParameterBindings) { final int rows = ExecuteWithTemporaryTableHelper.saveMatchingIdsIntoIdTable( - converter, + getConverter(), predicate, - idTable, - sessionUidAccess, + getIdTable(), + getSessionUidAccess(), jdbcParameterBindings, executionContext ); final QuerySpec idTableIdentifierSubQuery = ExecuteWithTemporaryTableHelper.createIdTableSelectQuerySpec( - idTable, - sessionUidAccess, - entityDescriptor, + getIdTable(), + getSessionUidAccess(), + getEntityDescriptor(), executionContext ); SqmMutationStrategyHelper.cleanUpCollectionTables( - entityDescriptor, + getEntityDescriptor(), (tableReference, attributeMapping) -> { final ForeignKeyDescriptor fkDescriptor = attributeMapping.getKeyDescriptor(); final QuerySpec idTableFkSubQuery; @@ -541,10 +523,10 @@ public class RestrictedDeleteExecutionDelegate implements TableBasedDeleteHandle } else { idTableFkSubQuery = ExecuteWithTemporaryTableHelper.createIdTableSelectQuerySpec( - idTable, + getIdTable(), fkDescriptor.getTargetPart(), - sessionUidAccess, - entityDescriptor, + getSessionUidAccess(), + getEntityDescriptor(), executionContext ); } @@ -557,7 +539,7 @@ public class RestrictedDeleteExecutionDelegate implements TableBasedDeleteHandle ), fkDescriptor, null, - sessionFactory + getSessionFactory() ), idTableFkSubQuery, false @@ -568,7 +550,7 @@ public class RestrictedDeleteExecutionDelegate implements TableBasedDeleteHandle executionContext ); - entityDescriptor.visitConstraintOrderedTables( + getEntityDescriptor().visitConstraintOrderedTables( (tableExpression, tableKeyColumnVisitationSupplier) -> deleteFromTableUsingIdTable( tableExpression, tableKeyColumnVisitationSupplier, @@ -585,11 +567,9 @@ public class RestrictedDeleteExecutionDelegate implements TableBasedDeleteHandle Supplier> tableKeyColumnVisitationSupplier, QuerySpec idTableSubQuery, ExecutionContext executionContext) { - log.tracef( "deleteFromTableUsingIdTable - %s", tableExpression ); + MUTATION_QUERY_LOGGER.tracef( "deleteFromTableUsingIdTable - %s", tableExpression ); - final SessionFactoryImplementor factory = executionContext.getSession().getFactory(); - - final TableKeyExpressionCollector keyColumnCollector = new TableKeyExpressionCollector( entityDescriptor ); + final TableKeyExpressionCollector keyColumnCollector = new TableKeyExpressionCollector( getEntityDescriptor() ); final NamedTableReference targetTable = new NamedTableReference( tableExpression, DeleteStatement.DEFAULT_ALIAS, diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/SoftDeleteExecutionDelegate.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/SoftDeleteExecutionDelegate.java new file mode 100644 index 0000000000..a2b6c72f7d --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/SoftDeleteExecutionDelegate.java @@ -0,0 +1,432 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.query.sqm.mutation.internal.temptable; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +import org.hibernate.boot.model.internal.SoftDeleteHelper; +import org.hibernate.dialect.temptable.TemporaryTable; +import org.hibernate.engine.jdbc.spi.JdbcServices; +import org.hibernate.engine.spi.LoadQueryInfluencers; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.internal.util.MutableBoolean; +import org.hibernate.metamodel.mapping.EntityMappingType; +import org.hibernate.metamodel.mapping.ForeignKeyDescriptor; +import org.hibernate.metamodel.mapping.MappingModelExpressible; +import org.hibernate.metamodel.mapping.TableDetails; +import org.hibernate.metamodel.mapping.internal.MappingModelCreationHelper; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.query.spi.DomainQueryExecutionContext; +import org.hibernate.query.spi.QueryOptions; +import org.hibernate.query.spi.QueryParameterBindings; +import org.hibernate.query.sqm.internal.DomainParameterXref; +import org.hibernate.query.sqm.internal.SqmJdbcExecutionContextAdapter; +import org.hibernate.query.sqm.internal.SqmUtil; +import org.hibernate.query.sqm.mutation.internal.MultiTableSqmMutationConverter; +import org.hibernate.query.sqm.mutation.internal.SqmMutationStrategyHelper; +import org.hibernate.query.sqm.spi.SqmParameterMappingModelResolutionAccess; +import org.hibernate.query.sqm.tree.delete.SqmDeleteStatement; +import org.hibernate.query.sqm.tree.expression.SqmParameter; +import org.hibernate.spi.NavigablePath; +import org.hibernate.sql.ast.tree.delete.DeleteStatement; +import org.hibernate.sql.ast.tree.expression.ColumnReference; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.JdbcParameter; +import org.hibernate.sql.ast.tree.expression.SqlTuple; +import org.hibernate.sql.ast.tree.from.MutatingTableReferenceGroupWrapper; +import org.hibernate.sql.ast.tree.from.NamedTableReference; +import org.hibernate.sql.ast.tree.from.TableGroup; +import org.hibernate.sql.ast.tree.predicate.InSubQueryPredicate; +import org.hibernate.sql.ast.tree.predicate.Predicate; +import org.hibernate.sql.ast.tree.predicate.PredicateCollector; +import org.hibernate.sql.ast.tree.select.QuerySpec; +import org.hibernate.sql.ast.tree.update.Assignment; +import org.hibernate.sql.ast.tree.update.UpdateStatement; +import org.hibernate.sql.exec.spi.ExecutionContext; +import org.hibernate.sql.exec.spi.JdbcOperationQueryUpdate; +import org.hibernate.sql.exec.spi.JdbcParameterBindings; +import org.hibernate.sql.results.internal.SqlSelectionImpl; + +import static org.hibernate.query.sqm.internal.SqmJdbcExecutionContextAdapter.omittingLockingAndPaging; + +/** + * @author Steve Ebersole + */ +public class SoftDeleteExecutionDelegate extends AbstractDeleteExecutionDelegate { + public SoftDeleteExecutionDelegate( + EntityMappingType entityDescriptor, + TemporaryTable idTable, + AfterUseAction afterUseAction, + SqmDeleteStatement sqmDelete, + DomainParameterXref domainParameterXref, + QueryOptions queryOptions, + LoadQueryInfluencers loadQueryInfluencers, + QueryParameterBindings queryParameterBindings, + Function sessionUidAccess, + SessionFactoryImplementor sessionFactory) { + super( + entityDescriptor, + idTable, + afterUseAction, + sqmDelete, + domainParameterXref, + queryOptions, + loadQueryInfluencers, + queryParameterBindings, + sessionUidAccess, + sessionFactory + ); + } + + @Override + public int execute(DomainQueryExecutionContext domainQueryExecutionContext) { + final String targetEntityName = getSqmDelete().getTarget().getEntityName(); + final EntityPersister targetEntityDescriptor = getSessionFactory() + .getRuntimeMetamodels() + .getMappingMetamodel() + .getEntityDescriptor( targetEntityName ); + + final EntityMappingType rootEntityDescriptor = targetEntityDescriptor.getRootEntityDescriptor(); + final boolean targetIsHierarchyRoot = rootEntityDescriptor == targetEntityDescriptor; + + // determine if we need to use a sub-query for matching ids - + // 1. if the target is not the root we will + // 2. if the supplied predicate (if any) refers to columns from a table + // other than the identifier table we will + final MutableBoolean needsSubQueryRef = new MutableBoolean( !targetIsHierarchyRoot ); + + final SqmJdbcExecutionContextAdapter executionContext = omittingLockingAndPaging( domainQueryExecutionContext ); + + final Map, List>> parameterResolutions; + final Map, MappingModelExpressible> paramTypeResolutions; + if ( getDomainParameterXref().getSqmParameterCount() == 0 ) { + parameterResolutions = Collections.emptyMap(); + paramTypeResolutions = Collections.emptyMap(); + } + else { + parameterResolutions = new IdentityHashMap<>(); + paramTypeResolutions = new LinkedHashMap<>(); + } + + final TableGroup deletingTableGroup = getConverter().getMutatingTableGroup(); + final TableDetails softDeleteTable = rootEntityDescriptor.getSoftDeleteTableDetails(); + final NamedTableReference rootTableReference = (NamedTableReference) deletingTableGroup.resolveTableReference( + deletingTableGroup.getNavigablePath(), + softDeleteTable.getTableName() + ); + assert rootTableReference != null; + + // NOTE : `converter.visitWhereClause` already applies the soft-delete restriction + final Predicate specifiedRestriction = getConverter().visitWhereClause( + getSqmDelete().getWhereClause(), + columnReference -> { + if ( !rootTableReference.getIdentificationVariable().equals( columnReference.getQualifier() ) ) { + // the predicate referred to a column from a table other than hierarchy identifier table + needsSubQueryRef.setValue( true ); + } + }, + (sqmParameter, mappingType, jdbcParameters) -> { + parameterResolutions.computeIfAbsent( + sqmParameter, + k -> new ArrayList<>( 1 ) + ).add( jdbcParameters ); + paramTypeResolutions.put( sqmParameter, mappingType ); + } + ); + + final PredicateCollector predicateCollector = new PredicateCollector( specifiedRestriction ); + targetEntityDescriptor.applyBaseRestrictions( + (filterPredicate) -> { + needsSubQueryRef.setValue( true ); + predicateCollector.applyPredicate( filterPredicate ); + }, + deletingTableGroup, + true, + executionContext.getSession().getLoadQueryInfluencers().getEnabledFilters(), + null, + getConverter() + ); + + getConverter().pruneTableGroupJoins(); + + final JdbcParameterBindings jdbcParameterBindings = SqmUtil.createJdbcParameterBindings( + executionContext.getQueryParameterBindings(), + getDomainParameterXref(), + SqmUtil.generateJdbcParamsXref( + getDomainParameterXref(), + () -> parameterResolutions + ), + getSessionFactory().getRuntimeMetamodels().getMappingMetamodel(), + navigablePath -> deletingTableGroup, + new SqmParameterMappingModelResolutionAccess() { + @Override @SuppressWarnings("unchecked") + public MappingModelExpressible getResolvedMappingModelType(SqmParameter parameter) { + return (MappingModelExpressible) paramTypeResolutions.get(parameter); + } + }, + executionContext.getSession() + ); + + final boolean needsSubQuery = needsSubQueryRef.getValue(); + if ( needsSubQuery ) { + if ( getSessionFactory().getJdbcServices().getDialect().supportsSubqueryOnMutatingTable() ) { + return performDeleteWithSubQuery( + targetEntityDescriptor, + rootEntityDescriptor, + deletingTableGroup, + rootTableReference, + predicateCollector, + jdbcParameterBindings, + getConverter(), + executionContext + ); + } + else { + return performDeleteWithIdTable( + rootEntityDescriptor, + rootTableReference, + predicateCollector, + jdbcParameterBindings, + executionContext + ); + } + } + else { + return performDirectDelete( + targetEntityDescriptor, + rootEntityDescriptor, + deletingTableGroup, + rootTableReference, + predicateCollector, + jdbcParameterBindings, + getConverter(), + executionContext + ); + } + } + + private int performDeleteWithIdTable( + EntityMappingType rootEntityDescriptor, + NamedTableReference targetTableReference, + PredicateCollector predicateCollector, + JdbcParameterBindings jdbcParameterBindings, + SqmJdbcExecutionContextAdapter executionContext) { + ExecuteWithTemporaryTableHelper.performBeforeTemporaryTableUseActions( + getIdTable(), + executionContext + ); + + try { + return deleteUsingIdTable( + rootEntityDescriptor, + targetTableReference, + predicateCollector, + jdbcParameterBindings, + executionContext + ); + } + finally { + ExecuteWithTemporaryTableHelper.performAfterTemporaryTableUseActions( + getIdTable(), + getSessionUidAccess(), + getAfterUseAction(), + executionContext + ); + } + } + + private int deleteUsingIdTable( + EntityMappingType rootEntityDescriptor, + NamedTableReference targetTableReference, + PredicateCollector predicateCollector, + JdbcParameterBindings jdbcParameterBindings, + SqmJdbcExecutionContextAdapter executionContext) { + final int rows = ExecuteWithTemporaryTableHelper.saveMatchingIdsIntoIdTable( + getConverter(), + predicateCollector.getPredicate(), + getIdTable(), + getSessionUidAccess(), + jdbcParameterBindings, + executionContext + ); + + final QuerySpec idTableIdentifierSubQuery = ExecuteWithTemporaryTableHelper.createIdTableSelectQuerySpec( + getIdTable(), + getSessionUidAccess(), + getEntityDescriptor(), + executionContext + ); + + SqmMutationStrategyHelper.cleanUpCollectionTables( + getEntityDescriptor(), + (tableReference, attributeMapping) -> { + final ForeignKeyDescriptor fkDescriptor = attributeMapping.getKeyDescriptor(); + final QuerySpec idTableFkSubQuery; + if ( fkDescriptor.getTargetPart().isEntityIdentifierMapping() ) { + idTableFkSubQuery = idTableIdentifierSubQuery; + } + else { + idTableFkSubQuery = ExecuteWithTemporaryTableHelper.createIdTableSelectQuerySpec( + getIdTable(), + fkDescriptor.getTargetPart(), + getSessionUidAccess(), + getEntityDescriptor(), + executionContext + ); + } + return new InSubQueryPredicate( + MappingModelCreationHelper.buildColumnReferenceExpression( + new MutatingTableReferenceGroupWrapper( + new NavigablePath( attributeMapping.getRootPathName() ), + attributeMapping, + (NamedTableReference) tableReference + ), + fkDescriptor, + null, + getSessionFactory() + ), + idTableFkSubQuery, + false + ); + + }, + JdbcParameterBindings.NO_BINDINGS, + executionContext + ); + + final Assignment softDeleteAssignment = SoftDeleteHelper.createSoftDeleteAssignment( + targetTableReference, + rootEntityDescriptor.getSoftDeleteMapping() + ); + + final TableDetails softDeleteTable = rootEntityDescriptor.getSoftDeleteTableDetails(); + final TableDetails.KeyDetails keyDetails = softDeleteTable.getKeyDetails(); + final List idExpressions = new ArrayList<>( keyDetails.getColumnCount() ); + keyDetails.forEachKeyColumn( (position, column) -> idExpressions.add( + new ColumnReference( targetTableReference, column ) + ) ); + final Expression idExpression = idExpressions.size() == 1 + ? idExpressions.get( 0 ) + : new SqlTuple( idExpressions, rootEntityDescriptor.getIdentifierMapping() ); + + final UpdateStatement updateStatement = new UpdateStatement( + targetTableReference, + Collections.singletonList( softDeleteAssignment ), + new InSubQueryPredicate( idExpression, idTableIdentifierSubQuery, false ) + ); + + executeUpdate( updateStatement, jdbcParameterBindings, executionContext ); + + return rows; + } + + private int performDeleteWithSubQuery( + EntityMappingType targetEntityDescriptor, + EntityMappingType rootEntityDescriptor, + TableGroup deletingTableGroup, + NamedTableReference rootTableReference, + PredicateCollector predicateCollector, + JdbcParameterBindings jdbcParameterBindings, + MultiTableSqmMutationConverter converter, + SqmJdbcExecutionContextAdapter executionContext) { + final QuerySpec matchingIdSubQuery = new QuerySpec( false, 1 ); + matchingIdSubQuery.getFromClause().addRoot( deletingTableGroup ); + + final TableDetails identifierTableDetails = rootEntityDescriptor.getIdentifierTableDetails(); + final TableDetails.KeyDetails keyDetails = identifierTableDetails.getKeyDetails(); + + final NamedTableReference targetTable = new NamedTableReference( + identifierTableDetails.getTableName(), + DeleteStatement.DEFAULT_ALIAS, + false + ); + + final List idExpressions = new ArrayList<>( keyDetails.getColumnCount() ); + keyDetails.forEachKeyColumn( (position, column) -> { + final Expression columnReference = converter.getSqlExpressionResolver().resolveSqlExpression( + rootTableReference, + column + ); + matchingIdSubQuery.getSelectClause().addSqlSelection( + new SqlSelectionImpl( position, columnReference ) + ); + idExpressions.add( new ColumnReference( targetTable, column ) ); + } ); + + matchingIdSubQuery.applyPredicate( predicateCollector.getPredicate() ); + final Expression idExpression = idExpressions.size() == 1 + ? idExpressions.get( 0 ) + : new SqlTuple( idExpressions, rootEntityDescriptor.getIdentifierMapping() ); + + final Assignment softDeleteAssignment = SoftDeleteHelper.createSoftDeleteAssignment( + targetTable, + rootEntityDescriptor.getSoftDeleteMapping() + ); + + final UpdateStatement updateStatement = new UpdateStatement( + targetTable, + Collections.singletonList( softDeleteAssignment ), + new InSubQueryPredicate( idExpression, matchingIdSubQuery, false ) + ); + + return executeUpdate( updateStatement, jdbcParameterBindings, executionContext ); + } + + private int performDirectDelete( + EntityMappingType targetEntityDescriptor, + EntityMappingType rootEntityDescriptor, + TableGroup deletingTableGroup, + NamedTableReference rootTableReference, + PredicateCollector predicateCollector, + JdbcParameterBindings jdbcParameterBindings, + MultiTableSqmMutationConverter converter, + SqmJdbcExecutionContextAdapter executionContext) { + final Assignment softDeleteAssignment = SoftDeleteHelper.createSoftDeleteAssignment( + rootTableReference, + rootEntityDescriptor.getSoftDeleteMapping() + ); + + final UpdateStatement updateStatement = new UpdateStatement( + rootTableReference, + Collections.singletonList( softDeleteAssignment ), + predicateCollector.getPredicate() + ); + + return executeUpdate( updateStatement, jdbcParameterBindings, executionContext ); + } + + private int executeUpdate( + UpdateStatement updateStatement, + JdbcParameterBindings jdbcParameterBindings, + ExecutionContext executionContext) { + final SessionFactoryImplementor factory = executionContext.getSession().getFactory(); + final JdbcServices jdbcServices = factory.getJdbcServices(); + + final JdbcOperationQueryUpdate jdbcUpdate = jdbcServices.getJdbcEnvironment() + .getSqlAstTranslatorFactory() + .buildUpdateTranslator( factory, updateStatement ) + .translate( jdbcParameterBindings, executionContext.getQueryOptions() ); + + return jdbcServices.getJdbcMutationExecutor().execute( + jdbcUpdate, + jdbcParameterBindings, + sql -> executionContext.getSession() + .getJdbcCoordinator() + .getStatementPreparer() + .prepareStatement( sql ), + (integer, preparedStatement) -> {}, + executionContext + ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/TableBasedDeleteHandler.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/TableBasedDeleteHandler.java index 2c2add2e15..b95a336de6 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/TableBasedDeleteHandler.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/TableBasedDeleteHandler.java @@ -65,16 +65,31 @@ public class TableBasedDeleteHandler } protected ExecutionDelegate resolveDelegate(DomainQueryExecutionContext executionContext) { + if ( getEntityDescriptor().getSoftDeleteMapping() != null ) { + return new SoftDeleteExecutionDelegate( + getEntityDescriptor(), + idTable, + afterUseAction, + getSqmDeleteOrUpdateStatement(), + domainParameterXref, + executionContext.getQueryOptions(), + executionContext.getSession().getLoadQueryInfluencers(), + executionContext.getQueryParameterBindings(), + sessionUidAccess, + getSessionFactory() + ); + } + return new RestrictedDeleteExecutionDelegate( getEntityDescriptor(), idTable, afterUseAction, getSqmDeleteOrUpdateStatement(), domainParameterXref, - sessionUidAccess, executionContext.getQueryOptions(), executionContext.getSession().getLoadQueryInfluencers(), executionContext.getQueryParameterBindings(), + sessionUidAccess, getSessionFactory() ); } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/UpdateExecutionDelegate.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/UpdateExecutionDelegate.java index 97a8bd7322..303d218f33 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/UpdateExecutionDelegate.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/UpdateExecutionDelegate.java @@ -14,6 +14,7 @@ import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; +import org.hibernate.boot.model.internal.SoftDeleteHelper; import org.hibernate.dialect.temptable.TemporaryTable; import org.hibernate.engine.jdbc.spi.JdbcServices; import org.hibernate.engine.spi.SessionFactoryImplementor; @@ -23,6 +24,7 @@ import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.mapping.MappingModelExpressible; import org.hibernate.metamodel.mapping.ModelPartContainer; import org.hibernate.metamodel.mapping.SelectableConsumer; +import org.hibernate.metamodel.mapping.SoftDeleteMapping; import org.hibernate.persister.entity.AbstractEntityPersister; import org.hibernate.query.SemanticException; import org.hibernate.query.results.TableGroupImpl; @@ -97,15 +99,29 @@ public class UpdateExecutionDelegate implements TableBasedUpdateHandler.Executio this.afterUseAction = afterUseAction; this.sessionUidAccess = sessionUidAccess; this.updatingTableGroup = updatingTableGroup; - this.suppliedPredicate = suppliedPredicate; - this.sessionFactory = executionContext.getSession().getFactory(); final ModelPartContainer updatingModelPart = updatingTableGroup.getModelPart(); assert updatingModelPart instanceof EntityMappingType; - this.entityDescriptor = (EntityMappingType) updatingModelPart; + final SoftDeleteMapping softDeleteMapping = entityDescriptor.getSoftDeleteMapping(); + if ( softDeleteMapping != null ) { + final NamedTableReference rootTableReference = (NamedTableReference) updatingTableGroup.resolveTableReference( + updatingTableGroup.getNavigablePath(), + entityDescriptor.getIdentifierTableDetails().getTableName() + ); + this.suppliedPredicate = Predicate.combinePredicates( + suppliedPredicate, + SoftDeleteHelper.createNonSoftDeletedRestriction( rootTableReference, softDeleteMapping ) + ); + } + else { + this.suppliedPredicate = suppliedPredicate; + } + + + this.assignmentsByTable = CollectionHelper.mapOfSize( updatingTableGroup.getTableReferenceJoins().size() + 1 ); jdbcParameterBindings = SqmUtil.createJdbcParameterBindings( @@ -513,4 +529,5 @@ public class UpdateExecutionDelegate implements TableBasedUpdateHandler.Executio protected SessionFactoryImplementor getSessionFactory() { return sessionFactory; } + } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java index 6c5cc33185..d82e9ff701 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java @@ -6127,7 +6127,7 @@ public abstract class AbstractSqlAstTranslator implemen @Override public void visitTableReferenceJoin(TableReferenceJoin tableReferenceJoin) { - // nothing to do... handled within TableGroup#render + // nothing to do... handled within TableGroupTableGroup#render } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/ColumnReference.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/ColumnReference.java index 94faeef469..98f6e177ff 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/ColumnReference.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/ColumnReference.java @@ -224,6 +224,15 @@ public class ColumnReference implements Expression, Assignable { @Override public String toString() { + if ( StringHelper.isNotEmpty( qualifier ) ) { + return String.format( + Locale.ROOT, + "%s(%s.%s)", + getClass().getSimpleName(), + qualifier, + getExpressionText() + ); + } return String.format( Locale.ROOT, "%s(%s)", diff --git a/hibernate-core/src/main/java/org/hibernate/sql/model/ast/ColumnWriteFragment.java b/hibernate-core/src/main/java/org/hibernate/sql/model/ast/ColumnWriteFragment.java index 1ac77d37ac..efe70fc607 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/model/ast/ColumnWriteFragment.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/model/ast/ColumnWriteFragment.java @@ -38,7 +38,7 @@ public class ColumnWriteFragment implements Expression { public ColumnWriteFragment(String fragment, ColumnValueParameter parameter, JdbcMapping jdbcMapping) { this( fragment, Collections.singletonList( parameter ), jdbcMapping ); - assert parameter != null; + assert !fragment.contains( "?" ) || parameter != null; } public ColumnWriteFragment(String fragment, List parameters, JdbcMapping jdbcMapping) { diff --git a/hibernate-core/src/main/java/org/hibernate/sql/model/ast/builder/AbstractRestrictedTableMutationBuilder.java b/hibernate-core/src/main/java/org/hibernate/sql/model/ast/builder/AbstractRestrictedTableMutationBuilder.java index 0043b28673..5809d67257 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/model/ast/builder/AbstractRestrictedTableMutationBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/model/ast/builder/AbstractRestrictedTableMutationBuilder.java @@ -76,6 +76,11 @@ public abstract class AbstractRestrictedTableMutationBuilder { + public static final BooleanAsBooleanConverter INSTANCE = new BooleanAsBooleanConverter(); + + @Override + public Boolean convertToDatabaseColumn(Boolean attribute) { + return toRelationalValue( attribute ); + } + + @Override + public Boolean convertToEntityAttribute(Boolean dbData) { + return toDomainValue( dbData ); + } + + @Override + public Boolean toDomainValue(Boolean relationalForm) { + return relationalForm; + } + + @Override + public Boolean toRelationalValue(Boolean domainForm) { + return domainForm; + } + + @Override + public JavaType getDomainJavaType() { + return BooleanJavaType.INSTANCE; + } + + @Override + public JavaType getRelationalJavaType() { + return BooleanJavaType.INSTANCE; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/type/CharBooleanConverter.java b/hibernate-core/src/main/java/org/hibernate/type/CharBooleanConverter.java index 905f6a6afa..785f9570cc 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/CharBooleanConverter.java +++ b/hibernate-core/src/main/java/org/hibernate/type/CharBooleanConverter.java @@ -6,8 +6,6 @@ */ package org.hibernate.type; -import jakarta.persistence.AttributeConverter; -import org.hibernate.type.descriptor.converter.spi.BasicValueConverter; import org.hibernate.type.descriptor.java.BooleanJavaType; import org.hibernate.type.descriptor.java.CharacterJavaType; import org.hibernate.type.descriptor.java.JavaType; @@ -18,11 +16,7 @@ import org.hibernate.type.descriptor.java.JavaType; * @author Steve Ebersole * @author Gavin King */ -public abstract class CharBooleanConverter - implements AttributeConverter, BasicValueConverter { - /** - * Singleton access - */ +public abstract class CharBooleanConverter implements StandardBooleanConverter { @Override public Character convertToDatabaseColumn(Boolean attribute) { return toRelationalValue( attribute ); diff --git a/hibernate-core/src/main/java/org/hibernate/type/NumericBooleanConverter.java b/hibernate-core/src/main/java/org/hibernate/type/NumericBooleanConverter.java index 11da20b1d6..c492358fb0 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/NumericBooleanConverter.java +++ b/hibernate-core/src/main/java/org/hibernate/type/NumericBooleanConverter.java @@ -6,12 +6,10 @@ */ package org.hibernate.type; -import org.hibernate.type.descriptor.converter.spi.BasicValueConverter; import org.hibernate.type.descriptor.java.BooleanJavaType; import org.hibernate.type.descriptor.java.IntegerJavaType; import org.hibernate.type.descriptor.java.JavaType; -import jakarta.persistence.AttributeConverter; import jakarta.persistence.Converter; /** @@ -20,8 +18,7 @@ import jakarta.persistence.Converter; * @author Steve Ebersole */ @Converter -public class NumericBooleanConverter implements AttributeConverter, - BasicValueConverter { +public class NumericBooleanConverter implements StandardBooleanConverter { /** * Singleton access */ diff --git a/hibernate-core/src/main/java/org/hibernate/type/StandardBooleanConverter.java b/hibernate-core/src/main/java/org/hibernate/type/StandardBooleanConverter.java new file mode 100644 index 0000000000..3f6fac65c6 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/type/StandardBooleanConverter.java @@ -0,0 +1,15 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.type; + +/** + * Marker for Hibernate defined converters of Boolean-typed domain values + * + * @author Steve Ebersole + */ +public interface StandardBooleanConverter extends StandardConverter { +} diff --git a/hibernate-core/src/main/java/org/hibernate/type/StandardConverter.java b/hibernate-core/src/main/java/org/hibernate/type/StandardConverter.java new file mode 100644 index 0000000000..dad06af025 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/type/StandardConverter.java @@ -0,0 +1,21 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.type; + +import org.hibernate.type.descriptor.converter.spi.BasicValueConverter; + +import jakarta.persistence.AttributeConverter; + +/** + * Marker for Hibernate supplied {@linkplain AttributeConverter converter} classes. + *

+ * Also implements the Hibernate-specific BasicValueConverter contract + * + * @author Steve Ebersole + */ +public interface StandardConverter extends AttributeConverter, BasicValueConverter { +} diff --git a/hibernate-core/src/main/java/org/hibernate/type/TrueFalseConverter.java b/hibernate-core/src/main/java/org/hibernate/type/TrueFalseConverter.java index c4350d3972..62397c1b69 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/TrueFalseConverter.java +++ b/hibernate-core/src/main/java/org/hibernate/type/TrueFalseConverter.java @@ -19,6 +19,7 @@ public class TrueFalseConverter extends CharBooleanConverter { * Singleton access */ public static final TrueFalseConverter INSTANCE = new TrueFalseConverter(); + private static final String[] VALUES = {"F", "T"}; @Override diff --git a/hibernate-core/src/main/java/org/hibernate/type/YesNoConverter.java b/hibernate-core/src/main/java/org/hibernate/type/YesNoConverter.java index df21220a4c..5f7004573a 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/YesNoConverter.java +++ b/hibernate-core/src/main/java/org/hibernate/type/YesNoConverter.java @@ -19,6 +19,7 @@ public class YesNoConverter extends CharBooleanConverter { * Singleton access */ public static final YesNoConverter INSTANCE = new YesNoConverter(); + private static final String[] VALUES = {"N", "Y"}; @Override diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/transaction/batch/FailingAddToBatchTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/transaction/batch/FailingAddToBatchTest.java index 7ebcd828dd..08cf55b531 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/transaction/batch/FailingAddToBatchTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/transaction/batch/FailingAddToBatchTest.java @@ -57,7 +57,7 @@ public class FailingAddToBatchTest extends AbstractBatchingTest { } @Test - @NotImplementedYet( reason = "Still need to work on entity update executors", strict = false ) + @NotImplementedYet( reason = "Still need to work on entity update executors" ) public void testUpdate(EntityManagerFactoryScope scope) { throw new RuntimeException(); // final Long id = scope.fromTransaction( (em) -> { @@ -80,7 +80,7 @@ public class FailingAddToBatchTest extends AbstractBatchingTest { } @Test - @NotImplementedYet( reason = "Still need to work on entity delete executors", strict = false ) + @NotImplementedYet( reason = "Still need to work on entity delete executors" ) public void testRemove(EntityManagerFactoryScope scope) { throw new RuntimeException(); // Long id = scope.fromTransaction( em -> { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/contributed/EntityHidingTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/contributed/EntityHidingTests.java index eb66ef24de..cc835aab78 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/contributed/EntityHidingTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/contributed/EntityHidingTests.java @@ -41,7 +41,7 @@ import static org.hamcrest.Matchers.nullValue; @SessionFactory public class EntityHidingTests { @Test - @NotImplementedYet( reason = "Contributed entity hiding is not yet implemented", strict = false ) + @NotImplementedYet( reason = "Contributed entity hiding is not yet implemented" ) public void testModel(SessionFactoryScope scope) { final SessionFactoryImplementor sessionFactory = scope.getSessionFactory(); final RuntimeMetamodels runtimeMetamodels = sessionFactory.getRuntimeMetamodels(); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/converted/converter/QueryTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/converted/converter/QueryTest.java index ace1ab874a..86e674c0f8 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/converted/converter/QueryTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/converted/converter/QueryTest.java @@ -95,7 +95,6 @@ public class QueryTest extends BaseNonConfigCoreFunctionalTestCase { @Test @FailureExpected( jiraKey = "HHH-14975", message = "Not yet implemented" ) - @NotImplementedYet @JiraKey( "HHH-14975" ) public void testAutoAppliedConverterAsNativeQueryResult() { inTransaction( (session) -> { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/CollectionOfSoftDeleteTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/CollectionOfSoftDeleteTests.java new file mode 100644 index 0000000000..1bd4365bdb --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/CollectionOfSoftDeleteTests.java @@ -0,0 +1,170 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.orm.test.softdelete; + +import java.sql.Statement; +import java.util.ArrayList; +import java.util.Collection; + +import org.hibernate.annotations.SoftDelete; +import org.hibernate.type.YesNoConverter; + +import org.hibernate.testing.jdbc.SQLStatementInspector; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test for collections which contain soft-deletable entities + * + * @author Steve Ebersole + */ +@DomainModel( annotatedClasses = { CollectionOfSoftDeleteTests.Shelf.class, CollectionOfSoftDeleteTests.Book.class } ) +@SessionFactory(useCollectingStatementInspector = true) +public class CollectionOfSoftDeleteTests { + @BeforeEach + void createTestData(SessionFactoryScope scope) { + scope.inTransaction( (session) -> { + final Shelf horror = new Shelf( 1, "Horror" ); + session.persist( horror ); + + final Book theShining = new Book( 1, "The Shining" ); + horror.addBook( theShining ); + session.persist( theShining ); + + session.flush(); + + session.doWork( (connection) -> { + final Statement statement = connection.createStatement(); + statement.execute( "update books set deleted = 'Y' where id = 1" ); + } ); + } ); + } + + @AfterEach + void dropTestData(SessionFactoryScope scope) { + scope.inTransaction( (session) -> session.doWork( (connection) -> { + final Statement statement = connection.createStatement(); + statement.execute( "delete from books" ); + statement.execute( "delete from shelves" ); + } ) ); + } + + @Test + void testLoading(SessionFactoryScope scope) { + final SQLStatementInspector sqlInspector = scope.getCollectingStatementInspector(); + sqlInspector.clear(); + + scope.inTransaction( (session) -> { + final Shelf loaded = session.get( Shelf.class, 1 ); + assertThat( loaded ).isNotNull(); + + assertThat( sqlInspector.getSqlQueries() ).hasSize( 1 ); + assertThat( sqlInspector.getSqlQueries().get( 0 ) ).doesNotContain( " join " ); + + sqlInspector.clear(); + + assertThat( loaded.getBooks() ).isNotNull(); + assertThat( loaded.getBooks() ).isEmpty(); + + assertThat( sqlInspector.getSqlQueries() ).hasSize( 1 ); + assertThat( sqlInspector.getSqlQueries().get( 0 ) ).contains( ".deleted='N'" ); + } ); + } + + @Test + void testQueryJoin(SessionFactoryScope scope) { + final SQLStatementInspector sqlInspector = scope.getCollectingStatementInspector(); + sqlInspector.clear(); + + scope.inTransaction( (session) -> { + final Shelf shelf = session + .createSelectionQuery( "from Shelf join fetch books", Shelf.class ) + .uniqueResult(); + assertThat( shelf ).isNotNull(); + assertThat( shelf.getBooks() ).isNotNull(); + + assertThat( sqlInspector.getSqlQueries() ).hasSize( 1 ); + assertThat( sqlInspector.getSqlQueries().get( 0 ) ).contains( " join " ); + assertThat( sqlInspector.getSqlQueries().get( 0 ) ).contains( ".deleted='N'" ); + } ); + } + + @Entity(name="Shelf") + @Table(name="shelves") + public static class Shelf { + @Id + private Integer id; + private String name; + @OneToMany + @JoinColumn(name = "shelf_fk") + private Collection books; + + public Shelf() { + } + + public Shelf(Integer id, String name) { + this.id = id; + this.name = name; + } + + public Integer getId() { + return id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Collection getBooks() { + return books; + } + + public void setBooks(Collection books) { + this.books = books; + } + + public void addBook(Book book) { + if ( books == null ) { + books = new ArrayList<>(); + } + books.add( book ); + } + } + + @Entity(name="Book") + @Table(name="books") + @SoftDelete(converter = YesNoConverter.class) + public static class Book { + @Id + private Integer id; + private String name; + + public Book() { + } + + public Book(Integer id, String name) { + this.id = id; + this.name = name; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/MappingTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/MappingTests.java new file mode 100644 index 0000000000..02b63f18cf --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/MappingTests.java @@ -0,0 +1,125 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.orm.test.softdelete; + +import org.hibernate.annotations.SoftDelete; +import org.hibernate.metamodel.spi.MappingMetamodelImplementor; +import org.hibernate.type.BooleanAsBooleanConverter; +import org.hibernate.type.NumericBooleanConverter; +import org.hibernate.type.TrueFalseConverter; +import org.hibernate.type.YesNoConverter; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +/** + * Centralizes the checks about column names, values, etc. + * to avoid problems across dialects + * + * @author Steve Ebersole + */ +@DomainModel(annotatedClasses = { + MappingTests.BooleanEntity.class, + MappingTests.NumericEntity.class, + MappingTests.TrueFalseEntity.class, + MappingTests.YesNoEntity.class, + MappingTests.ReversedYesNoEntity.class +}) +@SessionFactory(exportSchema = false) +@SuppressWarnings("unused") +public class MappingTests { + @Test + void verifyEntityMappings(SessionFactoryScope scope) { + final MappingMetamodelImplementor metamodel = scope.getSessionFactory().getMappingMetamodel(); + + MappingVerifier.verifyMapping( + metamodel.getEntityDescriptor( BooleanEntity.class ).getSoftDeleteMapping(), + "deleted", + "boolean_entity", + true + ); + + MappingVerifier.verifyMapping( + metamodel.getEntityDescriptor( NumericEntity.class ).getSoftDeleteMapping(), + "deleted", + "numeric_entity", + 1 + ); + + MappingVerifier.verifyMapping( + metamodel.getEntityDescriptor( TrueFalseEntity.class ).getSoftDeleteMapping(), + "deleted", + "true_false_entity", + 'T' + ); + + MappingVerifier.verifyMapping( + metamodel.getEntityDescriptor( YesNoEntity.class ).getSoftDeleteMapping(), + "deleted", + "yes_no_entity", + 'Y' + ); + + MappingVerifier.verifyMapping( + metamodel.getEntityDescriptor( ReversedYesNoEntity.class ).getSoftDeleteMapping(), + "active", + "reversed_yes_no_entity", + 'N' + ); + } + + @Entity(name="BooleanEntity") + @Table(name="boolean_entity") + @SoftDelete(converter = BooleanAsBooleanConverter.class) + public static class BooleanEntity { + @Id + private Integer id; + private String name; + } + + @Entity(name="NumericEntity") + @Table(name="numeric_entity") + @SoftDelete(converter = NumericBooleanConverter.class) + public static class NumericEntity { + @Id + private Integer id; + private String name; + } + + @Entity(name="TrueFalseEntity") + @Table(name="true_false_entity") + @SoftDelete(converter = TrueFalseConverter.class) + public static class TrueFalseEntity { + @Id + private Integer id; + private String name; + } + + @Entity(name="YesNoEntity") + @Table(name="yes_no_entity") + @SoftDelete(converter = YesNoConverter.class) + public static class YesNoEntity { + @Id + private Integer id; + private String name; + } + + @Entity(name="ReversedYesNoEntity") + @Table(name="reversed_yes_no_entity") + @SoftDelete(columnName = "active", converter = ReverseYesNoConverter.class) + public static class ReversedYesNoEntity { + @Id + private Integer id; + private String name; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/MappingVerifier.java b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/MappingVerifier.java new file mode 100644 index 0000000000..3af44e04cb --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/MappingVerifier.java @@ -0,0 +1,27 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.orm.test.softdelete; + +import org.hibernate.metamodel.mapping.SoftDeleteMapping; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Steve Ebersole + */ +public class MappingVerifier { + public static void verifyMapping( + SoftDeleteMapping mapping, + String expectedColumnName, + String expectedTableName, + Object expectedDeletedLiteralValue) { + assertThat( mapping ).isNotNull(); + assertThat( mapping.getColumnName() ).isEqualTo( expectedColumnName ); + assertThat( mapping.getTableName() ).isEqualTo( expectedTableName ); + assertThat( mapping.getDeletedLiteralValue() ).isEqualTo( expectedDeletedLiteralValue ); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/ReverseYesNoConverter.java b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/ReverseYesNoConverter.java new file mode 100644 index 0000000000..a8af90bc6a --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/ReverseYesNoConverter.java @@ -0,0 +1,32 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.orm.test.softdelete; + +import jakarta.persistence.AttributeConverter; + +/** + * @author Steve Ebersole + */ +public class ReverseYesNoConverter implements AttributeConverter { + @Override + public Character convertToDatabaseColumn(Boolean attribute) { + return attribute ? 'N' : 'Y'; + } + + @Override + public Boolean convertToEntityAttribute(Character dbData) { + if ( dbData == 'Y' ) { + return false; + } + + if ( dbData == 'N' ) { + return true; + } + + throw new IllegalArgumentException( "Illegal value [" + dbData + "]; expecting 'Y' or 'N'" ); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/SimpleSoftDeleteTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/SimpleSoftDeleteTests.java new file mode 100644 index 0000000000..722cb31627 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/SimpleSoftDeleteTests.java @@ -0,0 +1,274 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.orm.test.softdelete; + +import java.sql.Statement; +import java.util.List; + +import org.hibernate.Hibernate; +import org.hibernate.ObjectNotFoundException; +import org.hibernate.annotations.BatchSize; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.SoftDelete; +import org.hibernate.type.YesNoConverter; + +import org.hibernate.testing.jdbc.SQLStatementInspector; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +/** + * @author Steve Ebersole + */ +@DomainModel(annotatedClasses = { SimpleSoftDeleteTests.SimpleEntity.class, SimpleSoftDeleteTests.BatchLoadable.class }) +@SessionFactory(useCollectingStatementInspector = true) +public class SimpleSoftDeleteTests { + @BeforeEach + void createTestData(SessionFactoryScope scope) { + scope.inTransaction( (session) -> { + session.persist( new SimpleEntity( 1, "first" ) ); + session.persist( new SimpleEntity( 2, "second" ) ); + session.persist( new SimpleEntity( 3, "third" ) ); + + session.persist( new BatchLoadable( 1, "first" ) ); + session.persist( new BatchLoadable( 2, "second" ) ); + + session.flush(); + + session.doWork( (connection) -> { + final Statement statement = connection.createStatement(); + statement.execute( "update simple set removed = 'Y' where id = 1" ); + } ); + } ); + } + + @AfterEach + void tearDown(SessionFactoryScope scope) { + scope.inTransaction( (session) -> session.doWork( (connection) -> { + final Statement statement = connection.createStatement(); + statement.execute( "delete from simple" ); + statement.execute( "delete from batch_loadable" ); + } ) ); + } + + @Test + void testSelectionQuery(SessionFactoryScope scope) { + scope.inTransaction( (session) -> { + // should not return #1 + assertThat( session.createQuery( "from SimpleEntity" ).list() ).hasSize( 2 ); + } ); + } + + @Test + void testLoading(SessionFactoryScope scope) { + // Load + scope.inTransaction( (session) -> { + assertThat( session.get( SimpleEntity.class, 1 ) ).isNull(); + assertThat( session.get( SimpleEntity.class, 2 ) ).isNotNull(); + assertThat( session.get( SimpleEntity.class, 3 ) ).isNotNull(); + } ); + + // Proxy + scope.inTransaction( (session) -> { + final SimpleEntity reference = session.getReference( SimpleEntity.class, 1 ); + try { + reference.getName(); + fail( "Expecting to fail" ); + } + catch (ObjectNotFoundException expected) { + } + + final SimpleEntity reference2 = session.getReference( SimpleEntity.class, 2 ); + reference2.getName(); + + final SimpleEntity reference3 = session.getReference( SimpleEntity.class, 3 ); + reference3.getName(); + } ); + } + + @Test + void testMultiLoading(SessionFactoryScope scope) { + final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); + + scope.inTransaction( (session) -> { + statementInspector.clear(); + final List results = session + .byMultipleIds( SimpleEntity.class ) + // otherwise the first position would contain a null for #1 + .enableOrderedReturn( false ) + .multiLoad( 1, 2, 3 ); + assertThat( results ).hasSize( 2 ); + assertThat( statementInspector.getSqlQueries() ).hasSize( 1 ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( "removed='N'" ); + } ); + } + + @Test + void testNaturalIdLoading(SessionFactoryScope scope) { + final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); + + scope.inTransaction( (session) -> { + statementInspector.clear(); + session.bySimpleNaturalId( SimpleEntity.class ).load( "second" ); + assertThat( statementInspector.getSqlQueries() ).hasSize( 1 ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( "removed='N'" ); + } ); + } + + @Test + void testBatchLoading(SessionFactoryScope scope) { + final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); + + scope.inTransaction( (session) -> { + statementInspector.clear(); + final BatchLoadable first = session.getReference( BatchLoadable.class, 1 ); + final BatchLoadable second = session.getReference( BatchLoadable.class, 2 ); + assertThat( statementInspector.getSqlQueries() ).hasSize( 0 ); + + assertThat( Hibernate.isInitialized( first ) ).isFalse(); + assertThat( Hibernate.isInitialized( second ) ).isFalse(); + + // trigger load + first.getName(); + assertThat( statementInspector.getSqlQueries() ).hasSize( 1 ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( "active='Y'" ); + + assertThat( Hibernate.isInitialized( first ) ).isTrue(); + assertThat( Hibernate.isInitialized( second ) ).isTrue(); + } ); + } + + @Test + void testDeletion(SessionFactoryScope scope) { + scope.inTransaction( (session) -> { + final SimpleEntity reference = session.getReference( SimpleEntity.class, 2 ); + session.remove( reference ); + session.flush(); + + final List active = session + .createSelectionQuery( "from SimpleEntity", SimpleEntity.class ) + .list(); + // #1 was "deleted" up front and we just "deleted" #2... only #3 should be active + assertThat( active ).hasSize( 1 ); + assertThat( active.get(0).getId() ).isEqualTo( 3 ); + } ); + } + + @Test + void testFullUpdateMutationQuery(SessionFactoryScope scope) { + scope.inTransaction( (session) -> { + final int affected = session.createMutationQuery( "update SimpleEntity set name = null" ).executeUpdate(); + assertThat( affected ).isEqualTo( 2 ); + } ); + } + + @Test + void testRestrictedUpdateMutationQuery(SessionFactoryScope scope) { + scope.inTransaction( (session) -> { + final int affected = session + .createMutationQuery( "update SimpleEntity set name = null where name = 'second'" ) + .executeUpdate(); + assertThat( affected ).isEqualTo( 1 ); + } ); + } + + @Test + void testFullDeleteMutationQuery(SessionFactoryScope scope) { + final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); + + scope.inTransaction( (session) -> { + statementInspector.clear(); + final int affected = session.createMutationQuery( "delete SimpleEntity" ).executeUpdate(); + // only #2 and #3 + assertThat( affected ).isEqualTo( 2 ); + } ); + } + + @Test + void testRestrictedDeleteMutationQuery(SessionFactoryScope scope) { + final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); + + scope.inTransaction( (session) -> { + statementInspector.clear(); + final int affected = session + .createMutationQuery( "delete SimpleEntity where name = 'second'" ) + .executeUpdate(); + // only #2 + assertThat( affected ).isEqualTo( 1 ); + } ); + } + + /** + * @implNote Uses YesNoConverter to work across all databases, even those + * not supporting an actual BOOLEAN datatype + */ + @Entity(name="SimpleEntity") + @Table(name="simple") + @SoftDelete(columnName = "removed", converter = YesNoConverter.class) + public static class SimpleEntity { + @Id + private Integer id; + @NaturalId + private String name; + + public SimpleEntity() { + } + + public SimpleEntity(Integer id, String name) { + this.id = id; + this.name = name; + } + + public Integer getId() { + return id; + } + + public String getName() { + return name; + } + } + + @Entity(name="BatchLoadable") + @Table(name="batch_loadable") + @BatchSize(size = 5) + @SoftDelete(columnName = "active", converter = ReverseYesNoConverter.class) + public static class BatchLoadable { + @Id + private Integer id; + private String name; + + public BatchLoadable() { + } + + public BatchLoadable(Integer id, String name) { + this.id = id; + this.name = name; + } + + public Integer getId() { + return id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/ToOneTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/ToOneTests.java new file mode 100644 index 0000000000..56e728d41f --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/ToOneTests.java @@ -0,0 +1,162 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.orm.test.softdelete; + +import java.sql.Statement; + +import org.hibernate.annotations.Fetch; +import org.hibernate.annotations.FetchMode; +import org.hibernate.annotations.SoftDelete; + +import org.hibernate.testing.jdbc.SQLStatementInspector; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Steve Ebersole + */ +@DomainModel( annotatedClasses = { ToOneTests.Issue.class, ToOneTests.User.class } ) +@SessionFactory(useCollectingStatementInspector = true) +public class ToOneTests { + @BeforeEach + void createTestData(SessionFactoryScope scope) { + scope.inTransaction( (session) -> { + final User steve = new User( 1, "Steve" ); + final User john = new User( 2, "John" ); + final User jacob = new User( 3, "Jacob" ); + final User bobby = new User( 4, "Bobby" ); + session.persist( steve ); + session.persist( john ); + session.persist( jacob ); + session.persist( bobby ); + + final Issue first = new Issue( 1, "first", jacob, steve ); + final Issue second = new Issue( 2, "second", bobby, steve ); + final Issue third = new Issue( 3, "third", jacob, john ); + session.persist( first ); + session.persist( second ); + session.persist( third ); + + // soft-delete John and Bobby + session.createMutationQuery( "delete User where id in (2,4)" ).executeUpdate(); + } ); + } + + @AfterEach + void dropTestData(SessionFactoryScope scope) { + scope.inTransaction( (session) -> session.doWork( (connection) -> { + final Statement statement = connection.createStatement(); + statement.execute( "delete from issues" ); + statement.execute( "delete from users" ); + } ) ); + } + + @Test + void basicBaselineTest(SessionFactoryScope scope) { + final SQLStatementInspector sqlInspector = scope.getCollectingStatementInspector(); + sqlInspector.clear(); + + scope.inTransaction( (session) -> { + final Issue issue1 = session.get( Issue.class, 1 ); + assertThat( issue1 ).isNotNull(); + assertThat( issue1.reporter ).isNotNull(); + assertThat( issue1.assignee ).isNotNull(); + + assertThat( sqlInspector.getSqlQueries() ).hasSize( 2 ); + assertThat( sqlInspector.getSqlQueries().get( 0 ) ).contains( " join " ); + assertThat( sqlInspector.getSqlQueries().get( 0 ) ).contains( ".reporter_fk" ); + assertThat( sqlInspector.getSqlQueries().get( 0 ) ).contains( ".active='Y" ); + assertThat( sqlInspector.getSqlQueries().get( 0 ) ).containsOnlyOnce( "active" ); + assertThat( sqlInspector.getSqlQueries().get( 1 ) ).doesNotContain( " join " ); + assertThat( sqlInspector.getSqlQueries().get( 1 ) ).contains( ".active='Y" ); + assertThat( sqlInspector.getSqlQueries().get( 1 ) ).containsOnlyOnce( "active" ); + } ); + } + + @Test + void basicJoinedTest(SessionFactoryScope scope) { + scope.inTransaction( (session) -> { + final Issue issue2 = session.get( Issue.class, 2 ); + assertThat( issue2 ).isNotNull(); + assertThat( issue2.reporter ).isNull(); + assertThat( issue2.assignee ).isNotNull(); + } ); + } + + @Test + void basicSelectedTest(SessionFactoryScope scope) { + scope.inTransaction( (session) -> { + final Issue issue3 = session.get( Issue.class, 3 ); + assertThat( issue3 ).isNotNull(); + assertThat( issue3.reporter ).isNotNull(); + assertThat( issue3.assignee ).isNull(); + } ); + } + + @Entity(name="Issue") + @Table(name="issues") + public static class Issue { + @Id + private Integer id; + private String description; + @ManyToOne + @JoinColumn(name="reporter_fk") + @Fetch( FetchMode.JOIN ) + private User reporter; + @ManyToOne + @JoinColumn(name="assignee_fk") + @Fetch( FetchMode.SELECT ) + private User assignee; + + public Issue() { + } + + public Issue(Integer id, String description, User reporter) { + this.id = id; + this.description = description; + this.reporter = reporter; + } + + public Issue(Integer id, String description, User reporter, User assignee) { + this.id = id; + this.description = description; + this.reporter = reporter; + this.assignee = assignee; + } + } + + @Entity(name="User") + @Table(name="users") + @SoftDelete(columnName = "active", converter = ReverseYesNoConverter.class) + public static class User { + @Id + private Integer id; + private String name; + + public User() { + } + + public User(Integer id, String name) { + this.id = id; + this.name = name; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/ValidationTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/ValidationTests.java new file mode 100644 index 0000000000..4cf339785c --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/ValidationTests.java @@ -0,0 +1,83 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.orm.test.softdelete; + +import org.hibernate.SessionFactory; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.SoftDelete; +import org.hibernate.boot.Metadata; +import org.hibernate.boot.MetadataSources; +import org.hibernate.metamodel.UnsupportedMappingException; + +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; + +import static org.junit.jupiter.api.Assertions.fail; + +/** + * @author Steve Ebersole + */ +public class ValidationTests { + @Test + void testLazyToOne() { + final Metadata metadata = new MetadataSources().addAnnotatedClass( Person.class ) + .addAnnotatedClass( Address.class ) + .buildMetadata(); + try (SessionFactory sessionFactory = metadata.buildSessionFactory()) { + fail( "Expecting a failure" ); + } + catch (UnsupportedMappingException expected) { + } + } + + @Test + void testCustomSql() { + final Metadata metadata = new MetadataSources().addAnnotatedClass( NoNo.class ) + .buildMetadata(); + try (SessionFactory sessionFactory = metadata.buildSessionFactory()) { + fail( "Expecting a failure" ); + } + catch (UnsupportedMappingException expected) { + } + } + + @Entity(name="Person") + @Table(name="persons") + public static class Person { + @Id + private Integer id; + private String name; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name="address_fk") + private Address address; + } + + @Entity(name="Address") + @Table(name="addresses") + @SoftDelete(columnName = "active", converter = ReverseYesNoConverter.class) + public static class Address { + @Id + private Integer id; + private String name; + } + + @Entity(name="NoNo") + @Table(name="nonos") + @SoftDelete(columnName = "active", converter = ReverseYesNoConverter.class) + @SQLDelete( sql = "delete from nonos" ) + public static class NoNo { + @Id + private Integer id; + private String name; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/collections/CollectionOwned.java b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/collections/CollectionOwned.java new file mode 100644 index 0000000000..9d303444c6 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/collections/CollectionOwned.java @@ -0,0 +1,32 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.orm.test.softdelete.collections; + +import jakarta.persistence.Basic; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +/** + * @author Steve Ebersole + */ +@Entity +@Table(name = "coll_owned") +public class CollectionOwned { + @Id + private Integer id; + @Basic + private String name; + + public CollectionOwned() { + } + + public CollectionOwned(Integer id, String name) { + this.id = id; + this.name = name; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/collections/CollectionOwner.java b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/collections/CollectionOwner.java new file mode 100644 index 0000000000..583cffacbd --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/collections/CollectionOwner.java @@ -0,0 +1,103 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.orm.test.softdelete.collections; + +import java.util.ArrayList; +import java.util.Collection; + +import org.hibernate.annotations.SoftDelete; +import org.hibernate.type.NumericBooleanConverter; +import org.hibernate.type.YesNoConverter; + +import jakarta.persistence.Basic; +import jakarta.persistence.CollectionTable; +import jakarta.persistence.Column; +import jakarta.persistence.ElementCollection; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinTable; +import jakarta.persistence.ManyToMany; +import jakarta.persistence.Table; + +/** + * @author Steve Ebersole + */ +@Entity +@Table(name = "coll_owner") +public class CollectionOwner { + @Id + private Integer id; + @Basic + private String name; + + @ElementCollection + @CollectionTable(name = "elements", joinColumns = @JoinColumn(name = "owner_fk")) + @Column(name = "txt") + @SoftDelete(converter = YesNoConverter.class) + private Collection elements; + + @ManyToMany + @JoinTable( + name = "m2m", + joinColumns = @JoinColumn(name = "owner_fk"), + inverseJoinColumns = @JoinColumn(name = "owned_fk") + ) + @SoftDelete(columnName = "gone", converter = NumericBooleanConverter.class) + private Collection manyToMany; + + protected CollectionOwner() { + // for Hibernate use + } + + public CollectionOwner(Integer id, String name) { + this.id = id; + this.name = name; + } + + public Integer getId() { + return id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Collection getElements() { + return elements; + } + + public void setElements(Collection elements) { + this.elements = elements; + } + + public void addElement(String element) { + if ( elements == null ) { + elements = new ArrayList<>(); + } + elements.add( element ); + } + + public Collection getManyToMany() { + return manyToMany; + } + + public void setManyToMany(Collection manyToMany) { + this.manyToMany = manyToMany; + } + + public void addManyToMany(CollectionOwned element) { + if ( manyToMany == null ) { + manyToMany = new ArrayList<>(); + } + manyToMany.add( element ); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/collections/CollectionOwner2.java b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/collections/CollectionOwner2.java new file mode 100644 index 0000000000..893a93177b --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/collections/CollectionOwner2.java @@ -0,0 +1,81 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.orm.test.softdelete.collections; + +import java.util.Set; + +import org.hibernate.annotations.BatchSize; +import org.hibernate.annotations.Fetch; +import org.hibernate.annotations.FetchMode; +import org.hibernate.annotations.SoftDelete; +import org.hibernate.orm.test.softdelete.ReverseYesNoConverter; + +import jakarta.persistence.CollectionTable; +import jakarta.persistence.ElementCollection; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.Table; + +/** + * @author Steve Ebersole + */ +@Entity +@Table(name = "coll_owner2") +public class CollectionOwner2 { + @Id + private Integer id; + private String name; + + @ElementCollection + @CollectionTable(name="batch_loadables", joinColumns = @JoinColumn(name="owner_fk")) + @BatchSize(size = 5) + @SoftDelete(columnName = "active", converter = ReverseYesNoConverter.class) + private Set batchLoadable; + + @ElementCollection + @CollectionTable(name="subselect_loadables", joinColumns = @JoinColumn(name="owner_fk")) + @Fetch(FetchMode.SUBSELECT) + @SoftDelete(columnName = "active", converter = ReverseYesNoConverter.class) + private Set subSelectLoadable; + + public CollectionOwner2() { + } + + public CollectionOwner2(Integer id, String name) { + this.id = id; + this.name = name; + } + + public Integer getId() { + return id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Set getBatchLoadable() { + return batchLoadable; + } + + public void setBatchLoadable(Set batchLoadable) { + this.batchLoadable = batchLoadable; + } + + public Set getSubSelectLoadable() { + return subSelectLoadable; + } + + public void setSubSelectLoadable(Set subSelectLoadable) { + this.subSelectLoadable = subSelectLoadable; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/collections/FetchLoadableTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/collections/FetchLoadableTests.java new file mode 100644 index 0000000000..8c8ffd07e1 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/collections/FetchLoadableTests.java @@ -0,0 +1,129 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.orm.test.softdelete.collections; + +import java.sql.Statement; +import java.util.HashSet; +import java.util.List; + +import org.hibernate.Hibernate; + +import org.hibernate.testing.jdbc.SQLStatementInspector; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Steve Ebersole + */ +@DomainModel(annotatedClasses = CollectionOwner2.class) +@SessionFactory(useCollectingStatementInspector = true) +public class FetchLoadableTests { + @BeforeEach + void createTestData(SessionFactoryScope scope) { + scope.inTransaction( (session) -> { + final CollectionOwner2 owner = new CollectionOwner2( 1, "first" ); + final CollectionOwner2 owner2 = new CollectionOwner2( 2, "second" ); + + owner.setBatchLoadable( new HashSet<>() ); + owner.getBatchLoadable().add( "batchable1" ); + owner.getBatchLoadable().add( "batchable2" ); + owner.getBatchLoadable().add( "batchable3" ); + + owner.setSubSelectLoadable( new HashSet<>() ); + owner.getSubSelectLoadable().add( "subselectable1" ); + owner.getSubSelectLoadable().add( "subselectable2" ); + owner.getSubSelectLoadable().add( "subselectable3" ); + + owner2.setBatchLoadable( new HashSet<>() ); + owner2.getBatchLoadable().add( "batchable21" ); + owner2.getBatchLoadable().add( "batchable22" ); + owner2.getBatchLoadable().add( "batchable23" ); + + owner2.setSubSelectLoadable( new HashSet<>() ); + owner2.getSubSelectLoadable().add( "subselectable21" ); + owner2.getSubSelectLoadable().add( "subselectable22" ); + owner2.getSubSelectLoadable().add( "subselectable23" ); + + session.persist( owner ); + session.persist( owner2 ); + } ); + } + + @AfterEach + void dropTestData(SessionFactoryScope scope) { + scope.inTransaction( (session) -> session.doWork( (connection) -> { + final Statement statement = connection.createStatement(); + statement.execute( "delete from batch_loadables" ); + statement.execute( "delete from subselect_loadables" ); + statement.execute( "delete from coll_owner2" ); + } ) ); + } + + @Test + void testBatchLoading(SessionFactoryScope scope) { + final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); + + scope.inTransaction( (session) -> { + final List result = session.createQuery( + "from CollectionOwner2 order", + CollectionOwner2.class + ).list(); + + final CollectionOwner2 first = result.get( 0 ); + assertThat( Hibernate.isInitialized( first ) ).isTrue(); + assertThat( Hibernate.isInitialized( first.getBatchLoadable() ) ).isFalse(); + + final CollectionOwner2 second = result.get( 1 ); + assertThat( Hibernate.isInitialized( second ) ).isTrue(); + assertThat( Hibernate.isInitialized( second.getBatchLoadable() ) ).isFalse(); + + statementInspector.clear(); + + // trigger loading one of the batch-loadable collections + first.getBatchLoadable().size(); + assertThat( statementInspector.getSqlQueries() ).hasSize( 1 ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( "active='Y'" ); + assertThat( Hibernate.isInitialized( first.getBatchLoadable() ) ).isTrue(); + assertThat( Hibernate.isInitialized( second.getBatchLoadable() ) ).isTrue(); + } ); + } + + @Test + void testSubSelectLoading(SessionFactoryScope scope) { + final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); + + scope.inTransaction( (session) -> { + final List result = session.createQuery( + "from CollectionOwner2 order", + CollectionOwner2.class + ).list(); + + final CollectionOwner2 first = result.get( 0 ); + assertThat( Hibernate.isInitialized( first ) ).isTrue(); + assertThat( Hibernate.isInitialized( first.getSubSelectLoadable() ) ).isFalse(); + + final CollectionOwner2 second = result.get( 1 ); + assertThat( Hibernate.isInitialized( second ) ).isTrue(); + assertThat( Hibernate.isInitialized( second.getSubSelectLoadable() ) ).isFalse(); + + statementInspector.clear(); + + // trigger loading one of the subselect-loadable collections + first.getSubSelectLoadable().size(); + assertThat( statementInspector.getSqlQueries() ).hasSize( 1 ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( "active='Y'" ); + assertThat( Hibernate.isInitialized( first.getSubSelectLoadable() ) ).isTrue(); + assertThat( Hibernate.isInitialized( second.getSubSelectLoadable() ) ).isTrue(); + } ); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/collections/InvalidCollectionOwner.java b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/collections/InvalidCollectionOwner.java new file mode 100644 index 0000000000..18af519ede --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/collections/InvalidCollectionOwner.java @@ -0,0 +1,37 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.orm.test.softdelete.collections; + +import java.util.Collection; + +import org.hibernate.annotations.SoftDelete; +import org.hibernate.type.NumericBooleanConverter; + +import jakarta.persistence.Basic; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinTable; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; + +/** + * @author Steve Ebersole + */ +@Entity +@Table(name = "invalid_coll_owner") +public class InvalidCollectionOwner { + @Id + private Integer id; + @Basic + private String name; + + @OneToMany + @JoinColumn(name="owned_fk") + @SoftDelete(columnName = "gone", converter = NumericBooleanConverter.class) + private Collection oneToMany; +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/collections/MappingTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/collections/MappingTests.java new file mode 100644 index 0000000000..cccfb4c282 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/collections/MappingTests.java @@ -0,0 +1,45 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.orm.test.softdelete.collections; + +import org.hibernate.metamodel.spi.MappingMetamodelImplementor; +import org.hibernate.orm.test.softdelete.MappingVerifier; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; + +import org.junit.jupiter.api.Test; + +/** + * @author Steve Ebersole + */ +@DomainModel(annotatedClasses = { CollectionOwner.class, CollectionOwned.class }) +@SessionFactory(exportSchema = false) +@SuppressWarnings("unused") +public class MappingTests { + + @Test + void verifyCollectionMappings(SessionFactoryScope scope) { + final MappingMetamodelImplementor metamodel = scope.getSessionFactory().getMappingMetamodel(); + + MappingVerifier.verifyMapping( + metamodel.getCollectionDescriptor( CollectionOwner.class.getName() + ".elements" ).getAttributeMapping().getSoftDeleteMapping(), + "deleted", + "elements", + 'Y' + ); + + MappingVerifier.verifyMapping( + metamodel.getCollectionDescriptor( CollectionOwner.class.getName() + ".manyToMany" ).getAttributeMapping().getSoftDeleteMapping(), + "gone", + "m2m", + 1 + ); + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/collections/UsageTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/collections/UsageTests.java new file mode 100644 index 0000000000..98403679d8 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/collections/UsageTests.java @@ -0,0 +1,174 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.orm.test.softdelete.collections; + +import java.sql.Statement; + +import org.hibernate.testing.jdbc.SQLStatementInspector; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Steve Ebersole + */ +@DomainModel(annotatedClasses = { CollectionOwner.class, CollectionOwned.class }) +@SessionFactory(useCollectingStatementInspector = true) +public class UsageTests { + @BeforeEach + void createTestData(SessionFactoryScope scope) { + scope.inTransaction( (session) -> { + final CollectionOwned owned = new CollectionOwned( 1, "owned" ); + session.persist( owned ); + + final CollectionOwner owner = new CollectionOwner( 1, "owner" ); + owner.addElement( "an element" ); + owner.addElement( "another element" ); + owner.addManyToMany( owned ); + session.persist( owner ); + } ); + } + + @AfterEach + void dropTestData(SessionFactoryScope scope) { + scope.inTransaction( (session) -> session.doWork( (connection) -> { + final Statement statement = connection.createStatement(); + statement.execute( "delete from m2m" ); + statement.execute( "delete from elements" ); + statement.execute( "delete from coll_owned" ); + statement.execute( "delete from coll_owner" ); + } ) ); + } + + @Test + void testHqlElements(SessionFactoryScope scope) { + final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); + statementInspector.clear(); + + scope.inTransaction( (session) -> { + session + .createQuery( "select o from CollectionOwner o join fetch o.elements where o.id = 1", CollectionOwner.class ) + .uniqueResult(); + } ); + assertThat( statementInspector.getSqlQueries() ).hasSize( 1 ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( "deleted='N'" ); + } + + @Test + void testHqlManyToMany(SessionFactoryScope scope) { + final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); + statementInspector.clear(); + + scope.inTransaction( (session) -> { + session + .createQuery( "select o from CollectionOwner o join fetch o.manyToMany where o.id = 1", CollectionOwner.class ) + .uniqueResult(); + } ); + assertThat( statementInspector.getSqlQueries() ).hasSize( 1 ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( "gone=0" ); + } + + @Test + void testRemoveElements(SessionFactoryScope scope) { + final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); + statementInspector.clear(); + + scope.inTransaction( (session) -> { + final CollectionOwner owner = session + .createQuery( "select o from CollectionOwner o join fetch o.elements where o.id = 1", CollectionOwner.class ) + .uniqueResult(); + statementInspector.clear(); + owner.getElements().clear(); + } ); + assertThat( statementInspector.getSqlQueries() ).hasSize( 1 ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).startsWith( "update " ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( "deleted='Y'" ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).endsWith( "deleted='N'" ); + + scope.inTransaction( (session) -> { + final CollectionOwner owner = session.get( CollectionOwner.class, 1 ); + assertThat( owner.getElements() ).hasSize( 0 ); + } ); + } + + @Test + void testRemoveManyToMany(SessionFactoryScope scope) { + final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); + + scope.inTransaction( (session) -> { + final CollectionOwner owner = session + .createQuery( "select o from CollectionOwner o join fetch o.manyToMany where o.id = 1", CollectionOwner.class ) + .uniqueResult(); + statementInspector.clear(); + owner.getManyToMany().clear(); + } ); + assertThat( statementInspector.getSqlQueries() ).hasSize( 1 ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).startsWith( "update " ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( "gone=1" ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).endsWith( "gone=0" ); + + scope.inTransaction( (session) -> { + final CollectionOwner owner = session.get( CollectionOwner.class, 1 ); + statementInspector.clear(); + assertThat( owner.getManyToMany() ).hasSize( 0 ); + } ); + } + + @Test + void testDeleteElement(SessionFactoryScope scope) { + final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); + statementInspector.clear(); + + scope.inTransaction( (session) -> { + final CollectionOwner owner = session + .createQuery( "select o from CollectionOwner o join fetch o.elements where o.id = 1", CollectionOwner.class ) + .uniqueResult(); + statementInspector.clear(); + owner.getElements().remove( "an element" ); + } ); + // this will be a "recreate" + assertThat( statementInspector.getSqlQueries() ).hasSize( 2 ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).startsWith( "update " ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( "deleted='Y'" ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).endsWith( "deleted='N'" ); + assertThat( statementInspector.getSqlQueries().get( 1 ) ).startsWith( "insert " ); + + scope.inTransaction( (session) -> { + final CollectionOwner owner = session.get( CollectionOwner.class, 1 ); + assertThat( owner.getElements() ).hasSize( 1 ); + } ); + } + + @Test + void testDeleteManyToMany(SessionFactoryScope scope) { + final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); + statementInspector.clear(); + + scope.inTransaction( (session) -> { + final CollectionOwned owned = session.get( CollectionOwned.class, 1 ); + final CollectionOwner owner = session + .createQuery( "select o from CollectionOwner o join fetch o.manyToMany where o.id = 1", CollectionOwner.class ) + .uniqueResult(); + statementInspector.clear(); + owner.getManyToMany().remove( owned ); + } ); + assertThat( statementInspector.getSqlQueries() ).hasSize( 1 ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).startsWith( "update " ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( "gone=1" ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).endsWith( "gone=0" ); + + scope.inTransaction( (session) -> { + final CollectionOwner owner = session.get( CollectionOwner.class, 1 ); + assertThat( owner.getManyToMany() ).hasSize( 0 ); + } ); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/collections/ValidationTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/collections/ValidationTests.java new file mode 100644 index 0000000000..e71b59b14b --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/collections/ValidationTests.java @@ -0,0 +1,33 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.orm.test.softdelete.collections; + +import org.hibernate.SessionFactory; +import org.hibernate.boot.Metadata; +import org.hibernate.boot.MetadataSources; +import org.hibernate.metamodel.UnsupportedMappingException; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.fail; + +/** + * @author Steve Ebersole + */ +public class ValidationTests { + @Test + void testOneToMany() { + try { + final Metadata metadata = new MetadataSources() + .addAnnotatedClass( InvalidCollectionOwner.class ) + .addAnnotatedClass( CollectionOwned.class ) + .buildMetadata(); + } + catch (UnsupportedMappingException expected) { + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/converter/ConvertedSoftDeleteTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/converter/ConvertedSoftDeleteTests.java new file mode 100644 index 0000000000..4804bd1020 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/converter/ConvertedSoftDeleteTests.java @@ -0,0 +1,63 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.orm.test.softdelete.converter; + +import org.hibernate.metamodel.spi.MappingMetamodelImplementor; +import org.hibernate.orm.test.softdelete.MappingVerifier; + +import org.hibernate.testing.jdbc.SQLStatementInspector; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Steve Ebersole + */ +@DomainModel(annotatedClasses = TheEntity.class) +@SessionFactory( useCollectingStatementInspector = true) +public class ConvertedSoftDeleteTests { + @Test + void verifySchema(SessionFactoryScope scope) { + final MappingMetamodelImplementor metamodel = scope.getSessionFactory().getMappingMetamodel(); + + MappingVerifier.verifyMapping( + metamodel.getEntityDescriptor( TheEntity.class ).getSoftDeleteMapping(), + "deleted", + "the_entity", + 'Y' + ); + } + + @Test + void testUsage(SessionFactoryScope scope) { + final SQLStatementInspector sqlInspector = scope.getCollectingStatementInspector(); + sqlInspector.clear(); + + scope.inTransaction( (session) -> { + session.persist( new TheEntity( 1, "it" ) ); + } ); + + assertThat( sqlInspector.getSqlQueries() ).hasSize( 1 ); + assertThat( sqlInspector.getSqlQueries().get( 0 ) ).contains( "'N'" ); + assertThat( sqlInspector.getSqlQueries().get( 0 ) ).doesNotContain( "'Y'" ); + + sqlInspector.clear(); + + scope.inTransaction( (session) -> { + final TheEntity reference = session.getReference( TheEntity.class, 1 ); + session.remove( reference ); + } ); + + assertThat( sqlInspector.getSqlQueries() ).hasSize( 1 ); + assertThat( sqlInspector.getSqlQueries().get( 0 ) ).doesNotContainIgnoringCase( "delete " ); + assertThat( sqlInspector.getSqlQueries().get( 0 ) ).containsIgnoringCase( "update " ); + assertThat( sqlInspector.getSqlQueries().get( 0 ) ).contains( "deleted='Y'" ); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/converter/TheEntity.java b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/converter/TheEntity.java new file mode 100644 index 0000000000..8ca0e0d767 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/converter/TheEntity.java @@ -0,0 +1,49 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.orm.test.softdelete.converter; + +import org.hibernate.annotations.SoftDelete; +import org.hibernate.type.YesNoConverter; + +import jakarta.persistence.Basic; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +/** + * @author Steve Ebersole + */ +@Entity +@Table(name = "the_entity") +@SoftDelete(converter = YesNoConverter.class) +public class TheEntity { + @Id + private Integer id; + @Basic + private String name; + + protected TheEntity() { + // for Hibernate use + } + + public TheEntity(Integer id, String name) { + this.id = id; + this.name = name; + } + + public Integer getId() { + return id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/converter/package-info.java b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/converter/package-info.java new file mode 100644 index 0000000000..c35073f153 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/converter/package-info.java @@ -0,0 +1,13 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ + +/** + * Tests applying a custom {@linkplain org.hibernate.annotations.SoftDelete#converter() converter} + * + * @author Steve Ebersole + */ +package org.hibernate.orm.test.softdelete.converter; \ No newline at end of file diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/converter/reversed/ReversedSoftDeleteTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/converter/reversed/ReversedSoftDeleteTests.java new file mode 100644 index 0000000000..0f45184c56 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/converter/reversed/ReversedSoftDeleteTests.java @@ -0,0 +1,63 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.orm.test.softdelete.converter.reversed; + +import org.hibernate.metamodel.spi.MappingMetamodelImplementor; +import org.hibernate.orm.test.softdelete.MappingVerifier; +import org.hibernate.orm.test.softdelete.ReverseYesNoConverter; + +import org.hibernate.testing.jdbc.SQLStatementInspector; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Steve Ebersole + */ +@DomainModel(annotatedClasses = { ReverseYesNoConverter.class, TheEntity.class }) +@SessionFactory( useCollectingStatementInspector = true) +public class ReversedSoftDeleteTests { + @Test + void verifySchema(SessionFactoryScope scope) { + final MappingMetamodelImplementor metamodel = scope.getSessionFactory().getMappingMetamodel(); + MappingVerifier.verifyMapping( + metamodel.getEntityDescriptor( TheEntity.class ).getSoftDeleteMapping(), + "active", + "the_entity", + 'N' + ); + } + + @Test + void testUsage(SessionFactoryScope scope) { + final SQLStatementInspector sqlInspector = scope.getCollectingStatementInspector(); + sqlInspector.clear(); + + scope.inTransaction( (session) -> { + session.persist( new TheEntity( 1, "it" ) ); + } ); + + assertThat( sqlInspector.getSqlQueries() ).hasSize( 1 ); + assertThat( sqlInspector.getSqlQueries().get( 0 ) ).contains( "'Y'" ); + assertThat( sqlInspector.getSqlQueries().get( 0 ) ).doesNotContain( "'N'" ); + + sqlInspector.clear(); + + scope.inTransaction( (session) -> { + final TheEntity reference = session.getReference( TheEntity.class, 1 ); + session.remove( reference ); + } ); + + assertThat( sqlInspector.getSqlQueries() ).hasSize( 1 ); + assertThat( sqlInspector.getSqlQueries().get( 0 ) ).doesNotContainIgnoringCase( "delete " ); + assertThat( sqlInspector.getSqlQueries().get( 0 ) ).containsIgnoringCase( "update " ); + assertThat( sqlInspector.getSqlQueries().get( 0 ) ).contains( "active='N'" ); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/converter/reversed/TheEntity.java b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/converter/reversed/TheEntity.java new file mode 100644 index 0000000000..1469f4f8ea --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/converter/reversed/TheEntity.java @@ -0,0 +1,49 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.orm.test.softdelete.converter.reversed; + +import org.hibernate.annotations.SoftDelete; +import org.hibernate.orm.test.softdelete.ReverseYesNoConverter; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Basic; +import jakarta.persistence.Table; + +/** + * @author Steve Ebersole + */ +@Entity +@Table(name = "the_entity") +@SoftDelete(columnName = "active", converter = ReverseYesNoConverter.class) +public class TheEntity { + @Id + private Integer id; + @Basic + private String name; + + protected TheEntity() { + // for Hibernate use + } + + public TheEntity(Integer id, String name) { + this.id = id; + this.name = name; + } + + public Integer getId() { + return id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/package-info.java b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/package-info.java new file mode 100644 index 0000000000..a400673bcf --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/package-info.java @@ -0,0 +1,13 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ + +/** + * Tests for {@link org.hibernate.annotations.SoftDelete} and {@link org.hibernate.annotations.SoftDeleteColumn} + * + * @author Steve Ebersole + */ +package org.hibernate.orm.test.softdelete; diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/pkg/AnEntity.java b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/pkg/AnEntity.java new file mode 100644 index 0000000000..1e792343c6 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/pkg/AnEntity.java @@ -0,0 +1,55 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.orm.test.softdelete.pkg; + +import java.util.Collection; + +import jakarta.persistence.CollectionTable; +import jakarta.persistence.Column; +import jakarta.persistence.ElementCollection; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Basic; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.Table; + +/** + * @author Steve Ebersole + */ +@Entity +@Table(name = "the_table") +public class AnEntity { + @Id + private Integer id; + @Basic + private String name; + @ElementCollection + @CollectionTable(name="elements", joinColumns = @JoinColumn(name = "owner_fk")) + @Column(name="txt") + private Collection elements; + + protected AnEntity() { + // for Hibernate use + } + + public AnEntity(Integer id, String name) { + this.id = id; + this.name = name; + } + + public Integer getId() { + return id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/pkg/PackageLevelSoftDeleteTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/pkg/PackageLevelSoftDeleteTests.java new file mode 100644 index 0000000000..09705d19e0 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/pkg/PackageLevelSoftDeleteTests.java @@ -0,0 +1,45 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.orm.test.softdelete.pkg; + +import org.hibernate.metamodel.spi.MappingMetamodelImplementor; +import org.hibernate.orm.test.softdelete.MappingVerifier; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.NotImplementedYet; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +/** + * @author Steve Ebersole + */ +@DomainModel( annotatedClasses = AnEntity.class ) +@SessionFactory +public class PackageLevelSoftDeleteTests { + @Test + public void verifyEntitySchema(SessionFactoryScope scope) { + final MappingMetamodelImplementor metamodel = scope.getSessionFactory().getMappingMetamodel(); + MappingVerifier.verifyMapping( + metamodel.getEntityDescriptor( AnEntity.class ).getSoftDeleteMapping(), + "deleted", + "the_table", + 'Y' + ); + } + + @Test + public void verifyCollectionSchema(SessionFactoryScope scope) { + final MappingMetamodelImplementor metamodel = scope.getSessionFactory().getMappingMetamodel(); + MappingVerifier.verifyMapping( + metamodel.getCollectionDescriptor( AnEntity.class.getName() + ".elements" ).getAttributeMapping().getSoftDeleteMapping(), + "deleted", + "elements", + 'Y' + ); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/pkg/package-info.java b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/pkg/package-info.java new file mode 100644 index 0000000000..0b35c25846 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/pkg/package-info.java @@ -0,0 +1,18 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ + +/** + * Tests for {@link org.hibernate.annotations.SoftDelete} + * applied to a package + * + * @author Steve Ebersole + */ +@SoftDelete(converter = YesNoConverter.class) +package org.hibernate.orm.test.softdelete.pkg; + +import org.hibernate.annotations.SoftDelete; +import org.hibernate.type.YesNoConverter; \ No newline at end of file diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/pkg2/AnEntity.java b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/pkg2/AnEntity.java new file mode 100644 index 0000000000..25355fecca --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/pkg2/AnEntity.java @@ -0,0 +1,45 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.orm.test.softdelete.pkg2; + +import jakarta.persistence.Basic; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +/** + * @author Steve Ebersole + */ +@Entity +@Table(name = "the_table") +public class AnEntity { + @Id + private Integer id; + @Basic + private String name; + + protected AnEntity() { + // for Hibernate use + } + + public AnEntity(Integer id, String name) { + this.id = id; + this.name = name; + } + + public Integer getId() { + return id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/pkg2/CustomTrueFalseConverter.java b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/pkg2/CustomTrueFalseConverter.java new file mode 100644 index 0000000000..2d8ba28719 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/pkg2/CustomTrueFalseConverter.java @@ -0,0 +1,39 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.orm.test.softdelete.pkg2; + +import jakarta.persistence.AttributeConverter; + +/** + * A non-standard boolean converter to make sure check constraints get applied properly + * + * @author Steve Ebersole + */ +public class CustomTrueFalseConverter implements AttributeConverter { + @Override + public Character convertToDatabaseColumn(Boolean attribute) { + if ( attribute == null ) { + return null; + } + return attribute ? 'T' : 'F'; + } + + @Override + public Boolean convertToEntityAttribute(Character dbData) { + if ( dbData == null ) { + // NOTE : we assume active + return true; + } + if ( dbData.equals( 'T' ) ) { + return true; + } + if ( dbData.equals( 'F' ) ) { + return false; + } + throw new IllegalArgumentException( "Unexpected database value - `" + dbData + "`, expected 'T' or 'F'" ); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/pkg2/PackageLevelSoftDeleteTests2.java b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/pkg2/PackageLevelSoftDeleteTests2.java new file mode 100644 index 0000000000..e1357f6a24 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/pkg2/PackageLevelSoftDeleteTests2.java @@ -0,0 +1,33 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.orm.test.softdelete.pkg2; + +import org.hibernate.metamodel.spi.MappingMetamodelImplementor; +import org.hibernate.orm.test.softdelete.MappingVerifier; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +/** + * @author Steve Ebersole + */ +@DomainModel( annotatedClasses = AnEntity.class ) +@SessionFactory +public class PackageLevelSoftDeleteTests2 { + @Test + public void verifySchema(SessionFactoryScope scope) { + final MappingMetamodelImplementor metamodel = scope.getSessionFactory().getMappingMetamodel(); + MappingVerifier.verifyMapping( + metamodel.getEntityDescriptor( AnEntity.class ).getSoftDeleteMapping(), + "gone", + "the_table", + 'T' + ); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/pkg2/package-info.java b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/pkg2/package-info.java new file mode 100644 index 0000000000..b63dd80877 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/pkg2/package-info.java @@ -0,0 +1,17 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ + +/** + * Tests for {@link org.hibernate.annotations.SoftDelete} + * applied to a package + * + * @author Steve Ebersole + */ +@SoftDelete(columnName="gone", converter = CustomTrueFalseConverter.class) +package org.hibernate.orm.test.softdelete.pkg2; + +import org.hibernate.annotations.SoftDelete; diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/secondary/JoinedRoot.java b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/secondary/JoinedRoot.java new file mode 100644 index 0000000000..0d345e1779 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/secondary/JoinedRoot.java @@ -0,0 +1,55 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.orm.test.softdelete.secondary; + +import org.hibernate.annotations.SoftDelete; +import org.hibernate.type.YesNoConverter; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Basic; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; +import jakarta.persistence.Table; + +/** + * @implNote Uses YesNoConverter to work across all databases, even those + * not supporting an actual BOOLEAN datatype + * + * @author Steve Ebersole + */ +@Entity +@Inheritance(strategy = InheritanceType.JOINED) +@Table(name = "joined_root") +@SoftDelete(columnName = "removed", converter = YesNoConverter.class) +public abstract class JoinedRoot { + @Id + private Integer id; + @Basic + private String name; + + protected JoinedRoot() { + // for Hibernate use + } + + public JoinedRoot(Integer id, String name) { + this.id = id; + this.name = name; + } + + public Integer getId() { + return id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/secondary/JoinedSub.java b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/secondary/JoinedSub.java new file mode 100644 index 0000000000..333087008e --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/secondary/JoinedSub.java @@ -0,0 +1,31 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.orm.test.softdelete.secondary; + +import jakarta.persistence.Basic; +import jakarta.persistence.Entity; +import jakarta.persistence.PrimaryKeyJoinColumn; +import jakarta.persistence.Table; + +/** + * @author Steve Ebersole + */ +@Entity +@Table(name = "joined_sub") +@PrimaryKeyJoinColumn(name = "joined_fk") +public class JoinedSub extends JoinedRoot { + @Basic + String subDetails; + + public JoinedSub() { + } + + public JoinedSub(Integer id, String name, String subDetails) { + super( id, name ); + this.subDetails = subDetails; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/secondary/JoinedSubclassSoftDeleteTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/secondary/JoinedSubclassSoftDeleteTests.java new file mode 100644 index 0000000000..c60632becf --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/secondary/JoinedSubclassSoftDeleteTests.java @@ -0,0 +1,205 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.orm.test.softdelete.secondary; + +import java.sql.Statement; +import java.util.List; + +import org.hibernate.ObjectNotFoundException; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +/** + * @author Steve Ebersole + */ +@DomainModel(annotatedClasses = { JoinedRoot.class, JoinedSub.class }) +@SessionFactory() +public class JoinedSubclassSoftDeleteTests { + @BeforeEach + void createTestData(SessionFactoryScope scope) { + scope.inTransaction( (session) -> { + session.persist( new JoinedSub( 1, "first", "some details" ) ); + session.persist( new JoinedSub( 2, "second", "some details" ) ); + session.persist( new JoinedSub( 3, "third", "some details" ) ); + session.flush(); + session.doWork( (connection) -> { + final Statement statement = connection.createStatement(); + statement.execute( "update joined_root set removed='Y' where id=1" ); + } ); + } ); + } + + @AfterEach + void dropTestData(SessionFactoryScope scope) { + scope.inTransaction( (session) -> session.doWork( (connection) -> { + final Statement statement = connection.createStatement(); + statement.execute( "delete from joined_sub" ); + statement.execute( "delete from joined_root" ); + } ) ); + } + + @Test + void testSelectionQuery(SessionFactoryScope scope) { + scope.inTransaction( (session) -> { + // should not return #1 + assertThat( session.createQuery( "from JoinedRoot" ).list() ).hasSize( 2 ); + } ); + + scope.inTransaction( (session) -> { + // should not return #1 + assertThat( session.createQuery( "from JoinedSub" ).list() ).hasSize( 2 ); + } ); + } + + @Test + void testLoading(SessionFactoryScope scope) { + scope.inTransaction( (session) -> { + assertThat( session.get( JoinedRoot.class, 1 ) ).isNull(); + assertThat( session.get( JoinedRoot.class, 2 ) ).isNotNull(); + assertThat( session.get( JoinedRoot.class, 3 ) ).isNotNull(); + } ); + + scope.inTransaction( (session) -> { + assertThat( session.get( JoinedSub.class, 1 ) ).isNull(); + assertThat( session.get( JoinedSub.class, 2 ) ).isNotNull(); + assertThat( session.get( JoinedSub.class, 3 ) ).isNotNull(); + } ); + } + + @Test + void testProxies(SessionFactoryScope scope) { + // JoinedRoot + scope.inTransaction( (session) -> { + final JoinedRoot reference = session.getReference( JoinedRoot.class, 1 ); + try { + reference.getName(); + fail( "Expecting to fail" ); + } + catch (ObjectNotFoundException expected) { + } + + final JoinedRoot reference2 = session.getReference( JoinedRoot.class, 2 ); + reference2.getName(); + + final JoinedRoot reference3 = session.getReference( JoinedRoot.class, 3 ); + reference3.getName(); + } ); + + // JoinedSub + scope.inTransaction( (session) -> { + final JoinedSub reference = session.getReference( JoinedSub.class, 1 ); + try { + reference.getName(); + fail( "Expecting to fail" ); + } + catch (ObjectNotFoundException expected) { + } + + final JoinedSub reference2 = session.getReference( JoinedSub.class, 2 ); + reference2.getName(); + + final JoinedSub reference3 = session.getReference( JoinedSub.class, 3 ); + reference3.getName(); + } ); + } + + @Test + void testDeletion(SessionFactoryScope scope) { + scope.inTransaction( (session) -> { + final JoinedSub reference = session.getReference( JoinedSub.class, 2 ); + session.remove( reference ); + session.flush(); + + final List active = session + .createSelectionQuery( "from JoinedSub", JoinedSub.class ) + .list(); + // #1 was "deleted" up front and we just "deleted" #2... only #3 should be active + assertThat( active ).hasSize( 1 ); + assertThat( active.get(0).getId() ).isEqualTo( 3 ); + } ); + } + + @Test + void testFullRootUpdateMutationQuery(SessionFactoryScope scope) { + scope.inTransaction( (session) -> { + final int affected = session.createMutationQuery( "update JoinedRoot set name = null" ).executeUpdate(); + assertThat( affected ).isEqualTo( 2 ); + } ); + } + + @Test + void testRestrictedRootUpdateMutationQuery(SessionFactoryScope scope) { + scope.inTransaction( (session) -> { + final int affected = session + .createMutationQuery( "update JoinedRoot set name = null where name = 'second'" ) + .executeUpdate(); + assertThat( affected ).isEqualTo( 1 ); + } ); + } + + @Test + void testFullSubUpdateMutationQuery(SessionFactoryScope scope) { + scope.inTransaction( (session) -> { + final int affected = session.createMutationQuery( "update JoinedSub set name = null" ).executeUpdate(); + assertThat( affected ).isEqualTo( 2 ); + } ); + } + + @Test + void testRestrictedSubUpdateMutationQuery(SessionFactoryScope scope) { + scope.inTransaction( (session) -> { + final int affected = session + .createMutationQuery( "update JoinedSub set name = null where name = 'second'" ) + .executeUpdate(); + assertThat( affected ).isEqualTo( 1 ); + } ); + } + + @Test + void testFullRootDeleteMutationQuery(SessionFactoryScope scope) { + scope.inTransaction( (session) -> { + final int affected = session.createMutationQuery( "delete JoinedRoot" ).executeUpdate(); + // only #2 and #3 + assertThat( affected ).isEqualTo( 2 ); + } ); + } + + @Test + void testRestrictedRootDeleteMutationQuery(SessionFactoryScope scope) { + scope.inTransaction( (session) -> { + final int affected = session.createMutationQuery( "delete JoinedRoot where name = 'second'" ).executeUpdate(); + // only #2 + assertThat( affected ).isEqualTo( 1 ); + } ); + } + + @Test + void testFullSubDeleteMutationQuery(SessionFactoryScope scope) { + scope.inTransaction( (session) -> { + final int affected = session.createMutationQuery( "delete JoinedSub" ).executeUpdate(); + // only #2 and #3 + assertThat( affected ).isEqualTo( 2 ); + } ); + } + + @Test + void testRestrictedSubDeleteMutationQuery(SessionFactoryScope scope) { + scope.inTransaction( (session) -> { + final int affected = session.createMutationQuery( "delete JoinedSub where name = 'second'" ).executeUpdate(); + // only #2 + assertThat( affected ).isEqualTo( 1 ); + } ); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/secondary/MappingTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/secondary/MappingTests.java new file mode 100644 index 0000000000..b25e7202fc --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/secondary/MappingTests.java @@ -0,0 +1,53 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.orm.test.softdelete.secondary; + +import java.util.Set; + +import org.hibernate.metamodel.spi.MappingMetamodelImplementor; +import org.hibernate.orm.test.softdelete.MappingVerifier; +import org.hibernate.orm.test.util.SchemaUtil; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.DomainModelScope; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Steve Ebersole + */ +@DomainModel(annotatedClasses = { JoinedRoot.class, JoinedSub.class }) +@SessionFactory() +public class MappingTests { + @Test + void verifyMapping(SessionFactoryScope scope, DomainModelScope dmScope) { + final MappingMetamodelImplementor metamodel = scope.getSessionFactory().getMappingMetamodel(); + + MappingVerifier.verifyMapping( + metamodel.getEntityDescriptor( JoinedRoot.class ).getSoftDeleteMapping(), + "removed", + "joined_root", + 'Y' + ); + + MappingVerifier.verifyMapping( + metamodel.getEntityDescriptor( JoinedSub.class ).getSoftDeleteMapping(), + "removed", + "joined_root", + 'Y' + ); + + final Set rootTableColumns = SchemaUtil.getColumnNames( "joined_root", dmScope.getDomainModel() ); + assertThat( rootTableColumns ).contains( "removed" ); + + final Set subTableColumns = SchemaUtil.getColumnNames( "joined_sub", dmScope.getDomainModel() ); + assertThat( subTableColumns ).doesNotContain( "removed" ); + } +} diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/NotImplementedYet.java b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/NotImplementedYet.java index 821b30c3e8..d30f654095 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/NotImplementedYet.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/NotImplementedYet.java @@ -37,10 +37,4 @@ public @interface NotImplementedYet { * A version expectation by when this feature is supposed to become implemented */ String expectedVersion() default ""; - - /** - * Generally this handles tests failing due to {@link NotImplementedYetException} exceptions - * being thrown (strict). Setting this to false allows it to handle failure for any reason. - */ - boolean strict() default true; } diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/NotImplementedYetExtension.java b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/NotImplementedYetExtension.java index 69961e6491..82367d4ae8 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/NotImplementedYetExtension.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/NotImplementedYetExtension.java @@ -31,14 +31,13 @@ public class NotImplementedYetExtension private static final Logger log = Logger.getLogger( NotImplementedYetExtension.class ); private static final String IS_MARKED_STORE_KEY = "IS_MARKED"; - private static final String IS_STRICT_STORE_KEY = "IS_STRICT"; private static final String EXCEPTION_STORE_KEY = "NOT_IMPLEMENTED"; @Override public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) { log.debugf( "#evaluateExecutionCondition(%s)", context.getDisplayName() ); - if ( !context.getElement().isPresent() ) { + if ( context.getElement().isEmpty() ) { throw new RuntimeException( "Unable to determine how to handle given ExtensionContext : " + context.getDisplayName() ); } @@ -72,26 +71,15 @@ public class NotImplementedYetExtension final Optional annRef = TestingUtil.findEffectiveAnnotation( context, NotImplementedYet.class ); final boolean isMarked = annRef.isPresent(); - final boolean isStrict; - if ( isMarked ) { - final NotImplementedYet ann = annRef.get(); - isStrict = ann.strict(); - } - else { - isStrict = false; - } log.debugf( - "Checking `%s` for @NotImplementedYet - isMarked = %s, isStrict = %s", + "Checking `%s` for @NotImplementedYet - isMarked = %s", context.getDisplayName(), - isMarked, - isStrict + isMarked ); final ExtensionContext.Namespace namespace = generateNamespace( context ); context.getStore( namespace ).put( IS_MARKED_STORE_KEY, isMarked ); - context.getStore( namespace ).put( IS_STRICT_STORE_KEY, isStrict ); - } @Override @@ -106,15 +94,13 @@ public class NotImplementedYetExtension if ( isMarked == Boolean.TRUE ) { final Throwable expectedFailure = (Throwable) store.remove( EXCEPTION_STORE_KEY ); - final Boolean isStrict = (Boolean) store.remove( IS_STRICT_STORE_KEY ); log.debugf( " >> Captured exception - %s", expectedFailure ); if ( expectedFailure == null ) { // even though we expected a failure, the test did not fail throw new NotImplementedYetExceptionExpected( context.getRequiredTestClass().getName(), - context.getRequiredTestMethod().getName(), - isStrict + context.getRequiredTestMethod().getName() ); } } @@ -128,19 +114,11 @@ public class NotImplementedYetExtension final ExtensionContext.Store store = context.getStore( namespace ); final Boolean isMarked = (Boolean) store.get( IS_MARKED_STORE_KEY ); - final Boolean isStrict = (Boolean) store.get( IS_STRICT_STORE_KEY ); if ( isMarked ) { - Throwable t = throwable; - do { - if ( t instanceof NotImplementedYetException || ! isStrict ) { - store.put( EXCEPTION_STORE_KEY, t ); - - log.debugf( "#Captured exception %s - ignoring it as expected", t ); - return; - } - t = t.getCause(); - } while ( t != null ); + store.put( EXCEPTION_STORE_KEY, throwable ); + log.debugf( "#Captured exception %s - ignoring it as expected", throwable ); + return; } // Otherwise, rethrow @@ -157,16 +135,15 @@ public class NotImplementedYetExtension public static class NotImplementedYetExceptionExpected extends RuntimeException { - private NotImplementedYetExceptionExpected(String testClassName, String testMethodName, boolean strict) { + private NotImplementedYetExceptionExpected(String testClassName, String testMethodName) { super( String.format( Locale.ROOT, "`%s#%s` is marked with `@NotImplementedYet`, however the test did not " + - "fail (%s). If the functionality has been implemented the `@NotImplementedYet` " + + "fail. If the functionality has been implemented the `@NotImplementedYet` " + "annotation should be removed", testClassName, - testMethodName, - strict + testMethodName ) ); }