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 -
|
||||
|
||||
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`
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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[]
|
||||
|
|
|
@ -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[]
|
||||
|
|
|
@ -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.
|
Loading…
Reference in New Issue