diff --git a/documentation/src/main/asciidoc/userguide/chapters/domain/DomainModel.adoc b/documentation/src/main/asciidoc/userguide/chapters/domain/DomainModel.adoc index 74702582c2..429577cdf5 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/domain/DomainModel.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/domain/DomainModel.adoc @@ -33,6 +33,7 @@ include::associations.adoc[] include::collections.adoc[] include::natural_id.adoc[] include::partitioning.adoc[] +include::soft_delete.adoc[] include::dynamic_model.adoc[] include::inheritance.adoc[] include::immutability.adoc[] diff --git a/documentation/src/main/asciidoc/userguide/chapters/domain/soft_delete.adoc b/documentation/src/main/asciidoc/userguide/chapters/domain/soft_delete.adoc new file mode 100644 index 0000000000..4bcf3c231d --- /dev/null +++ b/documentation/src/main/asciidoc/userguide/chapters/domain/soft_delete.adoc @@ -0,0 +1,166 @@ +[[soft-delete]] +=== Soft Delete +:root-project-dir: ../../../../../../.. +:core-project-dir: {root-project-dir}/hibernate-core +:testing-dir: {core-project-dir}/src/test/java/org/hibernate/orm/test/softdelete + +An occasional requirement seen in the wild is to never physically remove rows from the database, but to +instead perform a "soft delete" where a column is updated to indicate that the row is no longer active. +Hibernate offers first-class support for this behavior through its `@SoftDelete` annotation. + +[NOTE] +==== +The `@SoftDelete` annotation is new in 6.4. + +It was possible to hack together support for soft deletes in previous versions using a combination of filters, +`@Where` and custom delete event handling. However, that approach was tedious and did not work in +all cases. `@SoftDelete` should be highly preferred. +==== + +Hibernate supports soft delete for both <> and <>. + +Soft delete support is defined by 3 main parts - + +1. The <> which contains the indicator. +2. A conversion from `Boolean` indicator value to the proper database type +3. Whether to <> the indicator values + + +[[soft-delete-column]] +==== Indicator column + +The column where the indicator value is stored is defined using `@SoftDelete#columnName` attribute. This +defaults to the name `deleted`. + +See <> for an example of customizing the column name. + +Depending on the conversion type, an appropriate check constraint may be applied to the column. + + +[[soft-delete-conversion]] +==== Indicator conversion + +The conversion is defined using a JPA <>. The "domain type" is always +boolean. The "relational type" can be any type, as defined by the converter; though generally speaking, +numerics and characters work best. + +An explicit conversion can be specified using `@SoftDelete#converter`. See <> +for an example of specifying an explicit conversion. Explicit conversions can leverage the 3 +Hibernate-provided converters for the 3 most common cases - + +`NumericBooleanConverter`:: Defines conversion using `0` for `false` and `1` for `true` +`YesNoConverter`:: Defines conversion using `'N'` for `false` and `'Y'` for `true` +`TrueFalseConverter`:: Defines conversion using `'F'` for `false` and `'T'` for `true` + +If an explicit converter is not specified, Hibernate will follow the same resolution steps defined in +<> to determine the proper database type. This breaks down into 3 categories - + +boolean (and bit):: the underlying type is boolean / bit and no conversion is applied +numeric:: the underlying type is integer and values are converted according to `NumericBooleanConverter` +character:: the underlying type is char and values are converted according to `TrueFalseConverter` + + +[[soft-delete-entity]] +==== Entity soft delete + +Hibernate supports the soft delete of entities, with the indicator column defined on the primary table. + +[[soft-delete-basic-example]] +.Basic entity soft-delete +==== +[source,java] +---- +include::{testing-dir}/SimpleEntity.java[tag=example-soft-delete-basic, indent=0] +---- +==== + +For entity hierarchies, the soft delete applies to all inheritance types. + +[[soft-delete-secondary-example]] +.Inherited entity soft-delete +==== +[source,java] +---- +include::{testing-dir}/secondary/JoinedRoot.java[tag=example-soft-delete-secondary, indent=0] +include::{testing-dir}/secondary/JoinedSub.java[tag=example-soft-delete-secondary, indent=0] +---- +==== + +See also <>. + + +[[soft-delete-collection]] +==== Collection soft delete + +Soft delete may be applied to collection mapped with a "collection table", aka `@ElementCollection` +and `@ManyToMany`. The soft delete applies to the collection table row. + +Annotating a `@OneToMany` association with `@SoftDelete` will throw an exception. + +In the case of `@OneToMany` and `@ManyToMany`, the mapped entity may itself be soft deletable which is +handled transparently. + +[[soft-delete-element-collection-example]] +.Soft delete for @ElementCollection +==== +[source,java] +---- +include::{testing-dir}/collections/CollectionOwner.java[tag=example-soft-delete-element-collection, indent=0] +---- +==== + +Given this `@ElementCollection` mapping, rows in the `elements` table will be soft deleted using an indicator column named `deleted`. + +[[soft-delete-many2many-example]] +.Soft delete for @ManyToMany +==== +[source,java] +---- +include::{testing-dir}/collections/CollectionOwner.java[tag=example-soft-delete-many-to-many, indent=0] +---- +==== + +Given this `@ManyToMany` mapping, rows in the `m2m` table will be soft deleted using an indicator column named `gone`. + +See also <>. + + +[[soft-delete-package]] +==== Package-level soft delete + +The `@SoftDelete` annotation may also be placed at the package level, in which case it applies to all +entities and collections defined within the package. + + +[[soft-delete-reverse]] +==== Reversed soft delete + +A common requirement in applications using soft delete is to track rows which are active as opposed to removed, +reversing the boolean value. For example: + +[[soft-delete-reverse-example]] +.Reversed soft-delete +==== +[source,java] +---- +include::{testing-dir}/converter/reversed/TheEntity.java[tag=example-soft-delete-reverse, indent=0] +---- +==== + +When an instance of `TheEntity` is persisted, the value `'Y'` will be inserted into the +`active` column. When an instance of `TheEntity` is removed, the column's value is updated to `'N'`. + +This example explicitly specifies the built-in `YesNoConverter`, but reversal works with any conversion +even implicit conversions - + +[[soft-delete-reverse-example-2]] +.Reversed soft-delete with implicit conversion +==== +[source,java] +---- +include::{testing-dir}/converter/reversed/TheEntity2.java[tag=example-soft-delete-reverse, indent=0] +---- +==== + +The important thing to remember is that the stored values are reversed from the "normal" soft delete state. +`active == true` is the same as `deleted == false` - both describe the same state. \ No newline at end of file diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/SoftDelete.java b/hibernate-core/src/main/java/org/hibernate/annotations/SoftDelete.java index 66acbba324..2bd30c5c18 100644 --- a/hibernate-core/src/main/java/org/hibernate/annotations/SoftDelete.java +++ b/hibernate-core/src/main/java/org/hibernate/annotations/SoftDelete.java @@ -12,7 +12,6 @@ import java.lang.annotation.Retention; import java.lang.annotation.Target; import org.hibernate.dialect.Dialect; -import org.hibernate.type.BooleanAsBooleanConverter; import jakarta.persistence.AttributeConverter; @@ -65,19 +64,6 @@ public @interface SoftDelete { */ 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:

