Skip to content

Commit 64a07e6

Browse files
ctailor2schauder
authored andcommitted
Add DeleteBatchingAggregateChange to batch deletes when deleting multiple aggregate roots.
+ Rename DefaultAggregateChange to DeleteAggregateChange. Original pull request #1230
1 parent cf6e174 commit 64a07e6

File tree

9 files changed

+405
-23
lines changed

9 files changed

+405
-23
lines changed

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

+18
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,15 @@ public interface JdbcAggregateOperations {
8080
*/
8181
<T> void deleteById(Object id, Class<T> domainType);
8282

83+
/**
84+
* Deletes all aggregates identified by their aggregate root ids.
85+
*
86+
* @param ids the ids of the aggregate roots of the aggregates to be deleted. Must not be {@code null}.
87+
* @param domainType the type of the aggregate root.
88+
* @param <T> the type of the aggregate root.
89+
*/
90+
<T> void deleteAllById(Iterable<?> ids, Class<T> domainType);
91+
8392
/**
8493
* Delete an aggregate identified by it's aggregate root.
8594
*
@@ -96,6 +105,15 @@ public interface JdbcAggregateOperations {
96105
*/
97106
void deleteAll(Class<?> domainType);
98107

108+
/**
109+
* Delete all aggregates identified by their aggregate roots.
110+
*
111+
* @param aggregateRoots to delete. Must not be {@code null}.
112+
* @param domainType type of the aggregate roots to be deleted. Must not be {@code null}.
113+
* @param <T> the type of the aggregate roots.
114+
*/
115+
<T> void deleteAll(Iterable<? extends T> aggregateRoots, Class<T> domainType);
116+
99117
/**
100118
* Counts the number of aggregates of a given type.
101119
*

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

+47-3
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717

1818
import java.util.ArrayList;
1919
import java.util.Iterator;
20+
import java.util.LinkedHashMap;
2021
import java.util.List;
22+
import java.util.Map;
2123
import java.util.function.Function;
2224
import java.util.stream.Collectors;
2325
import java.util.stream.StreamSupport;
@@ -33,6 +35,7 @@
3335
import org.springframework.data.mapping.callback.EntityCallbacks;
3436
import org.springframework.data.relational.core.conversion.AggregateChange;
3537
import org.springframework.data.relational.core.conversion.BatchingAggregateChange;
38+
import org.springframework.data.relational.core.conversion.DeleteAggregateChange;
3639
import org.springframework.data.relational.core.conversion.MutableAggregateChange;
3740
import org.springframework.data.relational.core.conversion.RelationalEntityDeleteWriter;
3841
import org.springframework.data.relational.core.conversion.RelationalEntityInsertWriter;
@@ -275,6 +278,25 @@ public <S> void deleteById(Object id, Class<S> domainType) {
275278
deleteTree(id, null, domainType);
276279
}
277280

281+
@Override
282+
public <T> void deleteAllById(Iterable<?> ids, Class<T> domainType) {
283+
284+
Assert.isTrue(ids.iterator().hasNext(), "Ids must not be empty!");
285+
286+
BatchingAggregateChange<T, DeleteAggregateChange<T>> batchingAggregateChange = BatchingAggregateChange
287+
.forDelete(domainType);
288+
289+
ids.forEach(id -> {
290+
DeleteAggregateChange<T> change = createDeletingChange(id, null, domainType);
291+
triggerBeforeDelete(null, id, change);
292+
batchingAggregateChange.add(change);
293+
});
294+
295+
executor.executeDelete(batchingAggregateChange);
296+
297+
ids.forEach(id -> triggerAfterDelete(null, id, batchingAggregateChange));
298+
}
299+
278300
@Override
279301
public void deleteAll(Class<?> domainType) {
280302

@@ -284,6 +306,28 @@ public void deleteAll(Class<?> domainType) {
284306
executor.executeDelete(change);
285307
}
286308

309+
@Override
310+
public <T> void deleteAll(Iterable<? extends T> instances, Class<T> domainType) {
311+
312+
Assert.isTrue(instances.iterator().hasNext(), "Aggregate instances must not be empty!");
313+
314+
BatchingAggregateChange<T, DeleteAggregateChange<T>> batchingAggregateChange = BatchingAggregateChange
315+
.forDelete(domainType);
316+
Map<Object, T> instancesBeforeExecute = new LinkedHashMap<>();
317+
318+
instances.forEach(instance -> {
319+
Object id = context.getRequiredPersistentEntity(domainType).getIdentifierAccessor(instance)
320+
.getRequiredIdentifier();
321+
DeleteAggregateChange<T> change = createDeletingChange(id, instance, domainType);
322+
instancesBeforeExecute.put(id, triggerBeforeDelete(instance, id, change));
323+
batchingAggregateChange.add(change);
324+
});
325+
326+
executor.executeDelete(batchingAggregateChange);
327+
328+
instancesBeforeExecute.forEach((id, instance) -> triggerAfterDelete(instance, id, batchingAggregateChange));
329+
}
330+
287331
private <T> T afterExecute(AggregateChange<T> change, T entityAfterExecution) {
288332

289333
Object identifier = context.getRequiredPersistentEntity(change.getEntityType())
@@ -416,7 +460,7 @@ private <T> RelationalPersistentEntity<T> getRequiredPersistentEntity(T instance
416460
return (RelationalPersistentEntity<T>) context.getRequiredPersistentEntity(instance.getClass());
417461
}
418462

419-
private <T> MutableAggregateChange<T> createDeletingChange(Object id, @Nullable T entity, Class<T> domainType) {
463+
private <T> DeleteAggregateChange<T> createDeletingChange(Object id, @Nullable T entity, Class<T> domainType) {
420464

421465
Number previousVersion = null;
422466
if (entity != null) {
@@ -425,7 +469,7 @@ private <T> MutableAggregateChange<T> createDeletingChange(Object id, @Nullable
425469
previousVersion = RelationalEntityVersionUtils.getVersionNumberFromEntity(entity, persistentEntity, converter);
426470
}
427471
}
428-
MutableAggregateChange<T> aggregateChange = MutableAggregateChange.forDelete(domainType, previousVersion);
472+
DeleteAggregateChange<T> aggregateChange = MutableAggregateChange.forDelete(domainType, previousVersion);
429473
jdbcEntityDeleteWriter.write(id, aggregateChange);
430474
return aggregateChange;
431475
}
@@ -475,7 +519,7 @@ private <T> T triggerAfterSave(T aggregateRoot, AggregateChange<T> change) {
475519
return entityCallbacks.callback(AfterSaveCallback.class, aggregateRoot);
476520
}
477521

478-
private <T> void triggerAfterDelete(@Nullable T aggregateRoot, Object id, MutableAggregateChange<T> change) {
522+
private <T> void triggerAfterDelete(@Nullable T aggregateRoot, Object id, AggregateChange<T> change) {
479523

480524
publisher.publishEvent(new AfterDeleteEvent<>(Identifier.of(id), aggregateRoot, change));
481525

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/SimpleJdbcRepository.java

+2-3
Original file line numberDiff line numberDiff line change
@@ -101,14 +101,13 @@ public void delete(T instance) {
101101

102102
@Override
103103
public void deleteAllById(Iterable<? extends ID> ids) {
104-
ids.forEach(it -> entityOperations.deleteById(it, entity.getType()));
104+
entityOperations.deleteAllById(ids, entity.getType());
105105
}
106106

107107
@Transactional
108108
@Override
109-
@SuppressWarnings("unchecked")
110109
public void deleteAll(Iterable<? extends T> entities) {
111-
entities.forEach(it -> entityOperations.delete(it, (Class<T>) it.getClass()));
110+
entityOperations.deleteAll(entities, entity.getType());
112111
}
113112

114113
@Transactional

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

+32
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,38 @@ void saveAndDeleteAllWithReferencedEntity() {
331331
softly.assertAll();
332332
}
333333

334+
@Test // GH-537
335+
@EnabledOnFeature(SUPPORTS_QUOTED_IDS)
336+
void saveAndDeleteAllByAggregateRootsWithReferencedEntity() {
337+
LegoSet legoSet1 = template.save(legoSet);
338+
LegoSet legoSet2 = template.save(createLegoSet("Some Name"));
339+
340+
template.deleteAll(List.of(legoSet1, legoSet2), LegoSet.class);
341+
342+
SoftAssertions softly = new SoftAssertions();
343+
344+
assertThat(template.findAll(LegoSet.class)).isEmpty();
345+
assertThat(template.findAll(Manual.class)).isEmpty();
346+
347+
softly.assertAll();
348+
}
349+
350+
@Test // GH-537
351+
@EnabledOnFeature(SUPPORTS_QUOTED_IDS)
352+
void saveAndDeleteAllByIdsWithReferencedEntity() {
353+
LegoSet legoSet1 = template.save(legoSet);
354+
LegoSet legoSet2 = template.save(createLegoSet("Some Name"));
355+
356+
template.deleteAllById(List.of(legoSet1.id, legoSet2.id), LegoSet.class);
357+
358+
SoftAssertions softly = new SoftAssertions();
359+
360+
assertThat(template.findAll(LegoSet.class)).isEmpty();
361+
assertThat(template.findAll(Manual.class)).isEmpty();
362+
363+
softly.assertAll();
364+
}
365+
334366
@Test // DATAJDBC-112
335367
@EnabledOnFeature({ SUPPORTS_QUOTED_IDS, SUPPORTS_GENERATED_IDS_IN_REFERENCED_ENTITIES })
336368
void updateReferencedEntityFromNull() {

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

+15
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,19 @@ static <T> BatchingAggregateChange<T, RootAggregateChange<T>> forSave(Class<T> e
4747

4848
return new SaveBatchingAggregateChange<>(entityClass);
4949
}
50+
51+
/**
52+
* Factory method to create a {@link BatchingAggregateChange} for deleting entities.
53+
*
54+
* @param entityClass aggregate root type.
55+
* @param <T> entity type.
56+
* @return the {@link BatchingAggregateChange} for deleting root entities.
57+
* @since 3.0
58+
*/
59+
static <T> BatchingAggregateChange<T, DeleteAggregateChange<T>> forDelete(Class<T> entityClass) {
60+
61+
Assert.notNull(entityClass, "Entity class must not be null");
62+
63+
return new DeleteBatchingAggregateChange<>(entityClass);
64+
}
5065
}
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,7 @@
3030
* @author Chirag Tailor
3131
* @since 2.0
3232
*/
33-
class DefaultAggregateChange<T> implements MutableAggregateChange<T> {
34-
35-
private final Kind kind;
33+
public class DeleteAggregateChange<T> implements MutableAggregateChange<T> {
3634

3735
/** Type of the aggregate root to be changed */
3836
private final Class<T> entityType;
@@ -42,9 +40,7 @@ class DefaultAggregateChange<T> implements MutableAggregateChange<T> {
4240
/** The previous version assigned to the instance being changed, if available */
4341
@Nullable private final Number previousVersion;
4442

45-
public DefaultAggregateChange(Kind kind, Class<T> entityType, @Nullable Number previousVersion) {
46-
47-
this.kind = kind;
43+
public DeleteAggregateChange(Class<T> entityType, @Nullable Number previousVersion) {
4844
this.entityType = entityType;
4945
this.previousVersion = previousVersion;
5046
}
@@ -64,7 +60,7 @@ public void addAction(DbAction<?> action) {
6460

6561
@Override
6662
public Kind getKind() {
67-
return this.kind;
63+
return Kind.DELETE;
6864
}
6965

7066
@Override
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package org.springframework.data.relational.core.conversion;
2+
3+
import org.springframework.data.mapping.PersistentPropertyPath;
4+
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
5+
6+
import java.util.ArrayList;
7+
import java.util.Comparator;
8+
import java.util.HashMap;
9+
import java.util.List;
10+
import java.util.Map;
11+
import java.util.function.Consumer;
12+
13+
import static java.util.Collections.*;
14+
15+
/**
16+
* A {@link BatchingAggregateChange} implementation for delete changes that can contain actions for one or more delete
17+
* operations. When consumed, actions are yielded in the appropriate entity tree order with deletes carried out from
18+
* leaves to root. All operations that can be batched are grouped and combined to offer the ability for an optimized
19+
* batch operation to be used.
20+
*
21+
* @author Chirag Tailor
22+
* @since 3.0
23+
*/
24+
public class DeleteBatchingAggregateChange<T> implements BatchingAggregateChange<T, DeleteAggregateChange<T>> {
25+
26+
private static final Comparator<PersistentPropertyPath<RelationalPersistentProperty>> pathLengthComparator = //
27+
Comparator.comparing(PersistentPropertyPath::getLength);
28+
29+
private final Class<T> entityType;
30+
private final List<DbAction.DeleteRoot<T>> rootActions = new ArrayList<>();
31+
private final List<DbAction.AcquireLockRoot<?>> lockActions = new ArrayList<>();
32+
private final Map<PersistentPropertyPath<RelationalPersistentProperty>, List<DbAction.Delete<Object>>> deleteActions = //
33+
new HashMap<>();
34+
35+
public DeleteBatchingAggregateChange(Class<T> entityType) {
36+
this.entityType = entityType;
37+
}
38+
39+
@Override
40+
public Kind getKind() {
41+
return Kind.DELETE;
42+
}
43+
44+
@Override
45+
public Class<T> getEntityType() {
46+
return entityType;
47+
}
48+
49+
@Override
50+
public void forEachAction(Consumer<? super DbAction<?>> consumer) {
51+
52+
lockActions.forEach(consumer);
53+
deleteActions.entrySet().stream().sorted(Map.Entry.comparingByKey(pathLengthComparator.reversed()))
54+
.forEach((entry) -> {
55+
List<DbAction.Delete<Object>> deletes = entry.getValue();
56+
if (deletes.size() > 1) {
57+
consumer.accept(new DbAction.BatchDelete<>(deletes));
58+
} else {
59+
deletes.forEach(consumer);
60+
}
61+
});
62+
rootActions.forEach(consumer);
63+
}
64+
65+
@Override
66+
public void add(DeleteAggregateChange<T> aggregateChange) {
67+
68+
aggregateChange.forEachAction(action -> {
69+
if (action instanceof DbAction.DeleteRoot<?> deleteRootAction) {
70+
//noinspection unchecked
71+
rootActions.add((DbAction.DeleteRoot<T>) deleteRootAction);
72+
} else if (action instanceof DbAction.Delete<?> deleteAction) {
73+
// noinspection unchecked
74+
addDelete((DbAction.Delete<Object>) deleteAction);
75+
} else if (action instanceof DbAction.AcquireLockRoot<?> lockRootAction) {
76+
lockActions.add(lockRootAction);
77+
}
78+
});
79+
}
80+
81+
private void addDelete(DbAction.Delete<Object> action) {
82+
83+
PersistentPropertyPath<RelationalPersistentProperty> propertyPath = action.getPropertyPath();
84+
deleteActions.merge(propertyPath, new ArrayList<>(singletonList(action)), (actions, defaultValue) -> {
85+
actions.add(action);
86+
return actions;
87+
});
88+
}
89+
}

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

+10-10
Original file line numberDiff line numberDiff line change
@@ -59,47 +59,47 @@ static <T> RootAggregateChange<T> forSave(T entity, @Nullable Number previousVer
5959
}
6060

6161
/**
62-
* Factory method to create an {@link MutableAggregateChange} for deleting entities.
62+
* Factory method to create a {@link DeleteAggregateChange} for deleting entities.
6363
*
6464
* @param entity aggregate root to delete.
6565
* @param <T> entity type.
66-
* @return the {@link MutableAggregateChange} for deleting the root {@code entity}.
66+
* @return the {@link DeleteAggregateChange} for deleting the root {@code entity}.
6767
* @since 1.2
6868
*/
6969
@SuppressWarnings("unchecked")
70-
static <T> MutableAggregateChange<T> forDelete(T entity) {
70+
static <T> DeleteAggregateChange<T> forDelete(T entity) {
7171

7272
Assert.notNull(entity, "Entity must not be null");
7373

7474
return forDelete((Class<T>) ClassUtils.getUserClass(entity));
7575
}
7676

7777
/**
78-
* Factory method to create an {@link MutableAggregateChange} for deleting entities.
78+
* Factory method to create a {@link DeleteAggregateChange} for deleting entities.
7979
*
8080
* @param entityClass aggregate root type.
8181
* @param <T> entity type.
82-
* @return the {@link MutableAggregateChange} for deleting the root {@code entity}.
82+
* @return the {@link DeleteAggregateChange} for deleting the root {@code entity}.
8383
* @since 1.2
8484
*/
85-
static <T> MutableAggregateChange<T> forDelete(Class<T> entityClass) {
85+
static <T> DeleteAggregateChange<T> forDelete(Class<T> entityClass) {
8686
return forDelete(entityClass, null);
8787
}
8888

8989
/**
90-
* Factory method to create an {@link MutableAggregateChange} for deleting entities.
90+
* Factory method to create a {@link DeleteAggregateChange} for deleting entities.
9191
*
9292
* @param entityClass aggregate root type.
9393
* @param previousVersion the previous version assigned to the instance being saved. May be {@literal null}.
9494
* @param <T> entity type.
95-
* @return the {@link MutableAggregateChange} for deleting the root {@code entity}.
95+
* @return the {@link DeleteAggregateChange} for deleting the root {@code entity}.
9696
* @since 2.4
9797
*/
98-
static <T> MutableAggregateChange<T> forDelete(Class<T> entityClass, @Nullable Number previousVersion) {
98+
static <T> DeleteAggregateChange<T> forDelete(Class<T> entityClass, @Nullable Number previousVersion) {
9999

100100
Assert.notNull(entityClass, "Entity class must not be null");
101101

102-
return new DefaultAggregateChange<>(Kind.DELETE, entityClass, previousVersion);
102+
return new DeleteAggregateChange<>(entityClass, previousVersion);
103103
}
104104

105105
/**

0 commit comments

Comments
 (0)