Skip to content

Commit b6990d2

Browse files
committed
HHH-17325 - @softdelete with timestamp
1 parent 79d1fa0 commit b6990d2

File tree

73 files changed

+1397
-766
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

73 files changed

+1397
-766
lines changed

hibernate-core/src/main/java/org/hibernate/annotations/SoftDelete.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import org.hibernate.dialect.Dialect;
1414

1515
import jakarta.persistence.AttributeConverter;
16+
import org.hibernate.metamodel.UnsupportedMappingException;
1617

1718
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
1819
import static java.lang.annotation.ElementType.FIELD;
@@ -87,7 +88,8 @@
8788
/**
8889
* The strategy to use for storing/reading values to/from the database.
8990
* <p/>
90-
* The strategy also affects the default {@linkplain #columnName() column name}.
91+
* The strategy also affects the default {@linkplain #columnName() column name} - see
92+
* {@linkplain SoftDeleteType#getDefaultColumnName}.
9193
*/
9294
SoftDeleteType strategy() default SoftDeleteType.DELETED;
9395

@@ -105,7 +107,11 @@
105107
* the {@linkplain Dialect#getPreferredSqlTypeCodeForBoolean() dialect}
106108
* and {@linkplain org.hibernate.cfg.MappingSettings#PREFERRED_BOOLEAN_JDBC_TYPE settings}
107109
*
108-
* @apiNote The converter should never return {@code null}
110+
* @apiNote Only valid when {@linkplain #strategy} is {@linkplain SoftDeleteType#DELETED}
111+
* or {@linkplain SoftDeleteType#ACTIVE}. Will lead to a {@linkplain UnsupportedMappingException}
112+
* when combined with {@linkplain SoftDeleteType#TIMESTAMP}.
113+
*
114+
* @implSpec The specified converter should never return {@code null}
109115
*/
110116
Class<? extends AttributeConverter<Boolean,?>> converter() default UnspecifiedConversion.class;
111117

hibernate-core/src/main/java/org/hibernate/annotations/SoftDeleteType.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,28 @@ public enum SoftDeleteType {
3232
* <dd>indicates that the row is non-deleted</dd>
3333
* </dl>
3434
*/
35-
DELETED;
35+
DELETED,
36+
37+
/**
38+
* Tracks rows which are deleted by the timestamp at which they were deleted. <dl>
39+
* <dt>{@code null}</dt>
40+
* <dd>indicates that the row is non-deleted</dd>
41+
* <dt>non-{@code null}</dt>
42+
* <dd>indicates that the row is deleted, at the given timestamp</dd>
43+
* </dl>
44+
*/
45+
TIMESTAMP( "deleted" );
3646

3747
private final String defaultColumnName;
3848

3949
SoftDeleteType() {
4050
this.defaultColumnName = name().toLowerCase( Locale.ROOT );
4151
}
4252

53+
SoftDeleteType(String defaultColumnName) {
54+
this.defaultColumnName = defaultColumnName;
55+
}
56+
4357
public String getDefaultColumnName() {
4458
return defaultColumnName;
4559
}

hibernate-core/src/main/java/org/hibernate/boot/model/internal/SoftDeleteHelper.java

Lines changed: 40 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -5,34 +5,25 @@
55
package org.hibernate.boot.model.internal;
66

77
import org.hibernate.annotations.SoftDelete;
8+
import org.hibernate.annotations.SoftDeleteType;
89
import org.hibernate.boot.model.convert.internal.ClassBasedConverterDescriptor;
910
import org.hibernate.boot.model.naming.Identifier;
1011
import org.hibernate.boot.model.naming.PhysicalNamingStrategy;
1112
import org.hibernate.boot.model.relational.Database;
1213
import org.hibernate.boot.spi.MetadataBuildingContext;
13-
import org.hibernate.dialect.Dialect;
1414
import org.hibernate.mapping.BasicValue;
1515
import org.hibernate.mapping.Column;
1616
import org.hibernate.mapping.SoftDeletable;
1717
import org.hibernate.mapping.Table;
18+
import org.hibernate.metamodel.UnsupportedMappingException;
1819
import org.hibernate.metamodel.mapping.SoftDeletableModelPart;
19-
import org.hibernate.metamodel.mapping.SoftDeleteMapping;
2020
import org.hibernate.metamodel.mapping.internal.MappingModelCreationProcess;
2121
import org.hibernate.metamodel.mapping.internal.SoftDeleteMappingImpl;
22-
import org.hibernate.sql.ast.spi.SqlExpressionResolver;
23-
import org.hibernate.sql.ast.tree.expression.ColumnReference;
24-
import org.hibernate.sql.ast.tree.expression.Expression;
25-
import org.hibernate.sql.ast.tree.expression.JdbcLiteral;
26-
import org.hibernate.sql.ast.tree.from.TableReference;
27-
import org.hibernate.sql.ast.tree.predicate.ComparisonPredicate;
28-
import org.hibernate.sql.ast.tree.predicate.Predicate;
29-
import org.hibernate.sql.ast.tree.update.Assignment;
30-
import org.hibernate.type.descriptor.converter.spi.BasicValueConverter;
31-
import org.hibernate.type.descriptor.jdbc.JdbcLiteralFormatter;
22+
23+
import java.time.Instant;
3224

3325
import static org.hibernate.internal.util.StringHelper.coalesce;
3426
import static org.hibernate.internal.util.StringHelper.isBlank;
35-
import static org.hibernate.query.sqm.ComparisonOperator.EQUAL;
3627

3728
/**
3829
* Helper for dealing with {@link org.hibernate.annotations.SoftDelete}
@@ -62,22 +53,35 @@ public static void bindSoftDeleteIndicator(
6253
context
6354
);
6455
table.addColumn( softDeleteIndicatorColumn );
65-
target.enableSoftDelete( softDeleteIndicatorColumn );
56+
target.enableSoftDelete( softDeleteIndicatorColumn, softDeleteConfig.strategy() );
6657
}
6758

6859
private static BasicValue createSoftDeleteIndicatorValue(
6960
SoftDelete softDeleteConfig,
7061
Table table,
7162
MetadataBuildingContext context) {
72-
final ClassBasedConverterDescriptor converterDescriptor = new ClassBasedConverterDescriptor(
73-
softDeleteConfig.converter(),
74-
context.getBootstrapContext().getClassmateContext()
75-
);
76-
7763
final BasicValue softDeleteIndicatorValue = new BasicValue( context, table );
7864
softDeleteIndicatorValue.makeSoftDelete( softDeleteConfig.strategy() );
79-
softDeleteIndicatorValue.setJpaAttributeConverterDescriptor( converterDescriptor );
80-
softDeleteIndicatorValue.setImplicitJavaTypeAccess( (typeConfiguration) -> converterDescriptor.getRelationalValueResolvedType().getErasedType() );
65+
66+
if ( softDeleteConfig.strategy() == SoftDeleteType.TIMESTAMP ) {
67+
if ( softDeleteConfig.converter() != SoftDelete.UnspecifiedConversion.class ) {
68+
throw new UnsupportedMappingException(
69+
"Specifying SoftDelete#converter in conjunction with SoftDeleteType.TIMESTAMP is not supported"
70+
);
71+
}
72+
softDeleteIndicatorValue.setImplicitJavaTypeAccess( (typeConfiguration) -> Instant.class );
73+
}
74+
else {
75+
final ClassBasedConverterDescriptor converterDescriptor = new ClassBasedConverterDescriptor(
76+
softDeleteConfig.converter(),
77+
context.getBootstrapContext().getClassmateContext()
78+
);
79+
softDeleteIndicatorValue.setJpaAttributeConverterDescriptor( converterDescriptor );
80+
softDeleteIndicatorValue.setImplicitJavaTypeAccess(
81+
(typeConfiguration) -> converterDescriptor.getRelationalValueResolvedType().getErasedType()
82+
);
83+
}
84+
8185
return softDeleteIndicatorValue;
8286
}
8387

@@ -87,11 +91,11 @@ private static Column createSoftDeleteIndicatorColumn(
8791
MetadataBuildingContext context) {
8892
final Column softDeleteColumn = new Column();
8993

94+
softDeleteColumn.setValue( softDeleteIndicatorValue );
95+
softDeleteIndicatorValue.addColumn( softDeleteColumn );
96+
9097
applyColumnName( softDeleteColumn, softDeleteConfig, context );
9198

92-
softDeleteColumn.setLength( 1 );
93-
softDeleteColumn.setNullable( false );
94-
softDeleteColumn.setUnique( false );
9599
softDeleteColumn.setOptions( softDeleteConfig.options() );
96100
if ( isBlank( softDeleteConfig.comment() ) ) {
97101
softDeleteColumn.setComment( "Soft-delete indicator" );
@@ -100,8 +104,15 @@ private static Column createSoftDeleteIndicatorColumn(
100104
softDeleteColumn.setComment( softDeleteConfig.comment() );
101105
}
102106

103-
softDeleteColumn.setValue( softDeleteIndicatorValue );
104-
softDeleteIndicatorValue.addColumn( softDeleteColumn );
107+
softDeleteColumn.setUnique( false );
108+
109+
if ( softDeleteConfig.strategy() == SoftDeleteType.TIMESTAMP ) {
110+
softDeleteColumn.setNullable( true );
111+
}
112+
else {
113+
softDeleteColumn.setLength( 1 );
114+
softDeleteColumn.setNullable( false );
115+
}
105116

106117
return softDeleteColumn;
107118
}
@@ -112,6 +123,7 @@ private static void applyColumnName(
112123
MetadataBuildingContext context) {
113124
final Database database = context.getMetadataCollector().getDatabase();
114125
final PhysicalNamingStrategy namingStrategy = context.getBuildingOptions().getPhysicalNamingStrategy();
126+
// NOTE : the argument order is strange here - the fallback value comes first
115127
final String logicalColumnName = coalesce(
116128
softDeleteConfig.strategy().getDefaultColumnName(),
117129
softDeleteConfig.columnName()
@@ -128,105 +140,10 @@ public static SoftDeleteMappingImpl resolveSoftDeleteMapping(
128140
SoftDeletable bootMapping,
129141
String tableName,
130142
MappingModelCreationProcess creationProcess) {
131-
return resolveSoftDeleteMapping(
132-
softDeletableModelPart,
133-
bootMapping,
134-
tableName,
135-
creationProcess.getCreationContext().getDialect()
136-
);
137-
}
138-
139-
public static SoftDeleteMappingImpl resolveSoftDeleteMapping(
140-
SoftDeletableModelPart softDeletableModelPart,
141-
SoftDeletable bootMapping,
142-
String tableName,
143-
Dialect dialect) {
144-
final Column softDeleteColumn = bootMapping.getSoftDeleteColumn();
145-
if ( softDeleteColumn == null ) {
143+
if ( bootMapping.getSoftDeleteColumn() == null ) {
146144
return null;
147145
}
148-
149-
final BasicValue columnValue = (BasicValue) softDeleteColumn.getValue();
150-
final BasicValue.Resolution<?> resolution = columnValue.resolve();
151-
//noinspection unchecked
152-
final BasicValueConverter<Boolean, Object> converter = (BasicValueConverter<Boolean, Object>) resolution.getValueConverter();
153-
//noinspection unchecked
154-
final JdbcLiteralFormatter<Object> literalFormatter = resolution.getJdbcMapping().getJdbcLiteralFormatter();
155-
156-
final Object deletedLiteralValue;
157-
final Object nonDeletedLiteralValue;
158-
if ( converter == null ) {
159-
// the database column is BIT or BOOLEAN : pass-thru
160-
deletedLiteralValue = true;
161-
nonDeletedLiteralValue = false;
162-
}
163-
else {
164-
deletedLiteralValue = converter.toRelationalValue( true );
165-
nonDeletedLiteralValue = converter.toRelationalValue( false );
166-
}
167-
168-
return new SoftDeleteMappingImpl(
169-
softDeletableModelPart,
170-
softDeleteColumn.getName(),
171-
tableName,
172-
deletedLiteralValue,
173-
literalFormatter.toJdbcLiteral( deletedLiteralValue, dialect, null ),
174-
nonDeletedLiteralValue,
175-
literalFormatter.toJdbcLiteral( nonDeletedLiteralValue, dialect, null ),
176-
resolution.getJdbcMapping()
177-
);
178-
}
179-
180-
/**
181-
* Create a SQL AST Predicate for restricting matches to non-deleted rows
182-
*
183-
* @param tableReference The table reference for the table containing the soft-delete column
184-
* @param softDeleteMapping The soft-delete mapping
185-
*/
186-
public static Predicate createNonSoftDeletedRestriction(
187-
TableReference tableReference,
188-
SoftDeleteMapping softDeleteMapping) {
189-
final ColumnReference softDeleteColumn = new ColumnReference( tableReference, softDeleteMapping );
190-
final JdbcLiteral<?> notDeletedLiteral = new JdbcLiteral<>(
191-
softDeleteMapping.getNonDeletedLiteralValue(),
192-
softDeleteMapping.getJdbcMapping()
193-
);
194-
return new ComparisonPredicate( softDeleteColumn, EQUAL, notDeletedLiteral );
195-
}
196-
197-
/**
198-
* Create a SQL AST Predicate for restricting matches to non-deleted rows
199-
*
200-
* @param tableReference The table reference for the table containing the soft-delete column
201-
* @param softDeleteMapping The soft-delete mapping
202-
*/
203-
public static Predicate createNonSoftDeletedRestriction(
204-
TableReference tableReference,
205-
SoftDeleteMapping softDeleteMapping,
206-
SqlExpressionResolver expressionResolver) {
207-
final Expression softDeleteColumn = expressionResolver.resolveSqlExpression( tableReference, softDeleteMapping );
208-
final JdbcLiteral<?> notDeletedLiteral = new JdbcLiteral<>(
209-
softDeleteMapping.getNonDeletedLiteralValue(),
210-
softDeleteMapping.getJdbcMapping()
211-
);
212-
return new ComparisonPredicate( softDeleteColumn, EQUAL, notDeletedLiteral );
146+
return new SoftDeleteMappingImpl( softDeletableModelPart, bootMapping, tableName, creationProcess );
213147
}
214148

215-
/**
216-
* Create a SQL AST Assignment for setting the soft-delete column to its
217-
* deleted indicate value
218-
*
219-
* @param tableReference The table reference for the table containing the soft-delete column
220-
* @param softDeleteMapping The soft-delete mapping
221-
*/
222-
public static Assignment createSoftDeleteAssignment(
223-
TableReference tableReference,
224-
SoftDeleteMapping softDeleteMapping) {
225-
final ColumnReference softDeleteColumn = new ColumnReference( tableReference, softDeleteMapping );
226-
final JdbcLiteral<?> softDeleteIndicator = new JdbcLiteral<>(
227-
softDeleteMapping.getDeletedLiteralValue(),
228-
softDeleteMapping.getJdbcMapping()
229-
);
230-
return new Assignment( softDeleteColumn, softDeleteIndicator );
231-
}
232149
}

hibernate-core/src/main/java/org/hibernate/mapping/BasicValue.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -455,7 +455,7 @@ private BasicJavaType<?> getExplicitJavaType() {
455455

456456
private ConverterDescriptor getConverterDescriptor(JavaType<?> javaType) {
457457
final ConverterDescriptor converterDescriptor = getAttributeConverterDescriptor();
458-
if ( isSoftDelete() ) {
458+
if ( isSoftDelete() && getSoftDeleteStrategy() != SoftDeleteType.TIMESTAMP ) {
459459
assert converterDescriptor != null;
460460
final ConverterDescriptor softDeleteConverterDescriptor =
461461
getSoftDeleteConverterDescriptor( converterDescriptor, javaType);

hibernate-core/src/main/java/org/hibernate/mapping/Collection.java

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,10 @@
44
*/
55
package org.hibernate.mapping;
66

7-
import java.util.ArrayList;
8-
import java.util.Comparator;
9-
import java.util.HashSet;
10-
import java.util.List;
11-
import java.util.Objects;
12-
import java.util.Properties;
13-
import java.util.function.Supplier;
14-
157
import org.hibernate.FetchMode;
168
import org.hibernate.MappingException;
179
import org.hibernate.annotations.CacheLayout;
10+
import org.hibernate.annotations.SoftDeleteType;
1811
import org.hibernate.boot.spi.BootstrapContext;
1912
import org.hibernate.boot.spi.MetadataBuildingContext;
2013
import org.hibernate.boot.spi.MetadataImplementor;
@@ -29,13 +22,21 @@
2922
import org.hibernate.service.ServiceRegistry;
3023
import org.hibernate.type.CollectionType;
3124
import org.hibernate.type.CustomCollectionType;
32-
import org.hibernate.type.Type;
3325
import org.hibernate.type.MappingContext;
26+
import org.hibernate.type.Type;
3427
import org.hibernate.usertype.UserCollectionType;
3528

29+
import java.util.ArrayList;
30+
import java.util.Comparator;
31+
import java.util.HashSet;
32+
import java.util.List;
33+
import java.util.Objects;
34+
import java.util.Properties;
35+
import java.util.function.Supplier;
36+
3637
import static java.util.Collections.emptyList;
37-
import static org.hibernate.internal.util.collections.ArrayHelper.EMPTY_BOOLEAN_ARRAY;
3838
import static org.hibernate.engine.spi.ExecuteUpdateResultCheckStyle.expectationConstructor;
39+
import static org.hibernate.internal.util.collections.ArrayHelper.EMPTY_BOOLEAN_ARRAY;
3940
import static org.hibernate.mapping.MappingHelper.classForName;
4041
import static org.hibernate.mapping.MappingHelper.createUserTypeBean;
4142

@@ -106,6 +107,7 @@ public abstract sealed class Collection
106107
private ExecuteUpdateResultCheckStyle deleteAllCheckStyle;
107108

108109
private Column softDeleteColumn;
110+
private SoftDeleteType softDeleteStrategy;
109111

110112
private String loaderName;
111113

@@ -842,8 +844,14 @@ public boolean isColumnUpdateable(int index) {
842844
}
843845

844846
@Override
845-
public void enableSoftDelete(Column indicatorColumn) {
847+
public void enableSoftDelete(Column indicatorColumn, SoftDeleteType strategy) {
846848
this.softDeleteColumn = indicatorColumn;
849+
this.softDeleteStrategy = strategy;
850+
}
851+
852+
@Override
853+
public SoftDeleteType getSoftDeleteStrategy() {
854+
return softDeleteStrategy;
847855
}
848856

849857
@Override

0 commit comments

Comments
 (0)