From 348217c899f949fe014eace012f447ddaea6ac95 Mon Sep 17 00:00:00 2001 From: Steve Ebersole Date: Sat, 14 Oct 2023 02:14:45 -0500 Subject: [PATCH] HHH-17164 - Proper, first-class soft-delete support HHH-17311 - Reversed soft delete support https://hibernate.atlassian.net/browse/HHH-17164 https://hibernate.atlassian.net/browse/HHH-17311 --- .../chapters/domain/soft_delete.adoc | 15 +++++---- .../org/hibernate/annotations/SoftDelete.java | 32 ++++++++++++++----- .../boot/model/internal/SoftDeleteHelper.java | 24 +++++++------- .../orm/test/softdelete/MappingTests.java | 2 +- .../softdelete/SimpleSoftDeleteTests.java | 2 +- .../orm/test/softdelete/ToOneTests.java | 2 +- .../orm/test/softdelete/ValidationTests.java | 4 +-- .../collections/CollectionOwner2.java | 4 +-- .../converter/reversed/TheEntity.java | 2 +- .../converter/reversed/TheEntity2.java | 3 +- migration-guide.adoc | 13 ++++++++ 11 files changed, 67 insertions(+), 36 deletions(-) diff --git a/documentation/src/main/asciidoc/userguide/chapters/domain/soft_delete.adoc b/documentation/src/main/asciidoc/userguide/chapters/domain/soft_delete.adoc index 4bcf3c231d..714d461e18 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/domain/soft_delete.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/domain/soft_delete.adoc @@ -22,14 +22,16 @@ Hibernate supports soft delete for both <> and <> which contains the indicator. -2. A conversion from `Boolean` indicator value to the proper database type -3. Whether to <> the indicator values +2. A <> from `Boolean` indicator value to the proper database type +3. Whether to <> the indicator values, tracking active/inactive instead [[soft-delete-column]] ==== Indicator column -The column where the indicator value is stored is defined using `@SoftDelete#columnName` attribute. This +The column where the indicator value is stored is defined using `@SoftDelete#columnName` attribute. + +When using <> mappings, the column name defaults to `active`; otherwise, it defaults to the name `deleted`. See <> for an example of customizing the column name. @@ -41,11 +43,10 @@ Depending on the conversion type, an appropriate check constraint may be applied ==== 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. +`boolean`. The "relational type" can be any type, as defined by the converter; generally `BOOLEAN`, `BIT`, `INTEGER` or `CHAR`. An explicit conversion can be specified using `@SoftDelete#converter`. See <> -for an example of specifying an explicit conversion. Explicit conversions can leverage the 3 +for an example of specifying an explicit conversion. Explicit conversions can specify a custom converter or leverage the 3 Hibernate-provided converters for the 3 most common cases - `NumericBooleanConverter`:: Defines conversion using `0` for `false` and `1` for `true` @@ -53,7 +54,7 @@ Hibernate-provided converters for the 3 most common cases - `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 - +<> to determine the proper database type - 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` 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 2bd30c5c18..b53d6fe937 100644 --- a/hibernate-core/src/main/java/org/hibernate/annotations/SoftDelete.java +++ b/hibernate-core/src/main/java/org/hibernate/annotations/SoftDelete.java @@ -11,6 +11,7 @@ import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.Target; +import org.hibernate.Incubating; import org.hibernate.dialect.Dialect; import jakarta.persistence.AttributeConverter; @@ -56,13 +57,34 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME; @Target({PACKAGE, TYPE, FIELD, METHOD, ANNOTATION_TYPE}) @Retention(RUNTIME) @Documented +@Incubating public @interface SoftDelete { /** * (Optional) The name of the column. *