@@ -94,5 +80,19 @@ public @interface SoftDelete { * * @apiNote The converter should never return {@code null} */ - Class> converter() default BooleanAsBooleanConverter.class; + Class> converter() default UnspecifiedConversion.class; + + /** + * Whether the stored values should be reversed. This is used when the application tracks + * rows that are active as opposed to rows that are deleted. + */ + boolean reversed() default false; + + /** + * Used as the default for {@linkplain SoftDelete#converter()}, indicating that + * {@linkplain Dialect#getPreferredSqlTypeCodeForBoolean() dialect} and + * {@linkplain org.hibernate.cfg.MappingSettings#PREFERRED_BOOLEAN_JDBC_TYPE settings} + * resolution should be used. + */ + interface UnspecifiedConversion extends AttributeConverter {} } 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 index 60b2f5df2c..d8e9c702e0 100644 --- 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 @@ -77,6 +77,7 @@ public class SoftDeleteHelper { ); final BasicValue softDeleteIndicatorValue = new BasicValue( context, table ); + softDeleteIndicatorValue.makeSoftDelete( softDelete.reversed() ); softDeleteIndicatorValue.setJpaAttributeConverterDescriptor( converterDescriptor ); softDeleteIndicatorValue.setImplicitJavaTypeAccess( (typeConfiguration) -> { return converterDescriptor.getRelationalValueResolvedType().getErasedType(); @@ -149,8 +150,17 @@ public class SoftDeleteHelper { //noinspection unchecked final JdbcLiteralFormatter literalFormatter = resolution.getJdbcMapping().getJdbcLiteralFormatter(); - final Object deletedLiteralValue = converter.toRelationalValue( true ); - final Object nonDeletedLiteralValue = converter.toRelationalValue( false ); + final Object deletedLiteralValue; + final Object nonDeletedLiteralValue; + if ( converter == null ) { + // the database column is BIT or BOOLEAN : pass-thru + deletedLiteralValue = true; + nonDeletedLiteralValue = false; + } + else { + deletedLiteralValue = converter.toRelationalValue( true ); + nonDeletedLiteralValue = converter.toRelationalValue( false ); + } return new SoftDeleteMappingImpl( softDeletableModelPart, diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/BasicValue.java b/hibernate-core/src/main/java/org/hibernate/mapping/BasicValue.java index 2c6100c53f..031f08fe8b 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/BasicValue.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/BasicValue.java @@ -16,9 +16,13 @@ import org.hibernate.Incubating; import org.hibernate.Internal; import org.hibernate.MappingException; import org.hibernate.TimeZoneStorageStrategy; +import org.hibernate.annotations.SoftDelete; import org.hibernate.annotations.TimeZoneStorageType; import org.hibernate.boot.model.TypeDefinition; +import org.hibernate.boot.model.convert.internal.AutoApplicableConverterDescriptorBypassedImpl; import org.hibernate.boot.model.convert.internal.ClassBasedConverterDescriptor; +import org.hibernate.boot.model.convert.internal.InstanceBasedConverterDescriptor; +import org.hibernate.boot.model.convert.spi.AutoApplicableConverterDescriptor; import org.hibernate.boot.model.convert.spi.ConverterDescriptor; import org.hibernate.boot.model.convert.spi.JpaAttributeConverterCreationContext; import org.hibernate.boot.model.process.internal.InferredBasicValueResolution; @@ -47,13 +51,17 @@ import org.hibernate.resource.beans.spi.ManagedBeanRegistry; import org.hibernate.tool.schema.extract.spi.ColumnTypeInformation; import org.hibernate.type.BasicType; import org.hibernate.type.CustomType; +import org.hibernate.type.NumericBooleanConverter; +import org.hibernate.type.TrueFalseConverter; import org.hibernate.type.Type; import org.hibernate.type.WrapperArrayHandling; import org.hibernate.type.descriptor.converter.spi.BasicValueConverter; +import org.hibernate.type.descriptor.converter.spi.JpaAttributeConverter; import org.hibernate.type.descriptor.java.BasicJavaType; import org.hibernate.type.descriptor.java.BasicPluralJavaType; import org.hibernate.type.descriptor.java.JavaType; import org.hibernate.type.descriptor.java.MutabilityPlan; +import org.hibernate.type.descriptor.jdbc.BooleanJdbcType; import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators; import org.hibernate.type.internal.BasicTypeImpl; @@ -62,6 +70,7 @@ import org.hibernate.type.spi.TypeConfigurationAware; import org.hibernate.usertype.DynamicParameterizedType; import org.hibernate.usertype.UserType; +import com.fasterxml.classmate.ResolvedType; import jakarta.persistence.AttributeConverter; import jakarta.persistence.EnumType; import jakarta.persistence.TemporalType; @@ -73,7 +82,7 @@ import static org.hibernate.mapping.MappingHelper.injectParameters; /** * @author Steve Ebersole */ -public class BasicValue extends SimpleValue implements JdbcTypeIndicators, Resolvable { +public class BasicValue extends SimpleValue implements JdbcTypeIndicators, Resolvable, JpaAttributeConverterCreationContext { private static final CoreMessageLogger log = CoreLogging.messageLogger( BasicValue.class ); // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -90,6 +99,8 @@ public class BasicValue extends SimpleValue implements JdbcTypeIndicators, Resol private EnumType enumerationStyle; private TemporalType temporalPrecision; private TimeZoneStorageType timeZoneStorageType; + private boolean isSoftDelete; + private boolean isSoftDeleteReversed; private java.lang.reflect.Type resolvedJavaType; @@ -135,6 +146,19 @@ public class BasicValue extends SimpleValue implements JdbcTypeIndicators, Resol return new BasicValue( this ); } + public boolean isSoftDelete() { + return isSoftDelete; + } + + public boolean isSoftDeleteReversed() { + return isSoftDeleteReversed; + } + + public void makeSoftDelete(boolean reversed) { + isSoftDelete = true; + isSoftDeleteReversed = reversed; + } + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Setters - in preparation of resolution @@ -417,16 +441,168 @@ public class BasicValue extends SimpleValue implements JdbcTypeIndicators, Resol } // determine JavaType if we can - final BasicJavaType explicitJavaType = - explicitJavaTypeAccess == null ? null : explicitJavaTypeAccess.apply( getTypeConfiguration() ); - final JavaType javaType = determineJavaType( explicitJavaType ); + final BasicJavaType explicitJavaType = explicitJavaTypeAccess == null + ? null + : explicitJavaTypeAccess.apply( getTypeConfiguration() ); + + JavaType javaType = determineJavaType( explicitJavaType ); + ConverterDescriptor attributeConverterDescriptor = getAttributeConverterDescriptor(); + + if ( isSoftDelete() ) { + assert attributeConverterDescriptor != null; + final boolean conversionWasUnspecified = SoftDelete.UnspecifiedConversion.class.equals( attributeConverterDescriptor.getAttributeConverterClass() ); + if ( conversionWasUnspecified ) { + final JdbcType jdbcType = BooleanJdbcType.INSTANCE.resolveIndicatedType( this, javaType ); + if ( jdbcType.isNumber() ) { + attributeConverterDescriptor = new InstanceBasedConverterDescriptor( + NumericBooleanConverter.INSTANCE, + getBuildingContext().getBootstrapContext().getClassmateContext() + ); + } + else if ( jdbcType.isString() ) { + // here we pick 'T' / 'F' storage, though 'Y' / 'N' is equally valid - its 50/50 + attributeConverterDescriptor = new InstanceBasedConverterDescriptor( + TrueFalseConverter.INSTANCE, + getBuildingContext().getBootstrapContext().getClassmateContext() + ); + } + else { + // should indicate BIT or BOOLEAN == no conversion needed + // - we still create the converter to properly set up JDBC type, etc + attributeConverterDescriptor = new InstanceBasedConverterDescriptor( + PassThruSoftDeleteConverter.INSTANCE, + getBuildingContext().getBootstrapContext().getClassmateContext() + ); + } + } + + if ( isSoftDeleteReversed() ) { + attributeConverterDescriptor = new ReversedConverterDescriptor<>( attributeConverterDescriptor ); + } + } - final ConverterDescriptor attributeConverterDescriptor = getAttributeConverterDescriptor(); return attributeConverterDescriptor != null ? converterResolution( javaType, attributeConverterDescriptor ) : resolution( explicitJavaType, javaType ); } + private static class ReversedConverterDescriptor implements ConverterDescriptor { + private final ConverterDescriptor underlyingDescriptor; + + public ReversedConverterDescriptor(ConverterDescriptor underlyingDescriptor) { + this.underlyingDescriptor = underlyingDescriptor; + } + + @Override + public Class> getAttributeConverterClass() { + //noinspection unchecked + return (Class>) getClass(); + } + + @Override + public ResolvedType getDomainValueResolvedType() { + return underlyingDescriptor.getDomainValueResolvedType(); + } + + @Override + public ResolvedType getRelationalValueResolvedType() { + return underlyingDescriptor.getRelationalValueResolvedType(); + } + + @Override + public AutoApplicableConverterDescriptor getAutoApplyDescriptor() { + return AutoApplicableConverterDescriptorBypassedImpl.INSTANCE; + } + + @Override + public JpaAttributeConverter createJpaAttributeConverter(JpaAttributeConverterCreationContext context) { + //noinspection unchecked + return new ReversedJpaAttributeConverter<>( + (JpaAttributeConverter) underlyingDescriptor.createJpaAttributeConverter( context ), + context.getJavaTypeRegistry().getDescriptor( ReversedJpaAttributeConverter.class ) + ); + } + } + + private static class ReversedJpaAttributeConverter> + implements JpaAttributeConverter, AttributeConverter, ManagedBean { + private final JpaAttributeConverter underlyingJpaConverter; + private final JavaType> converterJavaType; + + public ReversedJpaAttributeConverter( + JpaAttributeConverter underlyingJpaConverter, + JavaType> converterJavaType) { + this.underlyingJpaConverter = underlyingJpaConverter; + this.converterJavaType = converterJavaType; + } + + @Override + public Boolean toDomainValue(R relationalValue) { + return !underlyingJpaConverter.toDomainValue( relationalValue ); + } + + @Override + public R toRelationalValue(Boolean domainValue) { + return underlyingJpaConverter.toRelationalValue( !domainValue ); + } + + @Override + public Boolean convertToEntityAttribute(R relationalValue) { + return toDomainValue( relationalValue ); + } + + @Override + public R convertToDatabaseColumn(Boolean domainValue) { + return toRelationalValue( domainValue ); + } + + @Override + public JavaType getDomainJavaType() { + return underlyingJpaConverter.getDomainJavaType(); + } + + @Override + public JavaType getRelationalJavaType() { + return underlyingJpaConverter.getRelationalJavaType(); + } + + @Override + public JavaType> getConverterJavaType() { + return converterJavaType; + } + + @Override + public ManagedBean> getConverterBean() { + return this; + } + + @Override + public Class getBeanClass() { + //noinspection unchecked + return (Class) getClass(); + } + + @Override + public B getBeanInstance() { + //noinspection unchecked + return (B) this; + } + } + + private static class PassThruSoftDeleteConverter implements AttributeConverter { + private static final PassThruSoftDeleteConverter INSTANCE = new PassThruSoftDeleteConverter(); + + @Override + public Boolean convertToDatabaseColumn(Boolean domainValue) { + return domainValue; + } + + @Override + public Boolean convertToEntityAttribute(Boolean relationalValue) { + return relationalValue; + } + } + private Resolution resolution(BasicJavaType explicitJavaType, JavaType javaType) { final JavaType basicJavaType; final JdbcType jdbcType; @@ -471,24 +647,19 @@ public class BasicValue extends SimpleValue implements JdbcTypeIndicators, Resol ); } + @Override + public ManagedBeanRegistry getManagedBeanRegistry() { + return getServiceRegistry().getService( ManagedBeanRegistry.class ); + } + private Resolution converterResolution(JavaType javaType, ConverterDescriptor attributeConverterDescriptor) { - final ManagedBeanRegistry managedBeanRegistry = getServiceRegistry().getService( ManagedBeanRegistry.class ); final NamedConverterResolution converterResolution = NamedConverterResolution.from( attributeConverterDescriptor, explicitJavaTypeAccess, explicitJdbcTypeAccess, explicitMutabilityPlanAccess, this, - new JpaAttributeConverterCreationContext() { - @Override - public ManagedBeanRegistry getManagedBeanRegistry() { - return managedBeanRegistry; - } - @Override - public TypeConfiguration getTypeConfiguration() { - return BasicValue.this.getTypeConfiguration(); - } - }, + this, getBuildingContext() ); diff --git a/hibernate-core/src/main/java/org/hibernate/type/BooleanAsBooleanConverter.java b/hibernate-core/src/main/java/org/hibernate/type/BooleanAsBooleanConverter.java deleted file mode 100644 index 29352497c9..0000000000 --- a/hibernate-core/src/main/java/org/hibernate/type/BooleanAsBooleanConverter.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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.annotations.SoftDelete; -import org.hibernate.type.descriptor.java.BooleanJavaType; -import org.hibernate.type.descriptor.java.JavaType; - -/** - * Simple pass-through boolean value converter. - * Useful in {@linkplain SoftDelete#converter() certain scenarios}. - * - * @author Steve Ebersole - */ -public class BooleanAsBooleanConverter implements StandardBooleanConverter { - 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/test/java/org/hibernate/orm/test/softdelete/ImplicitSoftDeleteTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/ImplicitSoftDeleteTests.java new file mode 100644 index 0000000000..7a9300b0bb --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/ImplicitSoftDeleteTests.java @@ -0,0 +1,211 @@ +/* + * 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.ResultSet; +import java.sql.Statement; +import java.util.List; + +import org.hibernate.ObjectNotFoundException; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.SoftDelete; + +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 = ImplicitSoftDeleteTests.ImplicitEntity.class) +@SessionFactory +public class ImplicitSoftDeleteTests { + @BeforeEach + void createTestData(SessionFactoryScope scope) { + scope.inTransaction( (session) -> { + session.persist( new ImplicitEntity( 1, "first" ) ); + session.persist( new ImplicitEntity( 2, "second" ) ); + session.persist( new ImplicitEntity( 3, "third" ) ); + } ); + + scope.inTransaction( (session) -> { + final ImplicitEntity first = session.getReference( ImplicitEntity.class, 1 ); + session.remove( first ); + + session.flush(); + + // make sure all 3 are still physically there + session.doWork( (connection) -> { + final Statement statement = connection.createStatement(); + final ResultSet resultSet = statement.executeQuery( "select count(1) from implicit_entities" ); + resultSet.next(); + final int count = resultSet.getInt( 1 ); + assertThat( count ).isEqualTo( 3 ); + } ); + } ); + } + + @AfterEach + void tearDown(SessionFactoryScope scope) { + scope.inTransaction( (session) -> session.doWork( (connection) -> { + final Statement statement = connection.createStatement(); + statement.execute( "delete from implicit_entities" ); + } ) ); + } + + @Test + void testSelectionQuery(SessionFactoryScope scope) { + scope.inTransaction( (session) -> { + // should not return #1 + assertThat( session.createQuery( "from ImplicitEntity" ).list() ).hasSize( 2 ); + } ); + } + + @Test + void testLoading(SessionFactoryScope scope) { + // Load + scope.inTransaction( (session) -> { + assertThat( session.get( ImplicitEntity.class, 1 ) ).isNull(); + assertThat( session.get( ImplicitEntity.class, 2 ) ).isNotNull(); + assertThat( session.get( ImplicitEntity.class, 3 ) ).isNotNull(); + } ); + + // Proxy + scope.inTransaction( (session) -> { + final ImplicitEntity reference = session.getReference( ImplicitEntity.class, 1 ); + try { + reference.getName(); + fail( "Expecting to fail" ); + } + catch (ObjectNotFoundException expected) { + } + + final ImplicitEntity reference2 = session.getReference( ImplicitEntity.class, 2 ); + reference2.getName(); + + final ImplicitEntity reference3 = session.getReference( ImplicitEntity.class, 3 ); + reference3.getName(); + } ); + } + + @Test + void testMultiLoading(SessionFactoryScope scope) { + scope.inTransaction( (session) -> { + final List results = session + .byMultipleIds( ImplicitEntity.class ) + // otherwise the first position would contain a null for #1 + .enableOrderedReturn( false ) + .multiLoad( 1, 2, 3 ); + assertThat( results ).hasSize( 2 ); + } ); + } + + @Test + void testNaturalIdLoading(SessionFactoryScope scope) { + scope.inTransaction( (session) -> { + final ImplicitEntity first = session.bySimpleNaturalId( ImplicitEntity.class ).load( "first" ); + assertThat( first ).isNull(); + + final ImplicitEntity second = session.bySimpleNaturalId( ImplicitEntity.class ).load( "second" ); + assertThat( second ).isNotNull(); + } ); + } + + @Test + void testDeletion(SessionFactoryScope scope) { + scope.inTransaction( (session) -> { + final ImplicitEntity reference = session.getReference( ImplicitEntity.class, 2 ); + session.remove( reference ); + session.flush(); + + final List active = session + .createSelectionQuery( "from ImplicitEntity", ImplicitEntity.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 ImplicitEntity set name = null" ).executeUpdate(); + assertThat( affected ).isEqualTo( 2 ); + } ); + } + + @Test + void testRestrictedUpdateMutationQuery(SessionFactoryScope scope) { + scope.inTransaction( (session) -> { + final int affected = session + .createMutationQuery( "update ImplicitEntity set name = null where name = 'second'" ) + .executeUpdate(); + assertThat( affected ).isEqualTo( 1 ); + } ); + } + + @Test + void testFullDeleteMutationQuery(SessionFactoryScope scope) { + scope.inTransaction( (session) -> { + final int affected = session.createMutationQuery( "delete ImplicitEntity" ).executeUpdate(); + // only #2 and #3 + assertThat( affected ).isEqualTo( 2 ); + } ); + } + + @Test + void testRestrictedDeleteMutationQuery(SessionFactoryScope scope) { + scope.inTransaction( (session) -> { + final int affected = session + .createMutationQuery( "delete ImplicitEntity where name = 'second'" ) + .executeUpdate(); + // only #2 + assertThat( affected ).isEqualTo( 1 ); + } ); + } + + @Entity(name="ImplicitEntity") + @Table(name="implicit_entities") + @SoftDelete + public static class ImplicitEntity { + @Id + private Integer id; + @NaturalId + private String name; + + public ImplicitEntity() { + } + + public ImplicitEntity(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/MappingTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/MappingTests.java index 02b63f18cf..63f5ab6987 100644 --- 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 @@ -8,7 +8,6 @@ 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; @@ -80,7 +79,7 @@ public class MappingTests { @Entity(name="BooleanEntity") @Table(name="boolean_entity") - @SoftDelete(converter = BooleanAsBooleanConverter.class) + @SoftDelete() public static class BooleanEntity { @Id private Integer id; @@ -116,7 +115,7 @@ public class MappingTests { @Entity(name="ReversedYesNoEntity") @Table(name="reversed_yes_no_entity") - @SoftDelete(columnName = "active", converter = ReverseYesNoConverter.class) + @SoftDelete(columnName = "active", converter = YesNoConverter.class, reversed = true) public static class ReversedYesNoEntity { @Id private Integer id; 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 deleted file mode 100644 index a8af90bc6a..0000000000 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/ReverseYesNoConverter.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * 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/SimpleEntity.java b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/SimpleEntity.java new file mode 100644 index 0000000000..8ee96ee5c6 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/SimpleEntity.java @@ -0,0 +1,46 @@ +/* + * 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.NaturalId; +import org.hibernate.annotations.SoftDelete; +import org.hibernate.type.YesNoConverter; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +@Table(name = "simple") +//tag::example-soft-delete-basic[] +@Entity(name = "SimpleEntity") +@SoftDelete(columnName = "removed", converter = YesNoConverter.class) +public class SimpleEntity { + // ... +//end::example-soft-delete-basic[] + @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; + } +//tag::example-soft-delete-basic[] +} +//end::example-soft-delete-basic[] 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 index 7c7795f379..07b0b2928c 100644 --- 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 @@ -12,7 +12,6 @@ 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; @@ -34,7 +33,7 @@ import static org.assertj.core.api.Assertions.fail; /** * @author Steve Ebersole */ -@DomainModel(annotatedClasses = { SimpleSoftDeleteTests.SimpleEntity.class, SimpleSoftDeleteTests.BatchLoadable.class }) +@DomainModel(annotatedClasses = { SimpleEntity.class, SimpleSoftDeleteTests.BatchLoadable.class }) @SessionFactory(useCollectingStatementInspector = true) public class SimpleSoftDeleteTests { @BeforeEach @@ -212,40 +211,10 @@ public class SimpleSoftDeleteTests { } ); } - /** - * @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) + @SoftDelete(columnName = "active", converter = YesNoConverter.class, reversed = true) public static class BatchLoadable { @Id private Integer id; 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 index 4ab49601d4..b3bf089349 100644 --- 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 @@ -11,18 +11,17 @@ import java.sql.Statement; import org.hibernate.annotations.Fetch; import org.hibernate.annotations.FetchMode; 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.FetchType; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; @@ -145,7 +144,7 @@ public class ToOneTests { @Entity(name="User") @Table(name="users") - @SoftDelete(columnName = "active", converter = ReverseYesNoConverter.class) + @SoftDelete(columnName = "active", converter = YesNoConverter.class, reversed = true) public static class User { @Id private Integer id; 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 index 4cf339785c..59734ff938 100644 --- 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 @@ -12,6 +12,7 @@ import org.hibernate.annotations.SoftDelete; import org.hibernate.boot.Metadata; import org.hibernate.boot.MetadataSources; import org.hibernate.metamodel.UnsupportedMappingException; +import org.hibernate.type.YesNoConverter; import org.junit.jupiter.api.Test; @@ -64,7 +65,7 @@ public class ValidationTests { @Entity(name="Address") @Table(name="addresses") - @SoftDelete(columnName = "active", converter = ReverseYesNoConverter.class) + @SoftDelete(columnName = "active", converter = YesNoConverter.class, reversed = true) public static class Address { @Id private Integer id; @@ -73,7 +74,7 @@ public class ValidationTests { @Entity(name="NoNo") @Table(name="nonos") - @SoftDelete(columnName = "active", converter = ReverseYesNoConverter.class) + @SoftDelete(columnName = "active", converter = YesNoConverter.class, reversed = true) @SQLDelete( sql = "delete from nonos" ) public static class NoNo { @Id 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 index 583cffacbd..156e23ec0e 100644 --- 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 @@ -35,12 +35,15 @@ public class CollectionOwner { @Basic private String name; + //tag::example-soft-delete-element-collection[] @ElementCollection @CollectionTable(name = "elements", joinColumns = @JoinColumn(name = "owner_fk")) @Column(name = "txt") @SoftDelete(converter = YesNoConverter.class) private Collection elements; + //end::example-soft-delete-element-collection[] + //tag::example-soft-delete-many-to-many[] @ManyToMany @JoinTable( name = "m2m", @@ -49,6 +52,7 @@ public class CollectionOwner { ) @SoftDelete(columnName = "gone", converter = NumericBooleanConverter.class) private Collection manyToMany; + //end::example-soft-delete-many-to-many[] protected CollectionOwner() { // for Hibernate use 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 index 893a93177b..7f691b2bf5 100644 --- 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 @@ -12,7 +12,8 @@ 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 org.hibernate.type.NumericBooleanConverter; +import org.hibernate.type.YesNoConverter; import jakarta.persistence.CollectionTable; import jakarta.persistence.ElementCollection; @@ -34,13 +35,13 @@ public class CollectionOwner2 { @ElementCollection @CollectionTable(name="batch_loadables", joinColumns = @JoinColumn(name="owner_fk")) @BatchSize(size = 5) - @SoftDelete(columnName = "active", converter = ReverseYesNoConverter.class) + @SoftDelete(columnName = "active", converter = YesNoConverter.class, reversed = true) private Set batchLoadable; @ElementCollection @CollectionTable(name="subselect_loadables", joinColumns = @JoinColumn(name="owner_fk")) @Fetch(FetchMode.SUBSELECT) - @SoftDelete(columnName = "active", converter = ReverseYesNoConverter.class) + @SoftDelete(columnName = "active", converter = NumericBooleanConverter.class, reversed = true) private Set subSelectLoadable; public CollectionOwner2() { 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 index 5699ddf4e5..8fb4fbf049 100644 --- 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 @@ -121,7 +121,7 @@ public class FetchLoadableTests { // trigger loading one of the subselect-loadable collections first.getSubSelectLoadable().size(); assertThat( statementInspector.getSqlQueries() ).hasSize( 1 ); - assertThat( statementInspector.getSqlQueries().get( 0 ) ).containsAnyOf( "active='Y'", "active=N'Y'" ); + assertThat( statementInspector.getSqlQueries().get( 0 ) ).containsAnyOf( "active=1", "active=N'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/converter/reversed/ReversedSoftDeleteTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/converter/reversed/ReversedSoftDeleteTests.java index e47f443746..5234a337b6 100644 --- 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 @@ -6,22 +6,27 @@ */ package org.hibernate.orm.test.softdelete.converter.reversed; +import org.hibernate.cfg.AvailableSettings; 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.ServiceRegistry; import org.hibernate.testing.orm.junit.SessionFactory; import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.Setting; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; /** + * @implNote {@code preferred_boolean_jdbc_type=CHAR} will use T/F as the default (Entity2) + * * @author Steve Ebersole */ -@DomainModel(annotatedClasses = { ReverseYesNoConverter.class, TheEntity.class }) +@ServiceRegistry(settings = @Setting(name= AvailableSettings.PREFERRED_BOOLEAN_JDBC_TYPE, value = "CHAR")) +@DomainModel(annotatedClasses = {TheEntity.class, TheEntity2.class}) @SessionFactory( useCollectingStatementInspector = true) public class ReversedSoftDeleteTests { @Test @@ -33,6 +38,12 @@ public class ReversedSoftDeleteTests { "the_entity", 'N' ); + MappingVerifier.verifyMapping( + metamodel.getEntityDescriptor( TheEntity2.class ).getSoftDeleteMapping(), + "active", + "the_entity2", + 'F' + ); } @Test @@ -60,4 +71,30 @@ public class ReversedSoftDeleteTests { assertThat( sqlInspector.getSqlQueries().get( 0 ) ).containsIgnoringCase( "update " ); assertThat( sqlInspector.getSqlQueries().get( 0 ) ).containsAnyOf( "active='N'", "active=N'N'" ); } + + @Test + void testUsage2(SessionFactoryScope scope) { + final SQLStatementInspector sqlInspector = scope.getCollectingStatementInspector(); + sqlInspector.clear(); + + scope.inTransaction( (session) -> { + session.persist( new TheEntity2( 1, "it" ) ); + } ); + + assertThat( sqlInspector.getSqlQueries() ).hasSize( 1 ); + assertThat( sqlInspector.getSqlQueries().get( 0 ) ).contains( "'T'" ); + assertThat( sqlInspector.getSqlQueries().get( 0 ) ).doesNotContain( "'F'" ); + + sqlInspector.clear(); + + scope.inTransaction( (session) -> { + final TheEntity2 reference = session.getReference( TheEntity2.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='F'" ); + } } 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 index 1469f4f8ea..bb671a3c24 100644 --- 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 @@ -7,20 +7,23 @@ package org.hibernate.orm.test.softdelete.converter.reversed; import org.hibernate.annotations.SoftDelete; -import org.hibernate.orm.test.softdelete.ReverseYesNoConverter; +import org.hibernate.type.YesNoConverter; +import jakarta.persistence.Basic; 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) +//tag::example-soft-delete-reverse[] +@Entity +@SoftDelete(columnName = "active", converter = YesNoConverter.class, reversed = true) public class TheEntity { + // ... +//end::example-soft-delete-reverse[] @Id private Integer id; @Basic @@ -46,4 +49,7 @@ public class TheEntity { public void setName(String name) { this.name = name; } + +//tag::example-soft-delete-reverse[] } +//end::example-soft-delete-reverse[] diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/converter/reversed/TheEntity2.java b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/converter/reversed/TheEntity2.java new file mode 100644 index 0000000000..987fb2e9c6 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/converter/reversed/TheEntity2.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.converter.reversed; + +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 + */ +@Table(name = "the_entity2") +//tag::example-soft-delete-reverse[] +@Entity +@SoftDelete(columnName = "active", reversed = true) +public class TheEntity2 { + // ... +//end::example-soft-delete-reverse[] + @Id + private Integer id; + @Basic + private String name; + + protected TheEntity2() { + // for Hibernate use + } + + public TheEntity2(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; + } + +//tag::example-soft-delete-reverse[] +} +//end::example-soft-delete-reverse[] 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 index 0d345e1779..87c5ed6327 100644 --- 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 @@ -22,11 +22,14 @@ import jakarta.persistence.Table; * * @author Steve Ebersole */ +@Table(name = "joined_root") +//tag::example-soft-delete-secondary[] @Entity @Inheritance(strategy = InheritanceType.JOINED) -@Table(name = "joined_root") @SoftDelete(columnName = "removed", converter = YesNoConverter.class) public abstract class JoinedRoot { + // ... +//end::example-soft-delete-secondary[] @Id private Integer id; @Basic @@ -52,4 +55,6 @@ public abstract class JoinedRoot { public void setName(String name) { this.name = name; } +//tag::example-soft-delete-secondary[] } +//end::example-soft-delete-secondary[] 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 index 333087008e..8c40900432 100644 --- 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 @@ -14,10 +14,13 @@ import jakarta.persistence.Table; /** * @author Steve Ebersole */ +//tag::example-soft-delete-secondary[] @Entity @Table(name = "joined_sub") @PrimaryKeyJoinColumn(name = "joined_fk") public class JoinedSub extends JoinedRoot { + // ... +//end::example-soft-delete-secondary[] @Basic String subDetails; @@ -28,4 +31,6 @@ public class JoinedSub extends JoinedRoot { super( id, name ); this.subDetails = subDetails; } +//tag::example-soft-delete-secondary[] } +//end::example-soft-delete-secondary[] diff --git a/migration-guide.adoc b/migration-guide.adoc index 11c2c37ac7..483cb89fd3 100644 --- a/migration-guide.adoc +++ b/migration-guide.adoc @@ -16,3 +16,6 @@ earlier versions, see any other pertinent migration guides as well. * link:{docsBase}/6.0/migration-guide/migration-guide.html[6.0 Migration guide] +[[soft-delete]] +== Soft Delete +