Skip to content

Commit 972f557

Browse files
committed
HHH-19192 prevent physically deleting collections when soft delete is set
1 parent 33429e9 commit 972f557

File tree

2 files changed

+89
-19
lines changed

2 files changed

+89
-19
lines changed

hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/SqmMutationStrategyHelper.java

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,18 @@
4343
import org.hibernate.metamodel.mapping.internal.ToOneAttributeMapping;
4444
import org.hibernate.persister.entity.EntityPersister;
4545
import org.hibernate.query.spi.QueryOptions;
46+
import org.hibernate.sql.ast.tree.AbstractUpdateOrDeleteStatement;
4647
import org.hibernate.sql.ast.tree.delete.DeleteStatement;
4748
import org.hibernate.sql.ast.tree.from.NamedTableReference;
4849
import org.hibernate.sql.ast.tree.from.TableReference;
4950
import org.hibernate.sql.ast.tree.predicate.Predicate;
51+
import org.hibernate.sql.ast.tree.update.Assignment;
52+
import org.hibernate.sql.ast.tree.update.UpdateStatement;
5053
import org.hibernate.sql.exec.spi.JdbcOperationQueryMutation;
5154
import org.hibernate.sql.exec.spi.JdbcParameterBindings;
5255

56+
import static java.util.Collections.singletonList;
57+
5358
/**
5459
* @author Steve Ebersole
5560
*/
@@ -171,31 +176,42 @@ private static void visitCollectionTableDeletes(
171176
QueryOptions queryOptions,
172177
Consumer<JdbcOperationQueryMutation> jdbcOperationConsumer) {
173178
final String separateCollectionTable = attributeMapping.getSeparateCollectionTable();
179+
// Skip deleting rows in collection tables if cascade delete is enabled
180+
if ( separateCollectionTable == null || attributeMapping.getCollectionDescriptor().isCascadeDeleteEnabled() ) {
181+
return;
182+
}
174183
final SessionFactoryImplementor sessionFactory = attributeMapping.getCollectionDescriptor().getFactory();
175184
final JdbcServices jdbcServices = sessionFactory.getJdbcServices();
185+
// element-collection or many-to-many - delete the collection-table row
186+
final NamedTableReference tableReference = new NamedTableReference(
187+
separateCollectionTable,
188+
DeleteStatement.DEFAULT_ALIAS,
189+
true
190+
);
176191

177-
// Skip deleting rows in collection tables if cascade delete is enabled
178-
if ( separateCollectionTable != null && !attributeMapping.getCollectionDescriptor().isCascadeDeleteEnabled() ) {
179-
// element-collection or many-to-many - delete the collection-table row
180-
181-
final NamedTableReference tableReference = new NamedTableReference(
182-
separateCollectionTable,
183-
DeleteStatement.DEFAULT_ALIAS,
184-
true
185-
);
186-
187-
final DeleteStatement sqlAstDelete = new DeleteStatement(
188-
tableReference,
189-
restrictionProducer.apply( tableReference, attributeMapping )
192+
final AbstractUpdateOrDeleteStatement sqlAst;
193+
if ( attributeMapping.getSoftDeleteMapping() != null ) {
194+
final Assignment softDeleteAssignment = attributeMapping
195+
.getSoftDeleteMapping()
196+
.createSoftDeleteAssignment( tableReference );
197+
sqlAst = new UpdateStatement(
198+
tableReference,
199+
singletonList( softDeleteAssignment ),
200+
restrictionProducer.apply( tableReference, attributeMapping )
190201
);
191-
192-
jdbcOperationConsumer.accept(
193-
jdbcServices.getJdbcEnvironment()
194-
.getSqlAstTranslatorFactory()
195-
.buildMutationTranslator( sessionFactory, sqlAstDelete )
196-
.translate( jdbcParameterBindings, queryOptions )
202+
}
203+
else {
204+
sqlAst = new DeleteStatement(
205+
tableReference,
206+
restrictionProducer.apply( tableReference, attributeMapping )
197207
);
198208
}
209+
jdbcOperationConsumer.accept(
210+
jdbcServices.getJdbcEnvironment()
211+
.getSqlAstTranslatorFactory()
212+
.buildMutationTranslator( sessionFactory, sqlAst )
213+
.translate( jdbcParameterBindings, queryOptions )
214+
);
199215
}
200216

201217
public static boolean isId(JdbcMappingContainer type) {
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
* Copyright Red Hat Inc. and Hibernate Authors
4+
*/
5+
package org.hibernate.orm.test.softdelete.collections;
6+
7+
import jakarta.persistence.CollectionTable;
8+
import jakarta.persistence.ElementCollection;
9+
import jakarta.persistence.Entity;
10+
import jakarta.persistence.Id;
11+
import jakarta.persistence.JoinColumn;
12+
import jakarta.persistence.Table;
13+
import org.hibernate.annotations.SoftDelete;
14+
import org.hibernate.annotations.SoftDeleteType;
15+
import org.hibernate.testing.jdbc.SQLStatementInspector;
16+
import org.hibernate.testing.orm.junit.DomainModel;
17+
import org.hibernate.testing.orm.junit.Jira;
18+
import org.hibernate.testing.orm.junit.SessionFactory;
19+
import org.hibernate.testing.orm.junit.SessionFactoryScope;
20+
import org.junit.jupiter.api.Test;
21+
22+
import java.util.List;
23+
24+
import static org.assertj.core.api.Assertions.assertThat;
25+
26+
@SessionFactory
27+
@DomainModel( annotatedClasses = { BulkDeleteOwnerTest.Employee.class } )
28+
@Jira( "https://hibernate.atlassian.net/browse/HHH-19192" )
29+
public class BulkDeleteOwnerTest {
30+
@Test void test(SessionFactoryScope scope) {
31+
final SQLStatementInspector sqlInspector = scope.getCollectingStatementInspector();
32+
sqlInspector.clear();
33+
scope.inTransaction( session -> {
34+
session.createMutationQuery( "delete Employee where id = 1" ).executeUpdate();
35+
assertThat( sqlInspector.getSqlQueries() ).hasSize( 2 );
36+
assertThat( sqlInspector.getSqlQueries().get( 0 ) ).doesNotContain( "delete from" );
37+
assertThat( sqlInspector.getSqlQueries().get( 0 ) ).contains( "update employee_accolades" );
38+
assertThat( sqlInspector.getSqlQueries().get( 0 ) ).contains( "set deleted_on=localtimestamp" );
39+
assertThat( sqlInspector.getSqlQueries().get( 0 ) ).contains( ".employee_fk in (select" );
40+
} );
41+
}
42+
43+
@Entity(name = "Employee")
44+
@Table(name = "employees")
45+
@SoftDelete( strategy = SoftDeleteType.TIMESTAMP, columnName = "deleted_at" )
46+
public static class Employee {
47+
@Id
48+
long id;
49+
@ElementCollection
50+
@CollectionTable( name = "employee_accolades", joinColumns = @JoinColumn( name = "employee_fk" ) )
51+
@SoftDelete( strategy = SoftDeleteType.TIMESTAMP, columnName = "deleted_on" )
52+
private List<String> accolades;
53+
}
54+
}

0 commit comments

Comments
 (0)