- * Defaults to {@code deleted}. + * Default depends on {@linkplain #trackActive()} - {@code deleted} if {@code false} and + * {@code active} if {@code true}. */ - String columnName() default "deleted"; + String columnName() default ""; + + /** + * Whether the database value indicates active/inactive, as opposed to the + * default of tracking deleted/not-deleted + *

+ * By default, the database values are interpreted as

    + *
  • {@code true} means the row is considered deleted
  • + *
  • {@code false} means the row is considered NOT deleted
  • + *
+ *

+ * Setting this {@code true} reverses the interpretation of the database value

    + *
  • {@code true} means the row is active (NOT deleted)
  • + *
  • {@code false} means the row is inactive (deleted)
  • + *
+ * + * @implNote Causes the {@linkplain #converter() conversion} to be wrapped in + * a negated conversion. + */ + boolean trackActive() default false; /** * (Optional) Conversion to apply to determine the appropriate value to @@ -82,12 +104,6 @@ public @interface SoftDelete { */ 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 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 d8e9c702e0..2c68874956 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 @@ -43,6 +43,7 @@ import static org.hibernate.query.sqm.ComparisonOperator.EQUAL; public class SoftDeleteHelper { public static final String DEFAULT_COLUMN_NAME = "deleted"; + public static final String DEFAULT_REVERSED_COLUMN_NAME = "active"; /** * Creates and binds the column and value for modeling the soft-delete in the database @@ -57,6 +58,8 @@ public class SoftDeleteHelper { SoftDeletable target, Table table, MetadataBuildingContext context) { + assert softDeleteConfig != null; + final BasicValue softDeleteIndicatorValue = createSoftDeleteIndicatorValue( softDeleteConfig, table, context ); final Column softDeleteIndicatorColumn = createSoftDeleteIndicatorColumn( softDeleteConfig, @@ -67,25 +70,23 @@ public class SoftDeleteHelper { target.enableSoftDelete( softDeleteIndicatorColumn ); } - public static BasicValue createSoftDeleteIndicatorValue( - SoftDelete softDelete, + private static BasicValue createSoftDeleteIndicatorValue( + SoftDelete softDeleteConfig, Table table, MetadataBuildingContext context) { final ClassBasedConverterDescriptor converterDescriptor = new ClassBasedConverterDescriptor( - softDelete.converter(), + softDeleteConfig.converter(), context.getBootstrapContext().getClassmateContext() ); final BasicValue softDeleteIndicatorValue = new BasicValue( context, table ); - softDeleteIndicatorValue.makeSoftDelete( softDelete.reversed() ); + softDeleteIndicatorValue.makeSoftDelete( softDeleteConfig.trackActive() ); softDeleteIndicatorValue.setJpaAttributeConverterDescriptor( converterDescriptor ); - softDeleteIndicatorValue.setImplicitJavaTypeAccess( (typeConfiguration) -> { - return converterDescriptor.getRelationalValueResolvedType().getErasedType(); - } ); + softDeleteIndicatorValue.setImplicitJavaTypeAccess( (typeConfiguration) -> converterDescriptor.getRelationalValueResolvedType().getErasedType() ); return softDeleteIndicatorValue; } - public static Column createSoftDeleteIndicatorColumn( + private static Column createSoftDeleteIndicatorColumn( SoftDelete softDeleteConfig, BasicValue softDeleteIndicatorValue, MetadataBuildingContext context) { @@ -110,9 +111,10 @@ public class SoftDeleteHelper { 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 String logicalColumnName = coalesce( + softDeleteConfig.trackActive() ? DEFAULT_REVERSED_COLUMN_NAME : DEFAULT_COLUMN_NAME, + softDeleteConfig.columnName() + ); final Identifier physicalColumnName = namingStrategy.toPhysicalColumnName( database.toIdentifier( logicalColumnName ), database.getJdbcEnvironment() 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 63f5ab6987..e4a71744c8 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 @@ -115,7 +115,7 @@ public class MappingTests { @Entity(name="ReversedYesNoEntity") @Table(name="reversed_yes_no_entity") - @SoftDelete(columnName = "active", converter = YesNoConverter.class, reversed = true) + @SoftDelete(converter = YesNoConverter.class, trackActive = true) public static class ReversedYesNoEntity { @Id private Integer id; 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 07b0b2928c..137532656a 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 @@ -214,7 +214,7 @@ public class SimpleSoftDeleteTests { @Entity(name="BatchLoadable") @Table(name="batch_loadable") @BatchSize(size = 5) - @SoftDelete(columnName = "active", converter = YesNoConverter.class, reversed = true) + @SoftDelete(converter = YesNoConverter.class, trackActive = 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 b3bf089349..31b1594795 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 @@ -144,7 +144,7 @@ public class ToOneTests { @Entity(name="User") @Table(name="users") - @SoftDelete(columnName = "active", converter = YesNoConverter.class, reversed = true) + @SoftDelete(converter = YesNoConverter.class, trackActive = 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 59734ff938..bd4984e9fc 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 @@ -65,7 +65,7 @@ public class ValidationTests { @Entity(name="Address") @Table(name="addresses") - @SoftDelete(columnName = "active", converter = YesNoConverter.class, reversed = true) + @SoftDelete(converter = YesNoConverter.class, trackActive = true) public static class Address { @Id private Integer id; @@ -74,7 +74,7 @@ public class ValidationTests { @Entity(name="NoNo") @Table(name="nonos") - @SoftDelete(columnName = "active", converter = YesNoConverter.class, reversed = true) + @SoftDelete(converter = YesNoConverter.class, trackActive = 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/CollectionOwner2.java b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/collections/CollectionOwner2.java index 7f691b2bf5..e43e005a5a 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 @@ -35,13 +35,13 @@ public class CollectionOwner2 { @ElementCollection @CollectionTable(name="batch_loadables", joinColumns = @JoinColumn(name="owner_fk")) @BatchSize(size = 5) - @SoftDelete(columnName = "active", converter = YesNoConverter.class, reversed = true) + @SoftDelete(converter = YesNoConverter.class, trackActive = true) private Set batchLoadable; @ElementCollection @CollectionTable(name="subselect_loadables", joinColumns = @JoinColumn(name="owner_fk")) @Fetch(FetchMode.SUBSELECT) - @SoftDelete(columnName = "active", converter = NumericBooleanConverter.class, reversed = true) + @SoftDelete(converter = NumericBooleanConverter.class, trackActive = true) private Set subSelectLoadable; public CollectionOwner2() { 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 bb671a3c24..c29f2a930a 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 @@ -20,7 +20,7 @@ import jakarta.persistence.Table; @Table(name = "the_entity") //tag::example-soft-delete-reverse[] @Entity -@SoftDelete(columnName = "active", converter = YesNoConverter.class, reversed = true) +@SoftDelete(converter = YesNoConverter.class, trackActive = true) public class TheEntity { // ... //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 index 987fb2e9c6..94a94b2300 100644 --- 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 @@ -7,7 +7,6 @@ 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; @@ -20,7 +19,7 @@ import jakarta.persistence.Table; @Table(name = "the_entity2") //tag::example-soft-delete-reverse[] @Entity -@SoftDelete(columnName = "active", reversed = true) +@SoftDelete(trackActive = true) public class TheEntity2 { // ... //end::example-soft-delete-reverse[] diff --git a/migration-guide.adoc b/migration-guide.adoc index 483cb89fd3..15e2a7af60 100644 --- a/migration-guide.adoc +++ b/migration-guide.adoc @@ -19,3 +19,16 @@ earlier versions, see any other pertinent migration guides as well. [[soft-delete]] == Soft Delete +6.4 adds support for soft deletes against an entity's primary table and collection tables, using the +new `@SoftDelete` annotation. + +[source,java] +---- +@Entity +@SoftDelete +class Account { + ... +} +---- + +See the link:{userGuideBase}#soft-delete[User Guide] for details. \ No newline at end of file