From fbd070524bd2ba720c7a5a4031c8dfcc609a4afd Mon Sep 17 00:00:00 2001 From: Chirag Tailor Date: Tue, 29 Mar 2022 09:09:25 -0500 Subject: [PATCH 01/11] 537-batch-ops-across-aggregates - Prepare branch --- pom.xml | 2 +- spring-data-jdbc-distribution/pom.xml | 2 +- spring-data-jdbc/pom.xml | 4 ++-- spring-data-r2dbc/pom.xml | 4 ++-- spring-data-relational/pom.xml | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pom.xml b/pom.xml index dadda28174..35f041d86b 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-relational-parent - 3.0.0-SNAPSHOT + 3.0.0-537-batch-ops-across-aggregates-SNAPSHOT pom Spring Data Relational Parent diff --git a/spring-data-jdbc-distribution/pom.xml b/spring-data-jdbc-distribution/pom.xml index db3b7ddd1a..ba8d6a8859 100644 --- a/spring-data-jdbc-distribution/pom.xml +++ b/spring-data-jdbc-distribution/pom.xml @@ -14,7 +14,7 @@ org.springframework.data spring-data-relational-parent - 3.0.0-SNAPSHOT + 3.0.0-537-batch-ops-across-aggregates-SNAPSHOT ../pom.xml diff --git a/spring-data-jdbc/pom.xml b/spring-data-jdbc/pom.xml index 547ff62b8b..d17a4111a6 100644 --- a/spring-data-jdbc/pom.xml +++ b/spring-data-jdbc/pom.xml @@ -6,7 +6,7 @@ 4.0.0 spring-data-jdbc - 3.0.0-SNAPSHOT + 3.0.0-537-batch-ops-across-aggregates-SNAPSHOT Spring Data JDBC Spring Data module for JDBC repositories. @@ -15,7 +15,7 @@ org.springframework.data spring-data-relational-parent - 3.0.0-SNAPSHOT + 3.0.0-537-batch-ops-across-aggregates-SNAPSHOT diff --git a/spring-data-r2dbc/pom.xml b/spring-data-r2dbc/pom.xml index 22914016c3..99a1658ffa 100644 --- a/spring-data-r2dbc/pom.xml +++ b/spring-data-r2dbc/pom.xml @@ -6,7 +6,7 @@ 4.0.0 spring-data-r2dbc - 3.0.0-SNAPSHOT + 3.0.0-537-batch-ops-across-aggregates-SNAPSHOT Spring Data R2DBC Spring Data module for R2DBC @@ -15,7 +15,7 @@ org.springframework.data spring-data-relational-parent - 3.0.0-SNAPSHOT + 3.0.0-537-batch-ops-across-aggregates-SNAPSHOT diff --git a/spring-data-relational/pom.xml b/spring-data-relational/pom.xml index 717ac86edf..ea2f2d8b83 100644 --- a/spring-data-relational/pom.xml +++ b/spring-data-relational/pom.xml @@ -6,7 +6,7 @@ 4.0.0 spring-data-relational - 3.0.0-SNAPSHOT + 3.0.0-537-batch-ops-across-aggregates-SNAPSHOT Spring Data Relational Spring Data Relational support @@ -14,7 +14,7 @@ org.springframework.data spring-data-relational-parent - 3.0.0-SNAPSHOT + 3.0.0-537-batch-ops-across-aggregates-SNAPSHOT From 5ddb0fd5fae95ebb85696d2bdf3ffa27f8b56984 Mon Sep 17 00:00:00 2001 From: Chirag Tailor Date: Mon, 28 Mar 2022 15:29:32 -0500 Subject: [PATCH 02/11] Add SaveMergedAggregateChange which merges AggregateChangeWithRoot changes into one. --- .../data/jdbc/core/JdbcAggregateTemplate.java | 17 + .../conversion/MergedAggregateChange.java | 5 + .../conversion/MutableAggregateChange.java | 7 + .../conversion/SaveMergedAggregateChange.java | 92 ++++++ .../SaveMergedAggregateChangeTest.java | 301 ++++++++++++++++++ 5 files changed, 422 insertions(+) create mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/MergedAggregateChange.java create mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/SaveMergedAggregateChange.java create mode 100644 spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/SaveMergedAggregateChangeTest.java diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateTemplate.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateTemplate.java index 24c2de8689..21037a448d 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateTemplate.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateTemplate.java @@ -32,6 +32,7 @@ import org.springframework.data.mapping.callback.EntityCallbacks; import org.springframework.data.relational.core.conversion.AggregateChange; import org.springframework.data.relational.core.conversion.AggregateChangeWithRoot; +import org.springframework.data.relational.core.conversion.MergedAggregateChange; import org.springframework.data.relational.core.conversion.MutableAggregateChange; import org.springframework.data.relational.core.conversion.RelationalEntityDeleteWriter; import org.springframework.data.relational.core.conversion.RelationalEntityInsertWriter; @@ -44,6 +45,7 @@ import org.springframework.data.support.PageableExecutionUtils; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; /** * {@link JdbcAggregateOperations} implementation, storing aggregates in and obtaining them from a JDBC data store. @@ -148,6 +150,21 @@ public T save(T instance) { return store(instance, changeCreator, persistentEntity); } + private void saveAll(Iterable instances) { + + ArrayList instancesList = new ArrayList<>(); + instances.forEach(instancesList::add); + Assert.notEmpty(instancesList, "Aggregate instances must not be empty!"); + + //noinspection unchecked + MergedAggregateChange> mergedAggregateChange = instancesList.stream() + .map(MutableAggregateChange::forSave) + .reduce(MutableAggregateChange.mergedSave((Class) ClassUtils.getUserClass(instancesList.get(0))), + MergedAggregateChange::merge, + (left, right) -> right); + + } + /** * Dedicated insert function to do just the insert of an instance of an aggregate, including all the members of the * aggregate. diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/MergedAggregateChange.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/MergedAggregateChange.java new file mode 100644 index 0000000000..c64ec5e5c9 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/MergedAggregateChange.java @@ -0,0 +1,5 @@ +package org.springframework.data.relational.core.conversion; + +public interface MergedAggregateChange> extends AggregateChange { + MergedAggregateChange merge(C aggregateChange); +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/MutableAggregateChange.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/MutableAggregateChange.java index a76d17ab7a..0ab9f3fc44 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/MutableAggregateChange.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/MutableAggregateChange.java @@ -74,6 +74,13 @@ static MutableAggregateChange forDelete(T entity) { return forDelete((Class) ClassUtils.getUserClass(entity)); } + static MergedAggregateChange> mergedSave(Class entityClass) { + + Assert.notNull(entityClass, "Entity class must not be null"); + + return new SaveMergedAggregateChange<>(entityClass); + } + /** * Factory method to create an {@link MutableAggregateChange} for deleting entities. * diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/SaveMergedAggregateChange.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/SaveMergedAggregateChange.java new file mode 100644 index 0000000000..66147df614 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/SaveMergedAggregateChange.java @@ -0,0 +1,92 @@ +package org.springframework.data.relational.core.conversion; + +import static java.util.Collections.*; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +import org.springframework.data.mapping.PersistentPropertyPath; +import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; +import org.springframework.util.Assert; + +public class SaveMergedAggregateChange implements MergedAggregateChange> { + + private static final Comparator> pathLengthComparator = // + Comparator.comparing(PersistentPropertyPath::getLength); + + private final Class entityType; + private final List> rootActions = new ArrayList<>(); + private final Map, EnumMap>>> insertActions = // + new HashMap<>(); + private final Map, List>> deleteActions = // + new HashMap<>(); + + public SaveMergedAggregateChange(Class entityType) { + this.entityType = entityType; + } + + @Override + public Kind getKind() { + return Kind.SAVE; + } + + @Override + public Class getEntityType() { + return entityType; + } + + @Override + public void forEachAction(Consumer> consumer) { + + Assert.notNull(consumer, "Consumer must not be null."); + + rootActions.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) -> consumer.accept(new DbAction.InsertBatch<>(inserts, idValueSource)))); + } + + @Override + public MergedAggregateChange> merge(AggregateChangeWithRoot aggregateChange) { + aggregateChange.forEachAction(action -> { + if (action instanceof DbAction.WithRoot rootAction) { + rootActions.add(rootAction); + } else if (action instanceof DbAction.Insert) { + // noinspection unchecked + addInsert((DbAction.Insert) action); + } else if (action instanceof DbAction.Delete deleteAction) { + addDelete(deleteAction); + } + }); + return this; + } + + private void addInsert(DbAction.Insert action) { + PersistentPropertyPath propertyPath = action.getPropertyPath(); + insertActions.merge(propertyPath, + new EnumMap<>(singletonMap(action.getIdValueSource(), new ArrayList<>(singletonList(action)))), + (enumMap, enumMapDefaultValue) -> { + enumMap.merge(action.getIdValueSource(), new ArrayList<>(singletonList(action)), + (actions, listDefaultValue) -> { + actions.add(action); + return actions; + }); + return enumMap; + }); + } + + private void addDelete(DbAction.Delete action) { + PersistentPropertyPath propertyPath = action.getPropertyPath(); + deleteActions.merge(propertyPath, new ArrayList<>(singletonList(action)), (actions, defaultValue) -> { + actions.add(action); + return actions; + }); + } +} diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/SaveMergedAggregateChangeTest.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/SaveMergedAggregateChangeTest.java new file mode 100644 index 0000000000..45b3c53857 --- /dev/null +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/SaveMergedAggregateChangeTest.java @@ -0,0 +1,301 @@ +package org.springframework.data.relational.core.conversion; + +import static java.util.Collections.*; +import static org.assertj.core.api.Assertions.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import org.assertj.core.groups.Tuple; +import org.junit.jupiter.api.Test; +import org.springframework.data.annotation.Id; +import org.springframework.data.relational.core.mapping.RelationalMappingContext; + +import lombok.Value; + +class SaveMergedAggregateChangeTest { + + RelationalMappingContext context = new RelationalMappingContext(); + + @Test + void startsWithNoActions() { + + MergedAggregateChange> change = MutableAggregateChange.mergedSave(Root.class); + + assertThat(extractActions(change)).isEmpty(); + } + + @Test + void yieldsRootActions() { + + Root root1 = new Root(null, null); + DbAction.InsertRoot root1Insert = new DbAction.InsertRoot<>(root1, IdValueSource.GENERATED); + AggregateChangeWithRoot aggregateChange1 = MutableAggregateChange.forSave(root1); + aggregateChange1.setRootAction(root1Insert); + Root root2 = new Root(null, null); + DbAction.InsertRoot root2Insert = new DbAction.InsertRoot<>(root2, IdValueSource.GENERATED); + AggregateChangeWithRoot aggregateChange2 = MutableAggregateChange.forSave(root2); + aggregateChange2.setRootAction(root2Insert); + + MergedAggregateChange> change = // + MutableAggregateChange.mergedSave(Root.class) // + .merge(aggregateChange1) // + .merge(aggregateChange2); + + assertThat(extractActions(change)).containsExactly(root1Insert, root2Insert); + } + + @Test + void yieldsRootActionsBeforeDeleteActions() { + + Root root1 = new Root(null, null); + DbAction.UpdateRoot root1Update = new DbAction.UpdateRoot<>(root1, null); + AggregateChangeWithRoot aggregateChange1 = MutableAggregateChange.forSave(root1); + aggregateChange1.setRootAction(root1Update); + DbAction.Delete root1IntermediateDelete = new DbAction.Delete<>(1L, + context.getPersistentPropertyPath("intermediate", Root.class)); + aggregateChange1.addAction(root1IntermediateDelete); + Root root2 = new Root(null, null); + DbAction.InsertRoot root2Insert = new DbAction.InsertRoot<>(root2, IdValueSource.GENERATED); + AggregateChangeWithRoot aggregateChange2 = MutableAggregateChange.forSave(root2); + aggregateChange2.setRootAction(root2Insert); + + MergedAggregateChange> change = // + MutableAggregateChange.mergedSave(Root.class) // + .merge(aggregateChange1) // + .merge(aggregateChange2); + + assertThat(extractActions(change)).extracting(DbAction::getClass, DbAction::getEntityType).containsExactly( // + Tuple.tuple(DbAction.UpdateRoot.class, Root.class), // + Tuple.tuple(DbAction.InsertRoot.class, Root.class), // + Tuple.tuple(DbAction.Delete.class, Intermediate.class)); + } + + @Test + void yieldsNestedDeleteActionsInTreeOrderFromLeavesToRoot() { + + Root root1 = new Root(1L, null); + AggregateChangeWithRoot aggregateChange1 = MutableAggregateChange.forSave(root1); + aggregateChange1.setRootAction(new DbAction.UpdateRoot<>(root1, null)); + DbAction.Delete root1IntermediateDelete = new DbAction.Delete<>(1L, + context.getPersistentPropertyPath("intermediate", Root.class)); + aggregateChange1.addAction(root1IntermediateDelete); + + Root root2 = new Root(1L, null); + AggregateChangeWithRoot aggregateChange2 = MutableAggregateChange.forSave(root2); + aggregateChange2.setRootAction(new DbAction.UpdateRoot<>(root2, null)); + DbAction.Delete root2LeafDelete = new DbAction.Delete<>(1L, + context.getPersistentPropertyPath("intermediate.leaf", Root.class)); + aggregateChange2.addAction(root2LeafDelete); + DbAction.Delete root2IntermediateDelete = new DbAction.Delete<>(1L, + context.getPersistentPropertyPath("intermediate", Root.class)); + aggregateChange2.addAction(root2IntermediateDelete); + + MergedAggregateChange> change = // + MutableAggregateChange.mergedSave(Root.class) // + .merge(aggregateChange1) // + .merge(aggregateChange2); + + assertThat(extractActions(change)).containsSubsequence(root2LeafDelete, root1IntermediateDelete, + root2IntermediateDelete); + } + + @Test + void yieldsDeleteActionsBeforeInsertActions() { + + Root root1 = new Root(null, null); + DbAction.InsertRoot root1Insert = new DbAction.InsertRoot<>(root1, IdValueSource.GENERATED); + AggregateChangeWithRoot aggregateChange1 = MutableAggregateChange.forSave(root1); + aggregateChange1.setRootAction(root1Insert); + Intermediate root1Intermediate = new Intermediate(null, "root1Intermediate", null); + DbAction.Insert root1IntermediateInsert = new DbAction.Insert<>(root1Intermediate, + context.getPersistentPropertyPath("intermediate", Root.class), root1Insert, emptyMap(), + IdValueSource.GENERATED); + aggregateChange1.addAction(root1IntermediateInsert); + + Root root2 = new Root(1L, null); + DbAction.UpdateRoot root2Update = new DbAction.UpdateRoot<>(root2, null); + AggregateChangeWithRoot aggregateChange2 = MutableAggregateChange.forSave(root2); + aggregateChange2.setRootAction(root2Update); + DbAction.Delete root2IntermediateDelete = new DbAction.Delete<>(1L, + context.getPersistentPropertyPath("intermediate", Root.class)); + aggregateChange2.addAction(root2IntermediateDelete); + + MergedAggregateChange> change = // + MutableAggregateChange.mergedSave(Root.class) // + .merge(aggregateChange1) // + .merge(aggregateChange2); + + assertThat(extractActions(change)).extracting(DbAction::getClass, DbAction::getEntityType).containsSubsequence( // + Tuple.tuple(DbAction.Delete.class, Intermediate.class), // + Tuple.tuple(DbAction.InsertBatch.class, Intermediate.class)); + } + + @Test + void yieldsInsertActionsAsBatchInserts_groupedByIdValueSource() { + + Root root = new Root(null, null); + DbAction.InsertRoot rootInsert = new DbAction.InsertRoot<>(root, IdValueSource.GENERATED); + AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(root); + aggregateChange.setRootAction(rootInsert); + Intermediate intermediateGeneratedId = new Intermediate(null, "intermediateGeneratedId", null); + DbAction.Insert intermediateInsertGeneratedId = new DbAction.Insert<>(intermediateGeneratedId, + context.getPersistentPropertyPath("intermediate", Root.class), rootInsert, emptyMap(), IdValueSource.GENERATED); + aggregateChange.addAction(intermediateInsertGeneratedId); + Intermediate intermediateProvidedId = new Intermediate(123L, "intermediateProvidedId", null); + DbAction.Insert intermediateInsertProvidedId = new DbAction.Insert<>(intermediateProvidedId, + context.getPersistentPropertyPath("intermediate", Root.class), rootInsert, emptyMap(), IdValueSource.PROVIDED); + aggregateChange.addAction(intermediateInsertProvidedId); + + MergedAggregateChange> change = // + MutableAggregateChange.mergedSave(Root.class) // + .merge(aggregateChange); + + List> actions = extractActions(change); + assertThat(actions) + .extracting(DbAction::getClass, DbAction::getEntityType, DbActionTestSupport::insertIdValueSource) // + .containsSubsequence( // + Tuple.tuple(DbAction.InsertRoot.class, Root.class, IdValueSource.GENERATED), // + Tuple.tuple(DbAction.InsertBatch.class, Intermediate.class, IdValueSource.PROVIDED)) // + .containsSubsequence( // + Tuple.tuple(DbAction.InsertRoot.class, Root.class, IdValueSource.GENERATED), // + Tuple.tuple(DbAction.InsertBatch.class, Intermediate.class, IdValueSource.GENERATED)) // + .doesNotContain(Tuple.tuple(DbAction.Insert.class, Intermediate.class)); + assertThat(getInsertBatchAction(actions, Intermediate.class, IdValueSource.GENERATED).getInserts()) + .containsExactly(intermediateInsertGeneratedId); + assertThat(getInsertBatchAction(actions, Intermediate.class, IdValueSource.PROVIDED).getInserts()) + .containsExactly(intermediateInsertProvidedId); + } + + @Test + void yieldsNestedInsertActionsInTreeOrderFromRootToLeaves() { + + Root root1 = new Root(null, null); + DbAction.InsertRoot root1Insert = new DbAction.InsertRoot<>(root1, IdValueSource.GENERATED); + AggregateChangeWithRoot aggregateChange1 = MutableAggregateChange.forSave(root1); + aggregateChange1.setRootAction(root1Insert); + Intermediate root1Intermediate = new Intermediate(null, "root1Intermediate", null); + DbAction.Insert root1IntermediateInsert = new DbAction.Insert<>(root1Intermediate, + context.getPersistentPropertyPath("intermediate", Root.class), root1Insert, emptyMap(), + IdValueSource.GENERATED); + aggregateChange1.addAction(root1IntermediateInsert); + Leaf root1Leaf = new Leaf(null, "root1Leaf"); + DbAction.Insert root1LeafInsert = new DbAction.Insert<>(root1Leaf, + context.getPersistentPropertyPath("intermediate.leaf", Root.class), root1IntermediateInsert, emptyMap(), + IdValueSource.GENERATED); + aggregateChange1.addAction(root1LeafInsert); + + Root root2 = new Root(null, null); + DbAction.InsertRoot root2Insert = new DbAction.InsertRoot<>(root2, IdValueSource.GENERATED); + AggregateChangeWithRoot aggregateChange2 = MutableAggregateChange.forSave(root2); + aggregateChange2.setRootAction(root2Insert); + Intermediate root2Intermediate = new Intermediate(null, "root2Intermediate", null); + DbAction.Insert root2IntermediateInsert = new DbAction.Insert<>(root2Intermediate, + context.getPersistentPropertyPath("intermediate", Root.class), root2Insert, emptyMap(), + IdValueSource.GENERATED); + aggregateChange2.addAction(root2IntermediateInsert); + + MergedAggregateChange> change = // + MutableAggregateChange.mergedSave(Root.class) // + .merge(aggregateChange1) // + .merge(aggregateChange2); + + List> actions = extractActions(change); + assertThat(actions) + .extracting(DbAction::getClass, DbAction::getEntityType, DbActionTestSupport::insertIdValueSource) + .containsSubsequence( // + Tuple.tuple(DbAction.InsertBatch.class, Intermediate.class, IdValueSource.GENERATED), + Tuple.tuple(DbAction.InsertBatch.class, Leaf.class, IdValueSource.GENERATED)); + assertThat(getInsertBatchAction(actions, Intermediate.class).getInserts()) // + .containsExactly(root1IntermediateInsert, root2IntermediateInsert); + assertThat(getInsertBatchAction(actions, Leaf.class).getInserts()) // + .containsExactly(root1LeafInsert); + } + + @Test + void yieldsInsertsWithSameLengthReferences_asSeparateInserts() { + + RootWithSameLengthReferences root = new RootWithSameLengthReferences(null, null, null); + DbAction.InsertRoot rootInsert = new DbAction.InsertRoot<>(root, IdValueSource.GENERATED); + AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(root); + aggregateChange.setRootAction(rootInsert); + Intermediate one = new Intermediate(null, "one", null); + DbAction.Insert oneInsert = new DbAction.Insert<>(one, + context.getPersistentPropertyPath("one", RootWithSameLengthReferences.class), rootInsert, emptyMap(), IdValueSource.GENERATED); + aggregateChange.addAction(oneInsert); + Intermediate two = new Intermediate(null, "two", null); + DbAction.Insert twoInsert = new DbAction.Insert<>(two, + context.getPersistentPropertyPath("two", RootWithSameLengthReferences.class), rootInsert, emptyMap(), IdValueSource.GENERATED); + aggregateChange.addAction(twoInsert); + + MergedAggregateChange> change = // + MutableAggregateChange.mergedSave(RootWithSameLengthReferences.class) // + .merge(aggregateChange); + + List> actions = extractActions(change); + assertThat(actions) + .extracting(DbAction::getClass, DbAction::getEntityType, DbActionTestSupport::insertIdValueSource) + .containsSubsequence( // + Tuple.tuple(DbAction.InsertBatch.class, Intermediate.class, IdValueSource.GENERATED), + Tuple.tuple(DbAction.InsertBatch.class, Intermediate.class, IdValueSource.GENERATED)); + List> insertBatchActions = getInsertBatchActions(actions, Intermediate.class); + assertThat(insertBatchActions).hasSize(2); + assertThat(insertBatchActions.get(0).getInserts()).containsExactly(oneInsert); + assertThat(insertBatchActions.get(1).getInserts()).containsExactly(twoInsert); + } + + private DbAction.InsertBatch getInsertBatchAction(List> actions, Class entityType, + IdValueSource idValueSource) { + return getInsertBatchActions(actions, entityType).stream() + .filter(insertBatch -> insertBatch.getIdValueSource() == idValueSource).findFirst().orElseThrow( + () -> new RuntimeException(String.format("No InsertBatch with includeId '%s' found!", idValueSource))); + } + + private DbAction.InsertBatch getInsertBatchAction(List> actions, Class entityType) { + return getInsertBatchActions(actions, entityType).stream().findFirst() + .orElseThrow(() -> new RuntimeException("No InsertBatch action found!")); + } + + @SuppressWarnings("unchecked") + private List> getInsertBatchActions(List> actions, Class entityType) { + + return actions.stream() // + .filter(dbAction -> dbAction instanceof DbAction.InsertBatch) // + .filter(dbAction -> dbAction.getEntityType().equals(entityType)) // + .map(dbAction -> (DbAction.InsertBatch) dbAction).collect(Collectors.toList()); + } + + private List> extractActions(MergedAggregateChange> change) { + + List> actions = new ArrayList<>(); + change.forEachAction(actions::add); + return actions; + } + + @Value + static class RootWithSameLengthReferences { + @Id Long id; + Intermediate one; + Intermediate two; + } + + @Value + static class Root { + @Id Long id; + Intermediate intermediate; + } + + @Value + static class Intermediate { + @Id Long id; + String name; + Leaf leaf; + } + + @Value + static class Leaf { + @Id Long id; + String name; + } +} \ No newline at end of file From a337849d45675b433869dbc60b71fca6aab03801 Mon Sep 17 00:00:00 2001 From: Chirag Tailor Date: Wed, 30 Mar 2022 11:42:21 -0500 Subject: [PATCH 03/11] Remove behavior from WritingContext for creating InsertBatch in favor of SaveMergedAggregateChange. --- .../core/conversion/WritingContext.java | 11 +- .../RelationalEntityWriterUnitTests.java | 171 ++---------------- 2 files changed, 13 insertions(+), 169 deletions(-) diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/WritingContext.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/WritingContext.java index a453d279fb..078c21822e 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/WritingContext.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/WritingContext.java @@ -22,7 +22,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.mapping.PersistentPropertyPaths; @@ -149,15 +148,7 @@ private List> insertAll(PersistentPropertyPath (!entry.getValue().isEmpty())).map(entry -> { - - List> batch = entry.getValue(); - if (batch.size() > 1) { - return new DbAction.InsertBatch<>(batch, entry.getKey()); - } - return batch.get(0); - }).collect(Collectors.toList()); + return inserts; } private List> deleteReferenced() { 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 8405479aac..a29bb27e07 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 @@ -26,9 +26,7 @@ import java.util.List; import java.util.Map; import java.util.Set; -import java.util.stream.Collectors; -import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.junit.jupiter.MockitoExtension; @@ -37,7 +35,6 @@ import org.springframework.data.mapping.PersistentPropertyPaths; import org.springframework.data.relational.core.conversion.DbAction.Delete; import org.springframework.data.relational.core.conversion.DbAction.Insert; -import org.springframework.data.relational.core.conversion.DbAction.InsertBatch; import org.springframework.data.relational.core.conversion.DbAction.InsertRoot; import org.springframework.data.relational.core.conversion.DbAction.UpdateRoot; import org.springframework.data.relational.core.mapping.Embedded; @@ -284,7 +281,7 @@ public void newEntityWithEmptySetResultsInSingleInsert() { } @Test // DATAJDBC-113 - public void newEntityWithSetContainingMultipleElementsResultsInAnInsertForTheBatch() { + public void newEntityWithSetContainingMultipleElementsResultsInAnInsertForEach() { SetContainer entity = new SetContainer(null); entity.elements.add(new Element(null)); @@ -302,16 +299,6 @@ public void newEntityWithSetContainingMultipleElementsResultsInAnInsertForTheBat DbActionTestSupport::insertIdValueSource) // .containsExactly( // tuple(InsertRoot.class, SetContainer.class, "", SetContainer.class, false, IdValueSource.GENERATED), // - tuple(InsertBatch.class, Element.class, "", null, false, IdValueSource.GENERATED) // - ); - List> batchedInsertActions = getInsertBatchAction(actions, Element.class).getInserts(); - assertThat(batchedInsertActions).extracting(DbAction::getClass, // - DbAction::getEntityType, // - DbActionTestSupport::extractPath, // - DbActionTestSupport::actualEntityType, // - DbActionTestSupport::isWithDependsOn, // - DbActionTestSupport::insertIdValueSource) // - .containsExactly( // tuple(Insert.class, Element.class, "elements", Element.class, true, IdValueSource.GENERATED), // tuple(Insert.class, Element.class, "elements", Element.class, true, IdValueSource.GENERATED) // ); @@ -346,31 +333,10 @@ public void cascadingReferencesTriggerCascadingActions() { .containsExactly( // tuple(InsertRoot.class, CascadingReferenceEntity.class, "", CascadingReferenceEntity.class, false, IdValueSource.GENERATED), // - tuple(InsertBatch.class, CascadingReferenceMiddleElement.class, "", null, false, IdValueSource.GENERATED), // - tuple(InsertBatch.class, Element.class, "", null, false, IdValueSource.GENERATED) // - ); - List> middleElementInserts = getInsertBatchAction(actions, - CascadingReferenceMiddleElement.class).getInserts(); - assertThat(middleElementInserts).extracting(DbAction::getClass, // - DbAction::getEntityType, // - DbActionTestSupport::extractPath, // - DbActionTestSupport::actualEntityType, // - DbActionTestSupport::isWithDependsOn, // - DbActionTestSupport::insertIdValueSource) // - .containsExactly( // tuple(Insert.class, CascadingReferenceMiddleElement.class, "other", CascadingReferenceMiddleElement.class, true, IdValueSource.GENERATED), // tuple(Insert.class, CascadingReferenceMiddleElement.class, "other", CascadingReferenceMiddleElement.class, - true, IdValueSource.GENERATED) // - ); - List> leafElementInserts = getInsertBatchAction(actions, Element.class).getInserts(); - assertThat(leafElementInserts).extracting(DbAction::getClass, // - DbAction::getEntityType, // - DbActionTestSupport::extractPath, // - DbActionTestSupport::actualEntityType, // - DbActionTestSupport::isWithDependsOn, // - DbActionTestSupport::insertIdValueSource) // - .containsExactly( // + true, IdValueSource.GENERATED), // tuple(Insert.class, Element.class, "other.element", Element.class, true, IdValueSource.GENERATED), // tuple(Insert.class, Element.class, "other.element", Element.class, true, IdValueSource.GENERATED), // tuple(Insert.class, Element.class, "other.element", Element.class, true, IdValueSource.GENERATED), // @@ -408,31 +374,10 @@ public void cascadingReferencesTriggerCascadingActionsForUpdate() { tuple(UpdateRoot.class, CascadingReferenceEntity.class, "", CascadingReferenceEntity.class, false, null), // tuple(Delete.class, Element.class, "other.element", null, false, null), tuple(Delete.class, CascadingReferenceMiddleElement.class, "other", null, false, null), - tuple(InsertBatch.class, CascadingReferenceMiddleElement.class, "", null, false, IdValueSource.GENERATED), // - tuple(InsertBatch.class, Element.class, "", null, false, IdValueSource.GENERATED) // - ); - List> middleElementInserts = getInsertBatchAction(actions, - CascadingReferenceMiddleElement.class).getInserts(); - assertThat(middleElementInserts).extracting(DbAction::getClass, // - DbAction::getEntityType, // - DbActionTestSupport::extractPath, // - DbActionTestSupport::actualEntityType, // - DbActionTestSupport::isWithDependsOn, // - DbActionTestSupport::insertIdValueSource) // - .containsExactly( // tuple(Insert.class, CascadingReferenceMiddleElement.class, "other", CascadingReferenceMiddleElement.class, true, IdValueSource.GENERATED), // tuple(Insert.class, CascadingReferenceMiddleElement.class, "other", CascadingReferenceMiddleElement.class, - true, IdValueSource.GENERATED) // - ); - List> elementInserts = getInsertBatchAction(actions, Element.class).getInserts(); - assertThat(elementInserts).extracting(DbAction::getClass, // - DbAction::getEntityType, // - DbActionTestSupport::extractPath, // - DbActionTestSupport::actualEntityType, // - DbActionTestSupport::isWithDependsOn, // - DbActionTestSupport::insertIdValueSource) // - .containsExactly( // + true, IdValueSource.GENERATED), // tuple(Insert.class, Element.class, "other.element", Element.class, true, IdValueSource.GENERATED), // tuple(Insert.class, Element.class, "other.element", Element.class, true, IdValueSource.GENERATED), // tuple(Insert.class, Element.class, "other.element", Element.class, true, IdValueSource.GENERATED), // @@ -468,23 +413,20 @@ public void newEntityWithMapResultsInAdditionalInsertPerElement() { List> actions = extractActions(aggregateChange); assertThat(actions).extracting(DbAction::getClass, // - DbAction::getEntityType, // - this::getMapKey, // - DbActionTestSupport::extractPath, // - DbActionTestSupport::insertIdValueSource) // - .containsExactly( // - tuple(InsertRoot.class, MapContainer.class, null, "", IdValueSource.GENERATED), // - tuple(InsertBatch.class, Element.class, null, "", IdValueSource.GENERATED) // - ); - List> inserts = getInsertBatchAction(actions, Element.class).getInserts(); - assertThat(inserts).extracting(DbAction::getClass, // DbAction::getEntityType, // this::getMapKey, // DbActionTestSupport::extractPath, // DbActionTestSupport::insertIdValueSource) // .containsExactlyInAnyOrder( // + tuple(InsertRoot.class, MapContainer.class, null, "", IdValueSource.GENERATED), // tuple(Insert.class, Element.class, "one", "elements", IdValueSource.GENERATED), // tuple(Insert.class, Element.class, "two", "elements", IdValueSource.GENERATED) // + ).containsSubsequence( // container comes before the elements + tuple(InsertRoot.class, MapContainer.class, null, "", IdValueSource.GENERATED), // + tuple(Insert.class, Element.class, "two", "elements", IdValueSource.GENERATED) // + ).containsSubsequence( // container comes before the elements + tuple(InsertRoot.class, MapContainer.class, null, "", IdValueSource.GENERATED), // + tuple(Insert.class, Element.class, "one", "elements", IdValueSource.GENERATED) // ); } @@ -511,21 +453,12 @@ public void newEntityWithFullMapResultsInAdditionalInsertPerElement() { List> actions = extractActions(aggregateChange); assertThat(actions).extracting(DbAction::getClass, // - DbAction::getEntityType, // - this::getMapKey, // - DbActionTestSupport::extractPath, // - DbActionTestSupport::insertIdValueSource) // - .containsExactly( // - tuple(InsertRoot.class, MapContainer.class, null, "", IdValueSource.GENERATED), // - tuple(InsertBatch.class, Element.class, null, "", IdValueSource.GENERATED) // - ); - List> inserts = getInsertBatchAction(actions, Element.class).getInserts(); - assertThat(inserts).extracting(DbAction::getClass, // DbAction::getEntityType, // this::getMapKey, // DbActionTestSupport::extractPath, // DbActionTestSupport::insertIdValueSource) // .containsExactlyInAnyOrder( // + tuple(InsertRoot.class, MapContainer.class, null, "", IdValueSource.GENERATED), // tuple(Insert.class, Element.class, "1", "elements", IdValueSource.GENERATED), // tuple(Insert.class, Element.class, "2", "elements", IdValueSource.GENERATED), // tuple(Insert.class, Element.class, "3", "elements", IdValueSource.GENERATED), // @@ -575,15 +508,6 @@ public void newEntityWithListResultsInAdditionalInsertPerElement() { DbActionTestSupport::insertIdValueSource) // .containsExactly( // tuple(InsertRoot.class, ListContainer.class, null, "", IdValueSource.GENERATED), // - tuple(InsertBatch.class, Element.class, null, "", IdValueSource.GENERATED) // - ); - List> inserts = getInsertBatchAction(actions, Element.class).getInserts(); - assertThat(inserts).extracting(DbAction::getClass, // - DbAction::getEntityType, // - this::getListKey, // - DbActionTestSupport::extractPath, // - DbActionTestSupport::insertIdValueSource) // - .containsExactly( // tuple(Insert.class, Element.class, 0, "elements", IdValueSource.GENERATED), // tuple(Insert.class, Element.class, 1, "elements", IdValueSource.GENERATED) // ); @@ -741,55 +665,7 @@ public void savingInnerNullEmbeddedWithEntity() { } @Test - void newEntityWithCollectionWhereSomeElementsHaveIdSet_producesABatchInsertEachForElementsWithIdAndWithout() { - - ListContainer root = new ListContainer(null); - root.elements.add(new Element(null)); - root.elements.add(new Element(1L)); - root.elements.add(new Element(null)); - root.elements.add(new Element(2L)); - AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(root); - - new RelationalEntityWriter(context).write(root, aggregateChange); - - List> actions = extractActions(aggregateChange); - assertThat(actions).extracting(DbAction::getClass, // - DbAction::getEntityType, // - DbActionTestSupport::extractPath, // - DbActionTestSupport::actualEntityType, // - DbActionTestSupport::isWithDependsOn, // - DbActionTestSupport::insertIdValueSource) // - .containsSubsequence( - tuple(InsertRoot.class, ListContainer.class, "", ListContainer.class, false, IdValueSource.GENERATED), // - tuple(InsertBatch.class, Element.class, "", null, false, IdValueSource.PROVIDED) // - ).containsSubsequence( // - tuple(InsertRoot.class, ListContainer.class, "", ListContainer.class, false, IdValueSource.GENERATED), // - tuple(InsertBatch.class, Element.class, "", null, false, IdValueSource.GENERATED) // - ); - InsertBatch insertBatchWithoutId = getInsertBatchAction(actions, Element.class, IdValueSource.GENERATED); - assertThat(insertBatchWithoutId.getInserts()).extracting(DbAction::getClass, // - DbAction::getEntityType, // - this::getListKey, // - DbActionTestSupport::extractPath, // - DbActionTestSupport::insertIdValueSource) // - .containsExactly( // - tuple(Insert.class, Element.class, 0, "elements", IdValueSource.GENERATED), // - tuple(Insert.class, Element.class, 2, "elements", IdValueSource.GENERATED) // - ); - InsertBatch insertBatchWithId = getInsertBatchAction(actions, Element.class, IdValueSource.PROVIDED); - assertThat(insertBatchWithId.getInserts()).extracting(DbAction::getClass, // - DbAction::getEntityType, // - this::getListKey, // - DbActionTestSupport::extractPath, // - DbActionTestSupport::insertIdValueSource) // - .containsExactly( // - tuple(Insert.class, Element.class, 1, "elements", IdValueSource.PROVIDED), // - tuple(Insert.class, Element.class, 3, "elements", IdValueSource.PROVIDED) // - ); - } - - @Test - void newEntityWithCollection_whenElementHasPrimitiveId_batchInsertDoesNotIncludeId_whenIdValueIsZero() { + void newEntityWithCollection_whenElementHasPrimitiveId_doesNotIncludeId_whenIdValueIsZero() { EntityWithReferencesToPrimitiveIdEntity entity = new EntityWithReferencesToPrimitiveIdEntity(null); entity.primitiveLongIdEntities.add(new PrimitiveLongIdEntity()); @@ -824,29 +700,6 @@ private List> extractActions(MutableAggregateChange aggregateChan return actions; } - @NotNull - private InsertBatch getInsertBatchAction(List> actions, Class entityType) { - return getInsertBatchActions(actions, entityType).stream().findFirst() - .orElseThrow(() -> new RuntimeException("No InsertBatch action found!")); - } - - @NotNull - private InsertBatch getInsertBatchAction(List> actions, Class entityType, - IdValueSource idValueSource) { - return getInsertBatchActions(actions, entityType).stream() - .filter(insertBatch -> insertBatch.getIdValueSource() == idValueSource).findFirst().orElseThrow( - () -> new RuntimeException(String.format("No InsertBatch with includeId '%s' found!", idValueSource))); - } - - @NotNull - private List> getInsertBatchActions(List> actions, Class entityType) { - // noinspection unchecked - return actions.stream() // - .filter(dbAction -> dbAction instanceof InsertBatch) // - .filter(dbAction -> dbAction.getEntityType().equals(entityType)) // - .map(dbAction -> (InsertBatch) dbAction).collect(Collectors.toList()); - } - private CascadingReferenceMiddleElement createMiddleElement(Element first, Element second) { CascadingReferenceMiddleElement middleElement1 = new CascadingReferenceMiddleElement(null); From ba8a54abbc1cdafe382f2b365c4998119b085242 Mon Sep 17 00:00:00 2001 From: Chirag Tailor Date: Tue, 29 Mar 2022 11:01:25 -0500 Subject: [PATCH 04/11] Update all save paths to use SaveMergedAggregateChange. + Update #populateIdsIfNecessary return type from T to List --- .../jdbc/core/AggregateChangeExecutor.java | 15 +- .../JdbcAggregateChangeExecutionContext.java | 18 +- .../jdbc/core/JdbcAggregateOperations.java | 2 + .../data/jdbc/core/JdbcAggregateTemplate.java | 98 ++++++---- .../support/SimpleJdbcRepository.java | 5 +- ...eChangeIdGenerationImmutableUnitTests.java | 36 ++-- .../AggregateChangeIdGenerationUnitTests.java | 20 +- ...angeExecutorContextImmutableUnitTests.java | 52 +++++- ...gregateChangeExecutorContextUnitTests.java | 73 +++++--- .../JdbcRepositoryIntegrationTests.java | 16 +- ...RepositorySaveAllHsqlIntegrationTests.java | 173 ++++++++++++++++++ .../SimpleJdbcRepositoryEventsUnitTests.java | 18 +- ...sitorySaveAllHsqlIntegrationTests-hsql.sql | 21 +++ 13 files changed, 431 insertions(+), 116 deletions(-) create mode 100644 spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositorySaveAllHsqlIntegrationTests.java create mode 100644 spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositorySaveAllHsqlIntegrationTests-hsql.sql 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 d3947e3ba8..0e0997f806 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 @@ -18,11 +18,12 @@ import org.springframework.data.jdbc.core.convert.DataAccessStrategy; import org.springframework.data.jdbc.core.convert.JdbcConverter; import org.springframework.data.relational.core.conversion.AggregateChange; -import org.springframework.data.relational.core.conversion.AggregateChangeWithRoot; import org.springframework.data.relational.core.conversion.DbAction; import org.springframework.data.relational.core.conversion.DbActionExecutionException; import org.springframework.data.relational.core.conversion.MutableAggregateChange; +import java.util.List; + /** * Executes an {@link MutableAggregateChange}. * @@ -43,15 +44,15 @@ class AggregateChangeExecutor { } /** - * Execute an aggregate change which has a root entity. It returns the root entity, with all changes that might apply. - * This might be the original instance or a new instance, depending on its mutability. + * Execute a save aggregate change. It returns the resulting root entities, with all changes that might apply. This + * might be the original instances or new instances, depending on their mutability. * * @param aggregateChange the aggregate change to be executed. Must not be {@literal null}. * @param the type of the aggregate root. - * @return the potentially modified aggregate root. Guaranteed to be not {@literal null}. + * @return the aggregate roots resulting from the change, if there are any. May be empty. * @since 3.0 */ - T execute(AggregateChangeWithRoot aggregateChange) { + List executeSave(AggregateChange aggregateChange) { JdbcAggregateChangeExecutionContext executionContext = new JdbcAggregateChangeExecutionContext(converter, accessStrategy); @@ -62,13 +63,13 @@ T execute(AggregateChangeWithRoot aggregateChange) { } /** - * Execute an aggregate change without a root entity. + * Execute a delete aggregate change. * * @param aggregateChange the aggregate change to be executed. Must not be {@literal null}. * @param the type of the aggregate root. * @since 3.0 */ - void execute(AggregateChange aggregateChange) { + void executeDelete(AggregateChange aggregateChange) { JdbcAggregateChangeExecutionContext executionContext = new JdbcAggregateChangeExecutionContext(converter, accessStrategy); 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 4b8ab85700..23d4f2f2d4 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 @@ -216,7 +216,7 @@ private Object getIdFrom(DbAction.WithEntity idOwningAction) { return identifier; } - T populateIdsIfNecessary() { + List populateIdsIfNecessary() { // have the results so that the inserts on the leaves come first. List reverseResults = new ArrayList<>(results.values()); @@ -224,6 +224,8 @@ T populateIdsIfNecessary() { StagedValues cascadingValues = new StagedValues(); + List roots = new ArrayList<>(); + for (DbActionExecutionResult result : reverseResults) { DbAction.WithEntity action = result.getAction(); @@ -232,7 +234,7 @@ T populateIdsIfNecessary() { if (action instanceof DbAction.InsertRoot || action instanceof DbAction.UpdateRoot) { // noinspection unchecked - return (T) newEntity; + roots.add((T) newEntity); } // the id property was immutable so we have to propagate changes up the tree @@ -246,9 +248,15 @@ T populateIdsIfNecessary() { } } - throw new IllegalStateException( - String.format("Cannot retrieve the resulting instance unless a %s or %s action was successfully executed.", - DbAction.InsertRoot.class.getName(), DbAction.UpdateRoot.class.getName())); + if (roots.isEmpty()) { + throw new IllegalStateException( + String.format("Cannot retrieve the resulting instance(s) unless a %s or %s action was successfully executed.", + DbAction.InsertRoot.class.getName(), DbAction.UpdateRoot.class.getName())); + } + + Collections.reverse(roots); + + return roots; } private Object setIdAndCascadingProperties(DbAction.WithEntity action, @Nullable Object generatedId, diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateOperations.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateOperations.java index 73cc729525..bd995ad2f5 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateOperations.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateOperations.java @@ -38,6 +38,8 @@ public interface JdbcAggregateOperations { */ T save(T instance); + Iterable saveAll(Iterable instances); + /** * Dedicated insert function. This skips the test if the aggregate root is new and makes an insert. *

diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateTemplate.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateTemplate.java index 21037a448d..110b573e2e 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateTemplate.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateTemplate.java @@ -47,6 +47,8 @@ import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import static java.util.Collections.*; + /** * {@link JdbcAggregateOperations} implementation, storing aggregates in and obtaining them from a JDBC data store. * @@ -141,28 +143,14 @@ public T save(T instance) { Assert.notNull(instance, "Aggregate instance must not be null!"); - RelationalPersistentEntity persistentEntity = context.getRequiredPersistentEntity(instance.getClass()); - - Function> changeCreator = persistentEntity.isNew(instance) - ? entity -> createInsertChange(prepareVersionForInsert(entity)) - : entity -> createUpdateChange(prepareVersionForUpdate(entity)); - - return store(instance, changeCreator, persistentEntity); + return StreamSupport.stream(saveAll(singletonList(instance)).spliterator(), false).findFirst() + .orElseThrow(() -> new IllegalStateException( + String.format("Unable to retrieve the result of executing aggregate change for instance: %s", instance))); } - private void saveAll(Iterable instances) { - - ArrayList instancesList = new ArrayList<>(); - instances.forEach(instancesList::add); - Assert.notEmpty(instancesList, "Aggregate instances must not be empty!"); - - //noinspection unchecked - MergedAggregateChange> mergedAggregateChange = instancesList.stream() - .map(MutableAggregateChange::forSave) - .reduce(MutableAggregateChange.mergedSave((Class) ClassUtils.getUserClass(instancesList.get(0))), - MergedAggregateChange::merge, - (left, right) -> right); - + @Override + public Iterable saveAll(Iterable instances) { + return performSaveChange(instances, this::changeCreatorSelectorForSave); } /** @@ -177,9 +165,9 @@ public T insert(T instance) { Assert.notNull(instance, "Aggregate instance must not be null!"); - RelationalPersistentEntity persistentEntity = context.getRequiredPersistentEntity(instance.getClass()); - - return store(instance, entity -> createInsertChange(prepareVersionForInsert(entity)), persistentEntity); + return performSaveChange(singletonList(instance), this::changeCreatorSelectorForInsert).stream().findFirst() + .orElseThrow(() -> new IllegalStateException( + String.format("Unable to retrieve the result of executing aggregate change for instance: %s", instance))); } /** @@ -194,9 +182,9 @@ public T update(T instance) { Assert.notNull(instance, "Aggregate instance must not be null!"); - RelationalPersistentEntity persistentEntity = context.getRequiredPersistentEntity(instance.getClass()); - - return store(instance, entity -> createUpdateChange(prepareVersionForUpdate(entity)), persistentEntity); + return performSaveChange(singletonList(instance), this::changeCreatorSelectorForUpdate).stream().findFirst() + .orElseThrow(() -> new IllegalStateException( + String.format("Unable to retrieve the result of executing aggregate change for instance: %s", instance))); } @Override @@ -295,12 +283,20 @@ public void deleteAll(Class domainType) { Assert.notNull(domainType, "Domain type must not be null!"); MutableAggregateChange change = createDeletingChange(domainType); - executor.execute(change); + executor.executeDelete(change); } - private T store(T aggregateRoot, Function> changeCreator, - RelationalPersistentEntity persistentEntity) { + private T afterExecute(AggregateChange change, T entityAfterExecution) { + Object identifier = context.getRequiredPersistentEntity(change.getEntityType()) + .getIdentifierAccessor(entityAfterExecution).getIdentifier(); + Assert.notNull(identifier, "After saving the identifier must not be null!"); + + return triggerAfterSave(entityAfterExecution, change); + } + + private AggregateChangeWithRoot beforeExecute(T aggregateRoot, + Function> changeCreator) { Assert.notNull(aggregateRoot, "Aggregate instance must not be null!"); aggregateRoot = triggerBeforeConvert(aggregateRoot); @@ -310,14 +306,7 @@ private T store(T aggregateRoot, Function> cha aggregateRoot = triggerBeforeSave(change.getRoot(), change); change.setRoot(aggregateRoot); - - T entityAfterExecution = executor.execute(change); - - Object identifier = persistentEntity.getIdentifierAccessor(entityAfterExecution).getIdentifier(); - - Assert.notNull(identifier, "After saving the identifier must not be null!"); - - return triggerAfterSave(entityAfterExecution, change); + return change; } private void deleteTree(Object id, @Nullable T entity, Class domainType) { @@ -326,11 +315,41 @@ private void deleteTree(Object id, @Nullable T entity, Class domainType) entity = triggerBeforeDelete(entity, id, change); - executor.execute(change); + executor.executeDelete(change); triggerAfterDelete(entity, id, change); } + private List performSaveChange(Iterable instances, Function>> changeCreatorSelector) { + ArrayList instancesList = new ArrayList<>(); + instances.forEach(instancesList::add); + Assert.notEmpty(instancesList, "Aggregate instances must not be empty!"); + + // noinspection unchecked + MergedAggregateChange> mergedAggregateChange = instancesList.stream() // + .map(instance -> beforeExecute(instance, changeCreatorSelector.apply(instance))) // + .reduce(MutableAggregateChange.mergedSave((Class) ClassUtils.getUserClass(instancesList.get(0))), + MergedAggregateChange::merge, (left, right) -> right); + + return executor.executeSave(mergedAggregateChange).stream() + .map(entityAfterExecution -> afterExecute(mergedAggregateChange, entityAfterExecution)) + .collect(Collectors.toList()); + } + + private Function> changeCreatorSelectorForSave(T instance) { + return context.getRequiredPersistentEntity(instance.getClass()).isNew(instance) + ? changeCreatorSelectorForInsert(instance) + : changeCreatorSelectorForUpdate(instance); + } + + private Function> changeCreatorSelectorForInsert(T instance) { + return entity -> createInsertChange(prepareVersionForInsert(entity)); + } + + private Function> changeCreatorSelectorForUpdate(T instance) { + return entity -> createUpdateChange(prepareVersionForUpdate(entity)); + } + private AggregateChangeWithRoot createInsertChange(T instance) { AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(instance); @@ -342,7 +361,8 @@ private AggregateChangeWithRoot createUpdateChange(EntityAndPreviousVersi AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(entityAndVersion.entity, entityAndVersion.version); - new RelationalEntityUpdateWriter(context).write(entityAndVersion.entity, aggregateChange); + new RelationalEntityUpdateWriter(context).write(entityAndVersion.entity, + aggregateChange); return aggregateChange; } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/SimpleJdbcRepository.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/SimpleJdbcRepository.java index f67679d249..e7c70e5d74 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/SimpleJdbcRepository.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/SimpleJdbcRepository.java @@ -60,10 +60,7 @@ public S save(S instance) { @Transactional @Override public Iterable saveAll(Iterable entities) { - - return Streamable.of(entities).stream() // - .map(this::save) // - .collect(Collectors.toList()); + return entityOperations.saveAll(entities); } @Override diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AggregateChangeIdGenerationImmutableUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AggregateChangeIdGenerationImmutableUnitTests.java index 3a92cca271..c05601698a 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AggregateChangeIdGenerationImmutableUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AggregateChangeIdGenerationImmutableUnitTests.java @@ -83,7 +83,8 @@ public void singleRoot() { AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(entity); aggregateChange.setRootAction(rootInsert); - entity = executor.execute(aggregateChange); + List result = executor.executeSave(aggregateChange); + entity = result.get(0); assertThat(entity.rootId).isEqualTo(1); } @@ -97,7 +98,8 @@ public void simpleReference() { aggregateChange.setRootAction(rootInsert); aggregateChange.addAction(createInsert("single", content, null)); - entity = executor.execute(aggregateChange); + List result = executor.executeSave(aggregateChange); + entity = result.get(0); assertSoftly(softly -> { @@ -116,7 +118,8 @@ public void listReference() { aggregateChange.addAction(createInsert("contentList", content, 0)); aggregateChange.addAction(createInsert("contentList", content2, 1)); - entity = executor.execute(aggregateChange); + List result = executor.executeSave(aggregateChange); + entity = result.get(0); assertSoftly(softly -> { @@ -135,7 +138,8 @@ public void mapReference() { aggregateChange.addAction(createInsert("contentMap", content, "a")); aggregateChange.addAction(createInsert("contentMap", content2, "b")); - entity = executor.execute(aggregateChange); + List result = executor.executeSave(aggregateChange); + entity = result.get(0); assertThat(entity.rootId).isEqualTo(1); assertThat(entity.contentMap.values()).extracting(c -> c.id).containsExactly(2, 3); @@ -155,7 +159,8 @@ public void setIdForDeepReference() { aggregateChange.addAction(parentInsert); aggregateChange.addAction(insert); - entity = executor.execute(aggregateChange); + List result = executor.executeSave(aggregateChange); + entity = result.get(0); assertThat(entity.rootId).isEqualTo(1); assertThat(entity.single.id).isEqualTo(2); @@ -178,7 +183,8 @@ public void setIdForDeepReferenceElementList() { aggregateChange.addAction(insert1); aggregateChange.addAction(insert2); - entity = executor.execute(aggregateChange); + List result = executor.executeSave(aggregateChange); + entity = result.get(0); assertSoftly(softly -> { @@ -204,7 +210,8 @@ public void setIdForDeepElementSetElementSet() { aggregateChange.addAction(insert1); aggregateChange.addAction(insert2); - entity = executor.execute(aggregateChange); + List result = executor.executeSave(aggregateChange); + entity = result.get(0); assertSoftly(softly -> { @@ -238,7 +245,8 @@ public void setIdForDeepElementListSingleReference() { aggregateChange.addAction(insert1); aggregateChange.addAction(insert2); - entity = executor.execute(aggregateChange); + List result = executor.executeSave(aggregateChange); + entity = result.get(0); assertSoftly(softly -> { @@ -270,7 +278,8 @@ public void setIdForDeepElementListElementList() { aggregateChange.addAction(insert2); aggregateChange.addAction(insert3); - entity = executor.execute(aggregateChange); + List result = executor.executeSave(aggregateChange); + entity = result.get(0); assertSoftly(softly -> { @@ -305,7 +314,8 @@ public void setIdForDeepElementMapElementMap() { aggregateChange.addAction(insert2); aggregateChange.addAction(insert3); - entity = executor.execute(aggregateChange); + List result = executor.executeSave(aggregateChange); + entity = result.get(0); assertSoftly(softly -> { @@ -343,7 +353,8 @@ public void setIdForDeepElementListSingleReferenceWithIntermittentNoId() { aggregateChange.addAction(insert1); aggregateChange.addAction(insert2); - entity = executor.execute(aggregateChange); + List result = executor.executeSave(aggregateChange); + entity = result.get(0); assertSoftly(softly -> { @@ -366,7 +377,8 @@ public void setIdForEmbeddedDeepReference() { aggregateChange.setRootAction(rootInsert); aggregateChange.addAction(parentInsert); - entity = executor.execute(aggregateChange); + List result = executor.executeSave(aggregateChange); + entity = result.get(0); assertThat(entity.rootId).isEqualTo(1); assertThat(entity.embedded.single.id).isEqualTo(2); diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AggregateChangeIdGenerationUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AggregateChangeIdGenerationUnitTests.java index 1c7a31e9b0..38be034b3c 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AggregateChangeIdGenerationUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AggregateChangeIdGenerationUnitTests.java @@ -74,7 +74,7 @@ public void singleRoot() { AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(entity); aggregateChange.setRootAction(rootInsert); - executor.execute(aggregateChange); + executor.executeSave(aggregateChange); assertThat(entity.rootId).isEqualTo(1); } @@ -88,7 +88,7 @@ public void simpleReference() { aggregateChange.setRootAction(rootInsert); aggregateChange.addAction(createInsert("single", content, null)); - executor.execute(aggregateChange); + executor.executeSave(aggregateChange); assertSoftly(softly -> { @@ -108,7 +108,7 @@ public void listReference() { aggregateChange.addAction(createInsert("contentList", content, 0)); aggregateChange.addAction(createInsert("contentList", content2, 1)); - executor.execute(aggregateChange); + executor.executeSave(aggregateChange); assertSoftly(softly -> { @@ -128,7 +128,7 @@ public void mapReference() { aggregateChange.addAction(createInsert("contentMap", content, "a")); aggregateChange.addAction(createInsert("contentMap", content2, "b")); - executor.execute(aggregateChange); + executor.executeSave(aggregateChange); assertThat(entity.rootId).isEqualTo(1); assertThat(entity.contentMap.values()).extracting(c -> c.id).containsExactly(2, 3); @@ -148,7 +148,7 @@ public void setIdForDeepReference() { aggregateChange.addAction(parentInsert); aggregateChange.addAction(insert); - executor.execute(aggregateChange); + executor.executeSave(aggregateChange); assertThat(entity.rootId).isEqualTo(1); assertThat(entity.single.id).isEqualTo(2); @@ -172,7 +172,7 @@ public void setIdForDeepReferenceElementList() { aggregateChange.addAction(insert1); aggregateChange.addAction(insert2); - executor.execute(aggregateChange); + executor.executeSave(aggregateChange); assertSoftly(softly -> { @@ -199,7 +199,7 @@ public void setIdForDeepElementSetElementSet() { aggregateChange.addAction(insert1); aggregateChange.addAction(insert2); - executor.execute(aggregateChange); + executor.executeSave(aggregateChange); assertSoftly(softly -> { @@ -234,7 +234,7 @@ public void setIdForDeepElementListSingleReference() { aggregateChange.addAction(insert1); aggregateChange.addAction(insert2); - executor.execute(aggregateChange); + executor.executeSave(aggregateChange); assertSoftly(softly -> { @@ -268,7 +268,7 @@ public void setIdForDeepElementListElementList() { aggregateChange.addAction(insert2); aggregateChange.addAction(insert3); - executor.execute(aggregateChange); + executor.executeSave(aggregateChange); assertSoftly(softly -> { @@ -306,7 +306,7 @@ public void setIdForDeepElementMapElementMap() { aggregateChange.addAction(insert2); aggregateChange.addAction(insert3); - executor.execute(aggregateChange); + executor.executeSave(aggregateChange); assertSoftly(softly -> { diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutorContextImmutableUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutorContextImmutableUnitTests.java index 9195563e7f..8da4140e20 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutorContextImmutableUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutorContextImmutableUnitTests.java @@ -70,9 +70,10 @@ public void afterInsertRootIdMaybeUpdated() { executionContext.executeInsertRoot(new DbAction.InsertRoot<>(root, IdValueSource.GENERATED)); - DummyEntity newRoot = executionContext.populateIdsIfNecessary(); + List newRoots = executionContext.populateIdsIfNecessary(); - assertThat(newRoot).isNotNull(); + assertThat(newRoots).hasSize(1); + DummyEntity newRoot = newRoots.get(0); assertThat(newRoot.id).isEqualTo(23L); } @@ -83,16 +84,17 @@ public void idGenerationOfChild() { when(accessStrategy.insert(any(DummyEntity.class), eq(DummyEntity.class), eq(Identifier.empty()), eq(IdValueSource.GENERATED))).thenReturn(23L); - when(accessStrategy.insert(any(Content.class), eq(Content.class), eq(createBackRef()), eq(IdValueSource.GENERATED))) + when(accessStrategy.insert(any(Content.class), eq(Content.class), eq(createBackRef(23L)), eq(IdValueSource.GENERATED))) .thenReturn(24L); DbAction.InsertRoot rootInsert = new DbAction.InsertRoot<>(root, IdValueSource.GENERATED); executionContext.executeInsertRoot(rootInsert); executionContext.executeInsert(createInsert(rootInsert, "content", content, null)); - DummyEntity newRoot = executionContext.populateIdsIfNecessary(); + List newRoots = executionContext.populateIdsIfNecessary(); - assertThat(newRoot).isNotNull(); + assertThat(newRoots).hasSize(1); + DummyEntity newRoot = newRoots.get(0); assertThat(newRoot.id).isEqualTo(23L); assertThat(newRoot.content.id).isEqualTo(24L); @@ -112,14 +114,46 @@ public void idGenerationOfChildInList() { executionContext.executeInsertRoot(rootInsert); executionContext.executeInsert(createInsert(rootInsert, "list", content, 1)); - DummyEntity newRoot = executionContext.populateIdsIfNecessary(); + List newRoots = executionContext.populateIdsIfNecessary(); - assertThat(newRoot).isNotNull(); + assertThat(newRoots).hasSize(1); + DummyEntity newRoot = newRoots.get(0); assertThat(newRoot.id).isEqualTo(23L); assertThat(newRoot.list.get(0).id).isEqualTo(24L); } + @Test // GH-537 + void populatesIdsIfNecessaryForAllRootsThatWereProcessed() { + + DummyEntity root1 = new DummyEntity().withId(123L); + when(accessStrategy.update(root1, DummyEntity.class)).thenReturn(true); + DbAction.UpdateRoot rootUpdate1 = new DbAction.UpdateRoot<>(root1, null); + executionContext.executeUpdateRoot(rootUpdate1); + Content content1 = new Content(); + when(accessStrategy.insert(content1, Content.class, createBackRef(123L), IdValueSource.GENERATED)).thenReturn(11L); + executionContext.executeInsert(createInsert(rootUpdate1, "content", content1, null)); + + + 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); + executionContext.executeInsertRoot(rootInsert2); + Content content2 = new Content(); + when(accessStrategy.insert(content2, Content.class, createBackRef(456L), IdValueSource.GENERATED)).thenReturn(12L); + executionContext.executeInsert(createInsert(rootInsert2, "content", content2, null)); + + List newRoots = executionContext.populateIdsIfNecessary(); + + assertThat(newRoots).hasSize(2); + DummyEntity newRoot1 = newRoots.get(0); + assertThat(newRoot1.id).isEqualTo(123L); + assertThat(newRoot1.content.id).isEqualTo(11L); + DummyEntity newRoot2 = newRoots.get(1); + assertThat(newRoot2.id).isEqualTo(456L); + assertThat(newRoot2.content.id).isEqualTo(12L); + } + DbAction.Insert createInsert(DbAction.WithEntity parent, String propertyName, Object value, @Nullable Object key) { @@ -135,8 +169,8 @@ PersistentPropertyPath getPersistentPropertyPath(S return context.getPersistentPropertyPath(propertyName, DummyEntity.class); } - Identifier createBackRef() { - return JdbcIdentifierBuilder.forBackReferences(converter, toPathExt("content"), 23L).build(); + Identifier createBackRef(long value) { + return JdbcIdentifierBuilder.forBackReferences(converter, toPathExt("content"), value).build(); } PersistentPropertyPath toPath(String path) { 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 6936bb8d6d..2405cf3243 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 @@ -18,6 +18,7 @@ import static java.util.Collections.*; import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; +import static org.springframework.data.jdbc.core.convert.JdbcIdentifierBuilder.*; import lombok.Value; @@ -31,7 +32,6 @@ import org.springframework.data.jdbc.core.convert.Identifier; import org.springframework.data.jdbc.core.convert.InsertSubject; import org.springframework.data.jdbc.core.convert.JdbcConverter; -import org.springframework.data.jdbc.core.convert.JdbcIdentifierBuilder; import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.mapping.PersistentPropertyPaths; import org.springframework.data.relational.core.conversion.DbAction; @@ -69,9 +69,9 @@ public void afterInsertRootIdMaybeUpdated() { executionContext.executeInsertRoot(new DbAction.InsertRoot<>(root, IdValueSource.GENERATED)); - DummyEntity newRoot = executionContext.populateIdsIfNecessary(); + List newRoots = executionContext.populateIdsIfNecessary(); - assertThat(newRoot).isEqualTo(root); + assertThat(newRoots).containsExactly(root); assertThat(root.id).isEqualTo(23L); } @@ -81,15 +81,15 @@ public void idGenerationOfChild() { Content content = new Content(); when(accessStrategy.insert(root, DummyEntity.class, Identifier.empty(), IdValueSource.GENERATED)).thenReturn(23L); - when(accessStrategy.insert(content, Content.class, createBackRef(), IdValueSource.GENERATED)).thenReturn(24L); + when(accessStrategy.insert(content, Content.class, createBackRef(23L), IdValueSource.GENERATED)).thenReturn(24L); DbAction.InsertRoot rootInsert = new DbAction.InsertRoot<>(root, IdValueSource.GENERATED); executionContext.executeInsertRoot(rootInsert); executionContext.executeInsert(createInsert(rootInsert, "content", content, null)); - DummyEntity newRoot = executionContext.populateIdsIfNecessary(); + List newRoots = executionContext.populateIdsIfNecessary(); - assertThat(newRoot).isEqualTo(root); + assertThat(newRoots).containsExactly(root); assertThat(root.id).isEqualTo(23L); assertThat(content.id).isEqualTo(24L); @@ -108,9 +108,9 @@ public void idGenerationOfChildInList() { executionContext.executeInsertRoot(rootInsert); executionContext.executeInsert(createInsert(rootInsert, "list", content, 1)); - DummyEntity newRoot = executionContext.populateIdsIfNecessary(); + List newRoots = executionContext.populateIdsIfNecessary(); - assertThat(newRoot).isEqualTo(root); + assertThat(newRoots).containsExactly(root); assertThat(root.id).isEqualTo(23L); assertThat(content.id).isEqualTo(24L); @@ -133,9 +133,9 @@ void batchInsertOperation_withGeneratedIds() { singletonList(createInsert(rootInsert, "list", content, 0)), IdValueSource.GENERATED); executionContext.executeInsertBatch(insertBatch); - DummyEntity newRoot = executionContext.populateIdsIfNecessary(); + List newRoots = executionContext.populateIdsIfNecessary(); - assertThat(newRoot).isEqualTo(root); + assertThat(newRoots).containsExactly(root); assertThat(root.id).isEqualTo(123L); assertThat(content.id).isEqualTo(456L); } @@ -157,35 +157,66 @@ void batchInsertOperation_withoutGeneratedIds() { singletonList(createInsert(rootInsert, "list", content, 0)), IdValueSource.PROVIDED); executionContext.executeInsertBatch(insertBatch); - DummyEntity newRoot = executionContext.populateIdsIfNecessary(); + List newRoots = executionContext.populateIdsIfNecessary(); - assertThat(newRoot).isEqualTo(root); + assertThat(newRoots).containsExactly(root); assertThat(root.id).isEqualTo(123L); assertThat(content.id).isNull(); } @Test // GH-1201 void updates_whenReferencesWithImmutableIdAreInserted() { - when(accessStrategy.update(any(), any())).thenReturn(true); + root.id = 123L; - DbAction.UpdateRoot rootInsert = new DbAction.UpdateRoot<>(root, null); - executionContext.executeUpdateRoot(rootInsert); + when(accessStrategy.update(root, DummyEntity.class)).thenReturn(true); + DbAction.UpdateRoot rootUpdate = new DbAction.UpdateRoot<>(root, null); + executionContext.executeUpdateRoot(rootUpdate); ContentImmutableId contentImmutableId = new ContentImmutableId(null); root.contentImmutableId = contentImmutableId; 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(rootInsert, "contentImmutableId", contentImmutableId, null)); + executionContext.executeInsert(createInsert(rootUpdate, "contentImmutableId", contentImmutableId, null)); - DummyEntity newRoot = executionContext.populateIdsIfNecessary(); - assertThat(newRoot).isEqualTo(root); + List newRoots = executionContext.populateIdsIfNecessary(); + assertThat(newRoots).containsExactly(root); assertThat(root.id).isEqualTo(123L); assertThat(root.contentImmutableId.id).isEqualTo(456L); } + @Test // GH-537 + void populatesIdsIfNecessaryForAllRootsThatWereProcessed() { + + DummyEntity root1 = new DummyEntity(); + root1.id = 123L; + when(accessStrategy.update(root1, DummyEntity.class)).thenReturn(true); + DbAction.UpdateRoot rootUpdate1 = new DbAction.UpdateRoot<>(root1, null); + executionContext.executeUpdateRoot(rootUpdate1); + Content content1 = new Content(); + when(accessStrategy.insert(content1, Content.class, createBackRef(123L), IdValueSource.GENERATED)).thenReturn(11L); + executionContext.executeInsert(createInsert(rootUpdate1, "content", content1, null)); + + + 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); + executionContext.executeInsertRoot(rootInsert2); + Content content2 = new Content(); + when(accessStrategy.insert(content2, Content.class, createBackRef(456L), IdValueSource.GENERATED)).thenReturn(12L); + executionContext.executeInsert(createInsert(rootInsert2, "content", content2, null)); + + List newRoots = executionContext.populateIdsIfNecessary(); + + assertThat(newRoots).containsExactly(root1, root2); + assertThat(root1.id).isEqualTo(123L); + assertThat(content1.id).isEqualTo(11L); + assertThat(root2.id).isEqualTo(456L); + assertThat(content2.id).isEqualTo(12L); + } + DbAction.Insert createInsert(DbAction.WithEntity parent, String propertyName, Object value, - @Nullable Object key) { + @Nullable Object key) { return new DbAction.Insert<>(value, getPersistentPropertyPath(propertyName), parent, key == null ? emptyMap() : singletonMap(toPath(propertyName), key), IdValueSource.GENERATED); @@ -199,8 +230,8 @@ PersistentPropertyPath getPersistentPropertyPath(S return context.getPersistentPropertyPath(propertyName, DummyEntity.class); } - Identifier createBackRef() { - return JdbcIdentifierBuilder.forBackReferences(converter, toPathExt("content"), 23L).build(); + Identifier createBackRef(long value) { + return forBackReferences(converter, toPathExt("content"), value).build(); } PersistentPropertyPath toPath(String path) { diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java index 85f9e1e1f7..b5dff520dd 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java @@ -128,7 +128,7 @@ public void saveAndLoadAnEntity() { } @Test // DATAJDBC-97 - public void savesManyEntities() { + public void insertsManyEntities() { DummyEntity entity = createDummyEntity(); DummyEntity other = createDummyEntity(); @@ -282,6 +282,20 @@ public void updateMany() { .containsExactlyInAnyOrder(entity.getName(), other.getName()); } + @Test + void insertsOrUpdatesManyEntities() { + + DummyEntity entity = repository.save(createDummyEntity()); + entity.setName("something else"); + DummyEntity other = createDummyEntity(); + other.setName("others name"); + repository.saveAll(asList(other, entity)); + + assertThat(repository.findAll()) // + .extracting(DummyEntity::getName) // + .containsExactlyInAnyOrder(entity.getName(), other.getName()); + } + @Test // DATAJDBC-112 public void findByIdReturnsEmptyWhenNoneFound() { diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositorySaveAllHsqlIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositorySaveAllHsqlIntegrationTests.java new file mode 100644 index 0000000000..fd7e2082e9 --- /dev/null +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositorySaveAllHsqlIntegrationTests.java @@ -0,0 +1,173 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.repository; + +import static java.util.Arrays.*; +import static java.util.Collections.*; +import static org.assertj.core.api.Assertions.*; +import static org.springframework.test.context.TestExecutionListeners.MergeMode.*; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.data.annotation.Id; +import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory; +import org.springframework.data.jdbc.testing.AssumeFeatureTestExecutionListener; +import org.springframework.data.jdbc.testing.TestConfiguration; +import org.springframework.data.relational.core.mapping.MappedCollection; +import org.springframework.data.repository.ListCrudRepository; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.transaction.annotation.Transactional; + +import lombok.Value; + +/** + * Use cases of JdbcRepositories where operations are carried out across multiple aggregate roots at a time. + * + * @author Chirag Tailor + */ +@Transactional +@TestExecutionListeners(value = AssumeFeatureTestExecutionListener.class, mergeMode = MERGE_WITH_DEFAULTS) +@ExtendWith(SpringExtension.class) +@ActiveProfiles("hsql") +public class JdbcRepositorySaveAllHsqlIntegrationTests { + + @Autowired RootRepository rootRepository; + + @Test + void manyInsertsWithNestedEntities() { + Root root1 = createRoot("root1"); + Root root2 = createRoot("root2"); + + List savedRoots = rootRepository.saveAll(asList(root1, root2)); + + List reloadedRoots = rootRepository.findAll(); + assertThat(reloadedRoots).isEqualTo(savedRoots); + assertThat(reloadedRoots).hasSize(2); + assertIsEqualToWithNonNullIds(reloadedRoots.get(0), root1); + assertIsEqualToWithNonNullIds(reloadedRoots.get(1), root2); + } + + @Test + void manyUpdatesWithNestedEntities() { + Root root1 = createRoot("root1"); + Root root2 = createRoot("root2"); + List roots = rootRepository.saveAll(asList(root1, root2)); + Root savedRoot1 = roots.get(0); + Root updatedRoot1 = new Root(savedRoot1.id, "updated" + savedRoot1.name, + new Intermediate(savedRoot1.intermediate.id, "updated" + savedRoot1.intermediate.name, + new Leaf(savedRoot1.intermediate.leaf.id, "updated" + savedRoot1.intermediate.leaf.name), emptyList()), + savedRoot1.intermediates); + Root savedRoot2 = roots.get(1); + Root updatedRoot2 = new Root(savedRoot2.id, "updated" + savedRoot2.name, savedRoot2.intermediate, + singletonList( + new Intermediate(savedRoot2.intermediates.get(0).id, "updated" + savedRoot2.intermediates.get(0).name, null, + singletonList(new Leaf(savedRoot2.intermediates.get(0).leaves.get(0).id, + "updated" + savedRoot2.intermediates.get(0).leaves.get(0).name))))); + + List updatedRoots = rootRepository.saveAll(asList(updatedRoot1, updatedRoot2)); + + List reloadedRoots = rootRepository.findAll(); + assertThat(reloadedRoots).isEqualTo(updatedRoots); + assertThat(reloadedRoots).containsExactly(updatedRoot1, updatedRoot2); + } + + @Test + void manyInsertsAndUpdatedWithNesteEntities() { + Root root1 = createRoot("root1"); + Root savedRoot1 = rootRepository.save(root1); + Root updatedRoot1 = new Root(savedRoot1.id, "updated" + savedRoot1.name, + new Intermediate(savedRoot1.intermediate.id, "updated" + savedRoot1.intermediate.name, + new Leaf(savedRoot1.intermediate.leaf.id, "updated" + savedRoot1.intermediate.leaf.name), emptyList()), + savedRoot1.intermediates); + Root root2 = createRoot("root2"); + List savedRoots = rootRepository.saveAll(asList(updatedRoot1, root2)); + + List reloadedRoots = rootRepository.findAll(); + assertThat(reloadedRoots).isEqualTo(savedRoots); + assertThat(reloadedRoots.get(0)).isEqualTo(updatedRoot1); + assertIsEqualToWithNonNullIds(reloadedRoots.get(1), root2); + } + + private Root createRoot(String namePrefix) { + return new Root(null, namePrefix, + new Intermediate(null, namePrefix + "Intermediate", new Leaf(null, namePrefix + "Leaf"), emptyList()), + singletonList(new Intermediate(null, namePrefix + "QualifiedIntermediate", null, + singletonList(new Leaf(null, namePrefix + "QualifiedLeaf"))))); + } + + private void assertIsEqualToWithNonNullIds(Root reloadedRoot1, Root root1) { + assertThat(reloadedRoot1.id).isNotNull(); + assertThat(reloadedRoot1.name).isEqualTo(root1.name); + assertThat(reloadedRoot1.intermediate.id).isNotNull(); + assertThat(reloadedRoot1.intermediate.name).isEqualTo(root1.intermediate.name); + assertThat(reloadedRoot1.intermediates.get(0).id).isNotNull(); + assertThat(reloadedRoot1.intermediates.get(0).name).isEqualTo(root1.intermediates.get(0).name); + assertThat(reloadedRoot1.intermediate.leaf.id).isNotNull(); + assertThat(reloadedRoot1.intermediate.leaf.name).isEqualTo(root1.intermediate.leaf.name); + assertThat(reloadedRoot1.intermediates.get(0).leaves.get(0).id).isNotNull(); + assertThat(reloadedRoot1.intermediates.get(0).leaves.get(0).name) + .isEqualTo(root1.intermediates.get(0).leaves.get(0).name); + } + + @Configuration + @Import(TestConfiguration.class) + static class Config { + + @Autowired JdbcRepositoryFactory factory; + + @Bean + Class testClass() { + return JdbcRepositorySaveAllHsqlIntegrationTests.class; + } + + @Bean + RootRepository rootRepository() { + return factory.getRepository(RootRepository.class); + } + } + + interface RootRepository extends ListCrudRepository {} + + @Value + static class Root { + @Id Long id; + String name; + Intermediate intermediate; + @MappedCollection(idColumn = "ROOT_ID", keyColumn = "ROOT_KEY") List intermediates; + } + + @Value + static class Intermediate { + @Id Long id; + String name; + Leaf leaf; + @MappedCollection(idColumn = "INTERMEDIATE_ID", keyColumn = "INTERMEDIATE_KEY") List leaves; + } + + @Value + static class Leaf { + @Id Long id; + String name; + } +} diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/SimpleJdbcRepositoryEventsUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/SimpleJdbcRepositoryEventsUnitTests.java index 580bc988f6..b992944698 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/SimpleJdbcRepositoryEventsUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/SimpleJdbcRepositoryEventsUnitTests.java @@ -68,6 +68,8 @@ */ public class SimpleJdbcRepositoryEventsUnitTests { + private static final long generatedId = 4711L; + CollectingEventPublisher publisher = new CollectingEventPublisher(); DummyEntityRepository repository; @@ -125,14 +127,14 @@ public void publishesEventsOnSaveMany() { repository.saveAll(asList(entity1, entity2)); assertThat(publisher.events) // - .extracting(e -> (Class) e.getClass()) // + .extracting(RelationalEvent::getClass, e -> ((DummyEntity) e.getEntity()).getId()) // .containsExactly( // - BeforeConvertEvent.class, // - BeforeSaveEvent.class, // - AfterSaveEvent.class, // - BeforeConvertEvent.class, // - BeforeSaveEvent.class, // - AfterSaveEvent.class // + Tuple.tuple(BeforeConvertEvent.class, null), // + Tuple.tuple(BeforeSaveEvent.class, null), // + Tuple.tuple(BeforeConvertEvent.class, 23L), // + Tuple.tuple(BeforeSaveEvent.class, 23L), // + Tuple.tuple(AfterSaveEvent.class, generatedId), // + Tuple.tuple(AfterSaveEvent.class, 23L) // ); } @@ -284,7 +286,7 @@ private static NamedParameterJdbcOperations createIdGeneratingOperations() { Answer setIdInKeyHolder = invocation -> { HashMap keys = new HashMap<>(); - keys.put("id", 4711L); + keys.put("id", generatedId); KeyHolder keyHolder = invocation.getArgument(2); keyHolder.getKeyList().add(keys); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositorySaveAllHsqlIntegrationTests-hsql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositorySaveAllHsqlIntegrationTests-hsql.sql new file mode 100644 index 0000000000..bcf4b00332 --- /dev/null +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositorySaveAllHsqlIntegrationTests-hsql.sql @@ -0,0 +1,21 @@ +CREATE TABLE ROOT +( + ID BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, + NAME VARCHAR(100) +); +CREATE TABLE INTERMEDIATE +( + ID BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, + NAME VARCHAR(100), + ROOT BIGINT, + ROOT_ID BIGINT, + ROOT_KEY INTEGER +); +CREATE TABLE LEAF +( + ID BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, + NAME VARCHAR(100), + INTERMEDIATE BIGINT, + INTERMEDIATE_ID BIGINT, + INTERMEDIATE_KEY INTEGER +); From 1ad5dbc5dd8ddd98b6e85eab1afb9e01e0821e6b Mon Sep 17 00:00:00 2001 From: Chirag Tailor Date: Thu, 31 Mar 2022 11:09:56 -0500 Subject: [PATCH 05/11] Pull out an abstract BatchWithValue class from InsertBatch to use it for batching root inserts as well. --- .../JdbcAggregateChangeExecutionContext.java | 4 +- ...gregateChangeExecutorContextUnitTests.java | 18 +++---- .../relational/core/conversion/DbAction.java | 47 +++++++++++-------- .../conversion/SaveMergedAggregateChange.java | 2 +- .../core/conversion/DbActionTestSupport.java | 2 +- .../SaveMergedAggregateChangeTest.java | 14 +++--- 6 files changed, 48 insertions(+), 39 deletions(-) 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 23d4f2f2d4..af6c241f44 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 @@ -85,12 +85,12 @@ void executeInsert(DbAction.Insert insert) { void executeInsertBatch(DbAction.InsertBatch insertBatch) { - List> inserts = insertBatch.getInserts(); + List> inserts = insertBatch.getActions(); List> insertSubjects = inserts.stream() .map(insert -> InsertSubject.describedBy(insert.getEntity(), getParentKeys(insert, converter))) .collect(Collectors.toList()); - Object[] ids = accessStrategy.insert(insertSubjects, insertBatch.getEntityType(), insertBatch.getIdValueSource()); + Object[] ids = accessStrategy.insert(insertSubjects, insertBatch.getEntityType(), insertBatch.getBatchValue()); for (int i = 0; i < inserts.size(); i++) { add(new DbActionExecutionResult(inserts.get(i), ids.length > 0 ? ids[i] : null)); 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 2405cf3243..c958eca40a 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 @@ -85,7 +85,7 @@ public void idGenerationOfChild() { DbAction.InsertRoot rootInsert = new DbAction.InsertRoot<>(root, IdValueSource.GENERATED); executionContext.executeInsertRoot(rootInsert); - executionContext.executeInsert(createInsert(rootInsert, "content", content, null)); + executionContext.executeInsert(createInsert(rootInsert, "content", content, null, IdValueSource.GENERATED)); List newRoots = executionContext.populateIdsIfNecessary(); @@ -106,7 +106,7 @@ public void idGenerationOfChildInList() { DbAction.InsertRoot rootInsert = new DbAction.InsertRoot<>(root, IdValueSource.GENERATED); executionContext.executeInsertRoot(rootInsert); - executionContext.executeInsert(createInsert(rootInsert, "list", content, 1)); + executionContext.executeInsert(createInsert(rootInsert, "list", content, 1, IdValueSource.GENERATED)); List newRoots = executionContext.populateIdsIfNecessary(); @@ -130,7 +130,7 @@ void batchInsertOperation_withGeneratedIds() { when(accessStrategy.insert(singletonList(InsertSubject.describedBy(content, identifier)), Content.class, IdValueSource.GENERATED)).thenReturn(new Object[] { 456L }); DbAction.InsertBatch insertBatch = new DbAction.InsertBatch<>( - singletonList(createInsert(rootInsert, "list", content, 0)), IdValueSource.GENERATED); + singletonList(createInsert(rootInsert, "list", content, 0, IdValueSource.GENERATED))); executionContext.executeInsertBatch(insertBatch); List newRoots = executionContext.populateIdsIfNecessary(); @@ -154,7 +154,7 @@ void batchInsertOperation_withoutGeneratedIds() { when(accessStrategy.insert(singletonList(InsertSubject.describedBy(content, identifier)), Content.class, IdValueSource.PROVIDED)).thenReturn(new Object[] { null }); DbAction.InsertBatch insertBatch = new DbAction.InsertBatch<>( - singletonList(createInsert(rootInsert, "list", content, 0)), IdValueSource.PROVIDED); + singletonList(createInsert(rootInsert, "list", content, 0, IdValueSource.PROVIDED))); executionContext.executeInsertBatch(insertBatch); List newRoots = executionContext.populateIdsIfNecessary(); @@ -177,7 +177,7 @@ 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)); + executionContext.executeInsert(createInsert(rootUpdate, "contentImmutableId", contentImmutableId, null, IdValueSource.GENERATED)); List newRoots = executionContext.populateIdsIfNecessary(); assertThat(newRoots).containsExactly(root); @@ -195,7 +195,7 @@ void populatesIdsIfNecessaryForAllRootsThatWereProcessed() { executionContext.executeUpdateRoot(rootUpdate1); Content content1 = new Content(); when(accessStrategy.insert(content1, Content.class, createBackRef(123L), IdValueSource.GENERATED)).thenReturn(11L); - executionContext.executeInsert(createInsert(rootUpdate1, "content", content1, null)); + executionContext.executeInsert(createInsert(rootUpdate1, "content", content1, null, IdValueSource.GENERATED)); DummyEntity root2 = new DummyEntity(); @@ -204,7 +204,7 @@ void populatesIdsIfNecessaryForAllRootsThatWereProcessed() { executionContext.executeInsertRoot(rootInsert2); Content content2 = new Content(); when(accessStrategy.insert(content2, Content.class, createBackRef(456L), IdValueSource.GENERATED)).thenReturn(12L); - executionContext.executeInsert(createInsert(rootInsert2, "content", content2, null)); + executionContext.executeInsert(createInsert(rootInsert2, "content", content2, null, IdValueSource.GENERATED)); List newRoots = executionContext.populateIdsIfNecessary(); @@ -216,10 +216,10 @@ void populatesIdsIfNecessaryForAllRootsThatWereProcessed() { } DbAction.Insert createInsert(DbAction.WithEntity parent, String propertyName, Object value, - @Nullable Object key) { + @Nullable Object key, IdValueSource idValueSource) { return new DbAction.Insert<>(value, getPersistentPropertyPath(propertyName), parent, - key == null ? emptyMap() : singletonMap(toPath(propertyName), key), IdValueSource.GENERATED); + key == null ? emptyMap() : singletonMap(toPath(propertyName), key), idValueSource); } PersistentPropertyPathExtension toPathExt(String path) { 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 d9de720540..5714445024 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 @@ -19,6 +19,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Function; import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; @@ -344,38 +345,46 @@ public String toString() { } } - /** - * Represents a batch insert statement for a multiple entities that are not aggregate roots. - * - * @param type of the entity for which this represents a database interaction. - * @since 2.4 - */ - final class InsertBatch implements DbAction { - private final List> inserts; - private final IdValueSource idValueSource; + abstract class BatchWithValue, B> implements DbAction { + private final List actions; + private final B batchValue; - public InsertBatch(List> inserts, IdValueSource idValueSource) { - Assert.notEmpty(inserts, "Inserts must contains at least one insert"); - this.inserts = inserts; - this.idValueSource = idValueSource; + public BatchWithValue(List actions, Function batchValueExtractor) { + Assert.notEmpty(actions, "Actions must contain at least one action"); + this.batchValue = batchValueExtractor.apply(actions.get(0)); + Assert.isTrue(actions.stream().allMatch(a -> batchValueExtractor.apply(a).equals(batchValue)), + "All actions in the batch must have matching batchValue"); + this.actions = actions; } @Override public Class getEntityType() { - return inserts.get(0).getEntityType(); + return actions.get(0).getEntityType(); } - public List> getInserts() { - return inserts; + public List getActions() { + return actions; } - public IdValueSource getIdValueSource() { - return idValueSource; + public B getBatchValue() { + return batchValue; } @Override public String toString() { - return "InsertBatch{" + "inserts=" + inserts + ", idValueSource=" + idValueSource + '}'; + return "BatchWithValue{" + "actions=" + actions + ", batchValue=" + batchValue + '}'; + } + } + + /** + * Represents a batch insert statement for a multiple entities that are not aggregate roots. + * + * @param type of the entity for which this represents a database interaction. + * @since 2.4 + */ + final class InsertBatch extends BatchWithValue, IdValueSource> { + public InsertBatch(List> actions) { + super(actions, Insert::getIdValueSource); } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/SaveMergedAggregateChange.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/SaveMergedAggregateChange.java index 66147df614..f7b584bc92 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/SaveMergedAggregateChange.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/SaveMergedAggregateChange.java @@ -50,7 +50,7 @@ public void forEachAction(Consumer> consumer) { .forEach((entry) -> entry.getValue().forEach(consumer)); insertActions.entrySet().stream().sorted(Map.Entry.comparingByKey(pathLengthComparator)) .forEach((entry) -> entry.getValue() - .forEach((idValueSource, inserts) -> consumer.accept(new DbAction.InsertBatch<>(inserts, idValueSource)))); + .forEach((idValueSource, inserts) -> consumer.accept(new DbAction.InsertBatch<>(inserts)))); } @Override 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 22c083e3c2..d7575ecb67 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 @@ -57,7 +57,7 @@ static IdValueSource insertIdValueSource(DbAction action) { } else if (action instanceof DbAction.Insert) { return ((DbAction.Insert) action).getIdValueSource(); } else if (action instanceof DbAction.InsertBatch) { - return ((DbAction.InsertBatch) action).getIdValueSource(); + return ((DbAction.InsertBatch) action).getBatchValue(); } else { return null; } diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/SaveMergedAggregateChangeTest.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/SaveMergedAggregateChangeTest.java index 45b3c53857..24721c3504 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/SaveMergedAggregateChangeTest.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/SaveMergedAggregateChangeTest.java @@ -162,9 +162,9 @@ void yieldsInsertActionsAsBatchInserts_groupedByIdValueSource() { Tuple.tuple(DbAction.InsertRoot.class, Root.class, IdValueSource.GENERATED), // Tuple.tuple(DbAction.InsertBatch.class, Intermediate.class, IdValueSource.GENERATED)) // .doesNotContain(Tuple.tuple(DbAction.Insert.class, Intermediate.class)); - assertThat(getInsertBatchAction(actions, Intermediate.class, IdValueSource.GENERATED).getInserts()) + assertThat(getInsertBatchAction(actions, Intermediate.class, IdValueSource.GENERATED).getActions()) .containsExactly(intermediateInsertGeneratedId); - assertThat(getInsertBatchAction(actions, Intermediate.class, IdValueSource.PROVIDED).getInserts()) + assertThat(getInsertBatchAction(actions, Intermediate.class, IdValueSource.PROVIDED).getActions()) .containsExactly(intermediateInsertProvidedId); } @@ -207,9 +207,9 @@ void yieldsNestedInsertActionsInTreeOrderFromRootToLeaves() { .containsSubsequence( // Tuple.tuple(DbAction.InsertBatch.class, Intermediate.class, IdValueSource.GENERATED), Tuple.tuple(DbAction.InsertBatch.class, Leaf.class, IdValueSource.GENERATED)); - assertThat(getInsertBatchAction(actions, Intermediate.class).getInserts()) // + assertThat(getInsertBatchAction(actions, Intermediate.class).getActions()) // .containsExactly(root1IntermediateInsert, root2IntermediateInsert); - assertThat(getInsertBatchAction(actions, Leaf.class).getInserts()) // + assertThat(getInsertBatchAction(actions, Leaf.class).getActions()) // .containsExactly(root1LeafInsert); } @@ -241,14 +241,14 @@ void yieldsInsertsWithSameLengthReferences_asSeparateInserts() { Tuple.tuple(DbAction.InsertBatch.class, Intermediate.class, IdValueSource.GENERATED)); List> insertBatchActions = getInsertBatchActions(actions, Intermediate.class); assertThat(insertBatchActions).hasSize(2); - assertThat(insertBatchActions.get(0).getInserts()).containsExactly(oneInsert); - assertThat(insertBatchActions.get(1).getInserts()).containsExactly(twoInsert); + assertThat(insertBatchActions.get(0).getActions()).containsExactly(oneInsert); + assertThat(insertBatchActions.get(1).getActions()).containsExactly(twoInsert); } private DbAction.InsertBatch getInsertBatchAction(List> actions, Class entityType, IdValueSource idValueSource) { return getInsertBatchActions(actions, entityType).stream() - .filter(insertBatch -> insertBatch.getIdValueSource() == idValueSource).findFirst().orElseThrow( + .filter(insertBatch -> insertBatch.getBatchValue() == idValueSource).findFirst().orElseThrow( () -> new RuntimeException(String.format("No InsertBatch with includeId '%s' found!", idValueSource))); } From d25874231f5dca65084a7b5b4a4312e60cdfba00 Mon Sep 17 00:00:00 2001 From: Chirag Tailor Date: Thu, 31 Mar 2022 11:12:35 -0500 Subject: [PATCH 06/11] Rename InsertBatch to BatchInsert --- .../jdbc/core/AggregateChangeExecutor.java | 4 +- .../JdbcAggregateChangeExecutionContext.java | 6 +-- ...gregateChangeExecutorContextUnitTests.java | 8 +-- .../relational/core/conversion/DbAction.java | 4 +- .../conversion/SaveMergedAggregateChange.java | 2 +- .../core/conversion/DbActionTestSupport.java | 4 +- .../SaveMergedAggregateChangeTest.java | 52 +++++++++---------- 7 files changed, 40 insertions(+), 40 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 0e0997f806..acf9df817d 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 @@ -84,8 +84,8 @@ private void execute(DbAction action, JdbcAggregateChangeExecutionContext exe executionContext.executeInsertRoot((DbAction.InsertRoot) action); } else if (action instanceof DbAction.Insert) { executionContext.executeInsert((DbAction.Insert) action); - } else if (action instanceof DbAction.InsertBatch) { - executionContext.executeInsertBatch((DbAction.InsertBatch) action); + } else if (action instanceof DbAction.BatchInsert) { + executionContext.executeBatchInsert((DbAction.BatchInsert) action); } else if (action instanceof DbAction.UpdateRoot) { executionContext.executeUpdateRoot((DbAction.UpdateRoot) action); } else if (action instanceof DbAction.Delete) { 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 af6c241f44..3037d3f562 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 @@ -83,14 +83,14 @@ void executeInsert(DbAction.Insert insert) { add(new DbActionExecutionResult(insert, id)); } - void executeInsertBatch(DbAction.InsertBatch insertBatch) { + void executeBatchInsert(DbAction.BatchInsert batchInsert) { - List> inserts = insertBatch.getActions(); + List> inserts = batchInsert.getActions(); List> insertSubjects = inserts.stream() .map(insert -> InsertSubject.describedBy(insert.getEntity(), getParentKeys(insert, converter))) .collect(Collectors.toList()); - Object[] ids = accessStrategy.insert(insertSubjects, insertBatch.getEntityType(), insertBatch.getBatchValue()); + Object[] ids = accessStrategy.insert(insertSubjects, batchInsert.getEntityType(), batchInsert.getBatchValue()); for (int i = 0; i < inserts.size(); i++) { add(new DbActionExecutionResult(inserts.get(i), ids.length > 0 ? ids[i] : null)); 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 c958eca40a..800eb4f1fe 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 @@ -129,9 +129,9 @@ void batchInsertOperation_withGeneratedIds() { .withPart(SqlIdentifier.quoted("DUMMY_ENTITY_KEY"), 0, Integer.class); when(accessStrategy.insert(singletonList(InsertSubject.describedBy(content, identifier)), Content.class, IdValueSource.GENERATED)).thenReturn(new Object[] { 456L }); - DbAction.InsertBatch insertBatch = new DbAction.InsertBatch<>( + DbAction.BatchInsert batchInsert = new DbAction.BatchInsert<>( singletonList(createInsert(rootInsert, "list", content, 0, IdValueSource.GENERATED))); - executionContext.executeInsertBatch(insertBatch); + executionContext.executeBatchInsert(batchInsert); List newRoots = executionContext.populateIdsIfNecessary(); @@ -153,9 +153,9 @@ void batchInsertOperation_withoutGeneratedIds() { .withPart(SqlIdentifier.quoted("DUMMY_ENTITY_KEY"), 0, Integer.class); when(accessStrategy.insert(singletonList(InsertSubject.describedBy(content, identifier)), Content.class, IdValueSource.PROVIDED)).thenReturn(new Object[] { null }); - DbAction.InsertBatch insertBatch = new DbAction.InsertBatch<>( + DbAction.BatchInsert batchInsert = new DbAction.BatchInsert<>( singletonList(createInsert(rootInsert, "list", content, 0, IdValueSource.PROVIDED))); - executionContext.executeInsertBatch(insertBatch); + executionContext.executeBatchInsert(batchInsert); List newRoots = executionContext.populateIdsIfNecessary(); 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 5714445024..da2d8e7ff4 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 @@ -382,8 +382,8 @@ public String toString() { * @param type of the entity for which this represents a database interaction. * @since 2.4 */ - final class InsertBatch extends BatchWithValue, IdValueSource> { - public InsertBatch(List> actions) { + final class BatchInsert extends BatchWithValue, IdValueSource> { + public BatchInsert(List> actions) { super(actions, Insert::getIdValueSource); } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/SaveMergedAggregateChange.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/SaveMergedAggregateChange.java index f7b584bc92..8706007c49 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/SaveMergedAggregateChange.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/SaveMergedAggregateChange.java @@ -50,7 +50,7 @@ public void forEachAction(Consumer> consumer) { .forEach((entry) -> entry.getValue().forEach(consumer)); insertActions.entrySet().stream().sorted(Map.Entry.comparingByKey(pathLengthComparator)) .forEach((entry) -> entry.getValue() - .forEach((idValueSource, inserts) -> consumer.accept(new DbAction.InsertBatch<>(inserts)))); + .forEach((idValueSource, inserts) -> consumer.accept(new DbAction.BatchInsert<>(inserts)))); } @Override 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 d7575ecb67..2d91453050 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 @@ -56,8 +56,8 @@ static IdValueSource insertIdValueSource(DbAction action) { return ((DbAction.InsertRoot) action).getIdValueSource(); } else if (action instanceof DbAction.Insert) { return ((DbAction.Insert) action).getIdValueSource(); - } else if (action instanceof DbAction.InsertBatch) { - return ((DbAction.InsertBatch) action).getBatchValue(); + } else if (action instanceof DbAction.BatchInsert) { + return ((DbAction.BatchInsert) action).getBatchValue(); } else { return null; } diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/SaveMergedAggregateChangeTest.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/SaveMergedAggregateChangeTest.java index 24721c3504..7c051e086f 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/SaveMergedAggregateChangeTest.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/SaveMergedAggregateChangeTest.java @@ -129,7 +129,7 @@ void yieldsDeleteActionsBeforeInsertActions() { assertThat(extractActions(change)).extracting(DbAction::getClass, DbAction::getEntityType).containsSubsequence( // Tuple.tuple(DbAction.Delete.class, Intermediate.class), // - Tuple.tuple(DbAction.InsertBatch.class, Intermediate.class)); + Tuple.tuple(DbAction.BatchInsert.class, Intermediate.class)); } @Test @@ -157,14 +157,14 @@ void yieldsInsertActionsAsBatchInserts_groupedByIdValueSource() { .extracting(DbAction::getClass, DbAction::getEntityType, DbActionTestSupport::insertIdValueSource) // .containsSubsequence( // Tuple.tuple(DbAction.InsertRoot.class, Root.class, IdValueSource.GENERATED), // - Tuple.tuple(DbAction.InsertBatch.class, Intermediate.class, IdValueSource.PROVIDED)) // + Tuple.tuple(DbAction.BatchInsert.class, Intermediate.class, IdValueSource.PROVIDED)) // .containsSubsequence( // Tuple.tuple(DbAction.InsertRoot.class, Root.class, IdValueSource.GENERATED), // - Tuple.tuple(DbAction.InsertBatch.class, Intermediate.class, IdValueSource.GENERATED)) // + Tuple.tuple(DbAction.BatchInsert.class, Intermediate.class, IdValueSource.GENERATED)) // .doesNotContain(Tuple.tuple(DbAction.Insert.class, Intermediate.class)); - assertThat(getInsertBatchAction(actions, Intermediate.class, IdValueSource.GENERATED).getActions()) + assertThat(getBatchInsertAction(actions, Intermediate.class, IdValueSource.GENERATED).getActions()) .containsExactly(intermediateInsertGeneratedId); - assertThat(getInsertBatchAction(actions, Intermediate.class, IdValueSource.PROVIDED).getActions()) + assertThat(getBatchInsertAction(actions, Intermediate.class, IdValueSource.PROVIDED).getActions()) .containsExactly(intermediateInsertProvidedId); } @@ -205,11 +205,11 @@ void yieldsNestedInsertActionsInTreeOrderFromRootToLeaves() { assertThat(actions) .extracting(DbAction::getClass, DbAction::getEntityType, DbActionTestSupport::insertIdValueSource) .containsSubsequence( // - Tuple.tuple(DbAction.InsertBatch.class, Intermediate.class, IdValueSource.GENERATED), - Tuple.tuple(DbAction.InsertBatch.class, Leaf.class, IdValueSource.GENERATED)); - assertThat(getInsertBatchAction(actions, Intermediate.class).getActions()) // + Tuple.tuple(DbAction.BatchInsert.class, Intermediate.class, IdValueSource.GENERATED), + Tuple.tuple(DbAction.BatchInsert.class, Leaf.class, IdValueSource.GENERATED)); + assertThat(getBatchInsertAction(actions, Intermediate.class).getActions()) // .containsExactly(root1IntermediateInsert, root2IntermediateInsert); - assertThat(getInsertBatchAction(actions, Leaf.class).getActions()) // + assertThat(getBatchInsertAction(actions, Leaf.class).getActions()) // .containsExactly(root1LeafInsert); } @@ -237,33 +237,33 @@ void yieldsInsertsWithSameLengthReferences_asSeparateInserts() { assertThat(actions) .extracting(DbAction::getClass, DbAction::getEntityType, DbActionTestSupport::insertIdValueSource) .containsSubsequence( // - Tuple.tuple(DbAction.InsertBatch.class, Intermediate.class, IdValueSource.GENERATED), - Tuple.tuple(DbAction.InsertBatch.class, Intermediate.class, IdValueSource.GENERATED)); - List> insertBatchActions = getInsertBatchActions(actions, Intermediate.class); - assertThat(insertBatchActions).hasSize(2); - assertThat(insertBatchActions.get(0).getActions()).containsExactly(oneInsert); - assertThat(insertBatchActions.get(1).getActions()).containsExactly(twoInsert); + Tuple.tuple(DbAction.BatchInsert.class, Intermediate.class, IdValueSource.GENERATED), + Tuple.tuple(DbAction.BatchInsert.class, Intermediate.class, IdValueSource.GENERATED)); + List> batchInsertActions = getBatchInsertActions(actions, Intermediate.class); + assertThat(batchInsertActions).hasSize(2); + assertThat(batchInsertActions.get(0).getActions()).containsExactly(oneInsert); + assertThat(batchInsertActions.get(1).getActions()).containsExactly(twoInsert); } - private DbAction.InsertBatch getInsertBatchAction(List> actions, Class entityType, - IdValueSource idValueSource) { - return getInsertBatchActions(actions, entityType).stream() - .filter(insertBatch -> insertBatch.getBatchValue() == idValueSource).findFirst().orElseThrow( - () -> new RuntimeException(String.format("No InsertBatch with includeId '%s' found!", idValueSource))); + 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 DbAction.InsertBatch getInsertBatchAction(List> actions, Class entityType) { - return getInsertBatchActions(actions, entityType).stream().findFirst() - .orElseThrow(() -> new RuntimeException("No InsertBatch action found!")); + private DbAction.BatchInsert getBatchInsertAction(List> actions, Class entityType) { + return getBatchInsertActions(actions, entityType).stream().findFirst() + .orElseThrow(() -> new RuntimeException("No BatchInsert action found!")); } @SuppressWarnings("unchecked") - private List> getInsertBatchActions(List> actions, Class entityType) { + private List> getBatchInsertActions(List> actions, Class entityType) { return actions.stream() // - .filter(dbAction -> dbAction instanceof DbAction.InsertBatch) // + .filter(dbAction -> dbAction instanceof DbAction.BatchInsert) // .filter(dbAction -> dbAction.getEntityType().equals(entityType)) // - .map(dbAction -> (DbAction.InsertBatch) dbAction).collect(Collectors.toList()); + .map(dbAction -> (DbAction.BatchInsert) dbAction).collect(Collectors.toList()); } private List> extractActions(MergedAggregateChange> change) { From 681559011c5ae6080de538fdfe2d12176f0a949d Mon Sep 17 00:00:00 2001 From: Chirag Tailor Date: Thu, 31 Mar 2022 16:28:41 -0500 Subject: [PATCH 07/11] Polishing and documentation. --- .../jdbc/core/JdbcAggregateOperations.java | 11 +- .../data/jdbc/core/JdbcAggregateTemplate.java | 5 + .../support/SimpleJdbcRepository.java | 5 +- ...AggregateTemplateHsqlIntegrationTests.java | 4 +- ...angeExecutorContextImmutableUnitTests.java | 2 +- .../JdbcRepositoryIntegrationTests.java | 114 +++++++++++- ...RepositorySaveAllHsqlIntegrationTests.java | 173 ------------------ .../JdbcRepositoryIntegrationTests-db2.sql | 25 +++ .../JdbcRepositoryIntegrationTests-h2.sql | 22 +++ .../JdbcRepositoryIntegrationTests-hsql.sql | 22 +++ ...JdbcRepositoryIntegrationTests-mariadb.sql | 22 +++ .../JdbcRepositoryIntegrationTests-mssql.sql | 26 +++ .../JdbcRepositoryIntegrationTests-mysql.sql | 23 +++ .../JdbcRepositoryIntegrationTests-oracle.sql | 25 +++ ...dbcRepositoryIntegrationTests-postgres.sql | 26 +++ ...sitorySaveAllHsqlIntegrationTests-hsql.sql | 21 --- .../relational/core/conversion/DbAction.java | 17 +- .../conversion/MergedAggregateChange.java | 29 +++ .../conversion/MutableAggregateChange.java | 8 + .../conversion/SaveMergedAggregateChange.java | 27 +++ ...RelationalEntityDeleteWriterUnitTests.java | 2 +- ...RelationalEntityInsertWriterUnitTests.java | 2 +- ...RelationalEntityUpdateWriterUnitTests.java | 2 +- .../SaveMergedAggregateChangeTest.java | 20 ++ 24 files changed, 425 insertions(+), 208 deletions(-) delete mode 100644 spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositorySaveAllHsqlIntegrationTests.java delete mode 100644 spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositorySaveAllHsqlIntegrationTests-hsql.sql diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateOperations.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateOperations.java index bd995ad2f5..279502a175 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateOperations.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2021 the original author or authors. + * Copyright 2017-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,6 +26,7 @@ * @author Jens Schauder * @author Thomas Lang * @author Milan Milanov + * @author Chirag Tailor */ public interface JdbcAggregateOperations { @@ -38,6 +39,14 @@ public interface JdbcAggregateOperations { */ T save(T instance); + /** + * Saves all aggregate instances, including all the members of each aggregate instance. + * + * @param instances the aggregate roots to be saved. Must not be {@code null}. + * @param the type of the aggregate root. + * @return the saved instances. + * @since 3.0 + */ Iterable saveAll(Iterable instances); /** diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateTemplate.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateTemplate.java index 110b573e2e..9f089b9584 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateTemplate.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateTemplate.java @@ -287,6 +287,7 @@ public void deleteAll(Class domainType) { } private T afterExecute(AggregateChange change, T entityAfterExecution) { + Object identifier = context.getRequiredPersistentEntity(change.getEntityType()) .getIdentifierAccessor(entityAfterExecution).getIdentifier(); @@ -297,6 +298,7 @@ private T afterExecute(AggregateChange change, T entityAfterExecution) { private AggregateChangeWithRoot beforeExecute(T aggregateRoot, Function> changeCreator) { + Assert.notNull(aggregateRoot, "Aggregate instance must not be null!"); aggregateRoot = triggerBeforeConvert(aggregateRoot); @@ -306,6 +308,7 @@ private AggregateChangeWithRoot beforeExecute(T aggregateRoot, aggregateRoot = triggerBeforeSave(change.getRoot(), change); change.setRoot(aggregateRoot); + return change; } @@ -321,6 +324,7 @@ private void deleteTree(Object id, @Nullable T entity, Class domainType) } private List performSaveChange(Iterable instances, Function>> changeCreatorSelector) { + ArrayList instancesList = new ArrayList<>(); instances.forEach(instancesList::add); Assert.notEmpty(instancesList, "Aggregate instances must not be empty!"); @@ -337,6 +341,7 @@ private List performSaveChange(Iterable instances, Function Function> changeCreatorSelectorForSave(T instance) { + return context.getRequiredPersistentEntity(instance.getClass()).isNew(instance) ? changeCreatorSelectorForInsert(instance) : changeCreatorSelectorForUpdate(instance); diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/SimpleJdbcRepository.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/SimpleJdbcRepository.java index e7c70e5d74..42ad0b9f6e 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/SimpleJdbcRepository.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/SimpleJdbcRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2021 the original author or authors. + * Copyright 2017-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,6 @@ package org.springframework.data.jdbc.repository.support; import java.util.Optional; -import java.util.stream.Collectors; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -25,7 +24,6 @@ import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.PagingAndSortingRepository; -import org.springframework.data.util.Streamable; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.Assert; @@ -35,6 +33,7 @@ * @author Jens Schauder * @author Oliver Gierke * @author Milan Milanov + * @author Chirag Tailor */ @Transactional(readOnly = true) public class SimpleJdbcRepository implements CrudRepository, PagingAndSortingRepository { diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/ImmutableAggregateTemplateHsqlIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/ImmutableAggregateTemplateHsqlIntegrationTests.java index 85940183f1..173b40c049 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/ImmutableAggregateTemplateHsqlIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/ImmutableAggregateTemplateHsqlIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2021 the original author or authors. + * Copyright 2017-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -46,7 +46,7 @@ * * @author Jens Schauder * @author Salim Achouche - * @author Chirag Taylor + * @author Chirag Tailor */ @ContextConfiguration @Transactional diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutorContextImmutableUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutorContextImmutableUnitTests.java index 8da4140e20..88bd07f96e 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutorContextImmutableUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutorContextImmutableUnitTests.java @@ -46,7 +46,7 @@ * Test for the {@link JdbcAggregateChangeExecutionContext} when operating on immutable classes. * * @author Jens Schauder - * @author Chirag Taylor + * @author Chirag Tailor */ public class JdbcAggregateChangeExecutorContextImmutableUnitTests { diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java index b5dff520dd..4aeacaceb4 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java @@ -16,6 +16,7 @@ package org.springframework.data.jdbc.repository; import static java.util.Arrays.*; +import static java.util.Collections.*; import static org.assertj.core.api.Assertions.*; import static org.assertj.core.api.SoftAssertions.*; import static org.springframework.test.context.TestExecutionListeners.MergeMode.*; @@ -50,6 +51,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.data.jdbc.core.mapping.AggregateReference; +import org.springframework.data.relational.core.mapping.MappedCollection; import org.springframework.data.relational.repository.Lock; import org.springframework.data.jdbc.repository.query.Modifying; import org.springframework.data.jdbc.repository.query.Query; @@ -63,6 +65,7 @@ import org.springframework.data.relational.core.mapping.event.AfterLoadEvent; import org.springframework.data.relational.core.sql.LockMode; import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.ListCrudRepository; import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.core.support.PropertiesBasedNamedQueries; import org.springframework.data.repository.query.Param; @@ -80,6 +83,7 @@ * * @author Jens Schauder * @author Mark Paluch + * @author Chirag Tailor */ @Transactional @TestExecutionListeners(value = AssumeFeatureTestExecutionListener.class, mergeMode = MERGE_WITH_DEFAULTS) @@ -89,6 +93,7 @@ public class JdbcRepositoryIntegrationTests { @Autowired NamedParameterJdbcTemplate template; @Autowired DummyEntityRepository repository; @Autowired MyEventListener eventListener; + @Autowired RootRepository rootRepository; private static DummyEntity createDummyEntity() { @@ -282,7 +287,7 @@ public void updateMany() { .containsExactlyInAnyOrder(entity.getName(), other.getName()); } - @Test + @Test // GH-537 void insertsOrUpdatesManyEntities() { DummyEntity entity = repository.save(createDummyEntity()); @@ -589,6 +594,84 @@ void nullStringResult() { assertThat(repository.returnInput(null)).isNull(); } + @Test // GH-537 + void manyInsertsWithNestedEntities() { + Root root1 = createRoot("root1"); + Root root2 = createRoot("root2"); + + List savedRoots = rootRepository.saveAll(asList(root1, root2)); + + List reloadedRoots = rootRepository.findAll(); + assertThat(reloadedRoots).isEqualTo(savedRoots); + assertThat(reloadedRoots).hasSize(2); + assertIsEqualToWithNonNullIds(reloadedRoots.get(0), root1); + assertIsEqualToWithNonNullIds(reloadedRoots.get(1), root2); + } + + @Test // GH-537 + @EnabledOnFeature(TestDatabaseFeatures.Feature.SUPPORTS_GENERATED_IDS_IN_REFERENCED_ENTITIES) + void manyUpdatesWithNestedEntities() { + Root root1 = createRoot("root1"); + Root root2 = createRoot("root2"); + List roots = rootRepository.saveAll(asList(root1, root2)); + Root savedRoot1 = roots.get(0); + Root updatedRoot1 = new Root(savedRoot1.id, "updated" + savedRoot1.name, + new Intermediate(savedRoot1.intermediate.id, "updated" + savedRoot1.intermediate.name, + new Leaf(savedRoot1.intermediate.leaf.id, "updated" + savedRoot1.intermediate.leaf.name), emptyList()), + savedRoot1.intermediates); + Root savedRoot2 = roots.get(1); + Root updatedRoot2 = new Root(savedRoot2.id, "updated" + savedRoot2.name, savedRoot2.intermediate, + singletonList( + new Intermediate(savedRoot2.intermediates.get(0).id, "updated" + savedRoot2.intermediates.get(0).name, null, + singletonList(new Leaf(savedRoot2.intermediates.get(0).leaves.get(0).id, + "updated" + savedRoot2.intermediates.get(0).leaves.get(0).name))))); + + List updatedRoots = rootRepository.saveAll(asList(updatedRoot1, updatedRoot2)); + + List reloadedRoots = rootRepository.findAll(); + assertThat(reloadedRoots).isEqualTo(updatedRoots); + assertThat(reloadedRoots).containsExactly(updatedRoot1, updatedRoot2); + } + + @Test // GH-537 + @EnabledOnFeature(TestDatabaseFeatures.Feature.SUPPORTS_GENERATED_IDS_IN_REFERENCED_ENTITIES) + void manyInsertsAndUpdatesWithNestedEntities() { + Root root1 = createRoot("root1"); + Root savedRoot1 = rootRepository.save(root1); + Root updatedRoot1 = new Root(savedRoot1.id, "updated" + savedRoot1.name, + new Intermediate(savedRoot1.intermediate.id, "updated" + savedRoot1.intermediate.name, + new Leaf(savedRoot1.intermediate.leaf.id, "updated" + savedRoot1.intermediate.leaf.name), emptyList()), + savedRoot1.intermediates); + Root root2 = createRoot("root2"); + List savedRoots = rootRepository.saveAll(asList(updatedRoot1, root2)); + + List reloadedRoots = rootRepository.findAll(); + assertThat(reloadedRoots).isEqualTo(savedRoots); + assertThat(reloadedRoots.get(0)).isEqualTo(updatedRoot1); + assertIsEqualToWithNonNullIds(reloadedRoots.get(1), root2); + } + + private Root createRoot(String namePrefix) { + return new Root(null, namePrefix, + new Intermediate(null, namePrefix + "Intermediate", new Leaf(null, namePrefix + "Leaf"), emptyList()), + singletonList(new Intermediate(null, namePrefix + "QualifiedIntermediate", null, + singletonList(new Leaf(null, namePrefix + "QualifiedLeaf"))))); + } + + private void assertIsEqualToWithNonNullIds(Root reloadedRoot1, Root root1) { + assertThat(reloadedRoot1.id).isNotNull(); + assertThat(reloadedRoot1.name).isEqualTo(root1.name); + assertThat(reloadedRoot1.intermediate.id).isNotNull(); + assertThat(reloadedRoot1.intermediate.name).isEqualTo(root1.intermediate.name); + assertThat(reloadedRoot1.intermediates.get(0).id).isNotNull(); + assertThat(reloadedRoot1.intermediates.get(0).name).isEqualTo(root1.intermediates.get(0).name); + assertThat(reloadedRoot1.intermediate.leaf.id).isNotNull(); + assertThat(reloadedRoot1.intermediate.leaf.name).isEqualTo(root1.intermediate.leaf.name); + assertThat(reloadedRoot1.intermediates.get(0).leaves.get(0).id).isNotNull(); + assertThat(reloadedRoot1.intermediates.get(0).leaves.get(0).name) + .isEqualTo(root1.intermediates.get(0).leaves.get(0).name); + } + private Instant createDummyBeforeAndAfterNow() { Instant now = Instant.now(); @@ -692,6 +775,11 @@ DummyEntityRepository dummyEntityRepository() { return factory.getRepository(DummyEntityRepository.class); } + @Bean + RootRepository rootRepository() { + return factory.getRepository(RootRepository.class); + } + @Bean NamedQueries namedQueries() throws IOException { @@ -707,6 +795,30 @@ MyEventListener eventListener() { } } + interface RootRepository extends ListCrudRepository {} + + @Value + static class Root { + @Id Long id; + String name; + Intermediate intermediate; + @MappedCollection(idColumn = "ROOT_ID", keyColumn = "ROOT_KEY") List intermediates; + } + + @Value + static class Intermediate { + @Id Long id; + String name; + Leaf leaf; + @MappedCollection(idColumn = "INTERMEDIATE_ID", keyColumn = "INTERMEDIATE_KEY") List leaves; + } + + @Value + static class Leaf { + @Id Long id; + String name; + } + static class MyEventListener implements ApplicationListener> { private List> events = new ArrayList<>(); diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositorySaveAllHsqlIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositorySaveAllHsqlIntegrationTests.java deleted file mode 100644 index fd7e2082e9..0000000000 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositorySaveAllHsqlIntegrationTests.java +++ /dev/null @@ -1,173 +0,0 @@ -/* - * Copyright 2022 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.jdbc.repository; - -import static java.util.Arrays.*; -import static java.util.Collections.*; -import static org.assertj.core.api.Assertions.*; -import static org.springframework.test.context.TestExecutionListeners.MergeMode.*; - -import java.util.List; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import org.springframework.data.annotation.Id; -import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory; -import org.springframework.data.jdbc.testing.AssumeFeatureTestExecutionListener; -import org.springframework.data.jdbc.testing.TestConfiguration; -import org.springframework.data.relational.core.mapping.MappedCollection; -import org.springframework.data.repository.ListCrudRepository; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.TestExecutionListeners; -import org.springframework.test.context.junit.jupiter.SpringExtension; -import org.springframework.transaction.annotation.Transactional; - -import lombok.Value; - -/** - * Use cases of JdbcRepositories where operations are carried out across multiple aggregate roots at a time. - * - * @author Chirag Tailor - */ -@Transactional -@TestExecutionListeners(value = AssumeFeatureTestExecutionListener.class, mergeMode = MERGE_WITH_DEFAULTS) -@ExtendWith(SpringExtension.class) -@ActiveProfiles("hsql") -public class JdbcRepositorySaveAllHsqlIntegrationTests { - - @Autowired RootRepository rootRepository; - - @Test - void manyInsertsWithNestedEntities() { - Root root1 = createRoot("root1"); - Root root2 = createRoot("root2"); - - List savedRoots = rootRepository.saveAll(asList(root1, root2)); - - List reloadedRoots = rootRepository.findAll(); - assertThat(reloadedRoots).isEqualTo(savedRoots); - assertThat(reloadedRoots).hasSize(2); - assertIsEqualToWithNonNullIds(reloadedRoots.get(0), root1); - assertIsEqualToWithNonNullIds(reloadedRoots.get(1), root2); - } - - @Test - void manyUpdatesWithNestedEntities() { - Root root1 = createRoot("root1"); - Root root2 = createRoot("root2"); - List roots = rootRepository.saveAll(asList(root1, root2)); - Root savedRoot1 = roots.get(0); - Root updatedRoot1 = new Root(savedRoot1.id, "updated" + savedRoot1.name, - new Intermediate(savedRoot1.intermediate.id, "updated" + savedRoot1.intermediate.name, - new Leaf(savedRoot1.intermediate.leaf.id, "updated" + savedRoot1.intermediate.leaf.name), emptyList()), - savedRoot1.intermediates); - Root savedRoot2 = roots.get(1); - Root updatedRoot2 = new Root(savedRoot2.id, "updated" + savedRoot2.name, savedRoot2.intermediate, - singletonList( - new Intermediate(savedRoot2.intermediates.get(0).id, "updated" + savedRoot2.intermediates.get(0).name, null, - singletonList(new Leaf(savedRoot2.intermediates.get(0).leaves.get(0).id, - "updated" + savedRoot2.intermediates.get(0).leaves.get(0).name))))); - - List updatedRoots = rootRepository.saveAll(asList(updatedRoot1, updatedRoot2)); - - List reloadedRoots = rootRepository.findAll(); - assertThat(reloadedRoots).isEqualTo(updatedRoots); - assertThat(reloadedRoots).containsExactly(updatedRoot1, updatedRoot2); - } - - @Test - void manyInsertsAndUpdatedWithNesteEntities() { - Root root1 = createRoot("root1"); - Root savedRoot1 = rootRepository.save(root1); - Root updatedRoot1 = new Root(savedRoot1.id, "updated" + savedRoot1.name, - new Intermediate(savedRoot1.intermediate.id, "updated" + savedRoot1.intermediate.name, - new Leaf(savedRoot1.intermediate.leaf.id, "updated" + savedRoot1.intermediate.leaf.name), emptyList()), - savedRoot1.intermediates); - Root root2 = createRoot("root2"); - List savedRoots = rootRepository.saveAll(asList(updatedRoot1, root2)); - - List reloadedRoots = rootRepository.findAll(); - assertThat(reloadedRoots).isEqualTo(savedRoots); - assertThat(reloadedRoots.get(0)).isEqualTo(updatedRoot1); - assertIsEqualToWithNonNullIds(reloadedRoots.get(1), root2); - } - - private Root createRoot(String namePrefix) { - return new Root(null, namePrefix, - new Intermediate(null, namePrefix + "Intermediate", new Leaf(null, namePrefix + "Leaf"), emptyList()), - singletonList(new Intermediate(null, namePrefix + "QualifiedIntermediate", null, - singletonList(new Leaf(null, namePrefix + "QualifiedLeaf"))))); - } - - private void assertIsEqualToWithNonNullIds(Root reloadedRoot1, Root root1) { - assertThat(reloadedRoot1.id).isNotNull(); - assertThat(reloadedRoot1.name).isEqualTo(root1.name); - assertThat(reloadedRoot1.intermediate.id).isNotNull(); - assertThat(reloadedRoot1.intermediate.name).isEqualTo(root1.intermediate.name); - assertThat(reloadedRoot1.intermediates.get(0).id).isNotNull(); - assertThat(reloadedRoot1.intermediates.get(0).name).isEqualTo(root1.intermediates.get(0).name); - assertThat(reloadedRoot1.intermediate.leaf.id).isNotNull(); - assertThat(reloadedRoot1.intermediate.leaf.name).isEqualTo(root1.intermediate.leaf.name); - assertThat(reloadedRoot1.intermediates.get(0).leaves.get(0).id).isNotNull(); - assertThat(reloadedRoot1.intermediates.get(0).leaves.get(0).name) - .isEqualTo(root1.intermediates.get(0).leaves.get(0).name); - } - - @Configuration - @Import(TestConfiguration.class) - static class Config { - - @Autowired JdbcRepositoryFactory factory; - - @Bean - Class testClass() { - return JdbcRepositorySaveAllHsqlIntegrationTests.class; - } - - @Bean - RootRepository rootRepository() { - return factory.getRepository(RootRepository.class); - } - } - - interface RootRepository extends ListCrudRepository {} - - @Value - static class Root { - @Id Long id; - String name; - Intermediate intermediate; - @MappedCollection(idColumn = "ROOT_ID", keyColumn = "ROOT_KEY") List intermediates; - } - - @Value - static class Intermediate { - @Id Long id; - String name; - Leaf leaf; - @MappedCollection(idColumn = "INTERMEDIATE_ID", keyColumn = "INTERMEDIATE_KEY") List leaves; - } - - @Value - static class Leaf { - @Id Long id; - String name; - } -} diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-db2.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-db2.sql index 34be74ec51..6cf7b106bd 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-db2.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-db2.sql @@ -1,4 +1,7 @@ DROP TABLE dummy_entity; +DROP TABLE ROOT; +DROP TABLE INTERMEDIATE; +DROP TABLE LEAF; CREATE TABLE dummy_entity ( @@ -9,3 +12,25 @@ CREATE TABLE dummy_entity FLAG BOOLEAN, REF BIGINT ); + +CREATE TABLE ROOT +( + ID BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, + NAME VARCHAR(100) +); +CREATE TABLE INTERMEDIATE +( + ID BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, + NAME VARCHAR(100), + ROOT BIGINT, + ROOT_ID BIGINT, + ROOT_KEY INTEGER +); +CREATE TABLE LEAF +( + ID BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, + NAME VARCHAR(100), + INTERMEDIATE BIGINT, + INTERMEDIATE_ID BIGINT, + INTERMEDIATE_KEY INTEGER +); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-h2.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-h2.sql index b3b93bc744..2515db8847 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-h2.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-h2.sql @@ -7,3 +7,25 @@ CREATE TABLE dummy_entity FLAG BOOLEAN, REF BIGINT ); + +CREATE TABLE ROOT +( + ID BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, + NAME VARCHAR(100) +); +CREATE TABLE INTERMEDIATE +( + ID BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, + NAME VARCHAR(100), + ROOT BIGINT, + ROOT_ID BIGINT, + ROOT_KEY INTEGER +); +CREATE TABLE LEAF +( + ID BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, + NAME VARCHAR(100), + INTERMEDIATE BIGINT, + INTERMEDIATE_ID BIGINT, + INTERMEDIATE_KEY INTEGER +); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-hsql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-hsql.sql index b3b93bc744..2515db8847 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-hsql.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-hsql.sql @@ -7,3 +7,25 @@ CREATE TABLE dummy_entity FLAG BOOLEAN, REF BIGINT ); + +CREATE TABLE ROOT +( + ID BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, + NAME VARCHAR(100) +); +CREATE TABLE INTERMEDIATE +( + ID BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, + NAME VARCHAR(100), + ROOT BIGINT, + ROOT_ID BIGINT, + ROOT_KEY INTEGER +); +CREATE TABLE LEAF +( + ID BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, + NAME VARCHAR(100), + INTERMEDIATE BIGINT, + INTERMEDIATE_ID BIGINT, + INTERMEDIATE_KEY INTEGER +); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mariadb.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mariadb.sql index 949e626399..543c0ea57e 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mariadb.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mariadb.sql @@ -7,3 +7,25 @@ CREATE TABLE dummy_entity FLAG BOOLEAN, REF BIGINT ); + +CREATE TABLE ROOT +( + ID BIGINT AUTO_INCREMENT PRIMARY KEY, + NAME VARCHAR(100) +); +CREATE TABLE INTERMEDIATE +( + ID BIGINT AUTO_INCREMENT PRIMARY KEY, + NAME VARCHAR(100), + ROOT BIGINT, + ROOT_ID BIGINT, + ROOT_KEY INTEGER +); +CREATE TABLE LEAF +( + ID BIGINT AUTO_INCREMENT PRIMARY KEY, + NAME VARCHAR(100), + INTERMEDIATE BIGINT, + INTERMEDIATE_ID BIGINT, + INTERMEDIATE_KEY INTEGER +); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mssql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mssql.sql index 15f8881327..8405222840 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mssql.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mssql.sql @@ -1,4 +1,8 @@ DROP TABLE IF EXISTS dummy_entity; +DROP TABLE IF EXISTS ROOT; +DROP TABLE IF EXISTS INTERMEDIATE; +DROP TABLE IF EXISTS LEAF; + CREATE TABLE dummy_entity ( id_Prop BIGINT IDENTITY PRIMARY KEY, @@ -8,3 +12,25 @@ CREATE TABLE dummy_entity FLAG BIT, REF BIGINT ); + +CREATE TABLE ROOT +( + ID BIGINT IDENTITY PRIMARY KEY, + NAME VARCHAR(100) +); +CREATE TABLE INTERMEDIATE +( + ID BIGINT IDENTITY PRIMARY KEY, + NAME VARCHAR(100), + ROOT BIGINT, + ROOT_ID BIGINT, + ROOT_KEY INTEGER +); +CREATE TABLE LEAF +( + ID BIGINT IDENTITY PRIMARY KEY, + NAME VARCHAR(100), + INTERMEDIATE BIGINT, + INTERMEDIATE_ID BIGINT, + INTERMEDIATE_KEY INTEGER +); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mysql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mysql.sql index e3baa94602..035cf8a85a 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mysql.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mysql.sql @@ -10,3 +10,26 @@ CREATE TABLE DUMMY_ENTITY FLAG BIT(1), REF BIGINT ); + +CREATE TABLE ROOT +( + ID BIGINT AUTO_INCREMENT PRIMARY KEY, + NAME VARCHAR(100) +); +CREATE TABLE INTERMEDIATE +( + ID BIGINT AUTO_INCREMENT PRIMARY KEY, + NAME VARCHAR(100), + ROOT BIGINT, + ROOT_ID BIGINT, + ROOT_KEY INTEGER +); +CREATE TABLE LEAF +( + ID BIGINT AUTO_INCREMENT PRIMARY KEY, + NAME VARCHAR(100), + INTERMEDIATE BIGINT, + INTERMEDIATE_ID BIGINT, + INTERMEDIATE_KEY INTEGER +); + diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-oracle.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-oracle.sql index e71eb63286..348c777649 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-oracle.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-oracle.sql @@ -1,4 +1,7 @@ DROP TABLE DUMMY_ENTITY CASCADE CONSTRAINTS PURGE; +DROP TABLE ROOT CASCADE CONSTRAINTS PURGE; +DROP TABLE INTERMEDIATE CASCADE CONSTRAINTS PURGE; +DROP TABLE LEAF CASCADE CONSTRAINTS PURGE; CREATE TABLE DUMMY_ENTITY ( @@ -9,3 +12,25 @@ CREATE TABLE DUMMY_ENTITY FLAG NUMBER(1,0), REF NUMBER ); + +CREATE TABLE ROOT +( + ID NUMBER GENERATED BY DEFAULT ON NULL AS IDENTITY PRIMARY KEY, + NAME VARCHAR2(100) +); +CREATE TABLE INTERMEDIATE +( + ID NUMBER GENERATED BY DEFAULT ON NULL AS IDENTITY PRIMARY KEY, + NAME VARCHAR2(100), + ROOT NUMBER, + ROOT_ID NUMBER, + ROOT_KEY NUMBER +); +CREATE TABLE LEAF +( + ID NUMBER GENERATED BY DEFAULT ON NULL AS IDENTITY PRIMARY KEY, + NAME VARCHAR2(100), + INTERMEDIATE NUMBER, + INTERMEDIATE_ID NUMBER, + INTERMEDIATE_KEY NUMBER +); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-postgres.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-postgres.sql index 97fc78c9da..a41b3e522c 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-postgres.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-postgres.sql @@ -1,4 +1,8 @@ DROP TABLE dummy_entity; +DROP TABLE ROOT; +DROP TABLE INTERMEDIATE; +DROP TABLE LEAF; + CREATE TABLE dummy_entity ( id_Prop SERIAL PRIMARY KEY, @@ -8,3 +12,25 @@ CREATE TABLE dummy_entity FLAG BOOLEAN, REF BIGINT ); + +CREATE TABLE ROOT +( + ID SERIAL PRIMARY KEY, + NAME VARCHAR(100) +); +CREATE TABLE INTERMEDIATE +( + ID SERIAL PRIMARY KEY, + NAME VARCHAR(100), + ROOT BIGINT, + "ROOT_ID" BIGINT, + "ROOT_KEY" INTEGER +); +CREATE TABLE LEAF +( + ID SERIAL PRIMARY KEY, + NAME VARCHAR(100), + INTERMEDIATE BIGINT, + "INTERMEDIATE_ID" BIGINT, + "INTERMEDIATE_KEY" INTEGER +); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositorySaveAllHsqlIntegrationTests-hsql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositorySaveAllHsqlIntegrationTests-hsql.sql deleted file mode 100644 index bcf4b00332..0000000000 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositorySaveAllHsqlIntegrationTests-hsql.sql +++ /dev/null @@ -1,21 +0,0 @@ -CREATE TABLE ROOT -( - ID BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, - NAME VARCHAR(100) -); -CREATE TABLE INTERMEDIATE -( - ID BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, - NAME VARCHAR(100), - ROOT BIGINT, - ROOT_ID BIGINT, - ROOT_KEY INTEGER -); -CREATE TABLE LEAF -( - ID BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, - NAME VARCHAR(100), - INTERMEDIATE BIGINT, - INTERMEDIATE_ID BIGINT, - INTERMEDIATE_KEY INTEGER -); 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 da2d8e7ff4..ec3dad3169 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 @@ -17,6 +17,7 @@ import java.util.Collections; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.function.Function; @@ -345,15 +346,25 @@ public String toString() { } } + /** + * Represents a batch of {@link DbAction} that share a common value for a property of the action. + * + * @param type of the entity for which this represents a database interaction. + * @since 3.0 + */ abstract class BatchWithValue, B> implements DbAction { private final List actions; private final B batchValue; public BatchWithValue(List actions, Function batchValueExtractor) { Assert.notEmpty(actions, "Actions must contain at least one action"); - this.batchValue = batchValueExtractor.apply(actions.get(0)); - Assert.isTrue(actions.stream().allMatch(a -> batchValueExtractor.apply(a).equals(batchValue)), - "All actions in the batch must have matching batchValue"); + Iterator actionIterator = actions.iterator(); + this.batchValue = batchValueExtractor.apply(actionIterator.next()); + actionIterator.forEachRemaining(action -> { + if (!batchValueExtractor.apply(action).equals(batchValue)) { + throw new IllegalArgumentException("All actions in the batch must have matching batchValue"); + } + }); this.actions = actions; } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/MergedAggregateChange.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/MergedAggregateChange.java index c64ec5e5c9..80db45cc00 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/MergedAggregateChange.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/MergedAggregateChange.java @@ -1,5 +1,34 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.springframework.data.relational.core.conversion; +/** + * Represents the changes happening to one or more aggregates (as used in the context of Domain Driven Design) as a + * whole. This change allows additional {@link MutableAggregateChange} of a particular kind to be merged into it to + * broadly represent the changes to multiple aggregates across all such merged changes. + * + * @author Chirag Tailor + * @since 3.0 + */ public interface MergedAggregateChange> extends AggregateChange { + /** + * Merges a {@code MutableAggregateChange} into this {@code MergedAggregateChange}. + * + * @param aggregateChange must not be {@literal null}. + * @return the change resulting from the merge. Guaranteed to be not {@literal null}. + */ MergedAggregateChange merge(C aggregateChange); } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/MutableAggregateChange.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/MutableAggregateChange.java index 0ab9f3fc44..67b6186eda 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/MutableAggregateChange.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/MutableAggregateChange.java @@ -74,6 +74,14 @@ static MutableAggregateChange forDelete(T entity) { return forDelete((Class) ClassUtils.getUserClass(entity)); } + /** + * Factory method to create a {@link MergedAggregateChange} for saving entities. + * + * @param entityClass aggregate root type. + * @param entity type. + * @return the {@link MergedAggregateChange} for saving root entities. + * @since 3.0 + */ static MergedAggregateChange> mergedSave(Class entityClass) { Assert.notNull(entityClass, "Entity class must not be null"); diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/SaveMergedAggregateChange.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/SaveMergedAggregateChange.java index 8706007c49..a7ed03eeea 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/SaveMergedAggregateChange.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/SaveMergedAggregateChange.java @@ -1,3 +1,18 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.springframework.data.relational.core.conversion; import static java.util.Collections.*; @@ -14,6 +29,15 @@ import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; import org.springframework.util.Assert; +/** + * A {@link org.springframework.data.relational.core.conversion.MergedAggregateChange} implementation for save changes + * that can contain actions for any mix of insert and update operations. When consumed, actions are yielded in the + * appropriate entity tree order with inserts carried out from root to leaves and deletes in reverse. All insert + * operations are grouped into batches to offer the ability for an optimized batch operation to be used. + * + * @author Chirag Tailor + * @since 3.0 + */ public class SaveMergedAggregateChange implements MergedAggregateChange> { private static final Comparator> pathLengthComparator = // @@ -55,6 +79,7 @@ public void forEachAction(Consumer> consumer) { @Override public MergedAggregateChange> merge(AggregateChangeWithRoot aggregateChange) { + aggregateChange.forEachAction(action -> { if (action instanceof DbAction.WithRoot rootAction) { rootActions.add(rootAction); @@ -69,6 +94,7 @@ public MergedAggregateChange> merge(AggregateChang } private void addInsert(DbAction.Insert action) { + PersistentPropertyPath propertyPath = action.getPropertyPath(); insertActions.merge(propertyPath, new EnumMap<>(singletonMap(action.getIdValueSource(), new ArrayList<>(singletonList(action)))), @@ -83,6 +109,7 @@ private void addInsert(DbAction.Insert action) { } private void addDelete(DbAction.Delete action) { + PersistentPropertyPath propertyPath = action.getPropertyPath(); deleteActions.merge(propertyPath, new ArrayList<>(singletonList(action)), (actions, defaultValue) -> { actions.add(action); diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityDeleteWriterUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityDeleteWriterUnitTests.java index c7c2b02547..777936dfbd 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityDeleteWriterUnitTests.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityDeleteWriterUnitTests.java @@ -39,7 +39,7 @@ * * @author Jens Schauder * @author Myeonghyeon Lee - * @author Chirag Taylor + * @author Chirag Tailor */ @ExtendWith(MockitoExtension.class) public class RelationalEntityDeleteWriterUnitTests { diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityInsertWriterUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityInsertWriterUnitTests.java index d0e483ec0f..25ce6dd7b2 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityInsertWriterUnitTests.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityInsertWriterUnitTests.java @@ -33,7 +33,7 @@ * Unit tests for the {@link RelationalEntityInsertWriter} * * @author Thomas Lang - * @author Chirag Taylor + * @author Chirag Tailor */ @ExtendWith(MockitoExtension.class) public class RelationalEntityInsertWriterUnitTests { diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityUpdateWriterUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityUpdateWriterUnitTests.java index 516688a6c6..2accbb07da 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityUpdateWriterUnitTests.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityUpdateWriterUnitTests.java @@ -33,7 +33,7 @@ * * @author Thomas Lang * @author Myeonghyeon Lee - * @author Chirag Taylor + * @author Chirag Tailor */ @ExtendWith(MockitoExtension.class) public class RelationalEntityUpdateWriterUnitTests { diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/SaveMergedAggregateChangeTest.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/SaveMergedAggregateChangeTest.java index 7c051e086f..2764170a20 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/SaveMergedAggregateChangeTest.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/SaveMergedAggregateChangeTest.java @@ -1,3 +1,18 @@ +/* + * Copyright 2020-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.springframework.data.relational.core.conversion; import static java.util.Collections.*; @@ -14,6 +29,11 @@ import lombok.Value; +/** + * Unit tests for {@link SaveMergedAggregateChange}. + * + * @author Chirag Tailor + */ class SaveMergedAggregateChangeTest { RelationalMappingContext context = new RelationalMappingContext(); From 671074acf83a67845747e0e95c6b3c458af772b9 Mon Sep 17 00:00:00 2001 From: Chirag Tailor Date: Wed, 6 Apr 2022 09:17:40 -0500 Subject: [PATCH 08/11] Address review feedback. --- .../JdbcAggregateChangeExecutionContext.java | 2 +- .../data/jdbc/core/JdbcAggregateTemplate.java | 78 +++++++++++-------- .../JdbcRepositoryIntegrationTests.java | 10 ++- ...ange.java => BatchingAggregateChange.java} | 28 +++++-- .../conversion/MutableAggregateChange.java | 15 ---- ....java => SaveBatchingAggregateChange.java} | 20 +++-- ...a => SaveBatchingAggregateChangeTest.java} | 67 ++++++++-------- 7 files changed, 114 insertions(+), 106 deletions(-) rename spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/{MergedAggregateChange.java => BatchingAggregateChange.java} (55%) rename spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/{SaveMergedAggregateChange.java => SaveBatchingAggregateChange.java} (83%) rename spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/{SaveMergedAggregateChangeTest.java => SaveBatchingAggregateChangeTest.java} (87%) 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 3037d3f562..426e3fb2b7 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 @@ -224,7 +224,7 @@ List populateIdsIfNecessary() { StagedValues cascadingValues = new StagedValues(); - List roots = new ArrayList<>(); + List roots = new ArrayList<>(reverseResults.size()); for (DbActionExecutionResult result : reverseResults) { diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateTemplate.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateTemplate.java index 9f089b9584..1171036f10 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateTemplate.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateTemplate.java @@ -16,6 +16,7 @@ package org.springframework.data.jdbc.core; import java.util.ArrayList; +import java.util.Iterator; import java.util.List; import java.util.function.Function; import java.util.stream.Collectors; @@ -32,7 +33,7 @@ import org.springframework.data.mapping.callback.EntityCallbacks; import org.springframework.data.relational.core.conversion.AggregateChange; import org.springframework.data.relational.core.conversion.AggregateChangeWithRoot; -import org.springframework.data.relational.core.conversion.MergedAggregateChange; +import org.springframework.data.relational.core.conversion.BatchingAggregateChange; import org.springframework.data.relational.core.conversion.MutableAggregateChange; import org.springframework.data.relational.core.conversion.RelationalEntityDeleteWriter; import org.springframework.data.relational.core.conversion.RelationalEntityInsertWriter; @@ -47,8 +48,6 @@ import org.springframework.util.Assert; import org.springframework.util.ClassUtils; -import static java.util.Collections.*; - /** * {@link JdbcAggregateOperations} implementation, storing aggregates in and obtaining them from a JDBC data store. * @@ -143,14 +142,15 @@ public T save(T instance) { Assert.notNull(instance, "Aggregate instance must not be null!"); - return StreamSupport.stream(saveAll(singletonList(instance)).spliterator(), false).findFirst() - .orElseThrow(() -> new IllegalStateException( - String.format("Unable to retrieve the result of executing aggregate change for instance: %s", instance))); + return performSave(instance, changeCreatorSelectorForSave(instance)); } @Override public Iterable saveAll(Iterable instances) { - return performSaveChange(instances, this::changeCreatorSelectorForSave); + + Assert.isTrue(instances.iterator().hasNext(), "Aggregate instances must not be empty!"); + + return performSaveAll(instances); } /** @@ -165,9 +165,7 @@ public T insert(T instance) { Assert.notNull(instance, "Aggregate instance must not be null!"); - return performSaveChange(singletonList(instance), this::changeCreatorSelectorForInsert).stream().findFirst() - .orElseThrow(() -> new IllegalStateException( - String.format("Unable to retrieve the result of executing aggregate change for instance: %s", instance))); + return performSave(instance, entity -> createInsertChange(prepareVersionForInsert(entity))); } /** @@ -182,9 +180,7 @@ public T update(T instance) { Assert.notNull(instance, "Aggregate instance must not be null!"); - return performSaveChange(singletonList(instance), this::changeCreatorSelectorForUpdate).stream().findFirst() - .orElseThrow(() -> new IllegalStateException( - String.format("Unable to retrieve the result of executing aggregate change for instance: %s", instance))); + return performSave(instance, entity -> createUpdateChange(prepareVersionForUpdate(entity))); } @Override @@ -323,36 +319,50 @@ private void deleteTree(Object id, @Nullable T entity, Class domainType) triggerAfterDelete(entity, id, change); } - private List performSaveChange(Iterable instances, Function>> changeCreatorSelector) { - - ArrayList instancesList = new ArrayList<>(); - instances.forEach(instancesList::add); - Assert.notEmpty(instancesList, "Aggregate instances must not be empty!"); + private T performSave(T instance, Function> changeCreator) { // noinspection unchecked - MergedAggregateChange> mergedAggregateChange = instancesList.stream() // - .map(instance -> beforeExecute(instance, changeCreatorSelector.apply(instance))) // - .reduce(MutableAggregateChange.mergedSave((Class) ClassUtils.getUserClass(instancesList.get(0))), - MergedAggregateChange::merge, (left, right) -> right); + BatchingAggregateChange> batchingAggregateChange = // + BatchingAggregateChange.forSave((Class) ClassUtils.getUserClass(instance)); + batchingAggregateChange.add(beforeExecute(instance, changeCreator)); - return executor.executeSave(mergedAggregateChange).stream() - .map(entityAfterExecution -> afterExecute(mergedAggregateChange, entityAfterExecution)) - .collect(Collectors.toList()); - } + Iterator afterExecutionIterator = executor.executeSave(batchingAggregateChange).iterator(); - private Function> changeCreatorSelectorForSave(T instance) { + Assert.isTrue(afterExecutionIterator.hasNext(), "Instances after execution must not be empty!"); - return context.getRequiredPersistentEntity(instance.getClass()).isNew(instance) - ? changeCreatorSelectorForInsert(instance) - : changeCreatorSelectorForUpdate(instance); + return afterExecute(batchingAggregateChange, afterExecutionIterator.next()); } - private Function> changeCreatorSelectorForInsert(T instance) { - return entity -> createInsertChange(prepareVersionForInsert(entity)); + private List performSaveAll(Iterable instances) { + + Iterator iterator = instances.iterator(); + T firstInstance = iterator.next(); + + // noinspection unchecked + BatchingAggregateChange> batchingAggregateChange = // + BatchingAggregateChange.forSave((Class) ClassUtils.getUserClass(firstInstance)); + batchingAggregateChange.add(beforeExecute(firstInstance, changeCreatorSelectorForSave(firstInstance))); + + while (iterator.hasNext()) { + T instance = iterator.next(); + batchingAggregateChange.add(beforeExecute(instance, changeCreatorSelectorForSave(instance))); + } + + List instancesAfterExecution = executor.executeSave(batchingAggregateChange); + + ArrayList results = new ArrayList<>(instancesAfterExecution.size()); + for (T instance : instancesAfterExecution) { + results.add(afterExecute(batchingAggregateChange, instance)); + } + + return results; } - private Function> changeCreatorSelectorForUpdate(T instance) { - return entity -> createUpdateChange(prepareVersionForUpdate(entity)); + private Function> changeCreatorSelectorForSave(T instance) { + + return context.getRequiredPersistentEntity(instance.getClass()).isNew(instance) + ? entity -> createInsertChange(prepareVersionForInsert(entity)) + : entity -> createUpdateChange(prepareVersionForUpdate(entity)); } private AggregateChangeWithRoot createInsertChange(T instance) { diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java index 4aeacaceb4..a665b114fc 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java @@ -601,7 +601,7 @@ void manyInsertsWithNestedEntities() { List savedRoots = rootRepository.saveAll(asList(root1, root2)); - List reloadedRoots = rootRepository.findAll(); + List reloadedRoots = rootRepository.findAllByOrderByIdAsc(); assertThat(reloadedRoots).isEqualTo(savedRoots); assertThat(reloadedRoots).hasSize(2); assertIsEqualToWithNonNullIds(reloadedRoots.get(0), root1); @@ -628,7 +628,7 @@ void manyUpdatesWithNestedEntities() { List updatedRoots = rootRepository.saveAll(asList(updatedRoot1, updatedRoot2)); - List reloadedRoots = rootRepository.findAll(); + List reloadedRoots = rootRepository.findAllByOrderByIdAsc(); assertThat(reloadedRoots).isEqualTo(updatedRoots); assertThat(reloadedRoots).containsExactly(updatedRoot1, updatedRoot2); } @@ -645,7 +645,7 @@ void manyInsertsAndUpdatesWithNestedEntities() { Root root2 = createRoot("root2"); List savedRoots = rootRepository.saveAll(asList(updatedRoot1, root2)); - List reloadedRoots = rootRepository.findAll(); + List reloadedRoots = rootRepository.findAllByOrderByIdAsc(); assertThat(reloadedRoots).isEqualTo(savedRoots); assertThat(reloadedRoots.get(0)).isEqualTo(updatedRoot1); assertIsEqualToWithNonNullIds(reloadedRoots.get(1), root2); @@ -795,7 +795,9 @@ MyEventListener eventListener() { } } - interface RootRepository extends ListCrudRepository {} + interface RootRepository extends ListCrudRepository { + List findAllByOrderByIdAsc(); + } @Value static class Root { diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/MergedAggregateChange.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/BatchingAggregateChange.java similarity index 55% rename from spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/MergedAggregateChange.java rename to spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/BatchingAggregateChange.java index 80db45cc00..3ce54fc2c4 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/MergedAggregateChange.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/BatchingAggregateChange.java @@ -15,20 +15,36 @@ */ package org.springframework.data.relational.core.conversion; +import org.springframework.util.Assert; + /** * Represents the changes happening to one or more aggregates (as used in the context of Domain Driven Design) as a - * whole. This change allows additional {@link MutableAggregateChange} of a particular kind to be merged into it to - * broadly represent the changes to multiple aggregates across all such merged changes. + * whole. This change allows additional {@link MutableAggregateChange} of a particular kind to be added to it to + * broadly represent the changes to multiple aggregates across all such added changes. * * @author Chirag Tailor * @since 3.0 */ -public interface MergedAggregateChange> extends AggregateChange { +public interface BatchingAggregateChange> extends AggregateChange { /** - * Merges a {@code MutableAggregateChange} into this {@code MergedAggregateChange}. + * Adds a {@code MutableAggregateChange} into this {@code BatchingAggregateChange}. * * @param aggregateChange must not be {@literal null}. - * @return the change resulting from the merge. Guaranteed to be not {@literal null}. */ - MergedAggregateChange merge(C aggregateChange); + void add(C aggregateChange); + + /** + * Factory method to create a {@link BatchingAggregateChange} for saving entities. + * + * @param entityClass aggregate root type. + * @param entity type. + * @return the {@link BatchingAggregateChange} for saving root entities. + * @since 3.0 + */ + static BatchingAggregateChange> forSave(Class entityClass) { + + Assert.notNull(entityClass, "Entity class must not be null"); + + return new SaveBatchingAggregateChange<>(entityClass); + } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/MutableAggregateChange.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/MutableAggregateChange.java index 67b6186eda..a76d17ab7a 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/MutableAggregateChange.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/MutableAggregateChange.java @@ -74,21 +74,6 @@ static MutableAggregateChange forDelete(T entity) { return forDelete((Class) ClassUtils.getUserClass(entity)); } - /** - * Factory method to create a {@link MergedAggregateChange} for saving entities. - * - * @param entityClass aggregate root type. - * @param entity type. - * @return the {@link MergedAggregateChange} for saving root entities. - * @since 3.0 - */ - static MergedAggregateChange> mergedSave(Class entityClass) { - - Assert.notNull(entityClass, "Entity class must not be null"); - - return new SaveMergedAggregateChange<>(entityClass); - } - /** * Factory method to create an {@link MutableAggregateChange} for deleting entities. * diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/SaveMergedAggregateChange.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/SaveBatchingAggregateChange.java similarity index 83% rename from spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/SaveMergedAggregateChange.java rename to spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/SaveBatchingAggregateChange.java index a7ed03eeea..53bbc6206f 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/SaveMergedAggregateChange.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/SaveBatchingAggregateChange.java @@ -19,7 +19,6 @@ import java.util.ArrayList; import java.util.Comparator; -import java.util.EnumMap; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -30,7 +29,7 @@ import org.springframework.util.Assert; /** - * A {@link org.springframework.data.relational.core.conversion.MergedAggregateChange} implementation for save changes + * A {@link BatchingAggregateChange} implementation for save changes * that can contain actions for any mix of insert and update operations. When consumed, actions are yielded in the * appropriate entity tree order with inserts carried out from root to leaves and deletes in reverse. All insert * operations are grouped into batches to offer the ability for an optimized batch operation to be used. @@ -38,19 +37,19 @@ * @author Chirag Tailor * @since 3.0 */ -public class SaveMergedAggregateChange implements MergedAggregateChange> { +public class SaveBatchingAggregateChange implements BatchingAggregateChange> { private static final Comparator> pathLengthComparator = // Comparator.comparing(PersistentPropertyPath::getLength); private final Class entityType; private final List> rootActions = new ArrayList<>(); - private final Map, EnumMap>>> insertActions = // + private final Map, Map>>> insertActions = // new HashMap<>(); private final Map, List>> deleteActions = // new HashMap<>(); - public SaveMergedAggregateChange(Class entityType) { + public SaveBatchingAggregateChange(Class entityType) { this.entityType = entityType; } @@ -78,7 +77,7 @@ public void forEachAction(Consumer> consumer) { } @Override - public MergedAggregateChange> merge(AggregateChangeWithRoot aggregateChange) { + public void add(AggregateChangeWithRoot aggregateChange) { aggregateChange.forEachAction(action -> { if (action instanceof DbAction.WithRoot rootAction) { @@ -90,21 +89,20 @@ public MergedAggregateChange> merge(AggregateChang addDelete(deleteAction); } }); - return this; } private void addInsert(DbAction.Insert action) { PersistentPropertyPath propertyPath = action.getPropertyPath(); insertActions.merge(propertyPath, - new EnumMap<>(singletonMap(action.getIdValueSource(), new ArrayList<>(singletonList(action)))), - (enumMap, enumMapDefaultValue) -> { - enumMap.merge(action.getIdValueSource(), new ArrayList<>(singletonList(action)), + 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; }); - return enumMap; + return map; }); } diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/SaveMergedAggregateChangeTest.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/SaveBatchingAggregateChangeTest.java similarity index 87% rename from spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/SaveMergedAggregateChangeTest.java rename to spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/SaveBatchingAggregateChangeTest.java index 2764170a20..6c68cf9802 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/SaveMergedAggregateChangeTest.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/SaveBatchingAggregateChangeTest.java @@ -30,18 +30,18 @@ import lombok.Value; /** - * Unit tests for {@link SaveMergedAggregateChange}. + * Unit tests for {@link SaveBatchingAggregateChange}. * * @author Chirag Tailor */ -class SaveMergedAggregateChangeTest { +class SaveBatchingAggregateChangeTest { RelationalMappingContext context = new RelationalMappingContext(); @Test void startsWithNoActions() { - MergedAggregateChange> change = MutableAggregateChange.mergedSave(Root.class); + BatchingAggregateChange> change = BatchingAggregateChange.forSave(Root.class); assertThat(extractActions(change)).isEmpty(); } @@ -58,10 +58,9 @@ void yieldsRootActions() { AggregateChangeWithRoot aggregateChange2 = MutableAggregateChange.forSave(root2); aggregateChange2.setRootAction(root2Insert); - MergedAggregateChange> change = // - MutableAggregateChange.mergedSave(Root.class) // - .merge(aggregateChange1) // - .merge(aggregateChange2); + BatchingAggregateChange> change = BatchingAggregateChange.forSave(Root.class); + change.add(aggregateChange1); + change.add(aggregateChange2); assertThat(extractActions(change)).containsExactly(root1Insert, root2Insert); } @@ -81,10 +80,9 @@ void yieldsRootActionsBeforeDeleteActions() { AggregateChangeWithRoot aggregateChange2 = MutableAggregateChange.forSave(root2); aggregateChange2.setRootAction(root2Insert); - MergedAggregateChange> change = // - MutableAggregateChange.mergedSave(Root.class) // - .merge(aggregateChange1) // - .merge(aggregateChange2); + BatchingAggregateChange> change = BatchingAggregateChange.forSave(Root.class); + change.add(aggregateChange1); + change.add(aggregateChange2); assertThat(extractActions(change)).extracting(DbAction::getClass, DbAction::getEntityType).containsExactly( // Tuple.tuple(DbAction.UpdateRoot.class, Root.class), // @@ -112,10 +110,9 @@ void yieldsNestedDeleteActionsInTreeOrderFromLeavesToRoot() { context.getPersistentPropertyPath("intermediate", Root.class)); aggregateChange2.addAction(root2IntermediateDelete); - MergedAggregateChange> change = // - MutableAggregateChange.mergedSave(Root.class) // - .merge(aggregateChange1) // - .merge(aggregateChange2); + BatchingAggregateChange> change = BatchingAggregateChange.forSave(Root.class); + change.add(aggregateChange1); + change.add(aggregateChange2); assertThat(extractActions(change)).containsSubsequence(root2LeafDelete, root1IntermediateDelete, root2IntermediateDelete); @@ -142,10 +139,9 @@ void yieldsDeleteActionsBeforeInsertActions() { context.getPersistentPropertyPath("intermediate", Root.class)); aggregateChange2.addAction(root2IntermediateDelete); - MergedAggregateChange> change = // - MutableAggregateChange.mergedSave(Root.class) // - .merge(aggregateChange1) // - .merge(aggregateChange2); + BatchingAggregateChange> change = BatchingAggregateChange.forSave(Root.class); + change.add(aggregateChange1); + change.add(aggregateChange2); assertThat(extractActions(change)).extracting(DbAction::getClass, DbAction::getEntityType).containsSubsequence( // Tuple.tuple(DbAction.Delete.class, Intermediate.class), // @@ -168,9 +164,8 @@ void yieldsInsertActionsAsBatchInserts_groupedByIdValueSource() { context.getPersistentPropertyPath("intermediate", Root.class), rootInsert, emptyMap(), IdValueSource.PROVIDED); aggregateChange.addAction(intermediateInsertProvidedId); - MergedAggregateChange> change = // - MutableAggregateChange.mergedSave(Root.class) // - .merge(aggregateChange); + BatchingAggregateChange> change = BatchingAggregateChange.forSave(Root.class); + change.add(aggregateChange); List> actions = extractActions(change); assertThat(actions) @@ -216,10 +211,9 @@ void yieldsNestedInsertActionsInTreeOrderFromRootToLeaves() { IdValueSource.GENERATED); aggregateChange2.addAction(root2IntermediateInsert); - MergedAggregateChange> change = // - MutableAggregateChange.mergedSave(Root.class) // - .merge(aggregateChange1) // - .merge(aggregateChange2); + BatchingAggregateChange> change = BatchingAggregateChange.forSave(Root.class); + change.add(aggregateChange1); + change.add(aggregateChange2); List> actions = extractActions(change); assertThat(actions) @@ -237,21 +231,24 @@ void yieldsNestedInsertActionsInTreeOrderFromRootToLeaves() { void yieldsInsertsWithSameLengthReferences_asSeparateInserts() { RootWithSameLengthReferences root = new RootWithSameLengthReferences(null, null, null); - DbAction.InsertRoot rootInsert = new DbAction.InsertRoot<>(root, IdValueSource.GENERATED); + DbAction.InsertRoot rootInsert = new DbAction.InsertRoot<>(root, + IdValueSource.GENERATED); AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(root); aggregateChange.setRootAction(rootInsert); Intermediate one = new Intermediate(null, "one", null); DbAction.Insert oneInsert = new DbAction.Insert<>(one, - context.getPersistentPropertyPath("one", RootWithSameLengthReferences.class), rootInsert, emptyMap(), IdValueSource.GENERATED); + context.getPersistentPropertyPath("one", RootWithSameLengthReferences.class), rootInsert, emptyMap(), + IdValueSource.GENERATED); aggregateChange.addAction(oneInsert); Intermediate two = new Intermediate(null, "two", null); DbAction.Insert twoInsert = new DbAction.Insert<>(two, - context.getPersistentPropertyPath("two", RootWithSameLengthReferences.class), rootInsert, emptyMap(), IdValueSource.GENERATED); + context.getPersistentPropertyPath("two", RootWithSameLengthReferences.class), rootInsert, emptyMap(), + IdValueSource.GENERATED); aggregateChange.addAction(twoInsert); - MergedAggregateChange> change = // - MutableAggregateChange.mergedSave(RootWithSameLengthReferences.class) // - .merge(aggregateChange); + BatchingAggregateChange> change = // + BatchingAggregateChange.forSave(RootWithSameLengthReferences.class); + change.add(aggregateChange); List> actions = extractActions(change); assertThat(actions) @@ -266,7 +263,7 @@ void yieldsInsertsWithSameLengthReferences_asSeparateInserts() { } private DbAction.BatchInsert getBatchInsertAction(List> actions, Class entityType, - IdValueSource idValueSource) { + 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))); @@ -286,7 +283,7 @@ private List> getBatchInsertActions(List .map(dbAction -> (DbAction.BatchInsert) dbAction).collect(Collectors.toList()); } - private List> extractActions(MergedAggregateChange> change) { + private List> extractActions(BatchingAggregateChange> change) { List> actions = new ArrayList<>(); change.forEachAction(actions::add); @@ -318,4 +315,4 @@ static class Leaf { @Id Long id; String name; } -} \ No newline at end of file +} From 8ab97b904c67a2f31f5160af7e925cabdc1952a5 Mon Sep 17 00:00:00 2001 From: Chirag Tailor Date: Thu, 7 Apr 2022 09:32:17 -0500 Subject: [PATCH 09/11] Rename AggregateChangeWithRoot to RootAggregateChange. --- .../data/jdbc/core/JdbcAggregateTemplate.java | 24 +++++----- ...eChangeIdGenerationImmutableUnitTests.java | 26 +++++----- .../AggregateChangeIdGenerationUnitTests.java | 22 ++++----- .../conversion/BatchingAggregateChange.java | 2 +- ...t.java => DefaultRootAggregateChange.java} | 6 +-- .../conversion/MutableAggregateChange.java | 14 +++--- .../RelationalEntityInsertWriter.java | 6 +-- .../RelationalEntityUpdateWriter.java | 6 +-- .../conversion/RelationalEntityWriter.java | 6 +-- ...WithRoot.java => RootAggregateChange.java} | 4 +- .../SaveBatchingAggregateChange.java | 12 ++--- .../core/conversion/WritingContext.java | 4 +- ...RelationalEntityInsertWriterUnitTests.java | 4 +- ...RelationalEntityUpdateWriterUnitTests.java | 2 +- .../RelationalEntityWriterUnitTests.java | 48 +++++++++---------- .../SaveBatchingAggregateChangeTest.java | 42 ++++++++-------- 16 files changed, 114 insertions(+), 114 deletions(-) rename spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/{DefaultAggregateChangeWithRoot.java => DefaultRootAggregateChange.java} (94%) rename spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/{AggregateChangeWithRoot.java => RootAggregateChange.java} (93%) diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateTemplate.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateTemplate.java index 1171036f10..88a61e65c1 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateTemplate.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateTemplate.java @@ -32,7 +32,7 @@ import org.springframework.data.mapping.IdentifierAccessor; import org.springframework.data.mapping.callback.EntityCallbacks; import org.springframework.data.relational.core.conversion.AggregateChange; -import org.springframework.data.relational.core.conversion.AggregateChangeWithRoot; +import org.springframework.data.relational.core.conversion.RootAggregateChange; import org.springframework.data.relational.core.conversion.BatchingAggregateChange; import org.springframework.data.relational.core.conversion.MutableAggregateChange; import org.springframework.data.relational.core.conversion.RelationalEntityDeleteWriter; @@ -292,14 +292,14 @@ private T afterExecute(AggregateChange change, T entityAfterExecution) { return triggerAfterSave(entityAfterExecution, change); } - private AggregateChangeWithRoot beforeExecute(T aggregateRoot, - Function> changeCreator) { + private RootAggregateChange beforeExecute(T aggregateRoot, + Function> changeCreator) { Assert.notNull(aggregateRoot, "Aggregate instance must not be null!"); aggregateRoot = triggerBeforeConvert(aggregateRoot); - AggregateChangeWithRoot change = changeCreator.apply(aggregateRoot); + RootAggregateChange change = changeCreator.apply(aggregateRoot); aggregateRoot = triggerBeforeSave(change.getRoot(), change); @@ -319,10 +319,10 @@ private void deleteTree(Object id, @Nullable T entity, Class domainType) triggerAfterDelete(entity, id, change); } - private T performSave(T instance, Function> changeCreator) { + private T performSave(T instance, Function> changeCreator) { // noinspection unchecked - BatchingAggregateChange> batchingAggregateChange = // + BatchingAggregateChange> batchingAggregateChange = // BatchingAggregateChange.forSave((Class) ClassUtils.getUserClass(instance)); batchingAggregateChange.add(beforeExecute(instance, changeCreator)); @@ -339,7 +339,7 @@ private List performSaveAll(Iterable instances) { T firstInstance = iterator.next(); // noinspection unchecked - BatchingAggregateChange> batchingAggregateChange = // + BatchingAggregateChange> batchingAggregateChange = // BatchingAggregateChange.forSave((Class) ClassUtils.getUserClass(firstInstance)); batchingAggregateChange.add(beforeExecute(firstInstance, changeCreatorSelectorForSave(firstInstance))); @@ -358,23 +358,23 @@ private List performSaveAll(Iterable instances) { return results; } - private Function> changeCreatorSelectorForSave(T instance) { + private Function> changeCreatorSelectorForSave(T instance) { return context.getRequiredPersistentEntity(instance.getClass()).isNew(instance) ? entity -> createInsertChange(prepareVersionForInsert(entity)) : entity -> createUpdateChange(prepareVersionForUpdate(entity)); } - private AggregateChangeWithRoot createInsertChange(T instance) { + private RootAggregateChange createInsertChange(T instance) { - AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(instance); + RootAggregateChange aggregateChange = MutableAggregateChange.forSave(instance); new RelationalEntityInsertWriter(context).write(instance, aggregateChange); return aggregateChange; } - private AggregateChangeWithRoot createUpdateChange(EntityAndPreviousVersion entityAndVersion) { + private RootAggregateChange createUpdateChange(EntityAndPreviousVersion entityAndVersion) { - AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(entityAndVersion.entity, + RootAggregateChange aggregateChange = MutableAggregateChange.forSave(entityAndVersion.entity, entityAndVersion.version); new RelationalEntityUpdateWriter(context).write(entityAndVersion.entity, aggregateChange); diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AggregateChangeIdGenerationImmutableUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AggregateChangeIdGenerationImmutableUnitTests.java index c05601698a..937c8b7f8a 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AggregateChangeIdGenerationImmutableUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AggregateChangeIdGenerationImmutableUnitTests.java @@ -39,7 +39,7 @@ import org.springframework.data.jdbc.core.convert.JdbcConverter; import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.mapping.PersistentPropertyPaths; -import org.springframework.data.relational.core.conversion.AggregateChangeWithRoot; +import org.springframework.data.relational.core.conversion.RootAggregateChange; import org.springframework.data.relational.core.conversion.DbAction; import org.springframework.data.relational.core.conversion.IdValueSource; import org.springframework.data.relational.core.conversion.MutableAggregateChange; @@ -80,7 +80,7 @@ public class AggregateChangeIdGenerationImmutableUnitTests { @Test // DATAJDBC-291 public void singleRoot() { - AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(entity); + RootAggregateChange aggregateChange = MutableAggregateChange.forSave(entity); aggregateChange.setRootAction(rootInsert); List result = executor.executeSave(aggregateChange); @@ -94,7 +94,7 @@ public void simpleReference() { entity = entity.withSingle(content); - AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(entity); + RootAggregateChange aggregateChange = MutableAggregateChange.forSave(entity); aggregateChange.setRootAction(rootInsert); aggregateChange.addAction(createInsert("single", content, null)); @@ -113,7 +113,7 @@ public void listReference() { entity = entity.withContentList(asList(content, content2)); - AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(entity); + RootAggregateChange aggregateChange = MutableAggregateChange.forSave(entity); aggregateChange.setRootAction(rootInsert); aggregateChange.addAction(createInsert("contentList", content, 0)); aggregateChange.addAction(createInsert("contentList", content2, 1)); @@ -133,7 +133,7 @@ public void mapReference() { entity = entity.withContentMap(createContentMap("a", content, "b", content2)); - AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(entity); + RootAggregateChange aggregateChange = MutableAggregateChange.forSave(entity); aggregateChange.setRootAction(rootInsert); aggregateChange.addAction(createInsert("contentMap", content, "a")); aggregateChange.addAction(createInsert("contentMap", content2, "b")); @@ -154,7 +154,7 @@ public void setIdForDeepReference() { DbAction.Insert parentInsert = createInsert("single", content, null); DbAction.Insert insert = createDeepInsert("single", tag1, null, parentInsert); - AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(entity); + RootAggregateChange aggregateChange = MutableAggregateChange.forSave(entity); aggregateChange.setRootAction(rootInsert); aggregateChange.addAction(parentInsert); aggregateChange.addAction(insert); @@ -177,7 +177,7 @@ public void setIdForDeepReferenceElementList() { DbAction.Insert insert1 = createDeepInsert("tagList", tag1, 0, parentInsert); DbAction.Insert insert2 = createDeepInsert("tagList", tag2, 1, parentInsert); - AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(entity); + RootAggregateChange aggregateChange = MutableAggregateChange.forSave(entity); aggregateChange.setRootAction(rootInsert); aggregateChange.addAction(parentInsert); aggregateChange.addAction(insert1); @@ -204,7 +204,7 @@ public void setIdForDeepElementSetElementSet() { DbAction.Insert insert1 = createDeepInsert("tagSet", tag1, null, parentInsert); DbAction.Insert insert2 = createDeepInsert("tagSet", tag2, null, parentInsert); - AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(entity); + RootAggregateChange aggregateChange = MutableAggregateChange.forSave(entity); aggregateChange.setRootAction(rootInsert); aggregateChange.addAction(parentInsert); aggregateChange.addAction(insert1); @@ -238,7 +238,7 @@ public void setIdForDeepElementListSingleReference() { DbAction.Insert insert1 = createDeepInsert("single", tag1, null, parentInsert1); DbAction.Insert insert2 = createDeepInsert("single", tag2, null, parentInsert2); - AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(entity); + RootAggregateChange aggregateChange = MutableAggregateChange.forSave(entity); aggregateChange.setRootAction(rootInsert); aggregateChange.addAction(parentInsert1); aggregateChange.addAction(parentInsert2); @@ -270,7 +270,7 @@ public void setIdForDeepElementListElementList() { DbAction.Insert insert2 = createDeepInsert("tagList", tag2, 0, parentInsert2); DbAction.Insert insert3 = createDeepInsert("tagList", tag3, 1, parentInsert2); - AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(entity); + RootAggregateChange aggregateChange = MutableAggregateChange.forSave(entity); aggregateChange.setRootAction(rootInsert); aggregateChange.addAction(parentInsert1); aggregateChange.addAction(parentInsert2); @@ -306,7 +306,7 @@ public void setIdForDeepElementMapElementMap() { DbAction.Insert insert2 = createDeepInsert("tagMap", tag2, "222", parentInsert2); DbAction.Insert insert3 = createDeepInsert("tagMap", tag3, "333", parentInsert2); - AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(entity); + RootAggregateChange aggregateChange = MutableAggregateChange.forSave(entity); aggregateChange.setRootAction(rootInsert); aggregateChange.addAction(parentInsert1); aggregateChange.addAction(parentInsert2); @@ -346,7 +346,7 @@ public void setIdForDeepElementListSingleReferenceWithIntermittentNoId() { DbAction.Insert insert1 = createDeepInsert("single", tag1, null, parentInsert1); DbAction.Insert insert2 = createDeepInsert("single", tag2, null, parentInsert2); - AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(entity); + RootAggregateChange aggregateChange = MutableAggregateChange.forSave(entity); aggregateChange.setRootAction(rootInsert); aggregateChange.addAction(parentInsert1); aggregateChange.addAction(parentInsert2); @@ -373,7 +373,7 @@ public void setIdForEmbeddedDeepReference() { DbAction.Insert parentInsert = createInsert("embedded.single", tag1, null); - AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(entity); + RootAggregateChange aggregateChange = MutableAggregateChange.forSave(entity); aggregateChange.setRootAction(rootInsert); aggregateChange.addAction(parentInsert); diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AggregateChangeIdGenerationUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AggregateChangeIdGenerationUnitTests.java index 38be034b3c..734e27e9b0 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AggregateChangeIdGenerationUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AggregateChangeIdGenerationUnitTests.java @@ -36,7 +36,7 @@ import org.springframework.data.jdbc.core.convert.JdbcConverter; import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.mapping.PersistentPropertyPaths; -import org.springframework.data.relational.core.conversion.AggregateChangeWithRoot; +import org.springframework.data.relational.core.conversion.RootAggregateChange; import org.springframework.data.relational.core.conversion.DbAction; import org.springframework.data.relational.core.conversion.IdValueSource; import org.springframework.data.relational.core.conversion.MutableAggregateChange; @@ -71,7 +71,7 @@ public class AggregateChangeIdGenerationUnitTests { @Test // DATAJDBC-291 public void singleRoot() { - AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(entity); + RootAggregateChange aggregateChange = MutableAggregateChange.forSave(entity); aggregateChange.setRootAction(rootInsert); executor.executeSave(aggregateChange); @@ -84,7 +84,7 @@ public void simpleReference() { entity.single = content; - AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(entity); + RootAggregateChange aggregateChange = MutableAggregateChange.forSave(entity); aggregateChange.setRootAction(rootInsert); aggregateChange.addAction(createInsert("single", content, null)); @@ -103,7 +103,7 @@ public void listReference() { entity.contentList.add(content); entity.contentList.add(content2); - AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(entity); + RootAggregateChange aggregateChange = MutableAggregateChange.forSave(entity); aggregateChange.setRootAction(rootInsert); aggregateChange.addAction(createInsert("contentList", content, 0)); aggregateChange.addAction(createInsert("contentList", content2, 1)); @@ -123,7 +123,7 @@ public void mapReference() { entity.contentMap.put("a", content); entity.contentMap.put("b", content2); - AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(entity); + RootAggregateChange aggregateChange = MutableAggregateChange.forSave(entity); aggregateChange.setRootAction(rootInsert); aggregateChange.addAction(createInsert("contentMap", content, "a")); aggregateChange.addAction(createInsert("contentMap", content2, "b")); @@ -143,7 +143,7 @@ public void setIdForDeepReference() { DbAction.Insert parentInsert = createInsert("single", content, null); DbAction.Insert insert = createDeepInsert("single", tag1, null, parentInsert); - AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(entity); + RootAggregateChange aggregateChange = MutableAggregateChange.forSave(entity); aggregateChange.setRootAction(rootInsert); aggregateChange.addAction(parentInsert); aggregateChange.addAction(insert); @@ -166,7 +166,7 @@ public void setIdForDeepReferenceElementList() { DbAction.Insert insert1 = createDeepInsert("tagList", tag1, 0, parentInsert); DbAction.Insert insert2 = createDeepInsert("tagList", tag2, 1, parentInsert); - AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(entity); + RootAggregateChange aggregateChange = MutableAggregateChange.forSave(entity); aggregateChange.setRootAction(rootInsert); aggregateChange.addAction(parentInsert); aggregateChange.addAction(insert1); @@ -193,7 +193,7 @@ public void setIdForDeepElementSetElementSet() { DbAction.Insert insert1 = createDeepInsert("tagSet", tag1, null, parentInsert); DbAction.Insert insert2 = createDeepInsert("tagSet", tag2, null, parentInsert); - AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(entity); + RootAggregateChange aggregateChange = MutableAggregateChange.forSave(entity); aggregateChange.setRootAction(rootInsert); aggregateChange.addAction(parentInsert); aggregateChange.addAction(insert1); @@ -227,7 +227,7 @@ public void setIdForDeepElementListSingleReference() { DbAction.Insert insert1 = createDeepInsert("single", tag1, null, parentInsert1); DbAction.Insert insert2 = createDeepInsert("single", tag2, null, parentInsert2); - AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(entity); + RootAggregateChange aggregateChange = MutableAggregateChange.forSave(entity); aggregateChange.setRootAction(rootInsert); aggregateChange.addAction(parentInsert1); aggregateChange.addAction(parentInsert2); @@ -260,7 +260,7 @@ public void setIdForDeepElementListElementList() { DbAction.Insert insert2 = createDeepInsert("tagList", tag2, 0, parentInsert2); DbAction.Insert insert3 = createDeepInsert("tagList", tag3, 1, parentInsert2); - AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(entity); + RootAggregateChange aggregateChange = MutableAggregateChange.forSave(entity); aggregateChange.setRootAction(rootInsert); aggregateChange.addAction(parentInsert1); aggregateChange.addAction(parentInsert2); @@ -298,7 +298,7 @@ public void setIdForDeepElementMapElementMap() { DbAction.Insert insert2 = createDeepInsert("tagMap", tag2, "222", parentInsert2); DbAction.Insert insert3 = createDeepInsert("tagMap", tag3, "333", parentInsert2); - AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(entity); + RootAggregateChange aggregateChange = MutableAggregateChange.forSave(entity); aggregateChange.setRootAction(rootInsert); aggregateChange.addAction(parentInsert1); aggregateChange.addAction(parentInsert2); diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/BatchingAggregateChange.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/BatchingAggregateChange.java index 3ce54fc2c4..8f53bf1015 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/BatchingAggregateChange.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/BatchingAggregateChange.java @@ -41,7 +41,7 @@ public interface BatchingAggregateChange> * @return the {@link BatchingAggregateChange} for saving root entities. * @since 3.0 */ - static BatchingAggregateChange> forSave(Class entityClass) { + static BatchingAggregateChange> forSave(Class entityClass) { Assert.notNull(entityClass, "Entity class must not be null"); diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/DefaultAggregateChangeWithRoot.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/DefaultRootAggregateChange.java similarity index 94% rename from spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/DefaultAggregateChangeWithRoot.java rename to spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/DefaultRootAggregateChange.java index 60b481469e..b42a4ff7a9 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/DefaultAggregateChangeWithRoot.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/DefaultRootAggregateChange.java @@ -26,9 +26,9 @@ * Represents the change happening to the aggregate (as used in the context of Domain Driven Design) as a whole. * * @author Chirag Tailor - * @since 2.6 + * @since 3.0 */ -class DefaultAggregateChangeWithRoot implements AggregateChangeWithRoot { +class DefaultRootAggregateChange implements RootAggregateChange { private final Kind kind; @@ -42,7 +42,7 @@ class DefaultAggregateChangeWithRoot implements AggregateChangeWithRoot { /** The previous version assigned to the instance being changed, if available */ @Nullable private final Number previousVersion; - public DefaultAggregateChangeWithRoot(Kind kind, Class entityType, @Nullable Number previousVersion) { + public DefaultRootAggregateChange(Kind kind, Class entityType, @Nullable Number previousVersion) { this.kind = kind; this.entityType = entityType; diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/MutableAggregateChange.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/MutableAggregateChange.java index a76d17ab7a..f0984be4cd 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/MutableAggregateChange.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/MutableAggregateChange.java @@ -30,32 +30,32 @@ public interface MutableAggregateChange extends AggregateChange { /** - * Factory method to create an {@link AggregateChangeWithRoot} for saving entities. + * Factory method to create a {@link RootAggregateChange} for saving entities. * * @param entity aggregate root to save. * @param entity type. - * @return the {@link AggregateChangeWithRoot} for saving the root {@code entity}. + * @return the {@link RootAggregateChange} for saving the root {@code entity}. * @since 1.2 */ - static AggregateChangeWithRoot forSave(T entity) { + static RootAggregateChange forSave(T entity) { return forSave(entity, null); } /** - * Factory method to create an {@link AggregateChangeWithRoot} for saving entities. + * Factory method to create a {@link RootAggregateChange} for saving entities. * * @param entity aggregate root to save. * @param previousVersion the previous version assigned to the instance being saved. May be {@literal null}. * @param entity type. - * @return the {@link AggregateChangeWithRoot} for saving the root {@code entity}. + * @return the {@link RootAggregateChange} for saving the root {@code entity}. * @since 2.4 */ @SuppressWarnings("unchecked") - static AggregateChangeWithRoot forSave(T entity, @Nullable Number previousVersion) { + static RootAggregateChange forSave(T entity, @Nullable Number previousVersion) { Assert.notNull(entity, "Entity must not be null"); - return new DefaultAggregateChangeWithRoot<>(Kind.SAVE, (Class) ClassUtils.getUserClass(entity), previousVersion); + return new DefaultRootAggregateChange<>(Kind.SAVE, (Class) ClassUtils.getUserClass(entity), previousVersion); } /** diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/RelationalEntityInsertWriter.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/RelationalEntityInsertWriter.java index 0022379db6..e6b6e397bd 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/RelationalEntityInsertWriter.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/RelationalEntityInsertWriter.java @@ -19,7 +19,7 @@ import org.springframework.data.relational.core.mapping.RelationalMappingContext; /** - * Converts an aggregate represented by its root into an {@link AggregateChangeWithRoot}. Does not perform any isNew + * Converts an aggregate represented by its root into a {@link RootAggregateChange}. Does not perform any isNew * check. * * @author Thomas Lang @@ -27,7 +27,7 @@ * @author Chirag Tailor * @since 1.1 */ -public class RelationalEntityInsertWriter implements EntityWriter> { +public class RelationalEntityInsertWriter implements EntityWriter> { private final RelationalMappingContext context; @@ -36,7 +36,7 @@ public RelationalEntityInsertWriter(RelationalMappingContext context) { } @Override - public void write(T root, AggregateChangeWithRoot aggregateChange) { + public void write(T root, RootAggregateChange aggregateChange) { new WritingContext<>(context, root, aggregateChange).insert(); } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/RelationalEntityUpdateWriter.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/RelationalEntityUpdateWriter.java index 4f9ebd1b87..6ff1c7d238 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/RelationalEntityUpdateWriter.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/RelationalEntityUpdateWriter.java @@ -19,7 +19,7 @@ import org.springframework.data.relational.core.mapping.RelationalMappingContext; /** - * Converts an aggregate represented by its root into an {@link AggregateChangeWithRoot}. Does not perform any isNew + * Converts an aggregate represented by its root into a {@link RootAggregateChange}. Does not perform any isNew * check. * * @author Thomas Lang @@ -27,7 +27,7 @@ * @author Chirag Tailor * @since 1.1 */ -public class RelationalEntityUpdateWriter implements EntityWriter> { +public class RelationalEntityUpdateWriter implements EntityWriter> { private final RelationalMappingContext context; @@ -36,7 +36,7 @@ public RelationalEntityUpdateWriter(RelationalMappingContext context) { } @Override - public void write(T root, AggregateChangeWithRoot aggregateChange) { + public void write(T root, RootAggregateChange aggregateChange) { new WritingContext<>(context, root, aggregateChange).update(); } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/RelationalEntityWriter.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/RelationalEntityWriter.java index b551a21f09..087debe368 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/RelationalEntityWriter.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/RelationalEntityWriter.java @@ -19,13 +19,13 @@ import org.springframework.data.relational.core.mapping.RelationalMappingContext; /** - * Converts an aggregate represented by its root into an {@link AggregateChangeWithRoot}. + * Converts an aggregate represented by its root into a {@link RootAggregateChange}. * * @author Jens Schauder * @author Mark Paluch * @author Chirag Tailor */ -public class RelationalEntityWriter implements EntityWriter> { +public class RelationalEntityWriter implements EntityWriter> { private final RelationalMappingContext context; @@ -34,7 +34,7 @@ public RelationalEntityWriter(RelationalMappingContext context) { } @Override - public void write(T root, AggregateChangeWithRoot aggregateChange) { + public void write(T root, RootAggregateChange aggregateChange) { new WritingContext<>(context, root, aggregateChange).save(); } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/AggregateChangeWithRoot.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/RootAggregateChange.java similarity index 93% rename from spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/AggregateChangeWithRoot.java rename to spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/RootAggregateChange.java index e5f0d5afa4..a663d71c75 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/AggregateChangeWithRoot.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/RootAggregateChange.java @@ -19,9 +19,9 @@ * Represents the change happening to the aggregate (as used in the context of Domain Driven Design) as a whole. * * @author Chirag Tailor - * @since 2.6 + * @since 3.0 */ -public interface AggregateChangeWithRoot extends MutableAggregateChange { +public interface RootAggregateChange extends MutableAggregateChange { /** * The root object to which this {@link AggregateChange} relates. Guaranteed to be not {@code null}. 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 53bbc6206f..82e41748b1 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 @@ -29,15 +29,15 @@ import org.springframework.util.Assert; /** - * A {@link BatchingAggregateChange} implementation for save changes - * that can contain actions for any mix of insert and update operations. When consumed, actions are yielded in the - * appropriate entity tree order with inserts carried out from root to leaves and deletes in reverse. All insert - * operations are grouped into batches to offer the ability for an optimized batch operation to be used. + * A {@link BatchingAggregateChange} implementation for save changes that can contain actions for any mix of insert and + * update operations. When consumed, actions are yielded in the appropriate entity tree order with inserts carried out + * from root to leaves and deletes in reverse. All insert operations are grouped into batches to offer the ability for + * an optimized batch operation to be used. * * @author Chirag Tailor * @since 3.0 */ -public class SaveBatchingAggregateChange implements BatchingAggregateChange> { +public class SaveBatchingAggregateChange implements BatchingAggregateChange> { private static final Comparator> pathLengthComparator = // Comparator.comparing(PersistentPropertyPath::getLength); @@ -77,7 +77,7 @@ public void forEachAction(Consumer> consumer) { } @Override - public void add(AggregateChangeWithRoot aggregateChange) { + public void add(RootAggregateChange aggregateChange) { aggregateChange.forEachAction(action -> { if (action instanceof DbAction.WithRoot rootAction) { diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/WritingContext.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/WritingContext.java index 078c21822e..cb3af1f1dc 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/WritingContext.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/WritingContext.java @@ -51,9 +51,9 @@ class WritingContext { private final Map, List> nodesCache = new HashMap<>(); private final IdValueSource rootIdValueSource; @Nullable private final Number previousVersion; - private final AggregateChangeWithRoot aggregateChange; + private final RootAggregateChange aggregateChange; - WritingContext(RelationalMappingContext context, T root, AggregateChangeWithRoot aggregateChange) { + WritingContext(RelationalMappingContext context, T root, RootAggregateChange aggregateChange) { this.context = context; this.root = root; diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityInsertWriterUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityInsertWriterUnitTests.java index 25ce6dd7b2..90828eefc7 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityInsertWriterUnitTests.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityInsertWriterUnitTests.java @@ -45,7 +45,7 @@ public class RelationalEntityInsertWriterUnitTests { public void newEntityGetsConvertedToOneInsert() { SingleReferenceEntity entity = new SingleReferenceEntity(null); - AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(entity); + RootAggregateChange aggregateChange = MutableAggregateChange.forSave(entity); new RelationalEntityInsertWriter(context).write(entity, aggregateChange); @@ -62,7 +62,7 @@ public void existingEntityGetsNotConvertedToDeletePlusUpdate() { SingleReferenceEntity entity = new SingleReferenceEntity(SOME_ENTITY_ID); - AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(entity); + RootAggregateChange aggregateChange = MutableAggregateChange.forSave(entity); new RelationalEntityInsertWriter(context).write(entity, aggregateChange); diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityUpdateWriterUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityUpdateWriterUnitTests.java index 2accbb07da..f76289a7e1 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityUpdateWriterUnitTests.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityUpdateWriterUnitTests.java @@ -46,7 +46,7 @@ public void existingEntityGetsConvertedToDeletePlusUpdate() { SingleReferenceEntity entity = new SingleReferenceEntity(SOME_ENTITY_ID); - AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(entity); + RootAggregateChange aggregateChange = MutableAggregateChange.forSave(entity); new RelationalEntityUpdateWriter(context).write(entity, aggregateChange); 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 a29bb27e07..7c5945597a 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 @@ -80,7 +80,7 @@ public class RelationalEntityWriterUnitTests { public void newEntityGetsConvertedToOneInsert() { SingleReferenceEntity entity = new SingleReferenceEntity(null); - AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(entity); + RootAggregateChange aggregateChange = MutableAggregateChange.forSave(entity); new RelationalEntityWriter(context).write(entity, aggregateChange); @@ -101,7 +101,7 @@ public void newEntityGetsConvertedToOneInsert() { void newEntityWithPrimitiveLongId_insertDoesNotIncludeId_whenIdValueIsZero() { PrimitiveLongIdEntity entity = new PrimitiveLongIdEntity(); - AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(entity); + RootAggregateChange aggregateChange = MutableAggregateChange.forSave(entity); new RelationalEntityWriter(context).write(entity, aggregateChange); @@ -122,7 +122,7 @@ void newEntityWithPrimitiveLongId_insertDoesNotIncludeId_whenIdValueIsZero() { void newEntityWithPrimitiveIntId_insertDoesNotIncludeId_whenIdValueIsZero() { PrimitiveIntIdEntity entity = new PrimitiveIntIdEntity(); - AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(entity); + RootAggregateChange aggregateChange = MutableAggregateChange.forSave(entity); new RelationalEntityWriter(context).write(entity, aggregateChange); @@ -145,7 +145,7 @@ public void newEntityGetsConvertedToOneInsertByEmbeddedEntities() { EmbeddedReferenceEntity entity = new EmbeddedReferenceEntity(null); entity.other = new Element(2L); - AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(entity); + RootAggregateChange aggregateChange = MutableAggregateChange.forSave(entity); new RelationalEntityWriter(context).write(entity, aggregateChange); @@ -168,7 +168,7 @@ public void newEntityWithReferenceGetsConvertedToTwoInserts() { SingleReferenceEntity entity = new SingleReferenceEntity(null); entity.other = new Element(null); - AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(entity); + RootAggregateChange aggregateChange = MutableAggregateChange.forSave(entity); new RelationalEntityWriter(context).write(entity, aggregateChange); @@ -193,7 +193,7 @@ void newEntityWithReference_whenReferenceHasPrimitiveId_insertDoesNotIncludeId_w entity.primitiveLongIdEntity = new PrimitiveLongIdEntity(); entity.primitiveIntIdEntity = new PrimitiveIntIdEntity(); - AggregateChangeWithRoot aggregateChange = MutableAggregateChange + RootAggregateChange aggregateChange = MutableAggregateChange .forSave(entity); new RelationalEntityWriter(context).write(entity, aggregateChange); @@ -220,7 +220,7 @@ public void existingEntityGetsConvertedToDeletePlusUpdate() { SingleReferenceEntity entity = new SingleReferenceEntity(SOME_ENTITY_ID); - AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(entity, 1L); + RootAggregateChange aggregateChange = MutableAggregateChange.forSave(entity, 1L); new RelationalEntityWriter(context).write(entity, aggregateChange); @@ -243,7 +243,7 @@ public void newReferenceTriggersDeletePlusInsert() { SingleReferenceEntity entity = new SingleReferenceEntity(SOME_ENTITY_ID); entity.other = new Element(null); - AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(entity, 1L); + RootAggregateChange aggregateChange = MutableAggregateChange.forSave(entity, 1L); new RelationalEntityWriter(context).write(entity, aggregateChange); @@ -265,7 +265,7 @@ public void newReferenceTriggersDeletePlusInsert() { public void newEntityWithEmptySetResultsInSingleInsert() { SetContainer entity = new SetContainer(null); - AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(entity); + RootAggregateChange aggregateChange = MutableAggregateChange.forSave(entity); new RelationalEntityWriter(context).write(entity, aggregateChange); @@ -287,7 +287,7 @@ public void newEntityWithSetContainingMultipleElementsResultsInAnInsertForEach() entity.elements.add(new Element(null)); entity.elements.add(new Element(null)); - AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(entity); + RootAggregateChange aggregateChange = MutableAggregateChange.forSave(entity); new RelationalEntityWriter(context).write(entity, aggregateChange); List> actions = extractActions(aggregateChange); @@ -319,7 +319,7 @@ public void cascadingReferencesTriggerCascadingActions() { new Element(null)) // ); - AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(entity); + RootAggregateChange aggregateChange = MutableAggregateChange.forSave(entity); new RelationalEntityWriter(context).write(entity, aggregateChange); @@ -359,7 +359,7 @@ public void cascadingReferencesTriggerCascadingActionsForUpdate() { new Element(null)) // ); - AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(entity, 1L); + RootAggregateChange aggregateChange = MutableAggregateChange.forSave(entity, 1L); new RelationalEntityWriter(context).write(entity, aggregateChange); @@ -389,7 +389,7 @@ public void cascadingReferencesTriggerCascadingActionsForUpdate() { public void newEntityWithEmptyMapResultsInSingleInsert() { MapContainer entity = new MapContainer(null); - AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(entity); + RootAggregateChange aggregateChange = MutableAggregateChange.forSave(entity); new RelationalEntityWriter(context).write(entity, aggregateChange); @@ -408,7 +408,7 @@ public void newEntityWithMapResultsInAdditionalInsertPerElement() { entity.elements.put("one", new Element(null)); entity.elements.put("two", new Element(null)); - AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(entity); + RootAggregateChange aggregateChange = MutableAggregateChange.forSave(entity); new RelationalEntityWriter(context).write(entity, aggregateChange); List> actions = extractActions(aggregateChange); @@ -448,7 +448,7 @@ public void newEntityWithFullMapResultsInAdditionalInsertPerElement() { entity.elements.put("a", new Element(null)); entity.elements.put("b", new Element(null)); - AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(entity); + RootAggregateChange aggregateChange = MutableAggregateChange.forSave(entity); new RelationalEntityWriter(context).write(entity, aggregateChange); List> actions = extractActions(aggregateChange); @@ -478,7 +478,7 @@ public void newEntityWithFullMapResultsInAdditionalInsertPerElement() { public void newEntityWithEmptyListResultsInSingleInsert() { ListContainer entity = new ListContainer(null); - AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(entity); + RootAggregateChange aggregateChange = MutableAggregateChange.forSave(entity); new RelationalEntityWriter(context).write(entity, aggregateChange); @@ -497,7 +497,7 @@ public void newEntityWithListResultsInAdditionalInsertPerElement() { entity.elements.add(new Element(null)); entity.elements.add(new Element(null)); - AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(entity); + RootAggregateChange aggregateChange = MutableAggregateChange.forSave(entity); new RelationalEntityWriter(context).write(entity, aggregateChange); List> actions = extractActions(aggregateChange); @@ -519,7 +519,7 @@ public void mapTriggersDeletePlusInsert() { MapContainer entity = new MapContainer(SOME_ENTITY_ID); entity.elements.put("one", new Element(null)); - AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(entity, 1L); + RootAggregateChange aggregateChange = MutableAggregateChange.forSave(entity, 1L); new RelationalEntityWriter(context).write(entity, aggregateChange); @@ -542,7 +542,7 @@ public void listTriggersDeletePlusInsert() { ListContainer entity = new ListContainer(SOME_ENTITY_ID); entity.elements.add(new Element(null)); - AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(entity, 1L); + RootAggregateChange aggregateChange = MutableAggregateChange.forSave(entity, 1L); new RelationalEntityWriter(context).write(entity, aggregateChange); @@ -566,7 +566,7 @@ public void multiLevelQualifiedReferencesWithId() { listMapContainer.maps.add(new MapContainer(SOME_ENTITY_ID)); listMapContainer.maps.get(0).elements.put("one", new Element(null)); - AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(listMapContainer, 1L); + RootAggregateChange aggregateChange = MutableAggregateChange.forSave(listMapContainer, 1L); new RelationalEntityWriter(context).write(listMapContainer, aggregateChange); @@ -593,7 +593,7 @@ public void multiLevelQualifiedReferencesWithOutId() { listMapContainer.maps.add(new NoIdMapContainer()); listMapContainer.maps.get(0).elements.put("one", new NoIdElement()); - AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(listMapContainer, + RootAggregateChange aggregateChange = MutableAggregateChange.forSave(listMapContainer, 1L); new RelationalEntityWriter(context).write(listMapContainer, aggregateChange); @@ -620,7 +620,7 @@ public void savingANullEmbeddedWithEntity() { EmbeddedReferenceChainEntity entity = new EmbeddedReferenceChainEntity(null); // the embedded is null !!! - AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(entity); + RootAggregateChange aggregateChange = MutableAggregateChange.forSave(entity); new RelationalEntityWriter(context).write(entity, aggregateChange); @@ -644,7 +644,7 @@ public void savingInnerNullEmbeddedWithEntity() { root.other = new EmbeddedReferenceChainEntity(null); // the embedded is null !!! - AggregateChangeWithRoot aggregateChange = MutableAggregateChange + RootAggregateChange aggregateChange = MutableAggregateChange .forSave(root); new RelationalEntityWriter(context).write(root, aggregateChange); @@ -671,7 +671,7 @@ void newEntityWithCollection_whenElementHasPrimitiveId_doesNotIncludeId_whenIdVa entity.primitiveLongIdEntities.add(new PrimitiveLongIdEntity()); entity.primitiveIntIdEntities.add(new PrimitiveIntIdEntity()); - AggregateChangeWithRoot aggregateChange = MutableAggregateChange + RootAggregateChange aggregateChange = MutableAggregateChange .forSave(entity); new RelationalEntityWriter(context).write(entity, aggregateChange); 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 6c68cf9802..d756f165f0 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 @@ -41,7 +41,7 @@ class SaveBatchingAggregateChangeTest { @Test void startsWithNoActions() { - BatchingAggregateChange> change = BatchingAggregateChange.forSave(Root.class); + BatchingAggregateChange> change = BatchingAggregateChange.forSave(Root.class); assertThat(extractActions(change)).isEmpty(); } @@ -51,14 +51,14 @@ void yieldsRootActions() { Root root1 = new Root(null, null); DbAction.InsertRoot root1Insert = new DbAction.InsertRoot<>(root1, IdValueSource.GENERATED); - AggregateChangeWithRoot aggregateChange1 = MutableAggregateChange.forSave(root1); + RootAggregateChange aggregateChange1 = MutableAggregateChange.forSave(root1); aggregateChange1.setRootAction(root1Insert); Root root2 = new Root(null, null); DbAction.InsertRoot root2Insert = new DbAction.InsertRoot<>(root2, IdValueSource.GENERATED); - AggregateChangeWithRoot aggregateChange2 = MutableAggregateChange.forSave(root2); + RootAggregateChange aggregateChange2 = MutableAggregateChange.forSave(root2); aggregateChange2.setRootAction(root2Insert); - BatchingAggregateChange> change = BatchingAggregateChange.forSave(Root.class); + BatchingAggregateChange> change = BatchingAggregateChange.forSave(Root.class); change.add(aggregateChange1); change.add(aggregateChange2); @@ -70,17 +70,17 @@ void yieldsRootActionsBeforeDeleteActions() { Root root1 = new Root(null, null); DbAction.UpdateRoot root1Update = new DbAction.UpdateRoot<>(root1, null); - AggregateChangeWithRoot aggregateChange1 = MutableAggregateChange.forSave(root1); + RootAggregateChange aggregateChange1 = MutableAggregateChange.forSave(root1); aggregateChange1.setRootAction(root1Update); DbAction.Delete root1IntermediateDelete = new DbAction.Delete<>(1L, context.getPersistentPropertyPath("intermediate", Root.class)); aggregateChange1.addAction(root1IntermediateDelete); Root root2 = new Root(null, null); DbAction.InsertRoot root2Insert = new DbAction.InsertRoot<>(root2, IdValueSource.GENERATED); - AggregateChangeWithRoot aggregateChange2 = MutableAggregateChange.forSave(root2); + RootAggregateChange aggregateChange2 = MutableAggregateChange.forSave(root2); aggregateChange2.setRootAction(root2Insert); - BatchingAggregateChange> change = BatchingAggregateChange.forSave(Root.class); + BatchingAggregateChange> change = BatchingAggregateChange.forSave(Root.class); change.add(aggregateChange1); change.add(aggregateChange2); @@ -94,14 +94,14 @@ void yieldsRootActionsBeforeDeleteActions() { void yieldsNestedDeleteActionsInTreeOrderFromLeavesToRoot() { Root root1 = new Root(1L, null); - AggregateChangeWithRoot aggregateChange1 = MutableAggregateChange.forSave(root1); + RootAggregateChange aggregateChange1 = MutableAggregateChange.forSave(root1); aggregateChange1.setRootAction(new DbAction.UpdateRoot<>(root1, null)); DbAction.Delete root1IntermediateDelete = new DbAction.Delete<>(1L, context.getPersistentPropertyPath("intermediate", Root.class)); aggregateChange1.addAction(root1IntermediateDelete); Root root2 = new Root(1L, null); - AggregateChangeWithRoot aggregateChange2 = MutableAggregateChange.forSave(root2); + RootAggregateChange aggregateChange2 = MutableAggregateChange.forSave(root2); aggregateChange2.setRootAction(new DbAction.UpdateRoot<>(root2, null)); DbAction.Delete root2LeafDelete = new DbAction.Delete<>(1L, context.getPersistentPropertyPath("intermediate.leaf", Root.class)); @@ -110,7 +110,7 @@ void yieldsNestedDeleteActionsInTreeOrderFromLeavesToRoot() { context.getPersistentPropertyPath("intermediate", Root.class)); aggregateChange2.addAction(root2IntermediateDelete); - BatchingAggregateChange> change = BatchingAggregateChange.forSave(Root.class); + BatchingAggregateChange> change = BatchingAggregateChange.forSave(Root.class); change.add(aggregateChange1); change.add(aggregateChange2); @@ -123,7 +123,7 @@ void yieldsDeleteActionsBeforeInsertActions() { Root root1 = new Root(null, null); DbAction.InsertRoot root1Insert = new DbAction.InsertRoot<>(root1, IdValueSource.GENERATED); - AggregateChangeWithRoot aggregateChange1 = MutableAggregateChange.forSave(root1); + RootAggregateChange aggregateChange1 = MutableAggregateChange.forSave(root1); aggregateChange1.setRootAction(root1Insert); Intermediate root1Intermediate = new Intermediate(null, "root1Intermediate", null); DbAction.Insert root1IntermediateInsert = new DbAction.Insert<>(root1Intermediate, @@ -133,13 +133,13 @@ void yieldsDeleteActionsBeforeInsertActions() { Root root2 = new Root(1L, null); DbAction.UpdateRoot root2Update = new DbAction.UpdateRoot<>(root2, null); - AggregateChangeWithRoot aggregateChange2 = MutableAggregateChange.forSave(root2); + RootAggregateChange aggregateChange2 = MutableAggregateChange.forSave(root2); aggregateChange2.setRootAction(root2Update); DbAction.Delete root2IntermediateDelete = new DbAction.Delete<>(1L, context.getPersistentPropertyPath("intermediate", Root.class)); aggregateChange2.addAction(root2IntermediateDelete); - BatchingAggregateChange> change = BatchingAggregateChange.forSave(Root.class); + BatchingAggregateChange> change = BatchingAggregateChange.forSave(Root.class); change.add(aggregateChange1); change.add(aggregateChange2); @@ -153,7 +153,7 @@ void yieldsInsertActionsAsBatchInserts_groupedByIdValueSource() { Root root = new Root(null, null); DbAction.InsertRoot rootInsert = new DbAction.InsertRoot<>(root, IdValueSource.GENERATED); - AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(root); + RootAggregateChange aggregateChange = MutableAggregateChange.forSave(root); aggregateChange.setRootAction(rootInsert); Intermediate intermediateGeneratedId = new Intermediate(null, "intermediateGeneratedId", null); DbAction.Insert intermediateInsertGeneratedId = new DbAction.Insert<>(intermediateGeneratedId, @@ -164,7 +164,7 @@ void yieldsInsertActionsAsBatchInserts_groupedByIdValueSource() { context.getPersistentPropertyPath("intermediate", Root.class), rootInsert, emptyMap(), IdValueSource.PROVIDED); aggregateChange.addAction(intermediateInsertProvidedId); - BatchingAggregateChange> change = BatchingAggregateChange.forSave(Root.class); + BatchingAggregateChange> change = BatchingAggregateChange.forSave(Root.class); change.add(aggregateChange); List> actions = extractActions(change); @@ -188,7 +188,7 @@ void yieldsNestedInsertActionsInTreeOrderFromRootToLeaves() { Root root1 = new Root(null, null); DbAction.InsertRoot root1Insert = new DbAction.InsertRoot<>(root1, IdValueSource.GENERATED); - AggregateChangeWithRoot aggregateChange1 = MutableAggregateChange.forSave(root1); + RootAggregateChange aggregateChange1 = MutableAggregateChange.forSave(root1); aggregateChange1.setRootAction(root1Insert); Intermediate root1Intermediate = new Intermediate(null, "root1Intermediate", null); DbAction.Insert root1IntermediateInsert = new DbAction.Insert<>(root1Intermediate, @@ -203,7 +203,7 @@ void yieldsNestedInsertActionsInTreeOrderFromRootToLeaves() { Root root2 = new Root(null, null); DbAction.InsertRoot root2Insert = new DbAction.InsertRoot<>(root2, IdValueSource.GENERATED); - AggregateChangeWithRoot aggregateChange2 = MutableAggregateChange.forSave(root2); + RootAggregateChange aggregateChange2 = MutableAggregateChange.forSave(root2); aggregateChange2.setRootAction(root2Insert); Intermediate root2Intermediate = new Intermediate(null, "root2Intermediate", null); DbAction.Insert root2IntermediateInsert = new DbAction.Insert<>(root2Intermediate, @@ -211,7 +211,7 @@ void yieldsNestedInsertActionsInTreeOrderFromRootToLeaves() { IdValueSource.GENERATED); aggregateChange2.addAction(root2IntermediateInsert); - BatchingAggregateChange> change = BatchingAggregateChange.forSave(Root.class); + BatchingAggregateChange> change = BatchingAggregateChange.forSave(Root.class); change.add(aggregateChange1); change.add(aggregateChange2); @@ -233,7 +233,7 @@ void yieldsInsertsWithSameLengthReferences_asSeparateInserts() { RootWithSameLengthReferences root = new RootWithSameLengthReferences(null, null, null); DbAction.InsertRoot rootInsert = new DbAction.InsertRoot<>(root, IdValueSource.GENERATED); - AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(root); + RootAggregateChange aggregateChange = MutableAggregateChange.forSave(root); aggregateChange.setRootAction(rootInsert); Intermediate one = new Intermediate(null, "one", null); DbAction.Insert oneInsert = new DbAction.Insert<>(one, @@ -246,7 +246,7 @@ void yieldsInsertsWithSameLengthReferences_asSeparateInserts() { IdValueSource.GENERATED); aggregateChange.addAction(twoInsert); - BatchingAggregateChange> change = // + BatchingAggregateChange> change = // BatchingAggregateChange.forSave(RootWithSameLengthReferences.class); change.add(aggregateChange); @@ -283,7 +283,7 @@ private List> getBatchInsertActions(List .map(dbAction -> (DbAction.BatchInsert) dbAction).collect(Collectors.toList()); } - private List> extractActions(BatchingAggregateChange> change) { + private List> extractActions(BatchingAggregateChange> change) { List> actions = new ArrayList<>(); change.forEachAction(actions::add); From 59adf7e490344b36263630d0640c5f2686389d6b Mon Sep 17 00:00:00 2001 From: Chirag Tailor Date: Tue, 12 Apr 2022 09:34:05 -0500 Subject: [PATCH 10/11] Remove statement from javadoc that is no longer accurate. --- .../data/jdbc/core/convert/DataAccessStrategy.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DataAccessStrategy.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DataAccessStrategy.java index 297925887f..a4c12f0791 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DataAccessStrategy.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DataAccessStrategy.java @@ -52,8 +52,7 @@ public interface DataAccessStrategy extends RelationResolver { * {@link Map} or {@link List}. * @return the id generated by the database if any. * @since 1.1 - * @deprecated since 2.4, use {@link #insert(Object, Class, Identifier, IdValueSource)}. This will no longer insert as - * expected when the id property of the instance is pre-populated. + * @deprecated since 2.4, use {@link #insert(Object, Class, Identifier, IdValueSource)}. */ @Nullable @Deprecated From 4da0e787ce5324524ea2459de72987e4ed9e4d43 Mon Sep 17 00:00:00 2001 From: Chirag Tailor Date: Fri, 15 Apr 2022 11:27:26 -0500 Subject: [PATCH 11/11] Produce a BatchInsert only when there are multiple inserts in the batch. --- .../SaveBatchingAggregateChange.java | 8 +++- .../SaveBatchingAggregateChangeTest.java | 44 +++++++++---------- 2 files changed, 27 insertions(+), 25 deletions(-) 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 82e41748b1..46599c7e67 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 @@ -73,7 +73,13 @@ public void forEachAction(Consumer> consumer) { .forEach((entry) -> entry.getValue().forEach(consumer)); insertActions.entrySet().stream().sorted(Map.Entry.comparingByKey(pathLengthComparator)) .forEach((entry) -> entry.getValue() - .forEach((idValueSource, inserts) -> consumer.accept(new DbAction.BatchInsert<>(inserts)))); + .forEach((idValueSource, inserts) -> { + if (inserts.size() > 1) { + consumer.accept(new DbAction.BatchInsert<>(inserts)); + } else { + inserts.forEach(consumer); + } + })); } @Override 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 d756f165f0..a10aae5fdb 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 @@ -143,26 +143,32 @@ void yieldsDeleteActionsBeforeInsertActions() { change.add(aggregateChange1); change.add(aggregateChange2); - assertThat(extractActions(change)).extracting(DbAction::getClass, DbAction::getEntityType).containsSubsequence( // - Tuple.tuple(DbAction.Delete.class, Intermediate.class), // - Tuple.tuple(DbAction.BatchInsert.class, Intermediate.class)); + assertThat(extractActions(change)).containsSubsequence(root2IntermediateDelete, root1IntermediateInsert); } @Test - void yieldsInsertActionsAsBatchInserts_groupedByIdValueSource() { + void yieldsInsertActionsAsBatchInserts_groupedByIdValueSource_whenGroupContainsMultipleInserts() { Root root = new Root(null, null); DbAction.InsertRoot rootInsert = new DbAction.InsertRoot<>(root, IdValueSource.GENERATED); RootAggregateChange aggregateChange = MutableAggregateChange.forSave(root); aggregateChange.setRootAction(rootInsert); - Intermediate intermediateGeneratedId = new Intermediate(null, "intermediateGeneratedId", null); - DbAction.Insert intermediateInsertGeneratedId = new DbAction.Insert<>(intermediateGeneratedId, + Intermediate intermediateGeneratedId1 = new Intermediate(null, "intermediateGeneratedId1", null); + DbAction.Insert intermediateInsertGeneratedId1 = new DbAction.Insert<>(intermediateGeneratedId1, context.getPersistentPropertyPath("intermediate", Root.class), rootInsert, emptyMap(), IdValueSource.GENERATED); - aggregateChange.addAction(intermediateInsertGeneratedId); - Intermediate intermediateProvidedId = new Intermediate(123L, "intermediateProvidedId", null); - DbAction.Insert intermediateInsertProvidedId = new DbAction.Insert<>(intermediateProvidedId, + aggregateChange.addAction(intermediateInsertGeneratedId1); + Intermediate intermediateGeneratedId2 = new Intermediate(null, "intermediateGeneratedId2", null); + DbAction.Insert intermediateInsertGeneratedId2 = new DbAction.Insert<>(intermediateGeneratedId2, + context.getPersistentPropertyPath("intermediate", Root.class), rootInsert, emptyMap(), IdValueSource.GENERATED); + aggregateChange.addAction(intermediateInsertGeneratedId2); + Intermediate intermediateProvidedId1 = new Intermediate(123L, "intermediateProvidedId1", null); + DbAction.Insert intermediateInsertProvidedId1 = new DbAction.Insert<>(intermediateProvidedId1, + context.getPersistentPropertyPath("intermediate", Root.class), rootInsert, emptyMap(), IdValueSource.PROVIDED); + aggregateChange.addAction(intermediateInsertProvidedId1); + Intermediate intermediateProvidedId2 = new Intermediate(456L, "intermediateProvidedId2", null); + DbAction.Insert intermediateInsertProvidedId2 = new DbAction.Insert<>(intermediateProvidedId2, context.getPersistentPropertyPath("intermediate", Root.class), rootInsert, emptyMap(), IdValueSource.PROVIDED); - aggregateChange.addAction(intermediateInsertProvidedId); + aggregateChange.addAction(intermediateInsertProvidedId2); BatchingAggregateChange> change = BatchingAggregateChange.forSave(Root.class); change.add(aggregateChange); @@ -178,9 +184,9 @@ void yieldsInsertActionsAsBatchInserts_groupedByIdValueSource() { 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(intermediateInsertGeneratedId); + .containsExactly(intermediateInsertGeneratedId1, intermediateInsertGeneratedId2); assertThat(getBatchInsertAction(actions, Intermediate.class, IdValueSource.PROVIDED).getActions()) - .containsExactly(intermediateInsertProvidedId); + .containsExactly(intermediateInsertProvidedId1, intermediateInsertProvidedId2); } @Test @@ -220,11 +226,9 @@ void yieldsNestedInsertActionsInTreeOrderFromRootToLeaves() { .extracting(DbAction::getClass, DbAction::getEntityType, DbActionTestSupport::insertIdValueSource) .containsSubsequence( // Tuple.tuple(DbAction.BatchInsert.class, Intermediate.class, IdValueSource.GENERATED), - Tuple.tuple(DbAction.BatchInsert.class, Leaf.class, IdValueSource.GENERATED)); + Tuple.tuple(DbAction.Insert.class, Leaf.class, IdValueSource.GENERATED)); assertThat(getBatchInsertAction(actions, Intermediate.class).getActions()) // .containsExactly(root1IntermediateInsert, root2IntermediateInsert); - assertThat(getBatchInsertAction(actions, Leaf.class).getActions()) // - .containsExactly(root1LeafInsert); } @Test @@ -251,15 +255,7 @@ void yieldsInsertsWithSameLengthReferences_asSeparateInserts() { change.add(aggregateChange); List> actions = extractActions(change); - assertThat(actions) - .extracting(DbAction::getClass, DbAction::getEntityType, DbActionTestSupport::insertIdValueSource) - .containsSubsequence( // - Tuple.tuple(DbAction.BatchInsert.class, Intermediate.class, IdValueSource.GENERATED), - Tuple.tuple(DbAction.BatchInsert.class, Intermediate.class, IdValueSource.GENERATED)); - List> batchInsertActions = getBatchInsertActions(actions, Intermediate.class); - assertThat(batchInsertActions).hasSize(2); - assertThat(batchInsertActions.get(0).getActions()).containsExactly(oneInsert); - assertThat(batchInsertActions.get(1).getActions()).containsExactly(twoInsert); + assertThat(actions).containsSubsequence(oneInsert, twoInsert); } private DbAction.BatchInsert getBatchInsertAction(List> actions, Class entityType,