DATAJDBC-432 - Extracted AggregateChangeExecutor from AggregateChange.

This separates the execution plan of  a change encoded in the AggregateChange from its execution encoded in the AggregateChangeExecutor.
This commit is contained in:
Jens Schauder
2019-10-17 14:59:35 +02:00
parent 448f0e0d39
commit 7d8c78e9ec
5 changed files with 458 additions and 332 deletions

View File

@@ -0,0 +1,330 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.jdbc.core;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.mapping.PersistentPropertyPath;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.relational.core.conversion.AggregateChange;
import org.springframework.data.relational.core.conversion.DbAction;
import org.springframework.data.relational.core.conversion.Interpreter;
import org.springframework.data.relational.core.conversion.RelationalConverter;
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
import org.springframework.data.util.Pair;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Executes an {@link AggregateChange} by handing the included {@link DbAction} instances to the interpreter. In a
* second step ids generated by the {@link Interpreter} get propagated to the entities contained in the actions.
*
* @author Jens Schauder
* @author Mark Paluch
* @since 1.2
*/
class AggregateChangeExecutor {
private final Interpreter interpreter;
private final RelationalConverter converter;
private final MappingContext<? extends RelationalPersistentEntity<?>, ? extends RelationalPersistentProperty> context;
AggregateChangeExecutor(Interpreter interpreter, RelationalConverter converter) {
this.interpreter = interpreter;
this.converter = converter;
this.context = converter.getMappingContext();
}
@SuppressWarnings("unchecked")
<T> void execute(AggregateChange<T> aggregateChange) {
List<DbAction<?>> actions = new ArrayList<>();
aggregateChange.forEachAction(action -> {
action.executeWith(interpreter);
actions.add(action);
});
T newRoot = (T) populateIdsIfNecessary(actions);
if (newRoot != null) {
aggregateChange.setEntity(newRoot);
}
}
@Nullable
private Object populateIdsIfNecessary(List<DbAction<?>> actions) {
Object newRoot = null;
// have the actions so that the inserts on the leaves come first.
List<DbAction<?>> reverseActions = new ArrayList<>(actions);
Collections.reverse(reverseActions);
AggregateChangeExecutor.StagedValues cascadingValues = new AggregateChangeExecutor.StagedValues();
for (DbAction<?> action : reverseActions) {
if (!(action instanceof DbAction.WithGeneratedId)) {
continue;
}
DbAction.WithGeneratedId<?> withGeneratedId = (DbAction.WithGeneratedId<?>) action;
Object generatedId = withGeneratedId.getGeneratedId();
Object newEntity = setIdAndCascadingProperties(withGeneratedId, generatedId, cascadingValues);
// the id property was immutable so we have to propagate changes up the tree
if (newEntity != ((DbAction.WithGeneratedId<?>) action).getEntity()) {
if (action instanceof DbAction.Insert) {
DbAction.Insert insert = (DbAction.Insert) action;
Pair qualifier = insert.getQualifier();
cascadingValues.stage(insert.getDependingOn(), insert.getPropertyPath(),
qualifier == null ? null : qualifier.getSecond(), newEntity);
} else if (action instanceof DbAction.InsertRoot) {
newRoot = newEntity;
}
}
}
return newRoot;
}
@SuppressWarnings("unchecked")
private <S> Object setIdAndCascadingProperties(DbAction.WithGeneratedId<S> action, @Nullable Object generatedId,
AggregateChangeExecutor.StagedValues cascadingValues) {
S originalEntity = action.getEntity();
RelationalPersistentEntity<S> persistentEntity = (RelationalPersistentEntity<S>) context
.getRequiredPersistentEntity(action.getEntityType());
PersistentPropertyAccessor<S> propertyAccessor = converter.getPropertyAccessor(persistentEntity, originalEntity);
if (generatedId != null) {
propertyAccessor.setProperty(persistentEntity.getRequiredIdProperty(), generatedId);
}
// set values of changed immutables referenced by this entity
cascadingValues.forEachPath(action, (persistentPropertyPath, o) -> propertyAccessor
.setProperty(getRelativePath(action, persistentPropertyPath), o));
return propertyAccessor.getBean();
}
@SuppressWarnings("unchecked")
private PersistentPropertyPath getRelativePath(DbAction action, PersistentPropertyPath pathToValue) {
if (action instanceof DbAction.Insert) {
return pathToValue.getExtensionForBaseOf(((DbAction.Insert) action).getPropertyPath());
}
if (action instanceof DbAction.InsertRoot) {
return pathToValue;
}
throw new IllegalArgumentException(String.format("DbAction of type %s is not supported.", action.getClass()));
}
/**
* Accumulates information about staged immutable objects in an aggregate that require updating because their state
* changed because of {@link DbAction} execution.
*/
private static class StagedValues {
static final List<MultiValueAggregator> aggregators = Arrays.asList(SetAggregator.INSTANCE, MapAggregator.INSTANCE,
ListAggregator.INSTANCE, SingleElementAggregator.INSTANCE);
Map<DbAction, Map<PersistentPropertyPath, Object>> values = new HashMap<>();
/**
* Adds a value that needs to be set in an entity higher up in the tree of entities in the aggregate. If the
* attribute to be set is multivalued this method expects only a single element.
*
* @param action The action responsible for persisting the entity that needs the added value set. Must not be
* {@literal null}.
* @param path The path to the property in which to set the value. Must not be {@literal null}.
* @param qualifier If {@code path} is a qualified multivalued properties this parameter contains the qualifier. May
* be {@literal null}.
* @param value The value to be set. Must not be {@literal null}.
*/
@SuppressWarnings("unchecked")
<T> void stage(DbAction<?> action, PersistentPropertyPath path, @Nullable Object qualifier, Object value) {
MultiValueAggregator<T> aggregator = getAggregatorFor(path);
Map<PersistentPropertyPath, Object> valuesForPath = this.values.computeIfAbsent(action,
dbAction -> new HashMap<>());
T currentValue = (T) valuesForPath.computeIfAbsent(path,
persistentPropertyPath -> aggregator.createEmptyInstance());
Object newValue = aggregator.add(currentValue, qualifier, value);
valuesForPath.put(path, newValue);
}
private MultiValueAggregator getAggregatorFor(PersistentPropertyPath path) {
PersistentProperty property = path.getRequiredLeafProperty();
for (MultiValueAggregator aggregator : aggregators) {
if (aggregator.handles(property)) {
return aggregator;
}
}
throw new IllegalStateException(String.format("Can't handle path %s", path));
}
/**
* Performs the given action for each entry in this the staging area that are provided by {@link DbAction} until all
* {@link PersistentPropertyPath} have been processed or the action throws an exception. The {@link BiConsumer
* action} is called with each applicable {@link PersistentPropertyPath} and {@code value} that is assignable to the
* property.
*/
void forEachPath(DbAction<?> dbAction, BiConsumer<PersistentPropertyPath, Object> action) {
values.getOrDefault(dbAction, Collections.emptyMap()).forEach(action);
}
}
interface MultiValueAggregator<T> {
default Class<? super T> handledType() {
return Object.class;
}
default boolean handles(PersistentProperty property) {
return handledType().isAssignableFrom(property.getType());
}
@Nullable
T createEmptyInstance();
T add(@Nullable T aggregate, @Nullable Object qualifier, Object value);
}
private enum SetAggregator implements MultiValueAggregator<Set> {
INSTANCE;
@Override
public Class<Set> handledType() {
return Set.class;
}
@Override
public Set createEmptyInstance() {
return new HashSet();
}
@SuppressWarnings("unchecked")
@Override
public Set add(@Nullable Set set, @Nullable Object qualifier, Object value) {
Assert.notNull(set, "Set must not be null");
set.add(value);
return set;
}
}
private enum ListAggregator implements MultiValueAggregator<List> {
INSTANCE;
@Override
public boolean handles(PersistentProperty property) {
return property.isCollectionLike();
}
@Override
public List createEmptyInstance() {
return new ArrayList();
}
@SuppressWarnings("unchecked")
@Override
public List add(@Nullable List list, @Nullable Object qualifier, Object value) {
Assert.notNull(list, "List must not be null.");
int index = (int) qualifier;
if (index >= list.size()) {
list.add(value);
} else {
list.add(index, value);
}
return list;
}
}
private enum MapAggregator implements MultiValueAggregator<Map> {
INSTANCE;
@Override
public Class<Map> handledType() {
return Map.class;
}
@Override
public Map createEmptyInstance() {
return new HashMap();
}
@SuppressWarnings("unchecked")
@Override
public Map add(@Nullable Map map, @Nullable Object qualifier, Object value) {
Assert.notNull(map, "Map must not be null.");
map.put(qualifier, value);
return map;
}
}
private enum SingleElementAggregator implements MultiValueAggregator<Object> {
INSTANCE;
@Override
@Nullable
public Object createEmptyInstance() {
return null;
}
@Override
public Object add(@Nullable Object __null, @Nullable Object qualifier, Object value) {
return value;
}
}
}

