From d26d842a3944c71c4c356db83985e3db2cc82790 Mon Sep 17 00:00:00 2001 From: Chirag Tailor Date: Wed, 13 Apr 2022 13:37:14 -0500 Subject: [PATCH] Update SaveBatchingAggregateChange to batch InsertRoot actions as well. --- .../jdbc/core/AggregateChangeExecutor.java | 2 + .../JdbcAggregateChangeExecutionContext.java | 14 + ...gregateChangeExecutorContextUnitTests.java | 30 ++- .../relational/core/conversion/DbAction.java | 12 + .../SaveBatchingAggregateChange.java | 45 +++- .../core/conversion/DbActionTestSupport.java | 8 +- .../RelationalEntityWriterUnitTests.java | 12 +- .../SaveBatchingAggregateChangeTest.java | 254 +++++++++++++++--- 8 files changed, 313 insertions(+), 64 deletions(-) diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/AggregateChangeExecutor.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/AggregateChangeExecutor.java index acf9df817d..c0911e8fb6 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/AggregateChangeExecutor.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/AggregateChangeExecutor.java @@ -82,6 +82,8 @@ private void execute(DbAction action, JdbcAggregateChangeExecutionContext exe try { if (action instanceof DbAction.InsertRoot) { executionContext.executeInsertRoot((DbAction.InsertRoot) action); + } else if (action instanceof DbAction.BatchInsertRoot) { + executionContext.executeBatchInsertRoot((DbAction.BatchInsertRoot) action); } else if (action instanceof DbAction.Insert) { executionContext.executeInsert((DbAction.Insert) action); } else if (action instanceof DbAction.BatchInsert) { diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutionContext.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutionContext.java index 426e3fb2b7..97fbd258b4 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutionContext.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutionContext.java @@ -75,6 +75,20 @@ void executeInsertRoot(DbAction.InsertRoot insert) { add(new DbActionExecutionResult(insert, id)); } + void executeBatchInsertRoot(DbAction.BatchInsertRoot batchInsertRoot) { + + List> inserts = batchInsertRoot.getActions(); + List> insertSubjects = inserts.stream() + .map(insert -> InsertSubject.describedBy(insert.getEntity(), Identifier.empty())).collect(Collectors.toList()); + + Object[] ids = accessStrategy.insert(insertSubjects, batchInsertRoot.getEntityType(), + batchInsertRoot.getBatchValue()); + + for (int i = 0; i < inserts.size(); i++) { + add(new DbActionExecutionResult(inserts.get(i), ids.length > 0 ? ids[i] : null)); + } + } + void executeInsert(DbAction.Insert insert) { Identifier parentKeys = getParentKeys(insert, converter); diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutorContextUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutorContextUnitTests.java index 800eb4f1fe..62a7df919f 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutorContextUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutorContextUnitTests.java @@ -164,6 +164,32 @@ void batchInsertOperation_withoutGeneratedIds() { assertThat(content.id).isNull(); } + @Test // GH-537 + void batchInsertRootOperation_withGeneratedIds() { + + when(accessStrategy.insert(singletonList(InsertSubject.describedBy(root, Identifier.empty())), DummyEntity.class, IdValueSource.GENERATED)) + .thenReturn(new Object[] { 123L }); + executionContext.executeBatchInsertRoot(new DbAction.BatchInsertRoot<>(singletonList(new DbAction.InsertRoot<>(root, IdValueSource.GENERATED)))); + + List newRoots = executionContext.populateIdsIfNecessary(); + + assertThat(newRoots).containsExactly(root); + assertThat(root.id).isEqualTo(123L); + } + + @Test // GH-537 + void batchInsertRootOperation_withoutGeneratedIds() { + + when(accessStrategy.insert(singletonList(InsertSubject.describedBy(root, Identifier.empty())), DummyEntity.class, IdValueSource.PROVIDED)) + .thenReturn(new Object[] { null }); + executionContext.executeBatchInsertRoot(new DbAction.BatchInsertRoot<>(singletonList(new DbAction.InsertRoot<>(root, IdValueSource.PROVIDED)))); + + List newRoots = executionContext.populateIdsIfNecessary(); + + assertThat(newRoots).containsExactly(root); + assertThat(root.id).isNull(); + } + @Test // GH-1201 void updates_whenReferencesWithImmutableIdAreInserted() { @@ -177,7 +203,8 @@ void updates_whenReferencesWithImmutableIdAreInserted() { Identifier identifier = Identifier.empty().withPart(SqlIdentifier.quoted("DUMMY_ENTITY"), 123L, Long.class); when(accessStrategy.insert(contentImmutableId, ContentImmutableId.class, identifier, IdValueSource.GENERATED)) .thenReturn(456L); - executionContext.executeInsert(createInsert(rootUpdate, "contentImmutableId", contentImmutableId, null, IdValueSource.GENERATED)); + executionContext.executeInsert( + createInsert(rootUpdate, "contentImmutableId", contentImmutableId, null, IdValueSource.GENERATED)); List newRoots = executionContext.populateIdsIfNecessary(); assertThat(newRoots).containsExactly(root); @@ -197,7 +224,6 @@ void populatesIdsIfNecessaryForAllRootsThatWereProcessed() { when(accessStrategy.insert(content1, Content.class, createBackRef(123L), IdValueSource.GENERATED)).thenReturn(11L); executionContext.executeInsert(createInsert(rootUpdate1, "content", content1, null, IdValueSource.GENERATED)); - DummyEntity root2 = new DummyEntity(); DbAction.InsertRoot rootInsert2 = new DbAction.InsertRoot<>(root2, IdValueSource.GENERATED); when(accessStrategy.insert(root2, DummyEntity.class, Identifier.empty(), IdValueSource.GENERATED)).thenReturn(456L); diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/DbAction.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/DbAction.java index ec3dad3169..97e2f53470 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/DbAction.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/DbAction.java @@ -399,6 +399,18 @@ public BatchInsert(List> actions) { } } + /** + * Represents a batch insert statement for a multiple entities that are aggregate roots. + * + * @param type of the entity for which this represents a database interaction. + * @since 3.0 + */ + final class BatchInsertRoot extends BatchWithValue, IdValueSource> { + public BatchInsertRoot(List> actions) { + super(actions, InsertRoot::getIdValueSource); + } + } + /** * An action depending on another action for providing additional information like the id of a parent entity. * diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/SaveBatchingAggregateChange.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/SaveBatchingAggregateChange.java index 46599c7e67..49ee0b2727 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/SaveBatchingAggregateChange.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/SaveBatchingAggregateChange.java @@ -43,7 +43,8 @@ public class SaveBatchingAggregateChange implements BatchingAggregateChange entityType; - private final List> rootActions = new ArrayList<>(); + private final List> rootActions = new ArrayList<>(); + private final List> insertRootBatchCandidates = new ArrayList<>(); private final Map, Map>>> insertActions = // new HashMap<>(); private final Map, List>> deleteActions = // @@ -69,11 +70,15 @@ public void forEachAction(Consumer> consumer) { Assert.notNull(consumer, "Consumer must not be null."); rootActions.forEach(consumer); + if (insertRootBatchCandidates.size() > 1) { + consumer.accept(new DbAction.BatchInsertRoot<>(insertRootBatchCandidates)); + } else { + insertRootBatchCandidates.forEach(consumer); + } deleteActions.entrySet().stream().sorted(Map.Entry.comparingByKey(pathLengthComparator.reversed())) .forEach((entry) -> entry.getValue().forEach(consumer)); - insertActions.entrySet().stream().sorted(Map.Entry.comparingByKey(pathLengthComparator)) - .forEach((entry) -> entry.getValue() - .forEach((idValueSource, inserts) -> { + insertActions.entrySet().stream().sorted(Map.Entry.comparingByKey(pathLengthComparator)).forEach((entry) -> entry + .getValue().forEach((idValueSource, inserts) -> { if (inserts.size() > 1) { consumer.accept(new DbAction.BatchInsert<>(inserts)); } else { @@ -86,28 +91,44 @@ public void forEachAction(Consumer> consumer) { public void add(RootAggregateChange aggregateChange) { aggregateChange.forEachAction(action -> { - if (action instanceof DbAction.WithRoot rootAction) { + if (action instanceof DbAction.UpdateRoot rootAction) { + commitBatchCandidates(); rootActions.add(rootAction); - } else if (action instanceof DbAction.Insert) { + } else if (action instanceof DbAction.InsertRoot rootAction) { + if (!insertRootBatchCandidates.isEmpty() && !insertRootBatchCandidates.get(0).getIdValueSource().equals(rootAction.getIdValueSource())) { + commitBatchCandidates(); + } + //noinspection unchecked + insertRootBatchCandidates.add((DbAction.InsertRoot) rootAction); + } else if (action instanceof DbAction.Insert insertAction) { // noinspection unchecked - addInsert((DbAction.Insert) action); + addInsert((DbAction.Insert) insertAction); } else if (action instanceof DbAction.Delete deleteAction) { addDelete(deleteAction); } }); } + private void commitBatchCandidates() { + + if (insertRootBatchCandidates.size() > 1) { + rootActions.add(new DbAction.BatchInsertRoot<>(List.copyOf(insertRootBatchCandidates))); + } else { + rootActions.addAll(insertRootBatchCandidates); + } + insertRootBatchCandidates.clear(); + } + private void addInsert(DbAction.Insert action) { PersistentPropertyPath propertyPath = action.getPropertyPath(); insertActions.merge(propertyPath, new HashMap<>(singletonMap(action.getIdValueSource(), new ArrayList<>(singletonList(action)))), (map, mapDefaultValue) -> { - map.merge(action.getIdValueSource(), new ArrayList<>(singletonList(action)), - (actions, listDefaultValue) -> { - actions.add(action); - return actions; - }); + map.merge(action.getIdValueSource(), new ArrayList<>(singletonList(action)), (actions, listDefaultValue) -> { + actions.add(action); + return actions; + }); return map; }); } diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/DbActionTestSupport.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/DbActionTestSupport.java index 2d91453050..5f0e828032 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/DbActionTestSupport.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/DbActionTestSupport.java @@ -52,12 +52,12 @@ static Class actualEntityType(DbAction a) { @Nullable static IdValueSource insertIdValueSource(DbAction action) { - if (action instanceof DbAction.InsertRoot) { - return ((DbAction.InsertRoot) action).getIdValueSource(); - } else if (action instanceof DbAction.Insert) { - return ((DbAction.Insert) action).getIdValueSource(); + if (action instanceof DbAction.WithEntity) { + return ((DbAction.WithEntity) action).getIdValueSource(); } else if (action instanceof DbAction.BatchInsert) { return ((DbAction.BatchInsert) action).getBatchValue(); + } else if (action instanceof DbAction.BatchInsertRoot) { + return ((DbAction.BatchInsertRoot) action).getBatchValue(); } else { return null; } diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityWriterUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityWriterUnitTests.java index 7c5945597a..e2e88ff9e1 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityWriterUnitTests.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityWriterUnitTests.java @@ -255,7 +255,7 @@ public void newReferenceTriggersDeletePlusInsert() { DbActionTestSupport::isWithDependsOn, // DbActionTestSupport::insertIdValueSource) // .containsExactly( // - tuple(UpdateRoot.class, SingleReferenceEntity.class, "", SingleReferenceEntity.class, false, null), // + tuple(UpdateRoot.class, SingleReferenceEntity.class, "", SingleReferenceEntity.class, false, IdValueSource.PROVIDED), // tuple(Delete.class, Element.class, "other", null, false, null), // tuple(Insert.class, Element.class, "other", Element.class, true, IdValueSource.GENERATED) // ); @@ -371,7 +371,7 @@ public void cascadingReferencesTriggerCascadingActionsForUpdate() { DbActionTestSupport::isWithDependsOn, // DbActionTestSupport::insertIdValueSource) // .containsExactly( // - tuple(UpdateRoot.class, CascadingReferenceEntity.class, "", CascadingReferenceEntity.class, false, null), // + tuple(UpdateRoot.class, CascadingReferenceEntity.class, "", CascadingReferenceEntity.class, false, IdValueSource.PROVIDED), // tuple(Delete.class, Element.class, "other.element", null, false, null), tuple(Delete.class, CascadingReferenceMiddleElement.class, "other", null, false, null), tuple(Insert.class, CascadingReferenceMiddleElement.class, "other", CascadingReferenceMiddleElement.class, @@ -530,7 +530,7 @@ public void mapTriggersDeletePlusInsert() { DbActionTestSupport::extractPath, // DbActionTestSupport::insertIdValueSource) // .containsExactly( // - tuple(UpdateRoot.class, MapContainer.class, null, "", null), // + tuple(UpdateRoot.class, MapContainer.class, null, "", IdValueSource.PROVIDED), // tuple(Delete.class, Element.class, null, "elements", null), // tuple(Insert.class, Element.class, "one", "elements", IdValueSource.GENERATED) // ); @@ -553,7 +553,7 @@ public void listTriggersDeletePlusInsert() { DbActionTestSupport::extractPath, // DbActionTestSupport::insertIdValueSource) // .containsExactly( // - tuple(UpdateRoot.class, ListContainer.class, null, "", null), // + tuple(UpdateRoot.class, ListContainer.class, null, "", IdValueSource.PROVIDED), // tuple(Delete.class, Element.class, null, "elements", null), // tuple(Insert.class, Element.class, 0, "elements", IdValueSource.GENERATED) // ); @@ -578,7 +578,7 @@ public void multiLevelQualifiedReferencesWithId() { DbActionTestSupport::extractPath, // DbActionTestSupport::insertIdValueSource) // .containsExactly( // - tuple(UpdateRoot.class, ListMapContainer.class, null, null, "", null), // + tuple(UpdateRoot.class, ListMapContainer.class, null, null, "", IdValueSource.PROVIDED), // tuple(Delete.class, Element.class, null, null, "maps.elements", null), // tuple(Delete.class, MapContainer.class, null, null, "maps", null), // tuple(Insert.class, MapContainer.class, 0, null, "maps", IdValueSource.PROVIDED), // @@ -606,7 +606,7 @@ public void multiLevelQualifiedReferencesWithOutId() { DbActionTestSupport::extractPath, // DbActionTestSupport::insertIdValueSource) // .containsExactly( // - tuple(UpdateRoot.class, NoIdListMapContainer.class, null, null, "", null), // + tuple(UpdateRoot.class, NoIdListMapContainer.class, null, null, "", IdValueSource.PROVIDED), // tuple(Delete.class, NoIdElement.class, null, null, "maps.elements", null), // tuple(Delete.class, NoIdMapContainer.class, null, null, "maps", null), // tuple(Insert.class, NoIdMapContainer.class, 0, null, "maps", IdValueSource.NONE), // diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/SaveBatchingAggregateChangeTest.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/SaveBatchingAggregateChangeTest.java index a10aae5fdb..a56eaf59f9 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/SaveBatchingAggregateChangeTest.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/SaveBatchingAggregateChangeTest.java @@ -23,6 +23,7 @@ import java.util.stream.Collectors; import org.assertj.core.groups.Tuple; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.data.annotation.Id; import org.springframework.data.relational.core.mapping.RelationalMappingContext; @@ -46,23 +47,192 @@ void startsWithNoActions() { assertThat(extractActions(change)).isEmpty(); } - @Test - void yieldsRootActions() { - - Root root1 = new Root(null, null); - DbAction.InsertRoot root1Insert = new DbAction.InsertRoot<>(root1, IdValueSource.GENERATED); - RootAggregateChange aggregateChange1 = MutableAggregateChange.forSave(root1); - aggregateChange1.setRootAction(root1Insert); - Root root2 = new Root(null, null); - DbAction.InsertRoot root2Insert = new DbAction.InsertRoot<>(root2, IdValueSource.GENERATED); - RootAggregateChange aggregateChange2 = MutableAggregateChange.forSave(root2); - aggregateChange2.setRootAction(root2Insert); - - BatchingAggregateChange> change = BatchingAggregateChange.forSave(Root.class); - change.add(aggregateChange1); - change.add(aggregateChange2); - - assertThat(extractActions(change)).containsExactly(root1Insert, root2Insert); + @Nested + class RootActionsTests { + @Test + void yieldsUpdateRoot() { + + Root root = new Root(1L, null); + DbAction.UpdateRoot rootUpdate = new DbAction.UpdateRoot<>(root, null); + RootAggregateChange aggregateChange = MutableAggregateChange.forSave(root); + aggregateChange.setRootAction(rootUpdate); + + BatchingAggregateChange> change = BatchingAggregateChange.forSave(Root.class); + change.add(aggregateChange); + + assertThat(extractActions(change)).containsExactly(rootUpdate); + } + + @Test + void yieldsSingleInsertRoot_followedByUpdateRoot_asIndividualActions() { + + Root root1 = new Root(1L, null); + DbAction.InsertRoot root1Insert = new DbAction.InsertRoot<>(root1, IdValueSource.GENERATED); + RootAggregateChange aggregateChange1 = MutableAggregateChange.forSave(root1); + aggregateChange1.setRootAction(root1Insert); + Root root2 = new Root(1L, null); + DbAction.UpdateRoot root2Update = new DbAction.UpdateRoot<>(root2, null); + RootAggregateChange aggregateChange2 = MutableAggregateChange.forSave(root2); + aggregateChange2.setRootAction(root2Update); + + BatchingAggregateChange> change = BatchingAggregateChange.forSave(Root.class); + change.add(aggregateChange1); + change.add(aggregateChange2); + + assertThat(extractActions(change)) // + .extracting(DbAction::getClass, DbAction::getEntityType, DbActionTestSupport::insertIdValueSource) + .containsExactly( // + Tuple.tuple(DbAction.InsertRoot.class, Root.class, IdValueSource.GENERATED), // + Tuple.tuple(DbAction.UpdateRoot.class, Root.class, IdValueSource.PROVIDED)); + } + + @Test + void yieldsMultipleMatchingInsertRoot_followedByUpdateRoot_asBatchInsertRootAction() { + + Root root1 = new Root(1L, null); + DbAction.InsertRoot root1Insert = new DbAction.InsertRoot<>(root1, IdValueSource.GENERATED); + RootAggregateChange aggregateChange1 = MutableAggregateChange.forSave(root1); + aggregateChange1.setRootAction(root1Insert); + Root root2 = new Root(1L, null); + DbAction.InsertRoot root2Insert = new DbAction.InsertRoot<>(root2, IdValueSource.GENERATED); + RootAggregateChange aggregateChange2 = MutableAggregateChange.forSave(root2); + aggregateChange2.setRootAction(root2Insert); + Root root3 = new Root(1L, null); + DbAction.UpdateRoot root3Update = new DbAction.UpdateRoot<>(root3, null); + RootAggregateChange aggregateChange3 = MutableAggregateChange.forSave(root3); + aggregateChange3.setRootAction(root3Update); + + BatchingAggregateChange> change = BatchingAggregateChange.forSave(Root.class); + change.add(aggregateChange1); + change.add(aggregateChange2); + change.add(aggregateChange3); + + List> actions = extractActions(change); + assertThat(actions) // + .extracting(DbAction::getClass, DbAction::getEntityType, DbActionTestSupport::insertIdValueSource) + .containsExactly( // + Tuple.tuple(DbAction.BatchInsertRoot.class, Root.class, IdValueSource.GENERATED), // + Tuple.tuple(DbAction.UpdateRoot.class, Root.class, IdValueSource.PROVIDED)); + assertThat(getBatchWithValueAction(actions, Root.class, DbAction.BatchInsertRoot.class).getActions()) + .containsExactly(root1Insert, root2Insert); + } + + @Test + void yieldsInsertRoot() { + + Root root = new Root(1L, null); + DbAction.InsertRoot rootInsert = new DbAction.InsertRoot<>(root, IdValueSource.GENERATED); + RootAggregateChange aggregateChange = MutableAggregateChange.forSave(root); + aggregateChange.setRootAction(rootInsert); + + BatchingAggregateChange> change = BatchingAggregateChange.forSave(Root.class); + change.add(aggregateChange); + + assertThat(extractActions(change)).containsExactly(rootInsert); + } + + @Test + void yieldsSingleInsertRoot_followedByNonMatchingInsertRoot_asIndividualActions() { + + Root root1 = new Root(1L, null); + DbAction.InsertRoot root1Insert = new DbAction.InsertRoot<>(root1, IdValueSource.GENERATED); + RootAggregateChange aggregateChange1 = MutableAggregateChange.forSave(root1); + aggregateChange1.setRootAction(root1Insert); + Root root2 = new Root(1L, null); + DbAction.InsertRoot root2Insert = new DbAction.InsertRoot<>(root2, IdValueSource.PROVIDED); + RootAggregateChange aggregateChange2 = MutableAggregateChange.forSave(root2); + aggregateChange2.setRootAction(root2Insert); + + BatchingAggregateChange> change = BatchingAggregateChange.forSave(Root.class); + change.add(aggregateChange1); + change.add(aggregateChange2); + + assertThat(extractActions(change)).containsExactly(root1Insert, root2Insert); + } + + @Test + void yieldsMultipleMatchingInsertRoot_followedByNonMatchingInsertRoot_asBatchInsertRootAction() { + + Root root1 = new Root(1L, null); + DbAction.InsertRoot root1Insert = new DbAction.InsertRoot<>(root1, IdValueSource.GENERATED); + RootAggregateChange aggregateChange1 = MutableAggregateChange.forSave(root1); + aggregateChange1.setRootAction(root1Insert); + Root root2 = new Root(1L, null); + DbAction.InsertRoot root2Insert = new DbAction.InsertRoot<>(root2, IdValueSource.GENERATED); + RootAggregateChange aggregateChange2 = MutableAggregateChange.forSave(root2); + aggregateChange2.setRootAction(root2Insert); + Root root3 = new Root(1L, null); + DbAction.InsertRoot root3Insert = new DbAction.InsertRoot<>(root3, IdValueSource.PROVIDED); + RootAggregateChange aggregateChange3 = MutableAggregateChange.forSave(root3); + aggregateChange3.setRootAction(root3Insert); + + BatchingAggregateChange> change = BatchingAggregateChange.forSave(Root.class); + change.add(aggregateChange1); + change.add(aggregateChange2); + change.add(aggregateChange3); + + List> actions = extractActions(change); + assertThat(actions) // + .extracting(DbAction::getClass, DbAction::getEntityType, DbActionTestSupport::insertIdValueSource) + .containsExactly( // + Tuple.tuple(DbAction.BatchInsertRoot.class, Root.class, IdValueSource.GENERATED), // + Tuple.tuple(DbAction.InsertRoot.class, Root.class, IdValueSource.PROVIDED)); + assertThat(getBatchWithValueAction(actions, Root.class, DbAction.BatchInsertRoot.class).getActions()) + .containsExactly(root1Insert, root2Insert); + } + + @Test + void yieldsMultipleMatchingInsertRoot_asBatchInsertRootAction() { + + Root root1 = new Root(1L, null); + DbAction.InsertRoot root1Insert = new DbAction.InsertRoot<>(root1, IdValueSource.GENERATED); + RootAggregateChange aggregateChange1 = MutableAggregateChange.forSave(root1); + aggregateChange1.setRootAction(root1Insert); + Root root2 = new Root(1L, null); + DbAction.InsertRoot root2Insert = new DbAction.InsertRoot<>(root2, IdValueSource.GENERATED); + RootAggregateChange aggregateChange2 = MutableAggregateChange.forSave(root2); + aggregateChange2.setRootAction(root2Insert); + + BatchingAggregateChange> change = BatchingAggregateChange.forSave(Root.class); + change.add(aggregateChange1); + change.add(aggregateChange2); + + List> actions = extractActions(change); + assertThat(actions) // + .extracting(DbAction::getClass, DbAction::getEntityType, DbActionTestSupport::insertIdValueSource) + .containsExactly(Tuple.tuple(DbAction.BatchInsertRoot.class, Root.class, IdValueSource.GENERATED)); + assertThat(getBatchWithValueAction(actions, Root.class, DbAction.BatchInsertRoot.class).getActions()) + .containsExactly(root1Insert, root2Insert); + } + + @Test + void yieldsPreviouslyYieldedInsertRoot_asBatchInsertRootAction_whenAdditionalMatchingInsertRootIsAdded() { + + Root root1 = new Root(1L, null); + DbAction.InsertRoot root1Insert = new DbAction.InsertRoot<>(root1, IdValueSource.GENERATED); + RootAggregateChange aggregateChange1 = MutableAggregateChange.forSave(root1); + aggregateChange1.setRootAction(root1Insert); + Root root2 = new Root(2L, null); + DbAction.InsertRoot root2Insert = new DbAction.InsertRoot<>(root2, IdValueSource.GENERATED); + RootAggregateChange aggregateChange2 = MutableAggregateChange.forSave(root2); + aggregateChange2.setRootAction(root2Insert); + BatchingAggregateChange> change = BatchingAggregateChange.forSave(Root.class); + + change.add(aggregateChange1); + + assertThat(extractActions(change)) // + .extracting(DbAction::getClass, DbAction::getEntityType, DbActionTestSupport::insertIdValueSource) + .containsExactly(Tuple.tuple(DbAction.InsertRoot.class, Root.class, IdValueSource.GENERATED)); + + change.add(aggregateChange2); + + List> actions = extractActions(change); + assertThat(actions) // + .extracting(DbAction::getClass, DbAction::getEntityType, DbActionTestSupport::insertIdValueSource) + .containsExactly(Tuple.tuple(DbAction.BatchInsertRoot.class, Root.class, IdValueSource.GENERATED)); + assertThat(getBatchWithValueAction(actions, Root.class, DbAction.BatchInsertRoot.class).getActions()) + .containsExactly(root1Insert, root2Insert); + } } @Test @@ -183,10 +353,10 @@ void yieldsInsertActionsAsBatchInserts_groupedByIdValueSource_whenGroupContainsM Tuple.tuple(DbAction.InsertRoot.class, Root.class, IdValueSource.GENERATED), // Tuple.tuple(DbAction.BatchInsert.class, Intermediate.class, IdValueSource.GENERATED)) // .doesNotContain(Tuple.tuple(DbAction.Insert.class, Intermediate.class)); - assertThat(getBatchInsertAction(actions, Intermediate.class, IdValueSource.GENERATED).getActions()) - .containsExactly(intermediateInsertGeneratedId1, intermediateInsertGeneratedId2); - assertThat(getBatchInsertAction(actions, Intermediate.class, IdValueSource.PROVIDED).getActions()) - .containsExactly(intermediateInsertProvidedId1, intermediateInsertProvidedId2); + assertThat(getBatchWithValueAction(actions, Intermediate.class, DbAction.BatchInsert.class, IdValueSource.GENERATED) + .getActions()).containsExactly(intermediateInsertGeneratedId1, intermediateInsertGeneratedId2); + assertThat(getBatchWithValueAction(actions, Intermediate.class, DbAction.BatchInsert.class, IdValueSource.PROVIDED) + .getActions()).containsExactly(intermediateInsertProvidedId1, intermediateInsertProvidedId2); } @Test @@ -227,7 +397,7 @@ void yieldsNestedInsertActionsInTreeOrderFromRootToLeaves() { .containsSubsequence( // Tuple.tuple(DbAction.BatchInsert.class, Intermediate.class, IdValueSource.GENERATED), Tuple.tuple(DbAction.Insert.class, Leaf.class, IdValueSource.GENERATED)); - assertThat(getBatchInsertAction(actions, Intermediate.class).getActions()) // + assertThat(getBatchWithValueAction(actions, Intermediate.class, DbAction.BatchInsert.class).getActions()) // .containsExactly(root1IntermediateInsert, root2IntermediateInsert); } @@ -258,32 +428,36 @@ void yieldsInsertsWithSameLengthReferences_asSeparateInserts() { assertThat(actions).containsSubsequence(oneInsert, twoInsert); } - private DbAction.BatchInsert getBatchInsertAction(List> actions, Class entityType, - IdValueSource idValueSource) { - return getBatchInsertActions(actions, entityType).stream() - .filter(batchInsert -> batchInsert.getBatchValue() == idValueSource).findFirst().orElseThrow( - () -> new RuntimeException(String.format("No BatchInsert with batch value '%s' found!", idValueSource))); + private List> extractActions(BatchingAggregateChange> change) { + + List> actions = new ArrayList<>(); + change.forEachAction(actions::add); + return actions; } - private DbAction.BatchInsert getBatchInsertAction(List> actions, Class entityType) { - return getBatchInsertActions(actions, entityType).stream().findFirst() - .orElseThrow(() -> new RuntimeException("No BatchInsert action found!")); + private DbAction.BatchWithValue, Object> getBatchWithValueAction(List> actions, + Class entityType, Class batchActionType) { + + return getBatchWithValueActions(actions, entityType, batchActionType).stream().findFirst() + .orElseThrow(() -> new RuntimeException("No BatchWithValue action found!")); } - @SuppressWarnings("unchecked") - private List> getBatchInsertActions(List> actions, Class entityType) { + private DbAction.BatchWithValue, Object> getBatchWithValueAction(List> actions, + Class entityType, Class batchActionType, Object batchValue) { - return actions.stream() // - .filter(dbAction -> dbAction instanceof DbAction.BatchInsert) // - .filter(dbAction -> dbAction.getEntityType().equals(entityType)) // - .map(dbAction -> (DbAction.BatchInsert) dbAction).collect(Collectors.toList()); + return getBatchWithValueActions(actions, entityType, batchActionType).stream() + .filter(batchWithValue -> batchWithValue.getBatchValue() == batchValue).findFirst().orElseThrow( + () -> new RuntimeException(String.format("No BatchWithValue with batch value '%s' found!", batchValue))); } - private List> extractActions(BatchingAggregateChange> change) { + @SuppressWarnings("unchecked") + private List, Object>> getBatchWithValueActions( + List> actions, Class entityType, Class batchActionType) { - List> actions = new ArrayList<>(); - change.forEachAction(actions::add); - return actions; + return actions.stream() // + .filter(dbAction -> dbAction.getClass().equals(batchActionType)) // + .filter(dbAction -> dbAction.getEntityType().equals(entityType)) // + .map(dbAction -> (DbAction.BatchWithValue, Object>) dbAction).collect(Collectors.toList()); } @Value