Skip to content

Commit aa1610d

Browse files
ctailor2schauder
authored andcommitted
Update SaveBatchingAggregateChange to batch Delete actions.
Original pull request #1229
1 parent 64d9bbb commit aa1610d

File tree

12 files changed

+140
-13
lines changed

12 files changed

+140
-13
lines changed

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/AggregateChangeExecutor.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ private void execute(DbAction<?> action, JdbcAggregateChangeExecutionContext exe
9292
executionContext.executeUpdateRoot((DbAction.UpdateRoot<?>) action);
9393
} else if (action instanceof DbAction.Delete) {
9494
executionContext.executeDelete((DbAction.Delete<?>) action);
95+
} else if (action instanceof DbAction.BatchDelete<?>) {
96+
executionContext.executeBatchDelete((DbAction.BatchDelete<?>) action);
9597
} else if (action instanceof DbAction.DeleteAll) {
9698
executionContext.executeDeleteAll((DbAction.DeleteAll<?>) action);
9799
} else if (action instanceof DbAction.DeleteRoot) {

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutionContext.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,12 @@ <T> void executeDelete(DbAction.Delete<T> delete) {
135135
accessStrategy.delete(delete.getRootId(), delete.getPropertyPath());
136136
}
137137

138+
<T> void executeBatchDelete(DbAction.BatchDelete<T> batchDelete) {
139+
140+
List<Object> rootIds = batchDelete.getActions().stream().map(DbAction.Delete::getRootId).toList();
141+
accessStrategy.delete(rootIds, batchDelete.getBatchValue());
142+
}
143+
138144
<T> void executeDeleteAllRoot(DbAction.DeleteAllRoot<T> deleteAllRoot) {
139145

140146
accessStrategy.deleteAll(deleteAllRoot.getEntityType());

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/CascadingDataAccessStrategy.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,11 @@ public void delete(Object rootId, PersistentPropertyPath<RelationalPersistentPro
8787
collectVoid(das -> das.delete(rootId, propertyPath));
8888
}
8989

90+
@Override
91+
public void delete(Iterable<Object> rootIds, PersistentPropertyPath<RelationalPersistentProperty> propertyPath) {
92+
collectVoid(das -> das.delete(rootIds, propertyPath));
93+
}
94+
9095
@Override
9196
public <T> void deleteAll(Class<T> domainType) {
9297
collectVoid(das -> das.deleteAll(domainType));

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DataAccessStrategy.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,14 @@ public interface DataAccessStrategy extends RelationResolver {
152152
*/
153153
void delete(Object rootId, PersistentPropertyPath<RelationalPersistentProperty> propertyPath);
154154

155+
/**
156+
* Deletes all entities reachable via {@literal propertyPath} from the instances identified by {@literal rootIds}.
157+
*
158+
* @param rootIds Ids of the root objects on which the {@literal propertyPath} is based. Must not be {@code null} or empty.
159+
* @param propertyPath Leading from the root object to the entities to be deleted. Must not be {@code null}.
160+
*/
161+
void delete(Iterable<Object> rootIds, PersistentPropertyPath<RelationalPersistentProperty> propertyPath);
162+
155163
/**
156164
* Deletes all entities of the given domain type.
157165
*

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategy.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,21 @@ public void delete(Object rootId, PersistentPropertyPath<RelationalPersistentPro
196196
operations.update(delete, parameters);
197197
}
198198

199+
@Override
200+
public void delete(Iterable<Object> rootIds, PersistentPropertyPath<RelationalPersistentProperty> propertyPath) {
201+
202+
RelationalPersistentEntity<?> rootEntity = context
203+
.getRequiredPersistentEntity(propertyPath.getBaseProperty().getOwner().getType());
204+
205+
RelationalPersistentProperty referencingProperty = propertyPath.getLeafProperty();
206+
Assert.notNull(referencingProperty, "No property found matching the PropertyPath " + propertyPath);
207+
208+
String delete = sql(rootEntity.getType()).createDeleteInByPath(propertyPath);
209+
210+
SqlIdentifierParameterSource parameters = sqlParametersFactory.forQueryByIds(rootIds, rootEntity.getType());
211+
operations.update(delete, parameters);
212+
}
213+
199214
@Override
200215
public <T> void deleteAll(Class<T> domainType) {
201216
operations.getJdbcOperations().update(sql(domainType).createDeleteAllSql(null));

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DelegatingDataAccessStrategy.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,11 @@ public void delete(Object rootId, PersistentPropertyPath<RelationalPersistentPro
7171
delegate.delete(rootId, propertyPath);
7272
}
7373

74+
@Override
75+
public void delete(Iterable<Object> rootIds, PersistentPropertyPath<RelationalPersistentProperty> propertyPath) {
76+
delegate.delete(rootIds, propertyPath);
77+
}
78+
7479
@Override
7580
public void delete(Object id, Class<?> domainType) {
7681
delegate.delete(id, domainType);

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,8 @@ String createDeleteAllSql(@Nullable PersistentPropertyPath<RelationalPersistentP
360360
}
361361

362362
/**
363-
* Create a {@code DELETE} query and filter by {@link PersistentPropertyPath}.
363+
* Create a {@code DELETE} query and filter by {@link PersistentPropertyPath} using {@code WHERE} with the {@code =}
364+
* operator.
364365
*
365366
* @param path must not be {@literal null}.
366367
* @return the statement as a {@link String}. Guaranteed to be not {@literal null}.
@@ -370,6 +371,18 @@ String createDeleteByPath(PersistentPropertyPath<RelationalPersistentProperty> p
370371
filterColumn -> filterColumn.isEqualTo(getBindMarker(ROOT_ID_PARAMETER)));
371372
}
372373

374+
/**
375+
* Create a {@code DELETE} query and filter by {@link PersistentPropertyPath} using {@code WHERE} with the {@code IN}
376+
* operator.
377+
*
378+
* @param path must not be {@literal null}.
379+
* @return the statement as a {@link String}. Guaranteed to be not {@literal null}.
380+
*/
381+
String createDeleteInByPath(PersistentPropertyPath<RelationalPersistentProperty> path) {
382+
return createDeleteByPathAndCriteria(new PersistentPropertyPathExtension(mappingContext, path),
383+
filterColumn -> filterColumn.in(getBindMarker(IDS_SQL_PARAMETER)));
384+
}
385+
373386
private String createFindOneSql() {
374387

375388
Select select = selectBuilder().where(getIdColumn().isEqualTo(getBindMarker(ID_SQL_PARAMETER))) //

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/mybatis/MyBatisDataAccessStrategy.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,11 @@ public void delete(Object rootId, PersistentPropertyPath<RelationalPersistentPro
218218
sqlSession().delete(statement, parameter);
219219
}
220220

221+
@Override
222+
public void delete(Iterable<Object> rootIds, PersistentPropertyPath<RelationalPersistentProperty> propertyPath) {
223+
rootIds.forEach(rootId -> delete(rootId, propertyPath));
224+
}
225+
221226
@Override
222227
public <T> void deleteAll(Class<T> domainType) {
223228

spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,14 @@ void cascadingDeleteFirstLevel() {
147147
assertThat(sql).isEqualTo("DELETE FROM referenced_entity WHERE referenced_entity.dummy_entity = :rootId");
148148
}
149149

150+
@Test // GH-537
151+
void cascadingDeleteInByPathFirstLevel() {
152+
153+
String sql = sqlGenerator.createDeleteInByPath(getPath("ref", DummyEntity.class));
154+
155+
assertThat(sql).isEqualTo("DELETE FROM referenced_entity WHERE referenced_entity.dummy_entity IN (:ids)");
156+
}
157+
150158
@Test // DATAJDBC-112
151159
void cascadingDeleteByPathSecondLevel() {
152160

@@ -156,6 +164,15 @@ void cascadingDeleteByPathSecondLevel() {
156164
"DELETE FROM second_level_referenced_entity WHERE second_level_referenced_entity.referenced_entity IN (SELECT referenced_entity.x_l1id FROM referenced_entity WHERE referenced_entity.dummy_entity = :rootId)");
157165
}
158166

167+
@Test // GH-537
168+
void cascadingDeleteInByPathSecondLevel() {
169+
170+
String sql = sqlGenerator.createDeleteInByPath(getPath("ref.further", DummyEntity.class));
171+
172+
assertThat(sql).isEqualTo(
173+
"DELETE FROM second_level_referenced_entity WHERE second_level_referenced_entity.referenced_entity IN (SELECT referenced_entity.x_l1id FROM referenced_entity WHERE referenced_entity.dummy_entity IN (:ids))");
174+
}
175+
159176
@Test // DATAJDBC-112
160177
void deleteAll() {
161178

spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/DbAction.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,18 @@ public BatchInsertRoot(List<InsertRoot<T>> actions) {
422422
}
423423
}
424424

425+
/**
426+
* Represents a batch delete statement for multiple entities that are reachable via a given path from the aggregate root.
427+
*
428+
* @param <T> type of the entity for which this represents a database interaction.
429+
* @since 3.0
430+
*/
431+
final class BatchDelete<T> extends BatchWithValue<T, Delete<T>, PersistentPropertyPath<RelationalPersistentProperty>> {
432+
public BatchDelete(List<Delete<T>> actions) {
433+
super(actions, Delete::getPropertyPath);
434+
}
435+
}
436+
425437
/**
426438
* An action depending on another action for providing additional information like the id of a parent entity.
427439
*

spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/SaveBatchingAggregateChange.java

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@
3131
/**
3232
* A {@link BatchingAggregateChange} implementation for save changes that can contain actions for any mix of insert and
3333
* update operations. When consumed, actions are yielded in the appropriate entity tree order with inserts carried out
34-
* from root to leaves and deletes in reverse. All insert operations are grouped into batches to offer the ability for
35-
* an optimized batch operation to be used.
34+
* from root to leaves and deletes in reverse. All operations that can be batched are grouped and combined to offer the
35+
* ability for an optimized batch operation to be used.
3636
*
3737
* @author Chirag Tailor
3838
* @since 3.0
@@ -51,7 +51,7 @@ public class SaveBatchingAggregateChange<T> implements BatchingAggregateChange<T
5151
private final List<DbAction.InsertRoot<T>> insertRootBatchCandidates = new ArrayList<>();
5252
private final Map<PersistentPropertyPath<RelationalPersistentProperty>, Map<IdValueSource, List<DbAction.Insert<Object>>>> insertActions = //
5353
new HashMap<>();
54-
private final Map<PersistentPropertyPath<RelationalPersistentProperty>, List<DbAction.Delete<?>>> deleteActions = //
54+
private final Map<PersistentPropertyPath<RelationalPersistentProperty>, List<DbAction.Delete<Object>>> deleteActions = //
5555
new HashMap<>();
5656

5757
SaveBatchingAggregateChange(Class<T> entityType) {
@@ -80,9 +80,17 @@ public void forEachAction(Consumer<? super DbAction<?>> consumer) {
8080
insertRootBatchCandidates.forEach(consumer);
8181
}
8282
deleteActions.entrySet().stream().sorted(Map.Entry.comparingByKey(pathLengthComparator.reversed()))
83-
.forEach((entry) -> entry.getValue().forEach(consumer));
84-
insertActions.entrySet().stream().sorted(Map.Entry.comparingByKey(pathLengthComparator)).forEach((entry) -> entry
85-
.getValue().forEach((idValueSource, inserts) -> consumer.accept(new DbAction.BatchInsert<>(inserts))));
83+
.forEach((entry) -> {
84+
List<DbAction.Delete<Object>> deletes = entry.getValue();
85+
if (deletes.size() > 1) {
86+
consumer.accept(new DbAction.BatchDelete<>(deletes));
87+
} else {
88+
deletes.forEach(consumer);
89+
}
90+
});
91+
insertActions.entrySet().stream().sorted(Map.Entry.comparingByKey(pathLengthComparator))
92+
.forEach((entry) -> entry.getValue().forEach((idValueSource, inserts) ->
93+
consumer.accept(new DbAction.BatchInsert<>(inserts))));
8694
}
8795

8896
@Override
@@ -107,7 +115,8 @@ public void add(RootAggregateChange<T> aggregateChange) {
107115
// noinspection unchecked
108116
addInsert((DbAction.Insert<Object>) insertAction);
109117
} else if (action instanceof DbAction.Delete<?> deleteAction) {
110-
addDelete(deleteAction);
118+
// noinspection unchecked
119+
addDelete((DbAction.Delete<Object>) deleteAction);
111120
}
112121
});
113122
}
@@ -140,7 +149,7 @@ private void addInsert(DbAction.Insert<Object> action) {
140149
});
141150
}
142151

143-
private void addDelete(DbAction.Delete<?> action) {
152+
private void addDelete(DbAction.Delete<Object> action) {
144153

145154
PersistentPropertyPath<RelationalPersistentProperty> propertyPath = action.getPropertyPath();
146155
deleteActions.merge(propertyPath, new ArrayList<>(singletonList(action)), (actions, defaultValue) -> {

spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/SaveBatchingAggregateChangeTest.java

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ void yieldsNestedDeleteActionsInTreeOrderFromLeavesToRoot() {
277277
Root root1 = new Root(1L, null);
278278
RootAggregateChange<Root> aggregateChange1 = MutableAggregateChange.forSave(root1);
279279
aggregateChange1.setRootAction(new DbAction.UpdateRoot<>(root1, null));
280-
DbAction.Delete<?> root1IntermediateDelete = new DbAction.Delete<>(1L,
280+
DbAction.Delete<Intermediate> root1IntermediateDelete = new DbAction.Delete<>(1L,
281281
context.getPersistentPropertyPath("intermediate", Root.class));
282282
aggregateChange1.addAction(root1IntermediateDelete);
283283

@@ -289,16 +289,46 @@ void yieldsNestedDeleteActionsInTreeOrderFromLeavesToRoot() {
289289
context.getPersistentPropertyPath("intermediate.leaf", Root.class));
290290
aggregateChange2.addAction(root2LeafDelete);
291291

292-
DbAction.Delete<?> root2IntermediateDelete = new DbAction.Delete<>(1L,
292+
DbAction.Delete<Intermediate> root2IntermediateDelete = new DbAction.Delete<>(1L,
293293
context.getPersistentPropertyPath("intermediate", Root.class));
294294
aggregateChange2.addAction(root2IntermediateDelete);
295295

296296
BatchingAggregateChange<Root, RootAggregateChange<Root>> change = BatchingAggregateChange.forSave(Root.class);
297297
change.add(aggregateChange1);
298298
change.add(aggregateChange2);
299299

300-
assertThat(extractActions(change)).containsSubsequence(root2LeafDelete, root1IntermediateDelete,
301-
root2IntermediateDelete);
300+
List<DbAction<?>> actions = extractActions(change);
301+
assertThat(actions).extracting(DbAction::getClass, DbAction::getEntityType).containsSubsequence(
302+
Tuple.tuple(DbAction.Delete.class, Leaf.class), //
303+
Tuple.tuple(DbAction.BatchDelete.class, Intermediate.class));
304+
assertThat(getBatchWithValueAction(actions, Intermediate.class, DbAction.BatchDelete.class).getActions())
305+
.containsExactly(root1IntermediateDelete, root2IntermediateDelete);
306+
}
307+
308+
@Test
309+
void yieldsDeleteActionsAsBatchDeletes_groupedByPath_whenGroupContainsMultipleDeletes() {
310+
311+
Root root = new Root(1L, null);
312+
RootAggregateChange<Root> aggregateChange = MutableAggregateChange.forSave(root);
313+
DbAction.UpdateRoot<Root> updateRoot = new DbAction.UpdateRoot<>(root, null);
314+
aggregateChange.setRootAction(updateRoot);
315+
DbAction.Delete<Intermediate> intermediateDelete1 = new DbAction.Delete<>(1L,
316+
context.getPersistentPropertyPath("intermediate", Root.class));
317+
DbAction.Delete<Intermediate> intermediateDelete2 = new DbAction.Delete<>(2L,
318+
context.getPersistentPropertyPath("intermediate", Root.class));
319+
aggregateChange.addAction(intermediateDelete1);
320+
aggregateChange.addAction(intermediateDelete2);
321+
322+
BatchingAggregateChange<Root, RootAggregateChange<Root>> change = BatchingAggregateChange.forSave(Root.class);
323+
change.add(aggregateChange);
324+
325+
List<DbAction<?>> actions = extractActions(change);
326+
assertThat(actions).extracting(DbAction::getClass, DbAction::getEntityType) //
327+
.containsExactly( //
328+
Tuple.tuple(DbAction.UpdateRoot.class, Root.class), //
329+
Tuple.tuple(DbAction.BatchDelete.class, Intermediate.class));
330+
assertThat(getBatchWithValueAction(actions, Intermediate.class, DbAction.BatchDelete.class).getActions())
331+
.containsExactly(intermediateDelete1, intermediateDelete2);
302332
}
303333

304334
@Test

0 commit comments

Comments
 (0)