View File

@@ -60,6 +60,7 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations {
private final RelationalEntityUpdateWriter jdbcEntityUpdateWriter;
private final DataAccessStrategy accessStrategy;
private final AggregateChangeExecutor executor;
private EntityCallbacks entityCallbacks = EntityCallbacks.create();
@@ -91,6 +92,8 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations {
this.jdbcEntityDeleteWriter = new RelationalEntityDeleteWriter(context);
this.interpreter = new DefaultJdbcInterpreter(context, accessStrategy);
this.executor = new AggregateChangeExecutor(interpreter, converter);
setEntityCallbacks(EntityCallbacks.create(publisher));
}
@@ -120,6 +123,8 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations {
this.jdbcEntityUpdateWriter = new RelationalEntityUpdateWriter(context);
this.jdbcEntityDeleteWriter = new RelationalEntityDeleteWriter(context);
this.interpreter = new DefaultJdbcInterpreter(context, accessStrategy);
this.executor = new AggregateChangeExecutor(interpreter, converter);
}
/**
@@ -292,7 +297,7 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations {
Assert.notNull(domainType, "Domain type must not be null!");
AggregateChange<?> change = createDeletingChange(domainType);
change.executeWith(interpreter, context, converter);
executor.execute(change);
}
private <T> T store(T aggregateRoot, Function<T, AggregateChange<T>> changeCreator,
@@ -309,7 +314,7 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations {
change.setEntity(aggregateRoot);
change.executeWith(interpreter, context, converter);
executor.execute(change);
Object identifier = persistentEntity.getIdentifierAccessor(change.getEntity()).getIdentifier();
@@ -325,7 +330,7 @@ public class JdbcAggregateTemplate implements JdbcAggregateOperations {
entity = triggerBeforeDelete(entity, id, change);
change.setEntity(entity);
change.executeWith(interpreter, context, converter);
executor.execute(change);
triggerAfterDelete(entity, id, change);
}

View File

@@ -0,0 +1,570 @@
/*
* Copyright 2018-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.jdbc.core;
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 lombok.AllArgsConstructor;
import lombok.Value;
import lombok.experimental.Wither;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.junit.Test;
import org.springframework.data.annotation.Id;
import org.springframework.data.mapping.PersistentPropertyPath;
import org.springframework.data.mapping.PersistentPropertyPaths;
import org.springframework.data.relational.core.conversion.AggregateChange;
import org.springframework.data.relational.core.conversion.BasicRelationalConverter;
import org.springframework.data.relational.core.conversion.DbAction;
import org.springframework.data.relational.core.conversion.Interpreter;
import org.springframework.data.relational.core.conversion.RelationalConverter;
import org.springframework.data.relational.core.mapping.Embedded;
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
import org.springframework.lang.Nullable;
/**
* Unit tests for the {@link AggregateChange} testing the setting of generated ids in aggregates consisting of immutable
* entities.
*
* @author Jens Schauder
* @author Myeonghyeon-Lee
*/
public class AggregateChangeIdGenerationImmutableUnitTests {
DummyEntity entity = new DummyEntity();
Content content = new Content();
Content content2 = new Content();
Tag tag1 = new Tag("tag1");
Tag tag2 = new Tag("tag2");
Tag tag3 = new Tag("tag3");
ContentNoId contentNoId = new ContentNoId();
ContentNoId contentNoId2 = new ContentNoId();
RelationalMappingContext context = new RelationalMappingContext();
RelationalConverter converter = new BasicRelationalConverter(context);
DbAction.WithEntity<?> rootInsert = new DbAction.InsertRoot<>(entity);
AggregateChangeExecutor executor = new AggregateChangeExecutor(new IdSettingInterpreter(), converter);
@Test // DATAJDBC-291
public void singleRoot() {
AggregateChange<DummyEntity> aggregateChange = AggregateChange.forSave(entity);
aggregateChange.addAction(rootInsert);
executor.execute(aggregateChange);
entity = aggregateChange.getEntity();
assertThat(entity.rootId).isEqualTo(1);
}
@Test // DATAJDBC-291
public void simpleReference() {
entity = entity.withSingle(content);
AggregateChange<DummyEntity> aggregateChange = AggregateChange.forSave(entity);
aggregateChange.addAction(rootInsert);
aggregateChange.addAction(createInsert("single", content, null));
executor.execute(aggregateChange);
entity = aggregateChange.getEntity();
assertSoftly(softly -> {
softly.assertThat(entity.rootId).isEqualTo(1);
softly.assertThat(entity.single.id).isEqualTo(2);
});
}
@Test // DATAJDBC-291
public void listReference() {
entity = entity.withContentList(asList(content, content2));
AggregateChange<DummyEntity> aggregateChange = AggregateChange.forSave(entity);
aggregateChange.addAction(rootInsert);
aggregateChange.addAction(createInsert("contentList", content, 0));
aggregateChange.addAction(createInsert("contentList", content2, 1));
executor.execute(aggregateChange);
entity = aggregateChange.getEntity();
assertSoftly(softly -> {
softly.assertThat(entity.rootId).isEqualTo(1);
softly.assertThat(entity.contentList).extracting(c -> c.id).containsExactly(2, 3);
});
}
@Test // DATAJDBC-291
public void mapReference() {
entity = entity.withContentMap(createContentMap("a", content, "b", content2));
AggregateChange<DummyEntity> aggregateChange = AggregateChange.forSave(entity);
aggregateChange.addAction(rootInsert);
aggregateChange.addAction(createInsert("contentMap", content, "a"));
aggregateChange.addAction(createInsert("contentMap", content2, "b"));
executor.execute(aggregateChange);
entity = aggregateChange.getEntity();
assertThat(entity.rootId).isEqualTo(1);
assertThat(entity.contentMap.values()).extracting(c -> c.id).containsExactly(2, 3);
}
@Test // DATAJDBC-291
public void setIdForDeepReference() {
content = content.withSingle(tag1);
entity = entity.withSingle(content);
DbAction.Insert<?> parentInsert = createInsert("single", content, null);
DbAction.Insert<?> insert = createDeepInsert("single", tag1, null, parentInsert);
AggregateChange<DummyEntity> aggregateChange = AggregateChange.forSave(entity);
aggregateChange.addAction(rootInsert);
aggregateChange.addAction(parentInsert);
aggregateChange.addAction(insert);
executor.execute(aggregateChange);
entity = aggregateChange.getEntity();
assertThat(entity.rootId).isEqualTo(1);
assertThat(entity.single.id).isEqualTo(2);
assertThat(entity.single.single.id).isEqualTo(3);
}
@Test // DATAJDBC-291
public void setIdForDeepReferenceElementList() {
content = content.withTagList(asList(tag1, tag2));
entity = entity.withSingle(content);
DbAction.Insert<?> parentInsert = createInsert("single", content, null);
DbAction.Insert<?> insert1 = createDeepInsert("tagList", tag1, 0, parentInsert);
DbAction.Insert<?> insert2 = createDeepInsert("tagList", tag2, 1, parentInsert);
AggregateChange<DummyEntity> aggregateChange = AggregateChange.forSave(entity);
aggregateChange.addAction(rootInsert);
aggregateChange.addAction(parentInsert);
aggregateChange.addAction(insert1);
aggregateChange.addAction(insert2);
executor.execute(aggregateChange);
entity = aggregateChange.getEntity();
assertSoftly(softly -> {
softly.assertThat(entity.rootId).isEqualTo(1);
softly.assertThat(entity.single.id).isEqualTo(2);
softly.assertThat(entity.single.tagList).extracting(t -> t.id).containsExactly(3, 4);
});
}
@Test // DATAJDBC-291
public void setIdForDeepElementSetElementSet() {
content = content.withTagSet(Stream.of(tag1, tag2).collect(Collectors.toSet()));
entity = entity.withContentSet(singleton(content));
DbAction.Insert<?> parentInsert = createInsert("contentSet", content, null);
DbAction.Insert<?> insert1 = createDeepInsert("tagSet", tag1, null, parentInsert);
DbAction.Insert<?> insert2 = createDeepInsert("tagSet", tag2, null, parentInsert);
AggregateChange<DummyEntity> aggregateChange = AggregateChange.forSave(entity);
aggregateChange.addAction(rootInsert);
aggregateChange.addAction(parentInsert);
aggregateChange.addAction(insert1);
aggregateChange.addAction(insert2);
executor.execute(aggregateChange);
entity = aggregateChange.getEntity();
assertSoftly(softly -> {
softly.assertThat(entity.rootId).isEqualTo(1);
softly.assertThat(entity.contentSet) //
.extracting(c -> c.id) //
.containsExactly(2); //
softly.assertThat(entity.contentSet.stream() //
.flatMap(c -> c.tagSet.stream())) //
.extracting(t -> t.id) //
.containsExactlyInAnyOrder(3, 4); //
});
}
@Test // DATAJDBC-291
public void setIdForDeepElementListSingleReference() {
content = content.withSingle(tag1);
content2 = content2.withSingle(tag2);
entity = entity.withContentList(asList(content, content2));
DbAction.Insert<?> parentInsert1 = createInsert("contentList", content, 0);
DbAction.Insert<?> parentInsert2 = createInsert("contentList", content2, 1);
DbAction.Insert<?> insert1 = createDeepInsert("single", tag1, null, parentInsert1);
DbAction.Insert<?> insert2 = createDeepInsert("single", tag2, null, parentInsert2);
AggregateChange<DummyEntity> aggregateChange = AggregateChange.forSave(entity);
aggregateChange.addAction(rootInsert);
aggregateChange.addAction(parentInsert1);
aggregateChange.addAction(parentInsert2);
aggregateChange.addAction(insert1);
aggregateChange.addAction(insert2);
executor.execute(aggregateChange);
entity = aggregateChange.getEntity();
assertSoftly(softly -> {
softly.assertThat(entity.rootId).isEqualTo(1);
softly.assertThat(entity.contentList) //
.extracting(c -> c.id, c -> c.single.id) //
.containsExactly(tuple(2, 4), tuple(3, 5)); //
});
}
@Test // DATAJDBC-291
public void setIdForDeepElementListElementList() {
content = content.withTagList(singletonList(tag1));
content2 = content2.withTagList(asList(tag2, tag3));
entity = entity.withContentList(asList(content, content2));
DbAction.Insert<?> parentInsert1 = createInsert("contentList", content, 0);
DbAction.Insert<?> parentInsert2 = createInsert("contentList", content2, 1);
DbAction.Insert<?> insert1 = createDeepInsert("tagList", tag1, 0, parentInsert1);
DbAction.Insert<?> insert2 = createDeepInsert("tagList", tag2, 0, parentInsert2);
DbAction.Insert<?> insert3 = createDeepInsert("tagList", tag3, 1, parentInsert2);
AggregateChange<DummyEntity> aggregateChange = AggregateChange.forSave(entity);
aggregateChange.addAction(rootInsert);
aggregateChange.addAction(parentInsert1);
aggregateChange.addAction(parentInsert2);
aggregateChange.addAction(insert1);
aggregateChange.addAction(insert2);
aggregateChange.addAction(insert3);
executor.execute(aggregateChange);
entity = aggregateChange.getEntity();
assertSoftly(softly -> {
softly.assertThat(entity.rootId).isEqualTo(1);
softly.assertThat(entity.contentList) //
.extracting(c -> c.id) //
.containsExactly(2, 3); //
softly.assertThat(entity.contentList.stream() //
.flatMap(c -> c.tagList.stream()) //
).extracting(t -> t.id) //
.containsExactly(4, 5, 6); //
});
}
@Test // DATAJDBC-291
public void setIdForDeepElementMapElementMap() {
content = content.withTagMap(createTagMap("111", tag1, "222", tag2, "333", tag3));
entity = entity.withContentMap(createContentMap("one", content, "two", content2));
DbAction.Insert<?> parentInsert1 = createInsert("contentMap", content, "one");
DbAction.Insert<?> parentInsert2 = createInsert("contentMap", content2, "two");
DbAction.Insert<?> insert1 = createDeepInsert("tagMap", tag1, "111", parentInsert1);
DbAction.Insert<?> insert2 = createDeepInsert("tagMap", tag2, "222", parentInsert2);
DbAction.Insert<?> insert3 = createDeepInsert("tagMap", tag3, "333", parentInsert2);
AggregateChange<DummyEntity> aggregateChange = AggregateChange.forSave(entity);
aggregateChange.addAction(rootInsert);
aggregateChange.addAction(parentInsert1);
aggregateChange.addAction(parentInsert2);
aggregateChange.addAction(insert1);
aggregateChange.addAction(insert2);
aggregateChange.addAction(insert3);
executor.execute(aggregateChange);
entity = aggregateChange.getEntity();
assertSoftly(softly -> {
softly.assertThat(entity.rootId).isEqualTo(1);
softly.assertThat(entity.contentMap.entrySet()) //
.extracting(Map.Entry::getKey, e -> e.getValue().id) //
.containsExactly(tuple("one", 2), tuple("two", 3)); //
softly.assertThat(entity.contentMap.values().stream() //
.flatMap(c -> c.tagMap.entrySet().stream())) //
.extracting(Map.Entry::getKey, e -> e.getValue().id) //
.containsExactly( //
tuple("111", 4), //
tuple("222", 5), //
tuple("333", 6) //
); //
});
}
@Test // DATAJDBC-291
public void setIdForDeepElementListSingleReferenceWithIntermittentNoId() {
contentNoId = contentNoId.withSingle(tag1);
contentNoId2 = contentNoId2.withSingle(tag2);
entity = entity.withContentNoIdList(asList(contentNoId, contentNoId2));
DbAction.Insert<?> parentInsert1 = createInsert("contentNoIdList", contentNoId, 0);
DbAction.Insert<?> parentInsert2 = createInsert("contentNoIdList", contentNoId2, 1);
DbAction.Insert<?> insert1 = createDeepInsert("single", tag1, null, parentInsert1);
DbAction.Insert<?> insert2 = createDeepInsert("single", tag2, null, parentInsert2);
AggregateChange<DummyEntity> aggregateChange = AggregateChange.forSave(entity);
aggregateChange.addAction(rootInsert);
aggregateChange.addAction(parentInsert1);
aggregateChange.addAction(parentInsert2);
aggregateChange.addAction(insert1);
aggregateChange.addAction(insert2);
executor.execute(aggregateChange);
entity = aggregateChange.getEntity();
assertSoftly(softly -> {
softly.assertThat(entity.rootId).isEqualTo(1);
softly.assertThat(entity.contentNoIdList) //
.extracting(c -> c.single.id) //
.containsExactly(2, 3); //
});
}
@Test // DATAJDBC-291
public void setIdForEmbeddedDeepReference() {
contentNoId = contentNoId2.withSingle(tag1);
entity = entity.withEmbedded(contentNoId);
DbAction.Insert<?> parentInsert = createInsert("embedded.single", tag1, null);
AggregateChange<DummyEntity> aggregateChange = AggregateChange.forSave(entity);
aggregateChange.addAction(rootInsert);
aggregateChange.addAction(parentInsert);
executor.execute(aggregateChange);
entity = aggregateChange.getEntity();
assertThat(entity.rootId).isEqualTo(1);
assertThat(entity.embedded.single.id).isEqualTo(2);
}
private static Map<String, Content> createContentMap(Object... keysAndValues) {
Map<String, Content> contentMap = new HashMap<>();
for (int i = 0; i < keysAndValues.length; i += 2) {
contentMap.put((String) keysAndValues[i], (Content) keysAndValues[i + 1]);
}
return unmodifiableMap(contentMap);
}
private static Map<String, Tag> createTagMap(Object... keysAndValues) {
Map<String, Tag> contentMap = new HashMap<>();
for (int i = 0; i < keysAndValues.length; i += 2) {
contentMap.put((String) keysAndValues[i], (Tag) keysAndValues[i + 1]);
}
return unmodifiableMap(contentMap);
}
DbAction.Insert<?> createInsert(String propertyName, Object value, @Nullable Object key) {
DbAction.Insert<Object> insert = new DbAction.Insert<>(value,
context.getPersistentPropertyPath(propertyName, DummyEntity.class), rootInsert);
insert.getQualifiers().put(toPath(propertyName), key);
return insert;
}
DbAction.Insert<?> createDeepInsert(String propertyName, Object value, Object key,
@Nullable DbAction.Insert<?> parentInsert) {
PersistentPropertyPath<RelationalPersistentProperty> propertyPath = toPath(
parentInsert.getPropertyPath().toDotPath() + "." + propertyName);
DbAction.Insert<Object> insert = new DbAction.Insert<>(value, propertyPath, parentInsert);
insert.getQualifiers().put(propertyPath, key);
return insert;
}
PersistentPropertyPath<RelationalPersistentProperty> toPath(String path) {
PersistentPropertyPaths<?, RelationalPersistentProperty> persistentPropertyPaths = context
.findPersistentPropertyPaths(DummyEntity.class, p -> true);
return persistentPropertyPaths.filter(p -> p.toDotPath().equals(path)).stream().findFirst()
.orElseThrow(() -> new IllegalArgumentException("No matching path found"));
}
@Value
@Wither
@AllArgsConstructor
private static class DummyEntity {
@Id Integer rootId;
Content single;
Set<Content> contentSet;
List<Content> contentList;
Map<String, Content> contentMap;
List<ContentNoId> contentNoIdList;
@Embedded(onEmpty = Embedded.OnEmpty.USE_NULL) ContentNoId embedded;
DummyEntity() {
rootId = null;
single = null;
contentSet = emptySet();
contentList = emptyList();
contentMap = emptyMap();
contentNoIdList = emptyList();
embedded = new ContentNoId();
}
}
@Value
@Wither
@AllArgsConstructor
private static class Content {
@Id Integer id;
Tag single;
Set<Tag> tagSet;
List<Tag> tagList;
Map<String, Tag> tagMap;
Content() {
id = null;
single = null;
tagSet = emptySet();
tagList = emptyList();
tagMap = emptyMap();
}
}
@Value
@Wither
@AllArgsConstructor
private static class ContentNoId {
Tag single;
Set<Tag> tagSet;
List<Tag> tagList;
Map<String, Tag> tagMap;
ContentNoId() {
single = null;
tagSet = emptySet();
tagList = emptyList();
tagMap = emptyMap();
}
}
@Value
@Wither
@AllArgsConstructor
private static class Tag {
@Id Integer id;
String name;
Tag(String name) {
id = null;
this.name = name;
}
}
private static class IdSettingInterpreter implements Interpreter {
int id = 0;
@Override
public <T> void interpret(DbAction.Insert<T> insert) {
if (insert.getEntityType().getSimpleName().endsWith("NoId")) {
return;
}
insert.setGeneratedId(++id);
}
@Override
public <T> void interpret(DbAction.InsertRoot<T> insert) {
insert.setGeneratedId(++id);
}
@Override
public <T> void interpret(DbAction.Update<T> update) {
throw new UnsupportedOperationException();
}
@Override
public <T> void interpret(DbAction.UpdateRoot<T> update) {
throw new UnsupportedOperationException();
}
@Override
public <T> void interpret(DbAction.Merge<T> update) {
throw new UnsupportedOperationException();
}
@Override
public <T> void interpret(DbAction.Delete<T> delete) {
throw new UnsupportedOperationException();
}
@Override
public <T> void interpret(DbAction.DeleteRoot<T> deleteRoot) {
throw new UnsupportedOperationException();
}
@Override
public <T> void interpret(DbAction.DeleteAll<T> delete) {
throw new UnsupportedOperationException();
}
@Override
public <T> void interpret(DbAction.DeleteAllRoot<T> DeleteAllRoot) {
throw new UnsupportedOperationException();
}
}
}

View File

@@ -0,0 +1,425 @@
/*
* Copyright 2018-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.jdbc.core;
import static org.assertj.core.api.Assertions.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.assertj.core.api.SoftAssertions;
import org.junit.Test;
import org.springframework.data.annotation.Id;
import org.springframework.data.mapping.PersistentPropertyPath;
import org.springframework.data.mapping.PersistentPropertyPaths;
import org.springframework.data.relational.core.conversion.AggregateChange;
import org.springframework.data.relational.core.conversion.BasicRelationalConverter;
import org.springframework.data.relational.core.conversion.DbAction;
import org.springframework.data.relational.core.conversion.Interpreter;
import org.springframework.data.relational.core.conversion.RelationalConverter;
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
import org.springframework.lang.Nullable;
/**
* Unit tests for the {@link AggregateChange}.
*
* @author Jens Schauder
* @author Myeonghyeon-Lee
*/
public class AggregateChangeIdGenerationUnitTests {
DummyEntity entity = new DummyEntity();
Content content = new Content();
Content content2 = new Content();
Tag tag1 = new Tag();
Tag tag2 = new Tag();
Tag tag3 = new Tag();
RelationalMappingContext context = new RelationalMappingContext();
RelationalConverter converter = new BasicRelationalConverter(context);
DbAction.WithEntity<?> rootInsert = new DbAction.InsertRoot<>(entity);
AggregateChangeExecutor executor = new AggregateChangeExecutor(new IdSettingInterpreter(), converter);
@Test // DATAJDBC-291
public void singleRoot() {
AggregateChange<DummyEntity> aggregateChange = AggregateChange.forSave(entity);
aggregateChange.addAction(rootInsert);
executor.execute(aggregateChange);
assertThat(entity.rootId).isEqualTo(1);
}
@Test // DATAJDBC-291
public void simpleReference() {
entity.single = content;
AggregateChange<DummyEntity> aggregateChange = AggregateChange.forSave(entity);
aggregateChange.addAction(rootInsert);
aggregateChange.addAction(createInsert("single", content, null));
executor.execute(aggregateChange);
SoftAssertions.assertSoftly(softly -> {
softly.assertThat(entity.rootId).isEqualTo(1);
softly.assertThat(entity.single.id).isEqualTo(2);
});
}
@Test // DATAJDBC-291
public void listReference() {
entity.contentList.add(content);
entity.contentList.add(content2);
AggregateChange<DummyEntity> aggregateChange = AggregateChange.forSave(entity);
aggregateChange.addAction(rootInsert);
aggregateChange.addAction(createInsert("contentList", content, 0));
aggregateChange.addAction(createInsert("contentList", content2, 1));
executor.execute(aggregateChange);
SoftAssertions.assertSoftly(softly -> {
softly.assertThat(entity.rootId).isEqualTo(1);
softly.assertThat(entity.contentList).extracting(c -> c.id).containsExactly(2, 3);
});
}
@Test // DATAJDBC-291
public void mapReference() {
entity.contentMap.put("a", content);
entity.contentMap.put("b", content2);
AggregateChange<DummyEntity> aggregateChange = AggregateChange.forSave(entity);
aggregateChange.addAction(rootInsert);
aggregateChange.addAction(createInsert("contentMap", content, "a"));
aggregateChange.addAction(createInsert("contentMap", content2, "b"));
executor.execute(aggregateChange);
assertThat(entity.rootId).isEqualTo(1);
assertThat(entity.contentMap.values()).extracting(c -> c.id).containsExactly(2, 3);
}
@Test // DATAJDBC-291
public void setIdForDeepReference() {
content.single = tag1;
entity.single = content;
DbAction.Insert<?> parentInsert = createInsert("single", content, null);
DbAction.Insert<?> insert = createDeepInsert("single", tag1, null, parentInsert);
AggregateChange<DummyEntity> aggregateChange = AggregateChange.forSave(entity);
aggregateChange.addAction(rootInsert);
aggregateChange.addAction(parentInsert);
aggregateChange.addAction(insert);
executor.execute(aggregateChange);
assertThat(entity.rootId).isEqualTo(1);
assertThat(entity.single.id).isEqualTo(2);
assertThat(entity.single.single.id).isEqualTo(3);
}
@Test // DATAJDBC-291
public void setIdForDeepReferenceElementList() {
content.tagList.add(tag1);
content.tagList.add(tag2);
entity.single = content;
DbAction.Insert<?> parentInsert = createInsert("single", content, null);
DbAction.Insert<?> insert1 = createDeepInsert("tagList", tag1, 0, parentInsert);
DbAction.Insert<?> insert2 = createDeepInsert("tagList", tag2, 1, parentInsert);
AggregateChange<DummyEntity> aggregateChange = AggregateChange.forSave(entity);
aggregateChange.addAction(rootInsert);
aggregateChange.addAction(parentInsert);
aggregateChange.addAction(insert1);
aggregateChange.addAction(insert2);
executor.execute(aggregateChange);
SoftAssertions.assertSoftly(softly -> {
softly.assertThat(entity.rootId).isEqualTo(1);
softly.assertThat(entity.single.id).isEqualTo(2);
softly.assertThat(entity.single.tagList).extracting(t -> t.id).containsExactly(3, 4);
});
}
@Test // DATAJDBC-291
public void setIdForDeepElementSetElementSet() {
content.tagSet.add(tag1);
content.tagSet.add(tag2);
entity.contentSet.add(content);
DbAction.Insert<?> parentInsert = createInsert("contentSet", content, null);
DbAction.Insert<?> insert1 = createDeepInsert("tagSet", tag1, null, parentInsert);
DbAction.Insert<?> insert2 = createDeepInsert("tagSet", tag2, null, parentInsert);
AggregateChange<DummyEntity> aggregateChange = AggregateChange.forSave(entity);
aggregateChange.addAction(rootInsert);
aggregateChange.addAction(parentInsert);
aggregateChange.addAction(insert1);
aggregateChange.addAction(insert2);
executor.execute(aggregateChange);
SoftAssertions.assertSoftly(softly -> {
softly.assertThat(entity.rootId).isEqualTo(1);
softly.assertThat(entity.contentSet) //
.extracting(c -> content.id) //
.containsExactly(2); //
softly.assertThat(entity.contentSet.stream() //
.flatMap(c -> c.tagSet.stream())) //
.extracting(t -> t.id) //
.containsExactlyInAnyOrder(3, 4); //
});
}
@Test // DATAJDBC-291
public void setIdForDeepElementListSingleReference() {
content.single = tag1;
content2.single = tag2;
entity.contentList.add(content);
entity.contentList.add(content2);
DbAction.Insert<?> parentInsert1 = createInsert("contentList", content, 0);
DbAction.Insert<?> parentInsert2 = createInsert("contentList", content2, 1);
DbAction.Insert<?> insert1 = createDeepInsert("single", tag1, null, parentInsert1);
DbAction.Insert<?> insert2 = createDeepInsert("single", tag2, null, parentInsert2);
AggregateChange<DummyEntity> aggregateChange = AggregateChange.forSave(entity);
aggregateChange.addAction(rootInsert);
aggregateChange.addAction(parentInsert1);
aggregateChange.addAction(parentInsert2);
aggregateChange.addAction(insert1);
aggregateChange.addAction(insert2);
executor.execute(aggregateChange);
SoftAssertions.assertSoftly(softly -> {
softly.assertThat(entity.rootId).isEqualTo(1);
softly.assertThat(entity.contentList) //
.extracting(c -> c.id, c -> c.single.id) //
.containsExactly(tuple(2, 4), tuple(3, 5)); //
});
}
@Test // DATAJDBC-291
public void setIdForDeepElementListElementList() {
content.tagList.add(tag1);
content2.tagList.add(tag2);
content2.tagList.add(tag3);
entity.contentList.add(content);
entity.contentList.add(content2);
DbAction.Insert<?> parentInsert1 = createInsert("contentList", content, 0);
DbAction.Insert<?> parentInsert2 = createInsert("contentList", content2, 1);
DbAction.Insert<?> insert1 = createDeepInsert("tagList", tag1, 0, parentInsert1);
DbAction.Insert<?> insert2 = createDeepInsert("tagList", tag2, 0, parentInsert2);
DbAction.Insert<?> insert3 = createDeepInsert("tagList", tag3, 1, parentInsert2);
AggregateChange<DummyEntity> aggregateChange = AggregateChange.forSave(entity);
aggregateChange.addAction(rootInsert);
aggregateChange.addAction(parentInsert1);
aggregateChange.addAction(parentInsert2);
aggregateChange.addAction(insert1);
aggregateChange.addAction(insert2);
aggregateChange.addAction(insert3);
executor.execute(aggregateChange);
SoftAssertions.assertSoftly(softly -> {
softly.assertThat(entity.rootId).isEqualTo(1);
softly.assertThat(entity.contentList) //
.extracting(c -> c.id) //
.containsExactly(2, 3); //
softly.assertThat(entity.contentList.stream() //
.flatMap(c -> c.tagList.stream()) //
).extracting(t -> t.id) //
.containsExactly(4, 5, 6); //
});
}
@Test // DATAJDBC-291
public void setIdForDeepElementMapElementMap() {
content.tagMap.put("111", tag1);
content2.tagMap.put("222", tag2);
content2.tagMap.put("333", tag3);
entity.contentMap.put("one", content);
entity.contentMap.put("two", content2);
DbAction.Insert<?> parentInsert1 = createInsert("contentMap", content, "one");
DbAction.Insert<?> parentInsert2 = createInsert("contentMap", content2, "two");
DbAction.Insert<?> insert1 = createDeepInsert("tagMap", tag1, "111", parentInsert1);
DbAction.Insert<?> insert2 = createDeepInsert("tagMap", tag2, "222", parentInsert2);
DbAction.Insert<?> insert3 = createDeepInsert("tagMap", tag3, "333", parentInsert2);
AggregateChange<DummyEntity> aggregateChange = AggregateChange.forSave(entity);
aggregateChange.addAction(rootInsert);
aggregateChange.addAction(parentInsert1);
aggregateChange.addAction(parentInsert2);
aggregateChange.addAction(insert1);
aggregateChange.addAction(insert2);
aggregateChange.addAction(insert3);
executor.execute(aggregateChange);
SoftAssertions.assertSoftly(softly -> {
softly.assertThat(entity.rootId).isEqualTo(1);
softly.assertThat(entity.contentMap.entrySet()) //
.extracting(Map.Entry::getKey, e -> e.getValue().id) //
.containsExactly(tuple("one", 2), tuple("two", 3)); //
softly.assertThat(entity.contentMap.values().stream() //
.flatMap(c -> c.tagMap.entrySet().stream())) //
.extracting(Map.Entry::getKey, e -> e.getValue().id) //
.containsExactly( //
tuple("111", 4), //
tuple("222", 5), //
tuple("333", 6) //
); //
});
}
DbAction.Insert<?> createInsert(String propertyName, Object value, @Nullable Object key) {
DbAction.Insert<Object> insert = new DbAction.Insert<>(value,
context.getPersistentPropertyPath(propertyName, DummyEntity.class), rootInsert);
insert.getQualifiers().put(toPath(propertyName), key);
return insert;
}
DbAction.Insert<?> createDeepInsert(String propertyName, Object value, Object key,
@Nullable DbAction.Insert<?> parentInsert) {
PersistentPropertyPath<RelationalPersistentProperty> propertyPath = toPath(parentInsert.getPropertyPath().toDotPath() + "." + propertyName);
DbAction.Insert<Object> insert = new DbAction.Insert<>(value, propertyPath, parentInsert);
insert.getQualifiers().put(propertyPath, key);
return insert;
}
PersistentPropertyPath<RelationalPersistentProperty> toPath(String path) {
PersistentPropertyPaths<?, RelationalPersistentProperty> persistentPropertyPaths = context
.findPersistentPropertyPaths(DummyEntity.class, p -> true);
return persistentPropertyPaths.filter(p -> p.toDotPath().equals(path)).stream().findFirst()
.orElseThrow(() -> new IllegalArgumentException("No matching path found"));
}
private static class DummyEntity {
@Id Integer rootId;
Content single;
Set<Content> contentSet = new HashSet<>();
List<Content> contentList = new ArrayList<>();
Map<String, Content> contentMap = new HashMap<>();
}
private static class Content {
@Id Integer id;
Tag single;
Set<Tag> tagSet = new HashSet<>();
List<Tag> tagList = new ArrayList<>();
Map<String, Tag> tagMap = new HashMap<>();
}
private static class Tag {
@Id Integer id;
}
private static class IdSettingInterpreter implements Interpreter {
int id = 0;
@Override
public <T> void interpret(DbAction.Insert<T> insert) {
insert.setGeneratedId(++id);
}
@Override
public <T> void interpret(DbAction.InsertRoot<T> insert) {
insert.setGeneratedId(++id);
}
@Override
public <T> void interpret(DbAction.Update<T> update) {
throw new UnsupportedOperationException();
}
@Override
public <T> void interpret(DbAction.UpdateRoot<T> update) {
throw new UnsupportedOperationException();
}
@Override
public <T> void interpret(DbAction.Merge<T> update) {
throw new UnsupportedOperationException();
}
@Override
public <T> void interpret(DbAction.Delete<T> delete) {
throw new UnsupportedOperationException();
}
@Override
public <T> void interpret(DbAction.DeleteRoot<T> deleteRoot) {
throw new UnsupportedOperationException();
}
@Override
public <T> void interpret(DbAction.DeleteAll<T> delete) {
throw new UnsupportedOperationException();
}
@Override
public <T> void interpret(DbAction.DeleteAllRoot<T> DeleteAllRoot) {
throw new UnsupportedOperationException();
}
}
}