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
This commit is contained in:
Steve Ebersole 2023-10-14 02:14:45 -05:00
parent 51f2f4f75d
commit 348217c899
11 changed files with 67 additions and 36 deletions

View File

@ -22,14 +22,16 @@ Hibernate supports soft delete for both <<soft-delete-entity,entities>> and <<so
Soft delete support is defined by 3 main parts -
1. The <<soft-delete-column,column>> which contains the indicator.
2. A conversion from `Boolean` indicator value to the proper database type
3. Whether to <<soft-delete-reverse,reverse>> the indicator values
2. A <<soft-delete-conversion,conversion>> from `Boolean` indicator value to the proper database type
3. Whether to <<soft-delete-reverse,reverse>> 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 <<soft-delete-reverse,reversed>> mappings, the column name defaults to `active`; otherwise, it
defaults to the name `deleted`.
See <<soft-delete-basic-example>> 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 <<basic-jpa-convert,AttributeConverter>>. 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 <<soft-delete-basic-example>>
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
<<basic-boolean>> to determine the proper database type. This breaks down into 3 categories -
<<basic-boolean>> 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`

View File

@ -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.
* <p/>
* 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
* <p/>
* By default, the database values are interpreted as <ul>
* <li>{@code true} means the row is considered deleted</li>
* <li>{@code false} means the row is considered NOT deleted</li>
* </ul>
* <p/>
* Setting this {@code true} reverses the interpretation of the database value <ul>
* <li>{@code true} means the row is active (NOT deleted)</li>
* <li>{@code false} means the row is inactive (deleted)</li>
* </ul>
*
* @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<? extends AttributeConverter<Boolean,?>> 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

View File

@ -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()

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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

View File

@ -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<String> 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<String> subSelectLoadable;
public CollectionOwner2() {

View File

@ -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[]

View File

@ -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[]

View File

@ -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.