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-18 12:03:53 -05:00
parent 348217c899
commit 9d515dd182
11 changed files with 78 additions and 36 deletions

View File

@ -64,27 +64,17 @@
* <p/> * <p/>
* Default depends on {@linkplain #trackActive()} - {@code deleted} if {@code false} and * Default depends on {@linkplain #trackActive()} - {@code deleted} if {@code false} and
* {@code active} if {@code true}. * {@code active} if {@code true}.
*
* @see SoftDeleteType#getDefaultColumnName()
*/ */
String columnName() default ""; String columnName() default "";
/** /**
* Whether the database value indicates active/inactive, as opposed to the * The strategy to use for storing/reading values to/from the database.
* default of tracking deleted/not-deleted
* <p/> * <p/>
* By default, the database values are interpreted as <ul> * The strategy also affects the default {@linkplain #columnName() column name}.
* <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; SoftDeleteType strategy() default SoftDeleteType.DELETED;
/** /**
* (Optional) Conversion to apply to determine the appropriate value to * (Optional) Conversion to apply to determine the appropriate value to

View File

@ -0,0 +1,48 @@
/*
* 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.annotations;
import java.util.Locale;
/**
* Enumeration of defines styles of soft-delete
*
* @author Steve Ebersole
*/
public enum SoftDeleteType {
/**
* Tracks rows which are active. The values stored in the database:<dl>
* <dt>{@code true}</dt>
* <dd>indicates that the row is active (non-deleted)</dd>
* <dt>{@code false}</dt>
* <dd>indicates that the row is inactive (deleted)</dd>
* </dl>
*
* @implNote Causes the {@linkplain SoftDelete#converter() conversion} to be wrapped in a negation.
*/
ACTIVE,
/**
* Tracks rows which are deleted. The values stored in the database:<dl>
* <dt>{@code true}</dt>
* <dd>indicates that the row is deleted</dd>
* <dt>{@code false}</dt>
* <dd>indicates that the row is non-deleted</dd>
* </dl>
*/
DELETED;
private final String defaultColumnName;
SoftDeleteType() {
this.defaultColumnName = name().toLowerCase( Locale.ROOT );
}
public String getDefaultColumnName() {
return defaultColumnName;
}
}

View File

@ -41,10 +41,6 @@
* @author Steve Ebersole * @author Steve Ebersole
*/ */
public class SoftDeleteHelper { 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 * Creates and binds the column and value for modeling the soft-delete in the database
* *
@ -80,7 +76,7 @@ private static BasicValue createSoftDeleteIndicatorValue(
); );
final BasicValue softDeleteIndicatorValue = new BasicValue( context, table ); final BasicValue softDeleteIndicatorValue = new BasicValue( context, table );
softDeleteIndicatorValue.makeSoftDelete( softDeleteConfig.trackActive() ); softDeleteIndicatorValue.makeSoftDelete( softDeleteConfig.strategy() );
softDeleteIndicatorValue.setJpaAttributeConverterDescriptor( converterDescriptor ); softDeleteIndicatorValue.setJpaAttributeConverterDescriptor( converterDescriptor );
softDeleteIndicatorValue.setImplicitJavaTypeAccess( (typeConfiguration) -> converterDescriptor.getRelationalValueResolvedType().getErasedType() ); softDeleteIndicatorValue.setImplicitJavaTypeAccess( (typeConfiguration) -> converterDescriptor.getRelationalValueResolvedType().getErasedType() );
return softDeleteIndicatorValue; return softDeleteIndicatorValue;
@ -112,7 +108,7 @@ private static void applyColumnName(
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 = coalesce( final String logicalColumnName = coalesce(
softDeleteConfig.trackActive() ? DEFAULT_REVERSED_COLUMN_NAME : DEFAULT_COLUMN_NAME, softDeleteConfig.strategy().getDefaultColumnName(),
softDeleteConfig.columnName() softDeleteConfig.columnName()
); );
final Identifier physicalColumnName = namingStrategy.toPhysicalColumnName( final Identifier physicalColumnName = namingStrategy.toPhysicalColumnName(

View File

@ -17,6 +17,7 @@
import org.hibernate.MappingException; import org.hibernate.MappingException;
import org.hibernate.TimeZoneStorageStrategy; import org.hibernate.TimeZoneStorageStrategy;
import org.hibernate.annotations.SoftDelete; import org.hibernate.annotations.SoftDelete;
import org.hibernate.annotations.SoftDeleteType;
import org.hibernate.annotations.TimeZoneStorageType; import org.hibernate.annotations.TimeZoneStorageType;
import org.hibernate.boot.model.TypeDefinition; import org.hibernate.boot.model.TypeDefinition;
import org.hibernate.boot.model.convert.internal.AutoApplicableConverterDescriptorBypassedImpl; import org.hibernate.boot.model.convert.internal.AutoApplicableConverterDescriptorBypassedImpl;
@ -100,7 +101,7 @@ public class BasicValue extends SimpleValue implements JdbcTypeIndicators, Resol
private TemporalType temporalPrecision; private TemporalType temporalPrecision;
private TimeZoneStorageType timeZoneStorageType; private TimeZoneStorageType timeZoneStorageType;
private boolean isSoftDelete; private boolean isSoftDelete;
private boolean isSoftDeleteReversed; private SoftDeleteType softDeleteStrategy;
private java.lang.reflect.Type resolvedJavaType; private java.lang.reflect.Type resolvedJavaType;
@ -150,13 +151,13 @@ public boolean isSoftDelete() {
return isSoftDelete; return isSoftDelete;
} }
public boolean isSoftDeleteReversed() { public SoftDeleteType getSoftDeleteStrategy() {
return isSoftDeleteReversed; return softDeleteStrategy;
} }
public void makeSoftDelete(boolean reversed) { public void makeSoftDelete(SoftDeleteType strategy) {
isSoftDelete = true; isSoftDelete = true;
isSoftDeleteReversed = reversed; softDeleteStrategy = strategy;
} }
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -476,7 +477,7 @@ else if ( jdbcType.isString() ) {
} }
} }
if ( isSoftDeleteReversed() ) { if ( getSoftDeleteStrategy() == SoftDeleteType.ACTIVE ) {
attributeConverterDescriptor = new ReversedConverterDescriptor<>( attributeConverterDescriptor ); attributeConverterDescriptor = new ReversedConverterDescriptor<>( attributeConverterDescriptor );
} }
} }

