From 6b02a4e62746976814b1f5a54007d7fc3336d662 Mon Sep 17 00:00:00 2001 From: Chirag Tailor Date: Mon, 28 Mar 2022 15:29:32 -0500 Subject: [PATCH] Add SaveMergedAggregateChange which merges AggregateChangeWithRoot changes into one. Remove behavior from WritingContext for creating InsertBatch in favor of SaveMergedAggregateChange. Update all save paths to use SaveMergedAggregateChange. + Update #populateIdsIfNecessary return type from T to List Pull out an abstract BatchWithValue class from InsertBatch to use it for batching root inserts as well. Rename InsertBatch to BatchInsert Rename AggregateChangeWithRoot to RootAggregateChange. Original pull request #1211 --- .../jdbc/core/AggregateChangeExecutor.java | 19 +- .../JdbcAggregateChangeExecutionContext.java | 24 +- .../jdbc/core/JdbcAggregateOperations.java | 13 +- .../data/jdbc/core/JdbcAggregateTemplate.java | 110 ++++-- .../support/SimpleJdbcRepository.java | 10 +- ...eChangeIdGenerationImmutableUnitTests.java | 62 ++-- .../AggregateChangeIdGenerationUnitTests.java | 42 +-- ...AggregateTemplateHsqlIntegrationTests.java | 4 +- ...angeExecutorContextImmutableUnitTests.java | 54 ++- ...gregateChangeExecutorContextUnitTests.java | 91 +++-- .../JdbcRepositoryIntegrationTests.java | 130 ++++++- .../SimpleJdbcRepositoryEventsUnitTests.java | 18 +- .../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 ++ .../conversion/BatchingAggregateChange.java | 50 +++ .../relational/core/conversion/DbAction.java | 72 ++-- ...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 | 117 +++++++ .../core/conversion/WritingContext.java | 15 +- .../core/conversion/DbActionTestSupport.java | 4 +- ...RelationalEntityDeleteWriterUnitTests.java | 2 +- ...RelationalEntityInsertWriterUnitTests.java | 6 +- ...RelationalEntityUpdateWriterUnitTests.java | 4 +- .../RelationalEntityWriterUnitTests.java | 223 +++--------- .../SaveBatchingAggregateChangeTest.java | 318 ++++++++++++++++++ 36 files changed, 1218 insertions(+), 403 deletions(-) create mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/BatchingAggregateChange.java 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%) create mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/SaveBatchingAggregateChange.java create mode 100644 spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/SaveBatchingAggregateChangeTest.java 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 d3947e3b..acf9df81 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 @@ package org.springframework.data.jdbc.core; 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 @@ class AggregateChangeExecutor { } /** - * 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); @@ -83,8 +84,8 @@ class AggregateChangeExecutor { 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 4b8ab857..426e3fb2 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 @@ class JdbcAggregateChangeExecutionContext { add(new DbActionExecutionResult(insert, id)); } - void executeInsertBatch(DbAction.InsertBatch insertBatch) { + void executeBatchInsert(DbAction.BatchInsert batchInsert) { - List> inserts = insertBatch.getInserts(); + 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.getIdValueSource()); + 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)); @@ -216,7 +216,7 @@ class JdbcAggregateChangeExecutionContext { 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 @@ class JdbcAggregateChangeExecutionContext { StagedValues cascadingValues = new StagedValues(); + List roots = new ArrayList<>(reverseResults.size()); + for (DbActionExecutionResult result : reverseResults) { DbAction.WithEntity action = result.getAction(); @@ -232,7 +234,7 @@ class JdbcAggregateChangeExecutionContext { 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 @@ class JdbcAggregateChangeExecutionContext { } } - 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 73cc7295..279502a1 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 @@ import org.springframework.lang.Nullable; * @author Jens Schauder * @author Thomas Lang * @author Milan Milanov + * @author Chirag Tailor */ public interface JdbcAggregateOperations { @@ -38,6 +39,16 @@ 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); + /** * 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 cb2856ff..e5ffb13e 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; @@ -31,7 +32,8 @@ import org.springframework.data.jdbc.core.convert.JdbcConverter; 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; import org.springframework.data.relational.core.conversion.RelationalEntityInsertWriter; @@ -44,6 +46,7 @@ import org.springframework.data.relational.core.mapping.event.*; 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. @@ -141,13 +144,15 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations { Assert.notNull(instance, "Aggregate instance must not be null!"); - RelationalPersistentEntity persistentEntity = context.getRequiredPersistentEntity(instance.getClass()); + return performSave(instance, changeCreatorSelectorForSave(instance)); + } - Function> changeCreator = persistentEntity.isNew(instance) - ? entity -> createInsertChange(prepareVersionForInsert(entity)) - : entity -> createUpdateChange(prepareVersionForUpdate(entity)); + @Override + public Iterable saveAll(Iterable instances) { - return store(instance, changeCreator, persistentEntity); + Assert.isTrue(instances.iterator().hasNext(), "Aggregate instances must not be empty!"); + + return performSaveAll(instances); } /** @@ -162,9 +167,7 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations { Assert.notNull(instance, "Aggregate instance must not be null!"); - RelationalPersistentEntity persistentEntity = context.getRequiredPersistentEntity(instance.getClass()); - - return store(instance, entity -> createInsertChange(prepareVersionForInsert(entity)), persistentEntity); + return performSave(instance, entity -> createInsertChange(prepareVersionForInsert(entity))); } /** @@ -179,9 +182,7 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations { Assert.notNull(instance, "Aggregate instance must not be null!"); - RelationalPersistentEntity persistentEntity = context.getRequiredPersistentEntity(instance.getClass()); - - return store(instance, entity -> createUpdateChange(prepareVersionForUpdate(entity)), persistentEntity); + return performSave(instance, entity -> createUpdateChange(prepareVersionForUpdate(entity))); } @Override @@ -280,29 +281,33 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations { 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 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); 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) { @@ -311,23 +316,70 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations { entity = triggerBeforeDelete(entity, id, change); - executor.execute(change); + executor.executeDelete(change); triggerAfterDelete(entity, id, change); } - private AggregateChangeWithRoot createInsertChange(T instance) { + private T performSave(T instance, Function> changeCreator) { - AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(instance); + // noinspection unchecked + BatchingAggregateChange> batchingAggregateChange = // + BatchingAggregateChange.forSave((Class) ClassUtils.getUserClass(instance)); + batchingAggregateChange.add(beforeExecute(instance, changeCreator)); + + Iterator afterExecutionIterator = executor.executeSave(batchingAggregateChange).iterator(); + + Assert.isTrue(afterExecutionIterator.hasNext(), "Instances after execution must not be empty!"); + + return afterExecute(batchingAggregateChange, afterExecutionIterator.next()); + } + + 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> changeCreatorSelectorForSave(T instance) { + + return context.getRequiredPersistentEntity(instance.getClass()).isNew(instance) + ? entity -> createInsertChange(prepareVersionForInsert(entity)) + : entity -> createUpdateChange(prepareVersionForUpdate(entity)); + } + + private RootAggregateChange createInsertChange(T 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); + 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 f67679d2..42ad0b9f 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.jdbc.core.JdbcAggregateOperations; 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 @@ import org.springframework.util.Assert; * @author Jens Schauder * @author Oliver Gierke * @author Milan Milanov + * @author Chirag Tailor */ @Transactional(readOnly = true) public class SimpleJdbcRepository implements CrudRepository, PagingAndSortingRepository { @@ -60,10 +59,7 @@ public class SimpleJdbcRepository implements CrudRepository, Paging @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 3a92cca2..937c8b7f 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.DataAccessStrategy; 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,10 +80,11 @@ public class AggregateChangeIdGenerationImmutableUnitTests { @Test // DATAJDBC-291 public void singleRoot() { - AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(entity); + RootAggregateChange 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); } @@ -93,11 +94,12 @@ public class AggregateChangeIdGenerationImmutableUnitTests { entity = entity.withSingle(content); - AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(entity); + RootAggregateChange aggregateChange = MutableAggregateChange.forSave(entity); aggregateChange.setRootAction(rootInsert); aggregateChange.addAction(createInsert("single", content, null)); - entity = executor.execute(aggregateChange); + List result = executor.executeSave(aggregateChange); + entity = result.get(0); assertSoftly(softly -> { @@ -111,12 +113,13 @@ public class AggregateChangeIdGenerationImmutableUnitTests { 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)); - entity = executor.execute(aggregateChange); + List result = executor.executeSave(aggregateChange); + entity = result.get(0); assertSoftly(softly -> { @@ -130,12 +133,13 @@ public class AggregateChangeIdGenerationImmutableUnitTests { 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")); - 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); @@ -150,12 +154,13 @@ public class AggregateChangeIdGenerationImmutableUnitTests { 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); - entity = executor.execute(aggregateChange); + List result = executor.executeSave(aggregateChange); + entity = result.get(0); assertThat(entity.rootId).isEqualTo(1); assertThat(entity.single.id).isEqualTo(2); @@ -172,13 +177,14 @@ public class AggregateChangeIdGenerationImmutableUnitTests { 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); aggregateChange.addAction(insert2); - entity = executor.execute(aggregateChange); + List result = executor.executeSave(aggregateChange); + entity = result.get(0); assertSoftly(softly -> { @@ -198,13 +204,14 @@ public class AggregateChangeIdGenerationImmutableUnitTests { 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); aggregateChange.addAction(insert2); - entity = executor.execute(aggregateChange); + List result = executor.executeSave(aggregateChange); + entity = result.get(0); assertSoftly(softly -> { @@ -231,14 +238,15 @@ public class AggregateChangeIdGenerationImmutableUnitTests { 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); aggregateChange.addAction(insert1); aggregateChange.addAction(insert2); - entity = executor.execute(aggregateChange); + List result = executor.executeSave(aggregateChange); + entity = result.get(0); assertSoftly(softly -> { @@ -262,7 +270,7 @@ public class AggregateChangeIdGenerationImmutableUnitTests { 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); @@ -270,7 +278,8 @@ public class AggregateChangeIdGenerationImmutableUnitTests { aggregateChange.addAction(insert2); aggregateChange.addAction(insert3); - entity = executor.execute(aggregateChange); + List result = executor.executeSave(aggregateChange); + entity = result.get(0); assertSoftly(softly -> { @@ -297,7 +306,7 @@ public class AggregateChangeIdGenerationImmutableUnitTests { 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); @@ -305,7 +314,8 @@ public class AggregateChangeIdGenerationImmutableUnitTests { aggregateChange.addAction(insert2); aggregateChange.addAction(insert3); - entity = executor.execute(aggregateChange); + List result = executor.executeSave(aggregateChange); + entity = result.get(0); assertSoftly(softly -> { @@ -336,14 +346,15 @@ public class AggregateChangeIdGenerationImmutableUnitTests { 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); aggregateChange.addAction(insert1); aggregateChange.addAction(insert2); - entity = executor.execute(aggregateChange); + List result = executor.executeSave(aggregateChange); + entity = result.get(0); assertSoftly(softly -> { @@ -362,11 +373,12 @@ public class AggregateChangeIdGenerationImmutableUnitTests { DbAction.Insert parentInsert = createInsert("embedded.single", tag1, null); - AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(entity); + RootAggregateChange aggregateChange = MutableAggregateChange.forSave(entity); 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 1c7a31e9..734e27e9 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.DataAccessStrategy; 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,10 +71,10 @@ public class AggregateChangeIdGenerationUnitTests { @Test // DATAJDBC-291 public void singleRoot() { - AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(entity); + RootAggregateChange aggregateChange = MutableAggregateChange.forSave(entity); aggregateChange.setRootAction(rootInsert); - executor.execute(aggregateChange); + executor.executeSave(aggregateChange); assertThat(entity.rootId).isEqualTo(1); } @@ -84,11 +84,11 @@ public class AggregateChangeIdGenerationUnitTests { entity.single = content; - AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(entity); + RootAggregateChange aggregateChange = MutableAggregateChange.forSave(entity); aggregateChange.setRootAction(rootInsert); aggregateChange.addAction(createInsert("single", content, null)); - executor.execute(aggregateChange); + executor.executeSave(aggregateChange); assertSoftly(softly -> { @@ -103,12 +103,12 @@ public class AggregateChangeIdGenerationUnitTests { 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)); - executor.execute(aggregateChange); + executor.executeSave(aggregateChange); assertSoftly(softly -> { @@ -123,12 +123,12 @@ public class AggregateChangeIdGenerationUnitTests { 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")); - executor.execute(aggregateChange); + executor.executeSave(aggregateChange); assertThat(entity.rootId).isEqualTo(1); assertThat(entity.contentMap.values()).extracting(c -> c.id).containsExactly(2, 3); @@ -143,12 +143,12 @@ public class AggregateChangeIdGenerationUnitTests { 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); - executor.execute(aggregateChange); + executor.executeSave(aggregateChange); assertThat(entity.rootId).isEqualTo(1); assertThat(entity.single.id).isEqualTo(2); @@ -166,13 +166,13 @@ public class AggregateChangeIdGenerationUnitTests { 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); aggregateChange.addAction(insert2); - executor.execute(aggregateChange); + executor.executeSave(aggregateChange); assertSoftly(softly -> { @@ -193,13 +193,13 @@ public class AggregateChangeIdGenerationUnitTests { 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); aggregateChange.addAction(insert2); - executor.execute(aggregateChange); + executor.executeSave(aggregateChange); assertSoftly(softly -> { @@ -227,14 +227,14 @@ public class AggregateChangeIdGenerationUnitTests { 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); aggregateChange.addAction(insert1); aggregateChange.addAction(insert2); - executor.execute(aggregateChange); + executor.executeSave(aggregateChange); assertSoftly(softly -> { @@ -260,7 +260,7 @@ public class AggregateChangeIdGenerationUnitTests { 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); @@ -268,7 +268,7 @@ public class AggregateChangeIdGenerationUnitTests { aggregateChange.addAction(insert2); aggregateChange.addAction(insert3); - executor.execute(aggregateChange); + executor.executeSave(aggregateChange); assertSoftly(softly -> { @@ -298,7 +298,7 @@ public class AggregateChangeIdGenerationUnitTests { 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); @@ -306,7 +306,7 @@ public class AggregateChangeIdGenerationUnitTests { 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/ImmutableAggregateTemplateHsqlIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/ImmutableAggregateTemplateHsqlIntegrationTests.java index 85940183..173b40c0 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 @@ import org.springframework.transaction.annotation.Transactional; * * @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 9195563e..88bd07f9 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 @@ import org.springframework.lang.Nullable; * Test for the {@link JdbcAggregateChangeExecutionContext} when operating on immutable classes. * * @author Jens Schauder - * @author Chirag Taylor + * @author Chirag Tailor */ public class JdbcAggregateChangeExecutorContextImmutableUnitTests { @@ -70,9 +70,10 @@ public class JdbcAggregateChangeExecutorContextImmutableUnitTests { 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 class JdbcAggregateChangeExecutorContextImmutableUnitTests { 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 class JdbcAggregateChangeExecutorContextImmutableUnitTests { 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 @@ public class JdbcAggregateChangeExecutorContextImmutableUnitTests { 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 6936bb8d..800eb4f1 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 @@ package org.springframework.data.jdbc.core; 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.DataAccessStrategy; 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 class JdbcAggregateChangeExecutorContextUnitTests { 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 class JdbcAggregateChangeExecutorContextUnitTests { 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)); + executionContext.executeInsert(createInsert(rootInsert, "content", content, null, IdValueSource.GENERATED)); - 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); @@ -106,11 +106,11 @@ public class JdbcAggregateChangeExecutorContextUnitTests { 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)); - 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); @@ -129,13 +129,13 @@ public class JdbcAggregateChangeExecutorContextUnitTests { .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<>( - singletonList(createInsert(rootInsert, "list", content, 0)), IdValueSource.GENERATED); - executionContext.executeInsertBatch(insertBatch); + DbAction.BatchInsert batchInsert = new DbAction.BatchInsert<>( + singletonList(createInsert(rootInsert, "list", content, 0, IdValueSource.GENERATED))); + executionContext.executeBatchInsert(batchInsert); - 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); } @@ -153,42 +153,73 @@ public class JdbcAggregateChangeExecutorContextUnitTests { .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<>( - singletonList(createInsert(rootInsert, "list", content, 0)), IdValueSource.PROVIDED); - executionContext.executeInsertBatch(insertBatch); + DbAction.BatchInsert batchInsert = new DbAction.BatchInsert<>( + singletonList(createInsert(rootInsert, "list", content, 0, IdValueSource.PROVIDED))); + executionContext.executeBatchInsert(batchInsert); - 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, IdValueSource.GENERATED)); - 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, IdValueSource.GENERATED)); + + + DummyEntity root2 = new DummyEntity(); + DbAction.InsertRoot rootInsert2 = new DbAction.InsertRoot<>(root2, IdValueSource.GENERATED); + when(accessStrategy.insert(root2, DummyEntity.class, Identifier.empty(), IdValueSource.GENERATED)).thenReturn(456L); + 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, IdValueSource.GENERATED)); + + 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, 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) { @@ -199,8 +230,8 @@ public class JdbcAggregateChangeExecutorContextUnitTests { 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 6e505cda..f89e94bd 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.*; @@ -47,6 +48,8 @@ import org.springframework.data.domain.PageRequest; 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; import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory; @@ -59,6 +62,7 @@ import org.springframework.data.relational.core.mapping.event.AfterConvertEvent; import org.springframework.data.relational.core.sql.LockMode; import org.springframework.data.relational.repository.Lock; 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; @@ -90,6 +94,7 @@ public class JdbcRepositoryIntegrationTests { @Autowired NamedParameterJdbcTemplate template; @Autowired DummyEntityRepository repository; @Autowired MyEventListener eventListener; + @Autowired RootRepository rootRepository; private static DummyEntity createDummyEntity() { @@ -129,7 +134,7 @@ public class JdbcRepositoryIntegrationTests { } @Test // DATAJDBC-97 - public void savesManyEntities() { + public void insertsManyEntities() { DummyEntity entity = createDummyEntity(); DummyEntity other = createDummyEntity(); @@ -283,6 +288,20 @@ public class JdbcRepositoryIntegrationTests { .containsExactlyInAnyOrder(entity.getName(), other.getName()); } + @Test // GH-537 + 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() { @@ -608,6 +627,84 @@ public class JdbcRepositoryIntegrationTests { .containsExactlyInAnyOrder(Direction.CENTER); } + @Test // GH-537 + void manyInsertsWithNestedEntities() { + Root root1 = createRoot("root1"); + Root root2 = createRoot("root2"); + + List savedRoots = rootRepository.saveAll(asList(root1, root2)); + + List reloadedRoots = rootRepository.findAllByOrderByIdAsc(); + 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.findAllByOrderByIdAsc(); + 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.findAllByOrderByIdAsc(); + 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(); @@ -717,6 +814,11 @@ public class JdbcRepositoryIntegrationTests { return factory.getRepository(DummyEntityRepository.class); } + @Bean + RootRepository rootRepository() { + return factory.getRepository(RootRepository.class); + } + @Bean NamedQueries namedQueries() throws IOException { @@ -732,6 +834,32 @@ public class JdbcRepositoryIntegrationTests { } } + interface RootRepository extends ListCrudRepository { + List findAllByOrderByIdAsc(); + } + + @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/SimpleJdbcRepositoryEventsUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/SimpleJdbcRepositoryEventsUnitTests.java index ebbca090..418f1b57 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 @@ import org.springframework.lang.Nullable; */ public class SimpleJdbcRepositoryEventsUnitTests { + private static final long generatedId = 4711L; + CollectingEventPublisher publisher = new CollectingEventPublisher(); DummyEntityRepository repository; @@ -125,14 +127,14 @@ public class SimpleJdbcRepositoryEventsUnitTests { 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) // ); } @@ -275,7 +277,7 @@ public class SimpleJdbcRepositoryEventsUnitTests { 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/JdbcRepositoryIntegrationTests-db2.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-db2.sql index ce25ff64..ae2ebe02 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 ( @@ -10,3 +13,25 @@ CREATE TABLE dummy_entity REF BIGINT, DIRECTION VARCHAR(100) ); + +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 30a8df2e..0358d5db 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 @@ -8,3 +8,25 @@ CREATE TABLE dummy_entity REF BIGINT, DIRECTION VARCHAR(100) ); + +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 30a8df2e..0358d5db 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 @@ -8,3 +8,25 @@ CREATE TABLE dummy_entity REF BIGINT, DIRECTION VARCHAR(100) ); + +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 c505db35..a652b3ba 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 @@ -8,3 +8,25 @@ CREATE TABLE dummy_entity REF BIGINT, DIRECTION VARCHAR(100) ); + +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 94f7aadf..e0a307e7 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, @@ -9,3 +13,25 @@ CREATE TABLE dummy_entity REF BIGINT, DIRECTION VARCHAR(100) ); + +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 eb04d9ed..fdae0af0 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 @@ -11,3 +11,26 @@ CREATE TABLE DUMMY_ENTITY REF BIGINT, DIRECTION VARCHAR(100) ); + +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 c575139b..3cf241c2 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 ( @@ -10,3 +13,25 @@ CREATE TABLE DUMMY_ENTITY REF NUMBER, DIRECTION VARCHAR2(100) ); + +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 592b3ea2..4fd42572 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, @@ -9,3 +13,25 @@ CREATE TABLE dummy_entity REF BIGINT, DIRECTION VARCHAR(100) ); + +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-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 new file mode 100644 index 00000000..8f53bf10 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/BatchingAggregateChange.java @@ -0,0 +1,50 @@ +/* + * 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 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 added to it to + * broadly represent the changes to multiple aggregates across all such added changes. + * + * @author Chirag Tailor + * @since 3.0 + */ +public interface BatchingAggregateChange> extends AggregateChange { + /** + * Adds a {@code MutableAggregateChange} into this {@code BatchingAggregateChange}. + * + * @param aggregateChange must not be {@literal null}. + */ + 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/DbAction.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/DbAction.java index d9de7205..ec3dad31 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,8 +17,10 @@ package org.springframework.data.relational.core.conversion; 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; import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; @@ -344,38 +346,56 @@ public interface DbAction { } } + /** + * 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"); + 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; + } + + @Override + public Class getEntityType() { + return actions.get(0).getEntityType(); + } + + public List getActions() { + return actions; + } + + public B getBatchValue() { + return batchValue; + } + + @Override + public String toString() { + 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 implements DbAction { - private final List> inserts; - private final IdValueSource idValueSource; - - public InsertBatch(List> inserts, IdValueSource idValueSource) { - Assert.notEmpty(inserts, "Inserts must contains at least one insert"); - this.inserts = inserts; - this.idValueSource = idValueSource; - } - - @Override - public Class getEntityType() { - return inserts.get(0).getEntityType(); - } - - public List> getInserts() { - return inserts; - } - - public IdValueSource getIdValueSource() { - return idValueSource; - } - - @Override - public String toString() { - return "InsertBatch{" + "inserts=" + inserts + ", idValueSource=" + idValueSource + '}'; + 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/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 60b48146..b42a4ff7 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 @@ import org.springframework.util.Assert; * 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 a76d17ab..f0984be4 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 @@ import org.springframework.util.ClassUtils; 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 0022379d..e6b6e397 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.convert.EntityWriter; 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 @@ import org.springframework.data.relational.core.mapping.RelationalMappingContext * @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 class RelationalEntityInsertWriter implements EntityWriter 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 4f9ebd1b..6ff1c7d2 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.convert.EntityWriter; 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 @@ import org.springframework.data.relational.core.mapping.RelationalMappingContext * @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 class RelationalEntityUpdateWriter implements EntityWriter 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 b551a21f..087debe3 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.convert.EntityWriter; 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 class RelationalEntityWriter implements EntityWriter 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 e5f0d5af..a663d71c 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 @@ package org.springframework.data.relational.core.conversion; * 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 new file mode 100644 index 00000000..82e41748 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/SaveBatchingAggregateChange.java @@ -0,0 +1,117 @@ +/* + * 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.*; + +import java.util.ArrayList; +import java.util.Comparator; +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; + +/** + * 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> { + + private static final Comparator> pathLengthComparator = // + Comparator.comparing(PersistentPropertyPath::getLength); + + private final Class entityType; + private final List> rootActions = new ArrayList<>(); + private final Map, Map>>> insertActions = // + new HashMap<>(); + private final Map, List>> deleteActions = // + new HashMap<>(); + + public SaveBatchingAggregateChange(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.BatchInsert<>(inserts)))); + } + + @Override + public void add(RootAggregateChange 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); + } + }); + } + + private void addInsert(DbAction.Insert action) { + + PersistentPropertyPath propertyPath = action.getPropertyPath(); + insertActions.merge(propertyPath, + new HashMap<>(singletonMap(action.getIdValueSource(), new ArrayList<>(singletonList(action)))), + (map, mapDefaultValue) -> { + map.merge(action.getIdValueSource(), new ArrayList<>(singletonList(action)), + (actions, listDefaultValue) -> { + actions.add(action); + return actions; + }); + return map; + }); + } + + 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/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 6361b7a2..87f43d94 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.Collections; 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.relational.core.mapping.PersistentPropertyPathExtension; @@ -52,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; @@ -150,15 +149,7 @@ class WritingContext { inserts.add(insert); previousActions.put(node, insert); }); - return inserts.stream().collect(Collectors.groupingBy(DbAction.Insert::getIdValueSource)).entrySet().stream() - .filter(entry -> (!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/DbActionTestSupport.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/DbActionTestSupport.java index 22c083e3..2d914530 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 @@ class DbActionTestSupport { 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).getIdValueSource(); + } 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/RelationalEntityDeleteWriterUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/RelationalEntityDeleteWriterUnitTests.java index c0ccfade..afd0fd1e 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 @@ -42,7 +42,7 @@ import org.springframework.data.relational.core.mapping.RelationalMappingContext * * @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 62a99a0a..101608e8 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 @@ import org.springframework.data.relational.core.mapping.RelationalMappingContext * Unit tests for the {@link RelationalEntityInsertWriter} * * @author Thomas Lang - * @author Chirag Taylor + * @author Chirag Tailor */ @ExtendWith(MockitoExtension.class) public class RelationalEntityInsertWriterUnitTests { @@ -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 class RelationalEntityInsertWriterUnitTests { 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 516688a6..f76289a7 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 @@ import org.springframework.data.relational.core.mapping.RelationalMappingContext * * @author Thomas Lang * @author Myeonghyeon Lee - * @author Chirag Taylor + * @author Chirag Tailor */ @ExtendWith(MockitoExtension.class) public class RelationalEntityUpdateWriterUnitTests { @@ -46,7 +46,7 @@ public class RelationalEntityUpdateWriterUnitTests { 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 9870ed9f..6ca43701 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.HashSet; 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; @@ -38,7 +36,6 @@ import org.springframework.data.mapping.PersistentPropertyPath; 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; @@ -84,7 +81,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); @@ -105,7 +102,7 @@ public class RelationalEntityWriterUnitTests { void newEntityWithPrimitiveLongId_insertDoesNotIncludeId_whenIdValueIsZero() { PrimitiveLongIdEntity entity = new PrimitiveLongIdEntity(); - AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(entity); + RootAggregateChange aggregateChange = MutableAggregateChange.forSave(entity); new RelationalEntityWriter(context).write(entity, aggregateChange); @@ -126,7 +123,7 @@ public class RelationalEntityWriterUnitTests { void newEntityWithPrimitiveIntId_insertDoesNotIncludeId_whenIdValueIsZero() { PrimitiveIntIdEntity entity = new PrimitiveIntIdEntity(); - AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(entity); + RootAggregateChange aggregateChange = MutableAggregateChange.forSave(entity); new RelationalEntityWriter(context).write(entity, aggregateChange); @@ -149,7 +146,7 @@ public class RelationalEntityWriterUnitTests { 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); @@ -172,7 +169,7 @@ public class RelationalEntityWriterUnitTests { 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); @@ -197,7 +194,7 @@ public class RelationalEntityWriterUnitTests { entity.primitiveLongIdEntity = new PrimitiveLongIdEntity(); entity.primitiveIntIdEntity = new PrimitiveIntIdEntity(); - AggregateChangeWithRoot aggregateChange = MutableAggregateChange + RootAggregateChange aggregateChange = MutableAggregateChange .forSave(entity); new RelationalEntityWriter(context).write(entity, aggregateChange); @@ -224,7 +221,7 @@ public class RelationalEntityWriterUnitTests { 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); @@ -247,7 +244,7 @@ public class RelationalEntityWriterUnitTests { 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); @@ -269,7 +266,7 @@ public class RelationalEntityWriterUnitTests { public void newEntityWithEmptySetResultsInSingleInsert() { SetContainer entity = new SetContainer(null); - AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(entity); + RootAggregateChange aggregateChange = MutableAggregateChange.forSave(entity); new RelationalEntityWriter(context).write(entity, aggregateChange); @@ -285,13 +282,13 @@ public class RelationalEntityWriterUnitTests { } @Test // DATAJDBC-113 - public void newEntityWithSetContainingMultipleElementsResultsInAnInsertForTheBatch() { + public void newEntityWithSetContainingMultipleElementsResultsInAnInsertForEach() { SetContainer entity = new SetContainer(null); 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); @@ -303,16 +300,6 @@ public class RelationalEntityWriterUnitTests { 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) // ); @@ -333,7 +320,7 @@ public class RelationalEntityWriterUnitTests { new Element(null)) // ); - AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(entity); + RootAggregateChange aggregateChange = MutableAggregateChange.forSave(entity); new RelationalEntityWriter(context).write(entity, aggregateChange); @@ -347,31 +334,10 @@ public class RelationalEntityWriterUnitTests { .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), // @@ -394,7 +360,7 @@ public class RelationalEntityWriterUnitTests { new Element(null)) // ); - AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(entity, 1L); + RootAggregateChange aggregateChange = MutableAggregateChange.forSave(entity, 1L); new RelationalEntityWriter(context).write(entity, aggregateChange); @@ -409,31 +375,10 @@ public class RelationalEntityWriterUnitTests { 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), // @@ -445,7 +390,7 @@ public class RelationalEntityWriterUnitTests { public void newEntityWithEmptyMapResultsInSingleInsert() { MapContainer entity = new MapContainer(null); - AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(entity); + RootAggregateChange aggregateChange = MutableAggregateChange.forSave(entity); new RelationalEntityWriter(context).write(entity, aggregateChange); @@ -464,28 +409,25 @@ public class RelationalEntityWriterUnitTests { 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); 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) // ); } @@ -507,26 +449,17 @@ public class RelationalEntityWriterUnitTests { 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); 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), // @@ -546,7 +479,7 @@ public class RelationalEntityWriterUnitTests { public void newEntityWithEmptyListResultsInSingleInsert() { ListContainer entity = new ListContainer(null); - AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(entity); + RootAggregateChange aggregateChange = MutableAggregateChange.forSave(entity); new RelationalEntityWriter(context).write(entity, aggregateChange); @@ -565,7 +498,7 @@ public class RelationalEntityWriterUnitTests { 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); @@ -576,15 +509,6 @@ public class RelationalEntityWriterUnitTests { 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) // ); @@ -596,7 +520,7 @@ public class RelationalEntityWriterUnitTests { 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); @@ -619,7 +543,7 @@ public class RelationalEntityWriterUnitTests { 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); @@ -643,7 +567,7 @@ public class RelationalEntityWriterUnitTests { 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); @@ -670,7 +594,7 @@ public class RelationalEntityWriterUnitTests { 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); @@ -697,7 +621,7 @@ public class RelationalEntityWriterUnitTests { 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); @@ -721,7 +645,7 @@ public class RelationalEntityWriterUnitTests { root.other = new EmbeddedReferenceChainEntity(null); // the embedded is null !!! - AggregateChangeWithRoot aggregateChange = MutableAggregateChange + RootAggregateChange aggregateChange = MutableAggregateChange .forSave(root); new RelationalEntityWriter(context).write(root, aggregateChange); @@ -742,61 +666,13 @@ public class RelationalEntityWriterUnitTests { } @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()); entity.primitiveIntIdEntities.add(new PrimitiveIntIdEntity()); - AggregateChangeWithRoot aggregateChange = MutableAggregateChange + RootAggregateChange aggregateChange = MutableAggregateChange .forSave(entity); new RelationalEntityWriter(context).write(entity, aggregateChange); @@ -824,7 +700,7 @@ public class RelationalEntityWriterUnitTests { WithReadOnlyReference entity = new WithReadOnlyReference(null); entity.readOnly = new Element(SOME_ENTITY_ID); - AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(entity); + RootAggregateChange aggregateChange = MutableAggregateChange.forSave(entity); new RelationalEntityWriter(context).write(entity, aggregateChange); @@ -844,7 +720,7 @@ public class RelationalEntityWriterUnitTests { WithReadOnlyReference entity = new WithReadOnlyReference(SOME_ENTITY_ID); entity.readOnly = new Element(SOME_ENTITY_ID); - AggregateChangeWithRoot aggregateChange = MutableAggregateChange.forSave(entity); + RootAggregateChange aggregateChange = MutableAggregateChange.forSave(entity); new RelationalEntityWriter(context).write(entity, aggregateChange); @@ -865,29 +741,6 @@ public class RelationalEntityWriterUnitTests { 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); 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 new file mode 100644 index 00000000..d756f165 --- /dev/null +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/SaveBatchingAggregateChangeTest.java @@ -0,0 +1,318 @@ +/* + * 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.*; +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; + +/** + * Unit tests for {@link SaveBatchingAggregateChange}. + * + * @author Chirag Tailor + */ +class SaveBatchingAggregateChangeTest { + + RelationalMappingContext context = new RelationalMappingContext(); + + @Test + void startsWithNoActions() { + + BatchingAggregateChange> change = BatchingAggregateChange.forSave(Root.class); + + assertThat(extractActions(change)).isEmpty(); + } + + @Test + void yieldsRootActions() { + + Root root1 = new Root(null, null); + DbAction.InsertRoot root1Insert = new DbAction.InsertRoot<>(root1, IdValueSource.GENERATED); + RootAggregateChange aggregateChange1 = MutableAggregateChange.forSave(root1); + aggregateChange1.setRootAction(root1Insert); + Root root2 = new Root(null, null); + DbAction.InsertRoot root2Insert = new DbAction.InsertRoot<>(root2, IdValueSource.GENERATED); + RootAggregateChange aggregateChange2 = MutableAggregateChange.forSave(root2); + aggregateChange2.setRootAction(root2Insert); + + BatchingAggregateChange> change = BatchingAggregateChange.forSave(Root.class); + change.add(aggregateChange1); + change.add(aggregateChange2); + + assertThat(extractActions(change)).containsExactly(root1Insert, root2Insert); + } + + @Test + void yieldsRootActionsBeforeDeleteActions() { + + Root root1 = new Root(null, null); + DbAction.UpdateRoot root1Update = new DbAction.UpdateRoot<>(root1, null); + 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); + RootAggregateChange aggregateChange2 = MutableAggregateChange.forSave(root2); + aggregateChange2.setRootAction(root2Insert); + + 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), // + Tuple.tuple(DbAction.InsertRoot.class, Root.class), // + Tuple.tuple(DbAction.Delete.class, Intermediate.class)); + } + + @Test + void yieldsNestedDeleteActionsInTreeOrderFromLeavesToRoot() { + + Root root1 = new Root(1L, null); + 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); + 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)); + aggregateChange2.addAction(root2LeafDelete); + DbAction.Delete root2IntermediateDelete = new DbAction.Delete<>(1L, + context.getPersistentPropertyPath("intermediate", Root.class)); + aggregateChange2.addAction(root2IntermediateDelete); + + BatchingAggregateChange> change = BatchingAggregateChange.forSave(Root.class); + change.add(aggregateChange1); + change.add(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); + RootAggregateChange 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); + 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); + 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)); + } + + @Test + void yieldsInsertActionsAsBatchInserts_groupedByIdValueSource() { + + 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, + 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); + + BatchingAggregateChange> change = BatchingAggregateChange.forSave(Root.class); + change.add(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.BatchInsert.class, Intermediate.class, IdValueSource.PROVIDED)) // + .containsSubsequence( // + Tuple.tuple(DbAction.InsertRoot.class, Root.class, IdValueSource.GENERATED), // + Tuple.tuple(DbAction.BatchInsert.class, Intermediate.class, IdValueSource.GENERATED)) // + .doesNotContain(Tuple.tuple(DbAction.Insert.class, Intermediate.class)); + assertThat(getBatchInsertAction(actions, Intermediate.class, IdValueSource.GENERATED).getActions()) + .containsExactly(intermediateInsertGeneratedId); + assertThat(getBatchInsertAction(actions, Intermediate.class, IdValueSource.PROVIDED).getActions()) + .containsExactly(intermediateInsertProvidedId); + } + + @Test + void yieldsNestedInsertActionsInTreeOrderFromRootToLeaves() { + + Root root1 = new Root(null, null); + DbAction.InsertRoot root1Insert = new DbAction.InsertRoot<>(root1, IdValueSource.GENERATED); + RootAggregateChange 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); + RootAggregateChange 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); + + BatchingAggregateChange> change = BatchingAggregateChange.forSave(Root.class); + change.add(aggregateChange1); + change.add(aggregateChange2); + + List> actions = extractActions(change); + assertThat(actions) + .extracting(DbAction::getClass, DbAction::getEntityType, DbActionTestSupport::insertIdValueSource) + .containsSubsequence( // + 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(getBatchInsertAction(actions, Leaf.class).getActions()) // + .containsExactly(root1LeafInsert); + } + + @Test + void yieldsInsertsWithSameLengthReferences_asSeparateInserts() { + + RootWithSameLengthReferences root = new RootWithSameLengthReferences(null, null, null); + DbAction.InsertRoot rootInsert = new DbAction.InsertRoot<>(root, + IdValueSource.GENERATED); + RootAggregateChange 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); + + BatchingAggregateChange> change = // + BatchingAggregateChange.forSave(RootWithSameLengthReferences.class); + 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); + } + + 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.BatchInsert getBatchInsertAction(List> actions, Class entityType) { + return getBatchInsertActions(actions, entityType).stream().findFirst() + .orElseThrow(() -> new RuntimeException("No BatchInsert action found!")); + } + + @SuppressWarnings("unchecked") + private List> getBatchInsertActions(List> actions, Class entityType) { + + return actions.stream() // + .filter(dbAction -> dbAction instanceof DbAction.BatchInsert) // + .filter(dbAction -> dbAction.getEntityType().equals(entityType)) // + .map(dbAction -> (DbAction.BatchInsert) dbAction).collect(Collectors.toList()); + } + + private List> extractActions(BatchingAggregateChange> 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; + } +}