Skip to content

Commit 1a283fa

Browse files
ctailor2schauder
authored andcommitted
Add DeleteBatchingAggregateChange to batch DeleteRoot actions.
Original pull request #1231 See #537
1 parent 483b30e commit 1a283fa

File tree

12 files changed

+175
-9
lines changed

12 files changed

+175
-9
lines changed

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

+2
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ private void execute(DbAction<?> action, JdbcAggregateChangeExecutionContext exe
9898
executionContext.executeDeleteAll((DbAction.DeleteAll<?>) action);
9999
} else if (action instanceof DbAction.DeleteRoot) {
100100
executionContext.executeDeleteRoot((DbAction.DeleteRoot<?>) action);
101+
} else if (action instanceof DbAction.BatchDeleteRoot) {
102+
executionContext.executeBatchDeleteRoot((DbAction.BatchDeleteRoot<?>) action);
101103
} else if (action instanceof DbAction.DeleteAllRoot) {
102104
executionContext.executeDeleteAllRoot((DbAction.DeleteAllRoot<?>) action);
103105
} else if (action instanceof DbAction.AcquireLockRoot) {

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

+6
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,12 @@ <T> void executeDeleteRoot(DbAction.DeleteRoot<T> delete) {
131131
}
132132
}
133133

134+
<T> void executeBatchDeleteRoot(DbAction.BatchDeleteRoot<T> batchDelete) {
135+
136+
List<Object> rootIds = batchDelete.getActions().stream().map(DbAction.DeleteRoot::getId).toList();
137+
accessStrategy.delete(rootIds, batchDelete.getEntityType());
138+
}
139+
134140
<T> void executeDelete(DbAction.Delete<T> delete) {
135141

136142
accessStrategy.delete(delete.getRootId(), delete.getPropertyPath());

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

+5
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,11 @@ public void delete(Object id, Class<?> domainType) {
7979
collectVoid(das -> das.delete(id, domainType));
8080
}
8181

82+
@Override
83+
public void delete(Iterable<Object> ids, Class<?> domainType) {
84+
collectVoid(das -> das.delete(ids, domainType));
85+
}
86+
8287
@Override
8388
public <T> void deleteWithVersion(Object id, Class<T> domainType, Number previousVersion) {
8489
collectVoid(das -> das.deleteWithVersion(id, domainType, previousVersion));

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

+16-1
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,20 @@ public interface DataAccessStrategy extends RelationResolver {
130130
*/
131131
void delete(Object id, Class<?> domainType);
132132

133+
/**
134+
* Deletes multiple rows identified by the ids, from the table identified by the domainType. Does not handle cascading
135+
* deletes.
136+
* <P>
137+
* The statement will be of the form : {@code DELETE FROM … WHERE ID IN (:ids) } and throw an optimistic record
138+
* locking exception if no rows have been updated.
139+
*
140+
* @param ids the ids of the rows to be deleted. Must not be {@code null}.
141+
* @param domainType the type of entity to be deleted. Implicitly determines the table to operate on. Must not be
142+
* {@code null}.
143+
* @since 3.0
144+
*/
145+
void delete(Iterable<Object> ids, Class<?> domainType);
146+
133147
/**
134148
* Deletes a single entity from the database and enforce optimistic record locking using the version property. Does
135149
* not handle cascading deletes.
@@ -155,7 +169,8 @@ public interface DataAccessStrategy extends RelationResolver {
155169
/**
156170
* Deletes all entities reachable via {@literal propertyPath} from the instances identified by {@literal rootIds}.
157171
*
158-
* @param rootIds Ids of the root objects on which the {@literal propertyPath} is based. Must not be {@code null} or empty.
172+
* @param rootIds Ids of the root objects on which the {@literal propertyPath} is based. Must not be {@code null} or
173+
* empty.
159174
* @param propertyPath Leading from the root object to the entities to be deleted. Must not be {@code null}.
160175
*/
161176
void delete(Iterable<Object> rootIds, PersistentPropertyPath<RelationalPersistentProperty> propertyPath);

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

+9
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,15 @@ public void delete(Object id, Class<?> domainType) {
163163
operations.update(deleteByIdSql, parameter);
164164
}
165165

166+
@Override
167+
public void delete(Iterable<Object> ids, Class<?> domainType) {
168+
169+
String deleteByIdInSql = sql(domainType).getDeleteByIdIn();
170+
SqlParameterSource parameter = sqlParametersFactory.forQueryByIds(ids, domainType);
171+
172+
operations.update(deleteByIdInSql, parameter);
173+
}
174+
166175
@Override
167176
public <T> void deleteWithVersion(Object id, Class<T> domainType, Number previousVersion) {
168177

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

+5
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,11 @@ public void delete(Object id, Class<?> domainType) {
8181
delegate.delete(id, domainType);
8282
}
8383

84+
@Override
85+
public void delete(Iterable<Object> ids, Class<?> domainType) {
86+
delegate.delete(ids, domainType);
87+
}
88+
8489
@Override
8590
public <T> void deleteWithVersion(Object id, Class<T> domainType, Number previousVersion) {
8691
delegate.deleteWithVersion(id, domainType, previousVersion);

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

+40-2
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,10 @@ class SqlGenerator {
7979
private final Lazy<String> updateSql = Lazy.of(this::createUpdateSql);
8080
private final Lazy<String> updateWithVersionSql = Lazy.of(this::createUpdateWithVersionSql);
8181

82-
private final Lazy<String> deleteByIdSql = Lazy.of(this::createDeleteSql);
82+
private final Lazy<String> deleteByIdSql = Lazy.of(this::createDeleteByIdSql);
83+
private final Lazy<String> deleteByIdInSql = Lazy.of(this::createDeleteByIdInSql);
8384
private final Lazy<String> deleteByIdAndVersionSql = Lazy.of(this::createDeleteByIdAndVersionSql);
85+
private final Lazy<String> deleteByIdInAndVersionSql = Lazy.of(this::createDeleteByIdInAndVersionSql);
8486
private final Lazy<String> deleteByListSql = Lazy.of(this::createDeleteByListSql);
8587

8688
/**
@@ -322,6 +324,15 @@ String getDeleteById() {
322324
return deleteByIdSql.get();
323325
}
324326

327+
/**
328+
* Create a {@code DELETE FROM … WHERE :id IN …} statement.
329+
*
330+
* @return the statement as a {@link String}. Guaranteed to be not {@literal null}.
331+
*/
332+
String getDeleteByIdIn() {
333+
return deleteByIdInSql.get();
334+
}
335+
325336
/**
326337
* Create a {@code DELETE FROM … WHERE :id = … and :___oldOptimisticLockingVersion = ...} statement.
327338
*
@@ -331,6 +342,15 @@ String getDeleteByIdAndVersion() {
331342
return deleteByIdAndVersionSql.get();
332343
}
333344

345+
/**
346+
* Create a {@code DELETE FROM … WHERE :id In … and :___oldOptimisticLockingVersion = ...} statement.
347+
*
348+
* @return the statement as a {@link String}. Guaranteed to be not {@literal null}.
349+
*/
350+
String getDeleteByIdInAndVersion() {
351+
return deleteByIdInAndVersionSql.get();
352+
}
353+
334354
/**
335355
* Create a {@code DELETE FROM … WHERE :ids in (…)} statement.
336356
*
@@ -635,10 +655,14 @@ private UpdateBuilder.UpdateWhereAndOr createBaseUpdate() {
635655
.where(getIdColumn().isEqualTo(getBindMarker(entity.getIdColumn())));
636656
}
637657

638-
private String createDeleteSql() {
658+
private String createDeleteByIdSql() {
639659
return render(createBaseDeleteById(getTable()).build());
640660
}
641661

662+
private String createDeleteByIdInSql() {
663+
return render(createBaseDeleteByIdIn(getTable()).build());
664+
}
665+
642666
private String createDeleteByIdAndVersionSql() {
643667

644668
Delete delete = createBaseDeleteById(getTable()) //
@@ -648,11 +672,25 @@ private String createDeleteByIdAndVersionSql() {
648672
return render(delete);
649673
}
650674

675+
private String createDeleteByIdInAndVersionSql() {
676+
677+
Delete delete = createBaseDeleteByIdIn(getTable()) //
678+
.and(getVersionColumn().isEqualTo(SQL.bindMarker(":" + renderReference(VERSION_SQL_PARAMETER)))) //
679+
.build();
680+
681+
return render(delete);
682+
}
683+
651684
private DeleteBuilder.DeleteWhereAndOr createBaseDeleteById(Table table) {
652685
return Delete.builder().from(table)
653686
.where(getIdColumn().isEqualTo(SQL.bindMarker(":" + renderReference(ID_SQL_PARAMETER))));
654687
}
655688

689+
private DeleteBuilder.DeleteWhereAndOr createBaseDeleteByIdIn(Table table) {
690+
return Delete.builder().from(table)
691+
.where(getIdColumn().in(SQL.bindMarker(":" + renderReference(IDS_SQL_PARAMETER))));
692+
}
693+
656694
private String createDeleteByPathAndCriteria(PersistentPropertyPathExtension path,
657695
Function<Column, Condition> rootCondition) {
658696

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

+5
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,11 @@ public void delete(Object id, Class<?> domainType) {
192192
sqlSession().delete(statement, parameter);
193193
}
194194

195+
@Override
196+
public void delete(Iterable<Object> ids, Class<?> domainType) {
197+
ids.forEach(id -> delete(id, domainType));
198+
}
199+
195200
@Override
196201
public <T> void deleteWithVersion(Object id, Class<T> domainType, Number previousVersion) {
197202

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

+20
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import java.util.Collections;
3333
import java.util.HashMap;
3434
import java.util.HashSet;
35+
import java.util.Iterator;
3536
import java.util.List;
3637
import java.util.Map;
3738
import java.util.Set;
@@ -363,6 +364,25 @@ void saveAndDeleteAllByIdsWithReferencedEntity() {
363364
});
364365
}
365366

367+
@Test
368+
void saveAndDeleteAllByAggregateRootsWithVersion() {
369+
AggregateWithImmutableVersion aggregate1 = new AggregateWithImmutableVersion(null, null);
370+
AggregateWithImmutableVersion aggregate2 = new AggregateWithImmutableVersion(null, null);
371+
AggregateWithImmutableVersion aggregate3 = new AggregateWithImmutableVersion(null, null);
372+
Iterator<AggregateWithImmutableVersion> savedAggregatesIterator = template
373+
.saveAll(List.of(aggregate1, aggregate2, aggregate3)).iterator();
374+
AggregateWithImmutableVersion savedAggregate1 = savedAggregatesIterator.next();
375+
AggregateWithImmutableVersion twiceSavedAggregate2 = template.save(savedAggregatesIterator.next());
376+
AggregateWithImmutableVersion twiceSavedAggregate3 = template.save(savedAggregatesIterator.next());
377+
378+
assertThat(template.count(AggregateWithImmutableVersion.class)).isEqualTo(3);
379+
380+
template.deleteAll(List.of(savedAggregate1, twiceSavedAggregate2, twiceSavedAggregate3),
381+
AggregateWithImmutableVersion.class);
382+
383+
assertThat(template.count(AggregateWithImmutableVersion.class)).isEqualTo(0);
384+
}
385+
366386
@Test // DATAJDBC-112
367387
@EnabledOnFeature({ SUPPORTS_QUOTED_IDS, SUPPORTS_GENERATED_IDS_IN_REFERENCED_ENTITIES })
368388
void updateReferencedEntityFromNull() {

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

+12
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,18 @@ public BatchDelete(List<Delete<T>> actions) {
432432
}
433433
}
434434

435+
/**
436+
* Represents a batch delete statement for multiple entities that are aggregate roots.
437+
*
438+
* @param <T> type of the entity for which this represents a database interaction.
439+
* @since 3.0
440+
*/
441+
final class BatchDeleteRoot<T> extends BatchWithValue<T, DeleteRoot<T>, Class<T>> {
442+
public BatchDeleteRoot(List<DeleteRoot<T>> actions) {
443+
super(actions, DeleteRoot::getEntityType);
444+
}
445+
}
446+
435447
/**
436448
* An action depending on another action for providing additional information like the id of a parent entity.
437449
*

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

+23-6
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,16 @@
22

33
import java.util.ArrayList;
44
import java.util.Comparator;
5+
import java.util.HashMap;
56
import java.util.List;
7+
import java.util.Map;
68
import java.util.function.Consumer;
79

810
import org.springframework.data.mapping.PersistentPropertyPath;
911
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
1012

13+
import static java.util.Collections.*;
14+
1115
/**
1216
* A {@link BatchingAggregateChange} implementation for delete changes that can contain actions for one or more delete
1317
* operations. When consumed, actions are yielded in the appropriate entity tree order with deletes carried out from
@@ -19,11 +23,9 @@
1923
*/
2024
public class DeleteBatchingAggregateChange<T> implements BatchingAggregateChange<T, DeleteAggregateChange<T>> {
2125

22-
private static final Comparator<PersistentPropertyPath<RelationalPersistentProperty>> pathLengthComparator = //
23-
Comparator.comparing(PersistentPropertyPath::getLength);
24-
2526
private final Class<T> entityType;
26-
private final List<DbAction.DeleteRoot<T>> rootActions = new ArrayList<>();
27+
private final List<DbAction.DeleteRoot<T>> rootActionsWithoutVersion = new ArrayList<>();
28+
private final List<DbAction.DeleteRoot<T>> rootActionsWithVersion = new ArrayList<>();
2729
private final List<DbAction.AcquireLockRoot<?>> lockActions = new ArrayList<>();
2830
private final BatchedActions deleteActions = BatchedActions.batchedDeletes();
2931

@@ -46,20 +48,35 @@ public void forEachAction(Consumer<? super DbAction<?>> consumer) {
4648

4749
lockActions.forEach(consumer);
4850
deleteActions.forEach(consumer);
49-
rootActions.forEach(consumer);
51+
if (rootActionsWithoutVersion.size() > 1) {
52+
consumer.accept(new DbAction.BatchDeleteRoot<>(rootActionsWithoutVersion));
53+
} else {
54+
rootActionsWithoutVersion.forEach(consumer);
55+
}
56+
rootActionsWithVersion.forEach(consumer);
5057
}
5158

5259
@Override
5360
public void add(DeleteAggregateChange<T> aggregateChange) {
5461

5562
aggregateChange.forEachAction(action -> {
5663
if (action instanceof DbAction.DeleteRoot<?> deleteRootAction) {
57-
rootActions.add((DbAction.DeleteRoot<T>) deleteRootAction);
64+
// noinspection unchecked
65+
addDeleteRoot((DbAction.DeleteRoot<T>) deleteRootAction);
5866
} else if (action instanceof DbAction.Delete<?> deleteAction) {
5967
deleteActions.add(deleteAction);
6068
} else if (action instanceof DbAction.AcquireLockRoot<?> lockRootAction) {
6169
lockActions.add(lockRootAction);
6270
}
6371
});
6472
}
73+
74+
private void addDeleteRoot(DbAction.DeleteRoot<T> action) {
75+
76+
if (action.getPreviousVersion() == null) {
77+
rootActionsWithoutVersion.add(action);
78+
} else {
79+
rootActionsWithVersion.add(action);
80+
}
81+
}
6582
}

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

+32
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,38 @@ void yieldsLockRootActionsBeforeDeleteActions() {
144144
assertThat(extractActions(change)).containsExactly(lockRootAction, intermediateDelete);
145145
}
146146

147+
@Test // GH-537
148+
void yieldsDeleteRootActionsWithoutVersionAsBatchDeleteRoots_whenGroupContainsMultipleDeleteRoots() {
149+
150+
DeleteAggregateChange<Root> aggregateChange1 = MutableAggregateChange.forDelete(new Root(null, null));
151+
DbAction.DeleteRoot<Root> deleteRoot1 = new DbAction.DeleteRoot<>(1L, Root.class, null);
152+
aggregateChange1.addAction(deleteRoot1);
153+
DeleteAggregateChange<Root> aggregateChange2 = MutableAggregateChange.forDelete(Root.class);
154+
DbAction.DeleteRoot<Root> deleteRoot2 = new DbAction.DeleteRoot<>(2L, Root.class, 10);
155+
aggregateChange2.addAction(deleteRoot2);
156+
DeleteAggregateChange<Root> aggregateChange3 = MutableAggregateChange.forDelete(Root.class);
157+
DbAction.DeleteRoot<Root> deleteRoot3 = new DbAction.DeleteRoot<>(3L, Root.class, null);
158+
aggregateChange3.addAction(deleteRoot3);
159+
DeleteAggregateChange<Root> aggregateChange4 = MutableAggregateChange.forDelete(Root.class);
160+
DbAction.DeleteRoot<Root> deleteRoot4 = new DbAction.DeleteRoot<>(4L, Root.class, 10);
161+
aggregateChange4.addAction(deleteRoot4);
162+
163+
BatchingAggregateChange<Root, DeleteAggregateChange<Root>> change = BatchingAggregateChange.forDelete(Root.class);
164+
change.add(aggregateChange1);
165+
change.add(aggregateChange2);
166+
change.add(aggregateChange3);
167+
change.add(aggregateChange4);
168+
169+
List<DbAction<?>> actions = extractActions(change);
170+
assertThat(actions).extracting(DbAction::getClass, DbAction::getEntityType).containsExactly( //
171+
Tuple.tuple(DbAction.BatchDeleteRoot.class, Root.class), //
172+
Tuple.tuple(DbAction.DeleteRoot.class, Root.class), //
173+
Tuple.tuple(DbAction.DeleteRoot.class, Root.class));
174+
assertThat(getBatchWithValueAction(actions, Root.class, DbAction.BatchDeleteRoot.class).getActions())
175+
.containsExactly(deleteRoot1, deleteRoot3);
176+
assertThat(actions).containsSubsequence(deleteRoot2, deleteRoot4);
177+
}
178+
147179
private <T> List<DbAction<?>> extractActions(BatchingAggregateChange<T, ? extends MutableAggregateChange<T>> change) {
148180

149181
List<DbAction<?>> actions = new ArrayList<>();

0 commit comments

Comments
 (0)