View File

@ -7,6 +7,7 @@
package org.hibernate.orm.test.softdelete; package org.hibernate.orm.test.softdelete;
import org.hibernate.annotations.SoftDelete; import org.hibernate.annotations.SoftDelete;
import org.hibernate.annotations.SoftDeleteType;
import org.hibernate.metamodel.spi.MappingMetamodelImplementor; import org.hibernate.metamodel.spi.MappingMetamodelImplementor;
import org.hibernate.type.NumericBooleanConverter; import org.hibernate.type.NumericBooleanConverter;
import org.hibernate.type.TrueFalseConverter; import org.hibernate.type.TrueFalseConverter;
@ -115,7 +116,7 @@ public static class YesNoEntity {
@Entity(name="ReversedYesNoEntity") @Entity(name="ReversedYesNoEntity")
@Table(name="reversed_yes_no_entity") @Table(name="reversed_yes_no_entity")
@SoftDelete(converter = YesNoConverter.class, trackActive = true) @SoftDelete(converter = YesNoConverter.class, strategy = SoftDeleteType.ACTIVE)
public static class ReversedYesNoEntity { public static class ReversedYesNoEntity {
@Id @Id
private Integer id; private Integer id;

View File

@ -13,6 +13,7 @@
import org.hibernate.ObjectNotFoundException; import org.hibernate.ObjectNotFoundException;
import org.hibernate.annotations.BatchSize; import org.hibernate.annotations.BatchSize;
import org.hibernate.annotations.SoftDelete; import org.hibernate.annotations.SoftDelete;
import org.hibernate.annotations.SoftDeleteType;
import org.hibernate.type.YesNoConverter; import org.hibernate.type.YesNoConverter;
import org.hibernate.testing.jdbc.SQLStatementInspector; import org.hibernate.testing.jdbc.SQLStatementInspector;
@ -214,7 +215,7 @@ void testRestrictedDeleteMutationQuery(SessionFactoryScope scope) {
@Entity(name="BatchLoadable") @Entity(name="BatchLoadable")
@Table(name="batch_loadable") @Table(name="batch_loadable")
@BatchSize(size = 5) @BatchSize(size = 5)
@SoftDelete(converter = YesNoConverter.class, trackActive = true) @SoftDelete(converter = YesNoConverter.class, strategy = SoftDeleteType.ACTIVE)
public static class BatchLoadable { public static class BatchLoadable {
@Id @Id
private Integer id; private Integer id;

View File

@ -11,6 +11,7 @@
import org.hibernate.annotations.Fetch; import org.hibernate.annotations.Fetch;
import org.hibernate.annotations.FetchMode; import org.hibernate.annotations.FetchMode;
import org.hibernate.annotations.SoftDelete; import org.hibernate.annotations.SoftDelete;
import org.hibernate.annotations.SoftDeleteType;
import org.hibernate.type.YesNoConverter; import org.hibernate.type.YesNoConverter;
import org.hibernate.testing.jdbc.SQLStatementInspector; import org.hibernate.testing.jdbc.SQLStatementInspector;
@ -144,7 +145,7 @@ public Issue(Integer id, String description, User reporter, User assignee) {
@Entity(name="User") @Entity(name="User")
@Table(name="users") @Table(name="users")
@SoftDelete(converter = YesNoConverter.class, trackActive = true) @SoftDelete(converter = YesNoConverter.class, strategy = SoftDeleteType.ACTIVE)
public static class User { public static class User {
@Id @Id
private Integer id; private Integer id;

View File

@ -9,6 +9,7 @@
import org.hibernate.SessionFactory; import org.hibernate.SessionFactory;
import org.hibernate.annotations.SQLDelete; import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.SoftDelete; import org.hibernate.annotations.SoftDelete;
import org.hibernate.annotations.SoftDeleteType;
import org.hibernate.boot.Metadata; import org.hibernate.boot.Metadata;
import org.hibernate.boot.MetadataSources; import org.hibernate.boot.MetadataSources;
import org.hibernate.metamodel.UnsupportedMappingException; import org.hibernate.metamodel.UnsupportedMappingException;
@ -65,7 +66,7 @@ public static class Person {
@Entity(name="Address") @Entity(name="Address")
@Table(name="addresses") @Table(name="addresses")
@SoftDelete(converter = YesNoConverter.class, trackActive = true) @SoftDelete(converter = YesNoConverter.class, strategy = SoftDeleteType.ACTIVE)
public static class Address { public static class Address {
@Id @Id
private Integer id; private Integer id;
@ -74,7 +75,7 @@ public static class Address {
@Entity(name="NoNo") @Entity(name="NoNo")
@Table(name="nonos") @Table(name="nonos")
@SoftDelete(converter = YesNoConverter.class, trackActive = true) @SoftDelete(converter = YesNoConverter.class, strategy = SoftDeleteType.ACTIVE)
@SQLDelete( sql = "delete from nonos" ) @SQLDelete( sql = "delete from nonos" )
public static class NoNo { public static class NoNo {
@Id @Id

View File

@ -12,6 +12,7 @@
import org.hibernate.annotations.Fetch; import org.hibernate.annotations.Fetch;
import org.hibernate.annotations.FetchMode; import org.hibernate.annotations.FetchMode;
import org.hibernate.annotations.SoftDelete; import org.hibernate.annotations.SoftDelete;
import org.hibernate.annotations.SoftDeleteType;
import org.hibernate.type.NumericBooleanConverter; import org.hibernate.type.NumericBooleanConverter;
import org.hibernate.type.YesNoConverter; import org.hibernate.type.YesNoConverter;
@ -35,13 +36,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(converter = YesNoConverter.class, trackActive = true) @SoftDelete(converter = YesNoConverter.class, strategy = SoftDeleteType.ACTIVE)
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(converter = NumericBooleanConverter.class, trackActive = true) @SoftDelete(converter = NumericBooleanConverter.class, strategy = SoftDeleteType.ACTIVE)
private Set<String> subSelectLoadable; private Set<String> subSelectLoadable;
public CollectionOwner2() { public CollectionOwner2() {

View File

@ -7,6 +7,7 @@
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.annotations.SoftDeleteType;
import org.hibernate.type.YesNoConverter; import org.hibernate.type.YesNoConverter;
import jakarta.persistence.Basic; import jakarta.persistence.Basic;
@ -20,7 +21,7 @@
@Table(name = "the_entity") @Table(name = "the_entity")
//tag::example-soft-delete-reverse[] //tag::example-soft-delete-reverse[]
@Entity @Entity
@SoftDelete(converter = YesNoConverter.class, trackActive = true) @SoftDelete(converter = YesNoConverter.class, strategy = SoftDeleteType.ACTIVE)
public class TheEntity { public class TheEntity {
// ... // ...
//end::example-soft-delete-reverse[] //end::example-soft-delete-reverse[]

View File

@ -7,6 +7,7 @@
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.annotations.SoftDeleteType;
import jakarta.persistence.Basic; import jakarta.persistence.Basic;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
@ -19,7 +20,7 @@
@Table(name = "the_entity2") @Table(name = "the_entity2")
//tag::example-soft-delete-reverse[] //tag::example-soft-delete-reverse[]
@Entity @Entity
@SoftDelete(trackActive = true) @SoftDelete(strategy = SoftDeleteType.ACTIVE)
public class TheEntity2 { public class TheEntity2 {
// ... // ...
//end::example-soft-delete-reverse[] //end::example-soft-delete-reverse[]