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:
parent
51f2f4f75d
commit
348217c899
|
@ -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 -
|
Soft delete support is defined by 3 main parts -
|
||||||
|
|
||||||
1. The <<soft-delete-column,column>> which contains the indicator.
|
1. The <<soft-delete-column,column>> which contains the indicator.
|
||||||
2. A conversion from `Boolean` indicator value to the proper database type
|
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
|
3. Whether to <<soft-delete-reverse,reverse>> the indicator values, tracking active/inactive instead
|
||||||
|
|
||||||
|
|
||||||
[[soft-delete-column]]
|
[[soft-delete-column]]
|
||||||
==== Indicator 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`.
|
defaults to the name `deleted`.
|
||||||
|
|
||||||
See <<soft-delete-basic-example>> for an example of customizing the column name.
|
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
|
==== Indicator conversion
|
||||||
|
|
||||||
The conversion is defined using a JPA <<basic-jpa-convert,AttributeConverter>>. The "domain type" is always
|
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,
|
`boolean`. The "relational type" can be any type, as defined by the converter; generally `BOOLEAN`, `BIT`, `INTEGER` or `CHAR`.
|
||||||
numerics and characters work best.
|
|
||||||
|
|
||||||
An explicit conversion can be specified using `@SoftDelete#converter`. See <<soft-delete-basic-example>>
|
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 -
|
Hibernate-provided converters for the 3 most common cases -
|
||||||
|
|
||||||
`NumericBooleanConverter`:: Defines conversion using `0` for `false` and `1` for `true`
|
`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`
|
`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
|
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
|
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`
|
numeric:: the underlying type is integer and values are converted according to `NumericBooleanConverter`
|
||||||
|
|
|
@ -11,6 +11,7 @@ import java.lang.annotation.ElementType;
|
||||||
import java.lang.annotation.Retention;
|
import java.lang.annotation.Retention;
|
||||||
import java.lang.annotation.Target;
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
import org.hibernate.Incubating;
|
||||||
import org.hibernate.dialect.Dialect;
|
import org.hibernate.dialect.Dialect;
|
||||||
|
|
||||||
import jakarta.persistence.AttributeConverter;
|
import jakarta.persistence.AttributeConverter;
|
||||||
|
@ -56,13 +57,34 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||||
@Target({PACKAGE, TYPE, FIELD, METHOD, ANNOTATION_TYPE})
|
@Target({PACKAGE, TYPE, FIELD, METHOD, ANNOTATION_TYPE})
|
||||||
@Retention(RUNTIME)
|
@Retention(RUNTIME)
|
||||||
@Documented
|
@Documented
|
||||||
|
@Incubating
|
||||||
public @interface SoftDelete {
|
public @interface SoftDelete {
|
||||||
/**
|
/**
|
||||||
* (Optional) The name of the column.
|
* (Optional) The name of the column.
|
||||||
* <p/>
|
* <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
|
* (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;
|
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
|
* Used as the default for {@linkplain SoftDelete#converter()}, indicating that
|
||||||
* {@linkplain Dialect#getPreferredSqlTypeCodeForBoolean() dialect} and
|
* {@linkplain Dialect#getPreferredSqlTypeCodeForBoolean() dialect} and
|
||||||
|
|
|
@ -43,6 +43,7 @@ import static org.hibernate.query.sqm.ComparisonOperator.EQUAL;
|
||||||
public class SoftDeleteHelper {
|
public class SoftDeleteHelper {
|
||||||
|
|
||||||
public static final String DEFAULT_COLUMN_NAME = "deleted";
|
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
|
* Creates and binds the column and value for modeling the soft-delete in the database
|
||||||
|
@ -57,6 +58,8 @@ public class SoftDeleteHelper {
|
||||||
SoftDeletable target,
|
SoftDeletable target,
|
||||||
Table table,
|
Table table,
|
||||||
MetadataBuildingContext context) {
|
MetadataBuildingContext context) {
|
||||||
|
assert softDeleteConfig != null;
|
||||||
|
|
||||||
final BasicValue softDeleteIndicatorValue = createSoftDeleteIndicatorValue( softDeleteConfig, table, context );
|
final BasicValue softDeleteIndicatorValue = createSoftDeleteIndicatorValue( softDeleteConfig, table, context );
|
||||||
final Column softDeleteIndicatorColumn = createSoftDeleteIndicatorColumn(
|
final Column softDeleteIndicatorColumn = createSoftDeleteIndicatorColumn(
|
||||||
softDeleteConfig,
|
softDeleteConfig,
|
||||||
|
@ -67,25 +70,23 @@ public class SoftDeleteHelper {
|
||||||
target.enableSoftDelete( softDeleteIndicatorColumn );
|
target.enableSoftDelete( softDeleteIndicatorColumn );
|
||||||
}
|
}
|
||||||
|
|
||||||
public static BasicValue createSoftDeleteIndicatorValue(
|
private static BasicValue createSoftDeleteIndicatorValue(
|
||||||
SoftDelete softDelete,
|
SoftDelete softDeleteConfig,
|
||||||
Table table,
|
Table table,
|
||||||
MetadataBuildingContext context) {
|
MetadataBuildingContext context) {
|
||||||
final ClassBasedConverterDescriptor converterDescriptor = new ClassBasedConverterDescriptor(
|
final ClassBasedConverterDescriptor converterDescriptor = new ClassBasedConverterDescriptor(
|
||||||
softDelete.converter(),
|
softDeleteConfig.converter(),
|
||||||
context.getBootstrapContext().getClassmateContext()
|
context.getBootstrapContext().getClassmateContext()
|
||||||
);
|
);
|
||||||
|
|
||||||
final BasicValue softDeleteIndicatorValue = new BasicValue( context, table );
|
final BasicValue softDeleteIndicatorValue = new BasicValue( context, table );
|
||||||
softDeleteIndicatorValue.makeSoftDelete( softDelete.reversed() );
|
softDeleteIndicatorValue.makeSoftDelete( softDeleteConfig.trackActive() );
|
||||||
softDeleteIndicatorValue.setJpaAttributeConverterDescriptor( converterDescriptor );
|
softDeleteIndicatorValue.setJpaAttributeConverterDescriptor( converterDescriptor );
|
||||||
softDeleteIndicatorValue.setImplicitJavaTypeAccess( (typeConfiguration) -> {
|
softDeleteIndicatorValue.setImplicitJavaTypeAccess( (typeConfiguration) -> converterDescriptor.getRelationalValueResolvedType().getErasedType() );
|
||||||
return converterDescriptor.getRelationalValueResolvedType().getErasedType();
|
|
||||||
} );
|
|
||||||
return softDeleteIndicatorValue;
|
return softDeleteIndicatorValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Column createSoftDeleteIndicatorColumn(
|
private static Column createSoftDeleteIndicatorColumn(
|
||||||
SoftDelete softDeleteConfig,
|
SoftDelete softDeleteConfig,
|
||||||
BasicValue softDeleteIndicatorValue,
|
BasicValue softDeleteIndicatorValue,
|
||||||
MetadataBuildingContext context) {
|
MetadataBuildingContext context) {
|
||||||
|
@ -110,9 +111,10 @@ public class SoftDeleteHelper {
|
||||||
MetadataBuildingContext context) {
|
MetadataBuildingContext context) {
|
||||||
final Database database = context.getMetadataCollector().getDatabase();
|
final Database database = context.getMetadataCollector().getDatabase();
|
||||||
final PhysicalNamingStrategy namingStrategy = context.getBuildingOptions().getPhysicalNamingStrategy();
|
final PhysicalNamingStrategy namingStrategy = context.getBuildingOptions().getPhysicalNamingStrategy();
|
||||||
final String logicalColumnName = softDeleteConfig == null
|
final String logicalColumnName = coalesce(
|
||||||
? DEFAULT_COLUMN_NAME
|
softDeleteConfig.trackActive() ? DEFAULT_REVERSED_COLUMN_NAME : DEFAULT_COLUMN_NAME,
|
||||||
: coalesce( DEFAULT_COLUMN_NAME, softDeleteConfig.columnName() );
|
softDeleteConfig.columnName()
|
||||||
|
);
|
||||||
final Identifier physicalColumnName = namingStrategy.toPhysicalColumnName(
|
final Identifier physicalColumnName = namingStrategy.toPhysicalColumnName(
|
||||||
database.toIdentifier( logicalColumnName ),
|
database.toIdentifier( logicalColumnName ),
|
||||||
database.getJdbcEnvironment()
|
database.getJdbcEnvironment()
|
||||||
|
|
|
@ -115,7 +115,7 @@ public class MappingTests {
|
||||||
|
|
||||||
@Entity(name="ReversedYesNoEntity")
|
@Entity(name="ReversedYesNoEntity")
|
||||||
@Table(name="reversed_yes_no_entity")
|
@Table(name="reversed_yes_no_entity")
|
||||||
@SoftDelete(columnName = "active", converter = YesNoConverter.class, reversed = true)
|
@SoftDelete(converter = YesNoConverter.class, trackActive = true)
|
||||||
public static class ReversedYesNoEntity {
|
public static class ReversedYesNoEntity {
|
||||||
@Id
|
@Id
|
||||||
private Integer id;
|
private Integer id;
|
||||||
|
|
|
@ -214,7 +214,7 @@ public class SimpleSoftDeleteTests {
|
||||||
@Entity(name="BatchLoadable")
|
@Entity(name="BatchLoadable")
|
||||||
@Table(name="batch_loadable")
|
@Table(name="batch_loadable")
|
||||||
@BatchSize(size = 5)
|
@BatchSize(size = 5)
|
||||||
@SoftDelete(columnName = "active", converter = YesNoConverter.class, reversed = true)
|
@SoftDelete(converter = YesNoConverter.class, trackActive = true)
|
||||||
public static class BatchLoadable {
|
public static class BatchLoadable {
|
||||||
@Id
|
@Id
|
||||||
private Integer id;
|
private Integer id;
|
||||||
|
|
|
@ -144,7 +144,7 @@ public class ToOneTests {
|
||||||
|
|
||||||
@Entity(name="User")
|
@Entity(name="User")
|
||||||
@Table(name="users")
|
@Table(name="users")
|
||||||
@SoftDelete(columnName = "active", converter = YesNoConverter.class, reversed = true)
|
@SoftDelete(converter = YesNoConverter.class, trackActive = true)
|
||||||
public static class User {
|
public static class User {
|
||||||
@Id
|
@Id
|
||||||
private Integer id;
|
private Integer id;
|
||||||
|
|
|
@ -65,7 +65,7 @@ public class ValidationTests {
|
||||||
|
|
||||||
@Entity(name="Address")
|
@Entity(name="Address")
|
||||||
@Table(name="addresses")
|
@Table(name="addresses")
|
||||||
@SoftDelete(columnName = "active", converter = YesNoConverter.class, reversed = true)
|
@SoftDelete(converter = YesNoConverter.class, trackActive = true)
|
||||||
public static class Address {
|
public static class Address {
|
||||||
@Id
|
@Id
|
||||||
private Integer id;
|
private Integer id;
|
||||||
|
@ -74,7 +74,7 @@ public class ValidationTests {
|
||||||
|
|
||||||
@Entity(name="NoNo")
|
@Entity(name="NoNo")
|
||||||
@Table(name="nonos")
|
@Table(name="nonos")
|
||||||
@SoftDelete(columnName = "active", converter = YesNoConverter.class, reversed = true)
|
@SoftDelete(converter = YesNoConverter.class, trackActive = true)
|
||||||
@SQLDelete( sql = "delete from nonos" )
|
@SQLDelete( sql = "delete from nonos" )
|
||||||
public static class NoNo {
|
public static class NoNo {
|
||||||
@Id
|
@Id
|
||||||
|
|
|
@ -35,13 +35,13 @@ public class CollectionOwner2 {
|
||||||
@ElementCollection
|
@ElementCollection
|
||||||
@CollectionTable(name="batch_loadables", joinColumns = @JoinColumn(name="owner_fk"))
|
@CollectionTable(name="batch_loadables", joinColumns = @JoinColumn(name="owner_fk"))
|
||||||
@BatchSize(size = 5)
|
@BatchSize(size = 5)
|
||||||
@SoftDelete(columnName = "active", converter = YesNoConverter.class, reversed = true)
|
@SoftDelete(converter = YesNoConverter.class, trackActive = true)
|
||||||
private Set<String> batchLoadable;
|
private Set<String> batchLoadable;
|
||||||
|
|
||||||
@ElementCollection
|
@ElementCollection
|
||||||
@CollectionTable(name="subselect_loadables", joinColumns = @JoinColumn(name="owner_fk"))
|
@CollectionTable(name="subselect_loadables", joinColumns = @JoinColumn(name="owner_fk"))
|
||||||
@Fetch(FetchMode.SUBSELECT)
|
@Fetch(FetchMode.SUBSELECT)
|
||||||
@SoftDelete(columnName = "active", converter = NumericBooleanConverter.class, reversed = true)
|
@SoftDelete(converter = NumericBooleanConverter.class, trackActive = true)
|
||||||
private Set<String> subSelectLoadable;
|
private Set<String> subSelectLoadable;
|
||||||
|
|
||||||
public CollectionOwner2() {
|
public CollectionOwner2() {
|
||||||
|
|
|
@ -20,7 +20,7 @@ import jakarta.persistence.Table;
|
||||||
@Table(name = "the_entity")
|
@Table(name = "the_entity")
|
||||||
//tag::example-soft-delete-reverse[]
|
//tag::example-soft-delete-reverse[]
|
||||||
@Entity
|
@Entity
|
||||||
@SoftDelete(columnName = "active", converter = YesNoConverter.class, reversed = true)
|
@SoftDelete(converter = YesNoConverter.class, trackActive = true)
|
||||||
public class TheEntity {
|
public class TheEntity {
|
||||||
// ...
|
// ...
|
||||||
//end::example-soft-delete-reverse[]
|
//end::example-soft-delete-reverse[]
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
package org.hibernate.orm.test.softdelete.converter.reversed;
|
package org.hibernate.orm.test.softdelete.converter.reversed;
|
||||||
|
|
||||||
import org.hibernate.annotations.SoftDelete;
|
import org.hibernate.annotations.SoftDelete;
|
||||||
import org.hibernate.type.YesNoConverter;
|
|
||||||
|
|
||||||
import jakarta.persistence.Basic;
|
import jakarta.persistence.Basic;
|
||||||
import jakarta.persistence.Entity;
|
import jakarta.persistence.Entity;
|
||||||
|
@ -20,7 +19,7 @@ import jakarta.persistence.Table;
|
||||||
@Table(name = "the_entity2")
|
@Table(name = "the_entity2")
|
||||||
//tag::example-soft-delete-reverse[]
|
//tag::example-soft-delete-reverse[]
|
||||||
@Entity
|
@Entity
|
||||||
@SoftDelete(columnName = "active", reversed = true)
|
@SoftDelete(trackActive = true)
|
||||||
public class TheEntity2 {
|
public class TheEntity2 {
|
||||||
// ...
|
// ...
|
||||||
//end::example-soft-delete-reverse[]
|
//end::example-soft-delete-reverse[]
|
||||||
|
|
|
@ -19,3 +19,16 @@ earlier versions, see any other pertinent migration guides as well.
|
||||||
[[soft-delete]]
|
[[soft-delete]]
|
||||||
== 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.
|
Loading…
Reference in New Issue