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<T>

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
This commit is contained in:
Chirag Tailor
2022-03-28 15:29:32 -05:00
committed by Jens Schauder
parent d3d05039da
commit 6b02a4e627
36 changed files with 1218 additions and 403 deletions

View File

@@ -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 <T> 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> T execute(AggregateChangeWithRoot<T> aggregateChange) {
<T> List<T> executeSave(AggregateChange<T> 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 <T> the type of the aggregate root.
* @since 3.0
*/
<T> void execute(AggregateChange<T> aggregateChange) {
<T> void executeDelete(AggregateChange<T> 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) {

View File

@@ -83,14 +83,14 @@ class JdbcAggregateChangeExecutionContext {
add(new DbActionExecutionResult(insert, id));
}
<T> void executeInsertBatch(DbAction.InsertBatch<T> insertBatch) {
<T> void executeBatchInsert(DbAction.BatchInsert<T> batchInsert) {
List<DbAction.Insert<T>> inserts = insertBatch.getInserts();
List<DbAction.Insert<T>> inserts = batchInsert.getActions();
List<InsertSubject<T>> 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> T populateIdsIfNecessary() {
<T> List<T> populateIdsIfNecessary() {
// have the results so that the inserts on the leaves come first.
List<DbActionExecutionResult> reverseResults = new ArrayList<>(results.values());
@@ -224,6 +224,8 @@ class JdbcAggregateChangeExecutionContext {
StagedValues cascadingValues = new StagedValues();
List<T> 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 <S> Object setIdAndCascadingProperties(DbAction.WithEntity<S> action, @Nullable Object generatedId,

View File

@@ -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> 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 <T> the type of the aggregate root.
* @return the saved instances.
* @since 3.0
*/
<T> Iterable<T> saveAll(Iterable<T> instances);
/**
* Dedicated insert function. This skips the test if the aggregate root is new and makes an insert.
* <p>

View File

@@ -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<T, AggregateChangeWithRoot<T>> changeCreator = persistentEntity.isNew(instance)
? entity -> createInsertChange(prepareVersionForInsert(entity))
: entity -> createUpdateChange(prepareVersionForUpdate(entity));
@Override
public <T> Iterable<T> saveAll(Iterable<T> 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> T store(T aggregateRoot, Function<T, AggregateChangeWithRoot<T>> changeCreator,
RelationalPersistentEntity<?> persistentEntity) {
private <T> T afterExecute(AggregateChange<T> 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 <T> RootAggregateChange<T> beforeExecute(T aggregateRoot,
Function<T, RootAggregateChange<T>> changeCreator) {
Assert.notNull(aggregateRoot, "Aggregate instance must not be null!");
aggregateRoot = triggerBeforeConvert(aggregateRoot);
AggregateChangeWithRoot<T> change = changeCreator.apply(aggregateRoot);
RootAggregateChange<T> 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 <T> void deleteTree(Object id, @Nullable T entity, Class<T> 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 <T> AggregateChangeWithRoot<T> createInsertChange(T instance) {
private <T> T performSave(T instance, Function<T, RootAggregateChange<T>> changeCreator) {
AggregateChangeWithRoot<T> aggregateChange = MutableAggregateChange.forSave(instance);
// noinspection unchecked
BatchingAggregateChange<T, RootAggregateChange<T>> batchingAggregateChange = //
BatchingAggregateChange.forSave((Class<T>) ClassUtils.getUserClass(instance));
batchingAggregateChange.add(beforeExecute(instance, changeCreator));
Iterator<T> afterExecutionIterator = executor.executeSave(batchingAggregateChange).iterator();
Assert.isTrue(afterExecutionIterator.hasNext(), "Instances after execution must not be empty!");
return afterExecute(batchingAggregateChange, afterExecutionIterator.next());
}
private <T> List<T> performSaveAll(Iterable<T> instances) {
Iterator<T> iterator = instances.iterator();
T firstInstance = iterator.next();
// noinspection unchecked
BatchingAggregateChange<T, RootAggregateChange<T>> batchingAggregateChange = //
BatchingAggregateChange.forSave((Class<T>) ClassUtils.getUserClass(firstInstance));
batchingAggregateChange.add(beforeExecute(firstInstance, changeCreatorSelectorForSave(firstInstance)));
while (iterator.hasNext()) {
T instance = iterator.next();
batchingAggregateChange.add(beforeExecute(instance, changeCreatorSelectorForSave(instance)));
}
List<T> instancesAfterExecution = executor.executeSave(batchingAggregateChange);
ArrayList<T> results = new ArrayList<>(instancesAfterExecution.size());
for (T instance : instancesAfterExecution) {
results.add(afterExecute(batchingAggregateChange, instance));
}
return results;
}
private <T> Function<T, RootAggregateChange<T>> changeCreatorSelectorForSave(T instance) {
return context.getRequiredPersistentEntity(instance.getClass()).isNew(instance)
? entity -> createInsertChange(prepareVersionForInsert(entity))
: entity -> createUpdateChange(prepareVersionForUpdate(entity));
}
private <T> RootAggregateChange<T> createInsertChange(T instance) {
RootAggregateChange<T> aggregateChange = MutableAggregateChange.forSave(instance);
new RelationalEntityInsertWriter<T>(context).write(instance, aggregateChange);
return aggregateChange;
}
private <T> AggregateChangeWithRoot<T> createUpdateChange(EntityAndPreviousVersion<T> entityAndVersion) {
private <T> RootAggregateChange<T> createUpdateChange(EntityAndPreviousVersion<T> entityAndVersion) {
AggregateChangeWithRoot<T> aggregateChange = MutableAggregateChange.forSave(entityAndVersion.entity,
RootAggregateChange<T> aggregateChange = MutableAggregateChange.forSave(entityAndVersion.entity,
entityAndVersion.version);
new RelationalEntityUpdateWriter<T>(context).write(entityAndVersion.entity, aggregateChange);
new RelationalEntityUpdateWriter<T>(context).write(entityAndVersion.entity,
aggregateChange);
return aggregateChange;
}

View File

@@ -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<T, ID> implements CrudRepository<T,ID>, PagingAndSortingRepository<T, ID> {
@@ -60,10 +59,7 @@ public class SimpleJdbcRepository<T, ID> implements CrudRepository<T,ID>, Paging
@Transactional
@Override
public <S extends T> Iterable<S> saveAll(Iterable<S> entities) {
return Streamable.of(entities).stream() //
.map(this::save) //
.collect(Collectors.toList());
return entityOperations.saveAll(entities);
}
@Override

View File

@@ -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<DummyEntity> aggregateChange = MutableAggregateChange.forSave(entity);
RootAggregateChange<DummyEntity> aggregateChange = MutableAggregateChange.forSave(entity);
aggregateChange.setRootAction(rootInsert);
entity = executor.execute(aggregateChange);
List<DummyEntity> 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<DummyEntity> aggregateChange = MutableAggregateChange.forSave(entity);
RootAggregateChange<DummyEntity> aggregateChange = MutableAggregateChange.forSave(entity);
aggregateChange.setRootAction(rootInsert);
aggregateChange.addAction(createInsert("single", content, null));
entity = executor.execute(aggregateChange);
List<DummyEntity> result = executor.executeSave(aggregateChange);
entity = result.get(0);
assertSoftly(softly -> {
@@ -111,12 +113,13 @@ public class AggregateChangeIdGenerationImmutableUnitTests {
entity = entity.withContentList(asList(content, content2));
AggregateChangeWithRoot<DummyEntity> aggregateChange = MutableAggregateChange.forSave(entity);
RootAggregateChange<DummyEntity> aggregateChange = MutableAggregateChange.forSave(entity);
aggregateChange.setRootAction(rootInsert);
aggregateChange.addAction(createInsert("contentList", content, 0));
aggregateChange.addAction(createInsert("contentList", content2, 1));
entity = executor.execute(aggregateChange);
List<DummyEntity> 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<DummyEntity> aggregateChange = MutableAggregateChange.forSave(entity);
RootAggregateChange<DummyEntity> aggregateChange = MutableAggregateChange.forSave(entity);
aggregateChange.setRootAction(rootInsert);
aggregateChange.addAction(createInsert("contentMap", content, "a"));
aggregateChange.addAction(createInsert("contentMap", content2, "b"));
entity = executor.execute(aggregateChange);
List<DummyEntity> 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<DummyEntity> aggregateChange = MutableAggregateChange.forSave(entity);
RootAggregateChange<DummyEntity> aggregateChange = MutableAggregateChange.forSave(entity);
aggregateChange.setRootAction(rootInsert);
aggregateChange.addAction(parentInsert);
aggregateChange.addAction(insert);
entity = executor.execute(aggregateChange);
List<DummyEntity> 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<DummyEntity> aggregateChange = MutableAggregateChange.forSave(entity);
RootAggregateChange<DummyEntity> aggregateChange = MutableAggregateChange.forSave(entity);
aggregateChange.setRootAction(rootInsert);
aggregateChange.addAction(parentInsert);
aggregateChange.addAction(insert1);
aggregateChange.addAction(insert2);
entity = executor.execute(aggregateChange);
List<DummyEntity> 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<DummyEntity> aggregateChange = MutableAggregateChange.forSave(entity);
RootAggregateChange<DummyEntity> aggregateChange = MutableAggregateChange.forSave(entity);
aggregateChange.setRootAction(rootInsert);
aggregateChange.addAction(parentInsert);
aggregateChange.addAction(insert1);
aggregateChange.addAction(insert2);
entity = executor.execute(aggregateChange);
List<DummyEntity> 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<DummyEntity> aggregateChange = MutableAggregateChange.forSave(entity);
RootAggregateChange<DummyEntity> aggregateChange = MutableAggregateChange.forSave(entity);
aggregateChange.setRootAction(rootInsert);
aggregateChange.addAction(parentInsert1);
aggregateChange.addAction(parentInsert2);
aggregateChange.addAction(insert1);
aggregateChange.addAction(insert2);
entity = executor.execute(aggregateChange);
List<DummyEntity> 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<DummyEntity> aggregateChange = MutableAggregateChange.forSave(entity);
RootAggregateChange<DummyEntity> 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<DummyEntity> 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<DummyEntity> aggregateChange = MutableAggregateChange.forSave(entity);
RootAggregateChange<DummyEntity> 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<DummyEntity> 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<DummyEntity> aggregateChange = MutableAggregateChange.forSave(entity);
RootAggregateChange<DummyEntity> aggregateChange = MutableAggregateChange.forSave(entity);
aggregateChange.setRootAction(rootInsert);
aggregateChange.addAction(parentInsert1);
aggregateChange.addAction(parentInsert2);
aggregateChange.addAction(insert1);
aggregateChange.addAction(insert2);
entity = executor.execute(aggregateChange);
List<DummyEntity> 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<DummyEntity> aggregateChange = MutableAggregateChange.forSave(entity);
RootAggregateChange<DummyEntity> aggregateChange = MutableAggregateChange.forSave(entity);
aggregateChange.setRootAction(rootInsert);
aggregateChange.addAction(parentInsert);
entity = executor.execute(aggregateChange);
List<DummyEntity> result = executor.executeSave(aggregateChange);
entity = result.get(0);
assertThat(entity.rootId).isEqualTo(1);
assertThat(entity.embedded.single.id).isEqualTo(2);

View File

@@ -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<DummyEntity> aggregateChange = MutableAggregateChange.forSave(entity);
RootAggregateChange<DummyEntity> 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<DummyEntity> aggregateChange = MutableAggregateChange.forSave(entity);
RootAggregateChange<DummyEntity> 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<DummyEntity> aggregateChange = MutableAggregateChange.forSave(entity);
RootAggregateChange<DummyEntity> 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<DummyEntity> aggregateChange = MutableAggregateChange.forSave(entity);
RootAggregateChange<DummyEntity> 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<DummyEntity> aggregateChange = MutableAggregateChange.forSave(entity);
RootAggregateChange<DummyEntity> 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<DummyEntity> aggregateChange = MutableAggregateChange.forSave(entity);
RootAggregateChange<DummyEntity> 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<DummyEntity> aggregateChange = MutableAggregateChange.forSave(entity);
RootAggregateChange<DummyEntity> 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<DummyEntity> aggregateChange = MutableAggregateChange.forSave(entity);
RootAggregateChange<DummyEntity> 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<DummyEntity> aggregateChange = MutableAggregateChange.forSave(entity);
RootAggregateChange<DummyEntity> 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<DummyEntity> aggregateChange = MutableAggregateChange.forSave(entity);
RootAggregateChange<DummyEntity> 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 -> {

View File

@@ -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

View File

@@ -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<DummyEntity> 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<DummyEntity> rootInsert = new DbAction.InsertRoot<>(root, IdValueSource.GENERATED);
executionContext.executeInsertRoot(rootInsert);
executionContext.executeInsert(createInsert(rootInsert, "content", content, null));
DummyEntity newRoot = executionContext.populateIdsIfNecessary();
List<DummyEntity> 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<DummyEntity> 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<DummyEntity> 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<DummyEntity> 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<DummyEntity> 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<RelationalPersistentProperty> toPath(String path) {

View File

@@ -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<DummyEntity> 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<DummyEntity> 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<DummyEntity> 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<DummyEntity> 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<DummyEntity> 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<DummyEntity> 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<DummyEntity> 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<DummyEntity> rootInsert = new DbAction.UpdateRoot<>(root, null);
executionContext.executeUpdateRoot(rootInsert);
when(accessStrategy.update(root, DummyEntity.class)).thenReturn(true);
DbAction.UpdateRoot<DummyEntity> 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<DummyEntity> 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<DummyEntity> 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<DummyEntity> 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<DummyEntity> 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<RelationalPersistentProperty> toPath(String path) {

View File

@@ -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<Root> savedRoots = rootRepository.saveAll(asList(root1, root2));
List<Root> 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<Root> 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<Root> updatedRoots = rootRepository.saveAll(asList(updatedRoot1, updatedRoot2));
List<Root> 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<Root> savedRoots = rootRepository.saveAll(asList(updatedRoot1, root2));
List<Root> 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<Root, Long> {
List<Root> findAllByOrderByIdAsc();
}
@Value
static class Root {
@Id Long id;
String name;
Intermediate intermediate;
@MappedCollection(idColumn = "ROOT_ID", keyColumn = "ROOT_KEY") List<Intermediate> intermediates;
}
@Value
static class Intermediate {
@Id Long id;
String name;
Leaf leaf;
@MappedCollection(idColumn = "INTERMEDIATE_ID", keyColumn = "INTERMEDIATE_KEY") List<Leaf> leaves;
}
@Value
static class Leaf {
@Id Long id;
String name;
}
static class MyEventListener implements ApplicationListener<AbstractRelationalEvent<?>> {
private List<AbstractRelationalEvent<?>> events = new ArrayList<>();

View File

@@ -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<Integer> setIdInKeyHolder = invocation -> {
HashMap<String, Object> keys = new HashMap<>();
keys.put("id", 4711L);
keys.put("id", generatedId);
KeyHolder keyHolder = invocation.getArgument(2);
keyHolder.getKeyList().add(keys);

View File

@@ -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
);

View File

@@ -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
);

View File

@@ -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
);

View File

@@ -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
);

View File

@@ -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
);

View File

@@ -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
);

View File

@@ -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
);

View File

@@ -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
);

View File

@@ -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<T, C extends MutableAggregateChange<T>> extends AggregateChange<T> {
/**
* 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 <T> entity type.
* @return the {@link BatchingAggregateChange} for saving root entities.
* @since 3.0
*/
static <T> BatchingAggregateChange<T, RootAggregateChange<T>> forSave(Class<T> entityClass) {
Assert.notNull(entityClass, "Entity class must not be null");
return new SaveBatchingAggregateChange<>(entityClass);
}
}

View File

@@ -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<T> {
}
}
/**
* Represents a batch of {@link DbAction} that share a common value for a property of the action.
*
* @param <T> type of the entity for which this represents a database interaction.
* @since 3.0
*/
abstract class BatchWithValue<T, A extends DbAction<T>, B> implements DbAction<T> {
private final List<A> actions;
private final B batchValue;
public BatchWithValue(List<A> actions, Function<A, B> batchValueExtractor) {
Assert.notEmpty(actions, "Actions must contain at least one action");
Iterator<A> 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<T> getEntityType() {
return actions.get(0).getEntityType();
}
public List<A> 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 <T> type of the entity for which this represents a database interaction.
* @since 2.4
*/
final class InsertBatch<T> implements DbAction<T> {
private final List<Insert<T>> inserts;
private final IdValueSource idValueSource;
public InsertBatch(List<Insert<T>> inserts, IdValueSource idValueSource) {
Assert.notEmpty(inserts, "Inserts must contains at least one insert");
this.inserts = inserts;
this.idValueSource = idValueSource;
}
@Override
public Class<T> getEntityType() {
return inserts.get(0).getEntityType();
}
public List<Insert<T>> getInserts() {
return inserts;
}
public IdValueSource getIdValueSource() {
return idValueSource;
}
@Override
public String toString() {
return "InsertBatch{" + "inserts=" + inserts + ", idValueSource=" + idValueSource + '}';
final class BatchInsert<T> extends BatchWithValue<T, Insert<T>, IdValueSource> {
public BatchInsert(List<Insert<T>> actions) {
super(actions, Insert::getIdValueSource);
}
}

View File

@@ -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<T> implements AggregateChangeWithRoot<T> {
class DefaultRootAggregateChange<T> implements RootAggregateChange<T> {
private final Kind kind;
@@ -42,7 +42,7 @@ class DefaultAggregateChangeWithRoot<T> implements AggregateChangeWithRoot<T> {
/** The previous version assigned to the instance being changed, if available */
@Nullable private final Number previousVersion;
public DefaultAggregateChangeWithRoot(Kind kind, Class<T> entityType, @Nullable Number previousVersion) {
public DefaultRootAggregateChange(Kind kind, Class<T> entityType, @Nullable Number previousVersion) {
this.kind = kind;
this.entityType = entityType;

View File

@@ -30,32 +30,32 @@ import org.springframework.util.ClassUtils;
public interface MutableAggregateChange<T> extends AggregateChange<T> {
/**
* 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 <T> 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 <T> AggregateChangeWithRoot<T> forSave(T entity) {
static <T> RootAggregateChange<T> 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 <T> 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 <T> AggregateChangeWithRoot<T> forSave(T entity, @Nullable Number previousVersion) {
static <T> RootAggregateChange<T> forSave(T entity, @Nullable Number previousVersion) {
Assert.notNull(entity, "Entity must not be null");
return new DefaultAggregateChangeWithRoot<>(Kind.SAVE, (Class<T>) ClassUtils.getUserClass(entity), previousVersion);
return new DefaultRootAggregateChange<>(Kind.SAVE, (Class<T>) ClassUtils.getUserClass(entity), previousVersion);
}
/**

View File

@@ -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<T> implements EntityWriter<T, AggregateChangeWithRoot<T>> {
public class RelationalEntityInsertWriter<T> implements EntityWriter<T, RootAggregateChange<T>> {
private final RelationalMappingContext context;
@@ -36,7 +36,7 @@ public class RelationalEntityInsertWriter<T> implements EntityWriter<T, Aggregat
}
@Override
public void write(T root, AggregateChangeWithRoot<T> aggregateChange) {
public void write(T root, RootAggregateChange<T> aggregateChange) {
new WritingContext<>(context, root, aggregateChange).insert();
}
}

View File

@@ -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<T> implements EntityWriter<T, AggregateChangeWithRoot<T>> {
public class RelationalEntityUpdateWriter<T> implements EntityWriter<T, RootAggregateChange<T>> {
private final RelationalMappingContext context;
@@ -36,7 +36,7 @@ public class RelationalEntityUpdateWriter<T> implements EntityWriter<T, Aggregat
}
@Override
public void write(T root, AggregateChangeWithRoot<T> aggregateChange) {
public void write(T root, RootAggregateChange<T> aggregateChange) {
new WritingContext<>(context, root, aggregateChange).update();
}
}

View File

@@ -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<T> implements EntityWriter<T, AggregateChangeWithRoot<T>> {
public class RelationalEntityWriter<T> implements EntityWriter<T, RootAggregateChange<T>> {
private final RelationalMappingContext context;
@@ -34,7 +34,7 @@ public class RelationalEntityWriter<T> implements EntityWriter<T, AggregateChang
}
@Override
public void write(T root, AggregateChangeWithRoot<T> aggregateChange) {
public void write(T root, RootAggregateChange<T> aggregateChange) {
new WritingContext<>(context, root, aggregateChange).save();
}
}

View File

@@ -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<T> extends MutableAggregateChange<T> {
public interface RootAggregateChange<T> extends MutableAggregateChange<T> {
/**
* The root object to which this {@link AggregateChange} relates. Guaranteed to be not {@code null}.

View File

@@ -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<T> implements BatchingAggregateChange<T, RootAggregateChange<T>> {
private static final Comparator<PersistentPropertyPath<RelationalPersistentProperty>> pathLengthComparator = //
Comparator.comparing(PersistentPropertyPath::getLength);
private final Class<T> entityType;
private final List<DbAction.WithRoot<?>> rootActions = new ArrayList<>();
private final Map<PersistentPropertyPath<RelationalPersistentProperty>, Map<IdValueSource, List<DbAction.Insert<Object>>>> insertActions = //
new HashMap<>();
private final Map<PersistentPropertyPath<RelationalPersistentProperty>, List<DbAction.Delete<?>>> deleteActions = //
new HashMap<>();
public SaveBatchingAggregateChange(Class<T> entityType) {
this.entityType = entityType;
}
@Override
public Kind getKind() {
return Kind.SAVE;
}
@Override
public Class<T> getEntityType() {
return entityType;
}
@Override
public void forEachAction(Consumer<? super DbAction<?>> 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<T> aggregateChange) {
aggregateChange.forEachAction(action -> {
if (action instanceof DbAction.WithRoot<?> rootAction) {
rootActions.add(rootAction);
} else if (action instanceof DbAction.Insert<?>) {
// noinspection unchecked
addInsert((DbAction.Insert<Object>) action);
} else if (action instanceof DbAction.Delete<?> deleteAction) {
addDelete(deleteAction);
}
});
}
private void addInsert(DbAction.Insert<Object> action) {
PersistentPropertyPath<RelationalPersistentProperty> 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<RelationalPersistentProperty> propertyPath = action.getPropertyPath();
deleteActions.merge(propertyPath, new ArrayList<>(singletonList(action)), (actions, defaultValue) -> {
actions.add(action);
return actions;
});
}
}

View File

@@ -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<T> {
private final Map<PersistentPropertyPath<RelationalPersistentProperty>, List<PathNode>> nodesCache = new HashMap<>();
private final IdValueSource rootIdValueSource;
@Nullable private final Number previousVersion;
private final AggregateChangeWithRoot<T> aggregateChange;
private final RootAggregateChange<T> aggregateChange;
WritingContext(RelationalMappingContext context, T root, AggregateChangeWithRoot<T> aggregateChange) {
WritingContext(RelationalMappingContext context, T root, RootAggregateChange<T> aggregateChange) {
this.context = context;
this.root = root;
@@ -150,15 +149,7 @@ class WritingContext<T> {
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<DbAction.Insert<Object>> 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<DbAction<?>> deleteReferenced() {

View File

@@ -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;
}

View File

@@ -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 {

View File

@@ -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<SingleReferenceEntity> aggregateChange = MutableAggregateChange.forSave(entity);
RootAggregateChange<SingleReferenceEntity> aggregateChange = MutableAggregateChange.forSave(entity);
new RelationalEntityInsertWriter<SingleReferenceEntity>(context).write(entity, aggregateChange);
@@ -62,7 +62,7 @@ public class RelationalEntityInsertWriterUnitTests {
SingleReferenceEntity entity = new SingleReferenceEntity(SOME_ENTITY_ID);
AggregateChangeWithRoot<SingleReferenceEntity> aggregateChange = MutableAggregateChange.forSave(entity);
RootAggregateChange<SingleReferenceEntity> aggregateChange = MutableAggregateChange.forSave(entity);
new RelationalEntityInsertWriter<SingleReferenceEntity>(context).write(entity, aggregateChange);

View File

@@ -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<SingleReferenceEntity> aggregateChange = MutableAggregateChange.forSave(entity);
RootAggregateChange<SingleReferenceEntity> aggregateChange = MutableAggregateChange.forSave(entity);
new RelationalEntityUpdateWriter<SingleReferenceEntity>(context).write(entity, aggregateChange);

View File

@@ -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<SingleReferenceEntity> aggregateChange = MutableAggregateChange.forSave(entity);
RootAggregateChange<SingleReferenceEntity> aggregateChange = MutableAggregateChange.forSave(entity);
new RelationalEntityWriter<SingleReferenceEntity>(context).write(entity, aggregateChange);
@@ -105,7 +102,7 @@ public class RelationalEntityWriterUnitTests {
void newEntityWithPrimitiveLongId_insertDoesNotIncludeId_whenIdValueIsZero() {
PrimitiveLongIdEntity entity = new PrimitiveLongIdEntity();
AggregateChangeWithRoot<PrimitiveLongIdEntity> aggregateChange = MutableAggregateChange.forSave(entity);
RootAggregateChange<PrimitiveLongIdEntity> aggregateChange = MutableAggregateChange.forSave(entity);
new RelationalEntityWriter<PrimitiveLongIdEntity>(context).write(entity, aggregateChange);
@@ -126,7 +123,7 @@ public class RelationalEntityWriterUnitTests {
void newEntityWithPrimitiveIntId_insertDoesNotIncludeId_whenIdValueIsZero() {
PrimitiveIntIdEntity entity = new PrimitiveIntIdEntity();
AggregateChangeWithRoot<PrimitiveIntIdEntity> aggregateChange = MutableAggregateChange.forSave(entity);
RootAggregateChange<PrimitiveIntIdEntity> aggregateChange = MutableAggregateChange.forSave(entity);
new RelationalEntityWriter<PrimitiveIntIdEntity>(context).write(entity, aggregateChange);
@@ -149,7 +146,7 @@ public class RelationalEntityWriterUnitTests {
EmbeddedReferenceEntity entity = new EmbeddedReferenceEntity(null);
entity.other = new Element(2L);
AggregateChangeWithRoot<EmbeddedReferenceEntity> aggregateChange = MutableAggregateChange.forSave(entity);
RootAggregateChange<EmbeddedReferenceEntity> aggregateChange = MutableAggregateChange.forSave(entity);
new RelationalEntityWriter<EmbeddedReferenceEntity>(context).write(entity, aggregateChange);
@@ -172,7 +169,7 @@ public class RelationalEntityWriterUnitTests {
SingleReferenceEntity entity = new SingleReferenceEntity(null);
entity.other = new Element(null);
AggregateChangeWithRoot<SingleReferenceEntity> aggregateChange = MutableAggregateChange.forSave(entity);
RootAggregateChange<SingleReferenceEntity> aggregateChange = MutableAggregateChange.forSave(entity);
new RelationalEntityWriter<SingleReferenceEntity>(context).write(entity, aggregateChange);
@@ -197,7 +194,7 @@ public class RelationalEntityWriterUnitTests {
entity.primitiveLongIdEntity = new PrimitiveLongIdEntity();
entity.primitiveIntIdEntity = new PrimitiveIntIdEntity();
AggregateChangeWithRoot<EntityWithReferencesToPrimitiveIdEntity> aggregateChange = MutableAggregateChange
RootAggregateChange<EntityWithReferencesToPrimitiveIdEntity> aggregateChange = MutableAggregateChange
.forSave(entity);
new RelationalEntityWriter<EntityWithReferencesToPrimitiveIdEntity>(context).write(entity, aggregateChange);
@@ -224,7 +221,7 @@ public class RelationalEntityWriterUnitTests {
SingleReferenceEntity entity = new SingleReferenceEntity(SOME_ENTITY_ID);
AggregateChangeWithRoot<SingleReferenceEntity> aggregateChange = MutableAggregateChange.forSave(entity, 1L);
RootAggregateChange<SingleReferenceEntity> aggregateChange = MutableAggregateChange.forSave(entity, 1L);
new RelationalEntityWriter<SingleReferenceEntity>(context).write(entity, aggregateChange);
@@ -247,7 +244,7 @@ public class RelationalEntityWriterUnitTests {
SingleReferenceEntity entity = new SingleReferenceEntity(SOME_ENTITY_ID);
entity.other = new Element(null);
AggregateChangeWithRoot<SingleReferenceEntity> aggregateChange = MutableAggregateChange.forSave(entity, 1L);
RootAggregateChange<SingleReferenceEntity> aggregateChange = MutableAggregateChange.forSave(entity, 1L);
new RelationalEntityWriter<SingleReferenceEntity>(context).write(entity, aggregateChange);
@@ -269,7 +266,7 @@ public class RelationalEntityWriterUnitTests {
public void newEntityWithEmptySetResultsInSingleInsert() {
SetContainer entity = new SetContainer(null);
AggregateChangeWithRoot<SetContainer> aggregateChange = MutableAggregateChange.forSave(entity);
RootAggregateChange<SetContainer> aggregateChange = MutableAggregateChange.forSave(entity);
new RelationalEntityWriter<SetContainer>(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<SetContainer> aggregateChange = MutableAggregateChange.forSave(entity);
RootAggregateChange<SetContainer> aggregateChange = MutableAggregateChange.forSave(entity);
new RelationalEntityWriter<SetContainer>(context).write(entity, aggregateChange);
List<DbAction<?>> 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<Insert<Element>> 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<CascadingReferenceEntity> aggregateChange = MutableAggregateChange.forSave(entity);
RootAggregateChange<CascadingReferenceEntity> aggregateChange = MutableAggregateChange.forSave(entity);
new RelationalEntityWriter<CascadingReferenceEntity>(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<Insert<CascadingReferenceMiddleElement>> 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<Insert<Element>> 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<CascadingReferenceEntity> aggregateChange = MutableAggregateChange.forSave(entity, 1L);
RootAggregateChange<CascadingReferenceEntity> aggregateChange = MutableAggregateChange.forSave(entity, 1L);
new RelationalEntityWriter<CascadingReferenceEntity>(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<Insert<CascadingReferenceMiddleElement>> 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<Insert<Element>> 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<MapContainer> aggregateChange = MutableAggregateChange.forSave(entity);
RootAggregateChange<MapContainer> aggregateChange = MutableAggregateChange.forSave(entity);
new RelationalEntityWriter<MapContainer>(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<MapContainer> aggregateChange = MutableAggregateChange.forSave(entity);
RootAggregateChange<MapContainer> aggregateChange = MutableAggregateChange.forSave(entity);
new RelationalEntityWriter<MapContainer>(context).write(entity, aggregateChange);
List<DbAction<?>> 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<Insert<Element>> 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<MapContainer> aggregateChange = MutableAggregateChange.forSave(entity);
RootAggregateChange<MapContainer> aggregateChange = MutableAggregateChange.forSave(entity);
new RelationalEntityWriter<MapContainer>(context).write(entity, aggregateChange);
List<DbAction<?>> 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<Insert<Element>> 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<ListContainer> aggregateChange = MutableAggregateChange.forSave(entity);
RootAggregateChange<ListContainer> aggregateChange = MutableAggregateChange.forSave(entity);
new RelationalEntityWriter<ListContainer>(context).write(entity, aggregateChange);
@@ -565,7 +498,7 @@ public class RelationalEntityWriterUnitTests {
entity.elements.add(new Element(null));
entity.elements.add(new Element(null));
AggregateChangeWithRoot<ListContainer> aggregateChange = MutableAggregateChange.forSave(entity);
RootAggregateChange<ListContainer> aggregateChange = MutableAggregateChange.forSave(entity);
new RelationalEntityWriter<ListContainer>(context).write(entity, aggregateChange);
List<DbAction<?>> 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<Insert<Element>> 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<MapContainer> aggregateChange = MutableAggregateChange.forSave(entity, 1L);
RootAggregateChange<MapContainer> aggregateChange = MutableAggregateChange.forSave(entity, 1L);
new RelationalEntityWriter<MapContainer>(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<ListContainer> aggregateChange = MutableAggregateChange.forSave(entity, 1L);
RootAggregateChange<ListContainer> aggregateChange = MutableAggregateChange.forSave(entity, 1L);
new RelationalEntityWriter<ListContainer>(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<ListMapContainer> aggregateChange = MutableAggregateChange.forSave(listMapContainer, 1L);
RootAggregateChange<ListMapContainer> aggregateChange = MutableAggregateChange.forSave(listMapContainer, 1L);
new RelationalEntityWriter<ListMapContainer>(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<NoIdListMapContainer> aggregateChange = MutableAggregateChange.forSave(listMapContainer,
RootAggregateChange<NoIdListMapContainer> aggregateChange = MutableAggregateChange.forSave(listMapContainer,
1L);
new RelationalEntityWriter<NoIdListMapContainer>(context).write(listMapContainer, aggregateChange);
@@ -697,7 +621,7 @@ public class RelationalEntityWriterUnitTests {
EmbeddedReferenceChainEntity entity = new EmbeddedReferenceChainEntity(null);
// the embedded is null !!!
AggregateChangeWithRoot<EmbeddedReferenceChainEntity> aggregateChange = MutableAggregateChange.forSave(entity);
RootAggregateChange<EmbeddedReferenceChainEntity> aggregateChange = MutableAggregateChange.forSave(entity);
new RelationalEntityWriter<EmbeddedReferenceChainEntity>(context).write(entity, aggregateChange);
@@ -721,7 +645,7 @@ public class RelationalEntityWriterUnitTests {
root.other = new EmbeddedReferenceChainEntity(null);
// the embedded is null !!!
AggregateChangeWithRoot<RootWithEmbeddedReferenceChainEntity> aggregateChange = MutableAggregateChange
RootAggregateChange<RootWithEmbeddedReferenceChainEntity> aggregateChange = MutableAggregateChange
.forSave(root);
new RelationalEntityWriter<RootWithEmbeddedReferenceChainEntity>(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<ListContainer> aggregateChange = MutableAggregateChange.forSave(root);
new RelationalEntityWriter<ListContainer>(context).write(root, aggregateChange);
List<DbAction<?>> 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<Element> 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<Element> 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<EntityWithReferencesToPrimitiveIdEntity> aggregateChange = MutableAggregateChange
RootAggregateChange<EntityWithReferencesToPrimitiveIdEntity> aggregateChange = MutableAggregateChange
.forSave(entity);
new RelationalEntityWriter<EntityWithReferencesToPrimitiveIdEntity>(context).write(entity, aggregateChange);
@@ -824,7 +700,7 @@ public class RelationalEntityWriterUnitTests {
WithReadOnlyReference entity = new WithReadOnlyReference(null);
entity.readOnly = new Element(SOME_ENTITY_ID);
AggregateChangeWithRoot<WithReadOnlyReference> aggregateChange = MutableAggregateChange.forSave(entity);
RootAggregateChange<WithReadOnlyReference> aggregateChange = MutableAggregateChange.forSave(entity);
new RelationalEntityWriter<WithReadOnlyReference>(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<WithReadOnlyReference> aggregateChange = MutableAggregateChange.forSave(entity);
RootAggregateChange<WithReadOnlyReference> aggregateChange = MutableAggregateChange.forSave(entity);
new RelationalEntityWriter<WithReadOnlyReference>(context).write(entity, aggregateChange);
@@ -865,29 +741,6 @@ public class RelationalEntityWriterUnitTests {
return actions;
}
@NotNull
private <T> InsertBatch<T> getInsertBatchAction(List<DbAction<?>> actions, Class<T> entityType) {
return getInsertBatchActions(actions, entityType).stream().findFirst()
.orElseThrow(() -> new RuntimeException("No InsertBatch action found!"));
}
@NotNull
private <T> InsertBatch<T> getInsertBatchAction(List<DbAction<?>> actions, Class<T> 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 <T> List<InsertBatch<T>> getInsertBatchActions(List<DbAction<?>> actions, Class<T> entityType) {
// noinspection unchecked
return actions.stream() //
.filter(dbAction -> dbAction instanceof InsertBatch) //
.filter(dbAction -> dbAction.getEntityType().equals(entityType)) //
.map(dbAction -> (InsertBatch<T>) dbAction).collect(Collectors.toList());
}
private CascadingReferenceMiddleElement createMiddleElement(Element first, Element second) {
CascadingReferenceMiddleElement middleElement1 = new CascadingReferenceMiddleElement(null);

View File

@@ -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<Root, RootAggregateChange<Root>> change = BatchingAggregateChange.forSave(Root.class);
assertThat(extractActions(change)).isEmpty();
}
@Test
void yieldsRootActions() {
Root root1 = new Root(null, null);
DbAction.InsertRoot<Root> root1Insert = new DbAction.InsertRoot<>(root1, IdValueSource.GENERATED);
RootAggregateChange<Root> aggregateChange1 = MutableAggregateChange.forSave(root1);
aggregateChange1.setRootAction(root1Insert);
Root root2 = new Root(null, null);
DbAction.InsertRoot<Root> root2Insert = new DbAction.InsertRoot<>(root2, IdValueSource.GENERATED);
RootAggregateChange<Root> aggregateChange2 = MutableAggregateChange.forSave(root2);
aggregateChange2.setRootAction(root2Insert);
BatchingAggregateChange<Root, RootAggregateChange<Root>> 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<Root> root1Update = new DbAction.UpdateRoot<>(root1, null);
RootAggregateChange<Root> 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<Root> root2Insert = new DbAction.InsertRoot<>(root2, IdValueSource.GENERATED);
RootAggregateChange<Root> aggregateChange2 = MutableAggregateChange.forSave(root2);
aggregateChange2.setRootAction(root2Insert);
BatchingAggregateChange<Root, RootAggregateChange<Root>> 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<Root> 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<Root> 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<Root, RootAggregateChange<Root>> 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<Root> root1Insert = new DbAction.InsertRoot<>(root1, IdValueSource.GENERATED);
RootAggregateChange<Root> 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<Root> root2Update = new DbAction.UpdateRoot<>(root2, null);
RootAggregateChange<Root> aggregateChange2 = MutableAggregateChange.forSave(root2);
aggregateChange2.setRootAction(root2Update);
DbAction.Delete<?> root2IntermediateDelete = new DbAction.Delete<>(1L,
context.getPersistentPropertyPath("intermediate", Root.class));
aggregateChange2.addAction(root2IntermediateDelete);
BatchingAggregateChange<Root, RootAggregateChange<Root>> 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<Root> rootInsert = new DbAction.InsertRoot<>(root, IdValueSource.GENERATED);
RootAggregateChange<Root> aggregateChange = MutableAggregateChange.forSave(root);
aggregateChange.setRootAction(rootInsert);
Intermediate intermediateGeneratedId = new Intermediate(null, "intermediateGeneratedId", null);
DbAction.Insert<Intermediate> 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<Intermediate> intermediateInsertProvidedId = new DbAction.Insert<>(intermediateProvidedId,
context.getPersistentPropertyPath("intermediate", Root.class), rootInsert, emptyMap(), IdValueSource.PROVIDED);
aggregateChange.addAction(intermediateInsertProvidedId);
BatchingAggregateChange<Root, RootAggregateChange<Root>> change = BatchingAggregateChange.forSave(Root.class);
change.add(aggregateChange);
List<DbAction<?>> 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<Root> root1Insert = new DbAction.InsertRoot<>(root1, IdValueSource.GENERATED);
RootAggregateChange<Root> aggregateChange1 = MutableAggregateChange.forSave(root1);
aggregateChange1.setRootAction(root1Insert);
Intermediate root1Intermediate = new Intermediate(null, "root1Intermediate", null);
DbAction.Insert<Intermediate> 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<Leaf> 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<Root> root2Insert = new DbAction.InsertRoot<>(root2, IdValueSource.GENERATED);
RootAggregateChange<Root> aggregateChange2 = MutableAggregateChange.forSave(root2);
aggregateChange2.setRootAction(root2Insert);
Intermediate root2Intermediate = new Intermediate(null, "root2Intermediate", null);
DbAction.Insert<Intermediate> root2IntermediateInsert = new DbAction.Insert<>(root2Intermediate,
context.getPersistentPropertyPath("intermediate", Root.class), root2Insert, emptyMap(),
IdValueSource.GENERATED);
aggregateChange2.addAction(root2IntermediateInsert);
BatchingAggregateChange<Root, RootAggregateChange<Root>> change = BatchingAggregateChange.forSave(Root.class);
change.add(aggregateChange1);
change.add(aggregateChange2);
List<DbAction<?>> 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<RootWithSameLengthReferences> rootInsert = new DbAction.InsertRoot<>(root,
IdValueSource.GENERATED);
RootAggregateChange<RootWithSameLengthReferences> aggregateChange = MutableAggregateChange.forSave(root);
aggregateChange.setRootAction(rootInsert);
Intermediate one = new Intermediate(null, "one", null);
DbAction.Insert<Intermediate> 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<Intermediate> twoInsert = new DbAction.Insert<>(two,
context.getPersistentPropertyPath("two", RootWithSameLengthReferences.class), rootInsert, emptyMap(),
IdValueSource.GENERATED);
aggregateChange.addAction(twoInsert);
BatchingAggregateChange<RootWithSameLengthReferences, RootAggregateChange<RootWithSameLengthReferences>> change = //
BatchingAggregateChange.forSave(RootWithSameLengthReferences.class);
change.add(aggregateChange);
List<DbAction<?>> 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<DbAction.BatchInsert<Intermediate>> batchInsertActions = getBatchInsertActions(actions, Intermediate.class);
assertThat(batchInsertActions).hasSize(2);
assertThat(batchInsertActions.get(0).getActions()).containsExactly(oneInsert);
assertThat(batchInsertActions.get(1).getActions()).containsExactly(twoInsert);
}
private <T> DbAction.BatchInsert<T> getBatchInsertAction(List<DbAction<?>> actions, Class<T> 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 <T> DbAction.BatchInsert<T> getBatchInsertAction(List<DbAction<?>> actions, Class<T> entityType) {
return getBatchInsertActions(actions, entityType).stream().findFirst()
.orElseThrow(() -> new RuntimeException("No BatchInsert action found!"));
}
@SuppressWarnings("unchecked")
private <T> List<DbAction.BatchInsert<T>> getBatchInsertActions(List<DbAction<?>> actions, Class<T> entityType) {
return actions.stream() //
.filter(dbAction -> dbAction instanceof DbAction.BatchInsert) //
.filter(dbAction -> dbAction.getEntityType().equals(entityType)) //
.map(dbAction -> (DbAction.BatchInsert<T>) dbAction).collect(Collectors.toList());
}
private <T> List<DbAction<?>> extractActions(BatchingAggregateChange<T, RootAggregateChange<T>> change) {
List<DbAction<?>> 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;
}
}