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:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.relational.core.conversion;
|
||||
package org.springframework.data.jdbc.core;
|
||||
|
||||
import static java.util.Arrays.*;
|
||||
import static java.util.Collections.*;
|
||||
@@ -32,10 +32,14 @@ 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;
|
||||
@@ -63,13 +67,15 @@ public class AggregateChangeIdGenerationImmutableUnitTests {
|
||||
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);
|
||||
|
||||
aggregateChange.executeWith(new IdSettingInterpreter(), context, converter);
|
||||
executor.execute(aggregateChange);
|
||||
|
||||
entity = aggregateChange.getEntity();
|
||||
|
||||
@@ -85,7 +91,7 @@ public class AggregateChangeIdGenerationImmutableUnitTests {
|
||||
aggregateChange.addAction(rootInsert);
|
||||
aggregateChange.addAction(createInsert("single", content, null));
|
||||
|
||||
aggregateChange.executeWith(new IdSettingInterpreter(), context, converter);
|
||||
executor.execute(aggregateChange);
|
||||
|
||||
entity = aggregateChange.getEntity();
|
||||
|
||||
@@ -106,7 +112,7 @@ public class AggregateChangeIdGenerationImmutableUnitTests {
|
||||
aggregateChange.addAction(createInsert("contentList", content, 0));
|
||||
aggregateChange.addAction(createInsert("contentList", content2, 1));
|
||||
|
||||
aggregateChange.executeWith(new IdSettingInterpreter(), context, converter);
|
||||
executor.execute(aggregateChange);
|
||||
|
||||
entity = aggregateChange.getEntity();
|
||||
|
||||
@@ -127,7 +133,7 @@ public class AggregateChangeIdGenerationImmutableUnitTests {
|
||||
aggregateChange.addAction(createInsert("contentMap", content, "a"));
|
||||
aggregateChange.addAction(createInsert("contentMap", content2, "b"));
|
||||
|
||||
aggregateChange.executeWith(new IdSettingInterpreter(), context, converter);
|
||||
executor.execute(aggregateChange);
|
||||
|
||||
entity = aggregateChange.getEntity();
|
||||
|
||||
@@ -149,7 +155,7 @@ public class AggregateChangeIdGenerationImmutableUnitTests {
|
||||
aggregateChange.addAction(parentInsert);
|
||||
aggregateChange.addAction(insert);
|
||||
|
||||
aggregateChange.executeWith(new IdSettingInterpreter(), context, converter);
|
||||
executor.execute(aggregateChange);
|
||||
|
||||
entity = aggregateChange.getEntity();
|
||||
|
||||
@@ -174,7 +180,7 @@ public class AggregateChangeIdGenerationImmutableUnitTests {
|
||||
aggregateChange.addAction(insert1);
|
||||
aggregateChange.addAction(insert2);
|
||||
|
||||
aggregateChange.executeWith(new IdSettingInterpreter(), context, converter);
|
||||
executor.execute(aggregateChange);
|
||||
|
||||
entity = aggregateChange.getEntity();
|
||||
|
||||
@@ -202,7 +208,7 @@ public class AggregateChangeIdGenerationImmutableUnitTests {
|
||||
aggregateChange.addAction(insert1);
|
||||
aggregateChange.addAction(insert2);
|
||||
|
||||
aggregateChange.executeWith(new IdSettingInterpreter(), context, converter);
|
||||
executor.execute(aggregateChange);
|
||||
|
||||
entity = aggregateChange.getEntity();
|
||||
|
||||
@@ -238,7 +244,7 @@ public class AggregateChangeIdGenerationImmutableUnitTests {
|
||||
aggregateChange.addAction(insert1);
|
||||
aggregateChange.addAction(insert2);
|
||||
|
||||
aggregateChange.executeWith(new IdSettingInterpreter(), context, converter);
|
||||
executor.execute(aggregateChange);
|
||||
|
||||
entity = aggregateChange.getEntity();
|
||||
|
||||
@@ -272,7 +278,7 @@ public class AggregateChangeIdGenerationImmutableUnitTests {
|
||||
aggregateChange.addAction(insert2);
|
||||
aggregateChange.addAction(insert3);
|
||||
|
||||
aggregateChange.executeWith(new IdSettingInterpreter(), context, converter);
|
||||
executor.execute(aggregateChange);
|
||||
|
||||
entity = aggregateChange.getEntity();
|
||||
|
||||
@@ -309,7 +315,7 @@ public class AggregateChangeIdGenerationImmutableUnitTests {
|
||||
aggregateChange.addAction(insert2);
|
||||
aggregateChange.addAction(insert3);
|
||||
|
||||
aggregateChange.executeWith(new IdSettingInterpreter(), context, converter);
|
||||
executor.execute(aggregateChange);
|
||||
|
||||
entity = aggregateChange.getEntity();
|
||||
|
||||
@@ -349,7 +355,7 @@ public class AggregateChangeIdGenerationImmutableUnitTests {
|
||||
aggregateChange.addAction(insert1);
|
||||
aggregateChange.addAction(insert2);
|
||||
|
||||
aggregateChange.executeWith(new IdSettingInterpreter(), context, converter);
|
||||
executor.execute(aggregateChange);
|
||||
|
||||
entity = aggregateChange.getEntity();
|
||||
|
||||
@@ -374,7 +380,7 @@ public class AggregateChangeIdGenerationImmutableUnitTests {
|
||||
aggregateChange.addAction(rootInsert);
|
||||
aggregateChange.addAction(parentInsert);
|
||||
|
||||
aggregateChange.executeWith(new IdSettingInterpreter(), context, converter);
|
||||
executor.execute(aggregateChange);
|
||||
|
||||
entity = aggregateChange.getEntity();
|
||||
|
||||
@@ -413,9 +419,10 @@ public class AggregateChangeIdGenerationImmutableUnitTests {
|
||||
|
||||
DbAction.Insert<?> createDeepInsert(String propertyName, Object value, Object key,
|
||||
@Nullable DbAction.Insert<?> parentInsert) {
|
||||
|
||||
DbAction.Insert<Object> insert = new DbAction.Insert<>(value, toPath(entity, value), parentInsert);
|
||||
insert.getQualifiers().put(toPath(parentInsert.getPropertyPath().toDotPath() + "." + propertyName), key);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -428,14 +435,6 @@ public class AggregateChangeIdGenerationImmutableUnitTests {
|
||||
.orElseThrow(() -> new IllegalArgumentException("No matching path found"));
|
||||
}
|
||||
|
||||
PersistentPropertyPath<RelationalPersistentProperty> toPath(DummyEntity root, Object pathValue) {
|
||||
// DefaultPersistentPropertyPath is package-public
|
||||
return new WritingContext(context, entity, AggregateChange.forSave(root)).insert().stream()
|
||||
.filter(a -> a instanceof DbAction.Insert).map(DbAction.Insert.class::cast)
|
||||
.filter(a -> a.getEntity() == pathValue).map(DbAction.Insert::getPropertyPath).findFirst()
|
||||
.orElseThrow(() -> new IllegalArgumentException("No matching path found for " + pathValue));
|
||||
}
|
||||
|
||||
@Value
|
||||
@Wither
|
||||
@AllArgsConstructor
|
||||
@@ -13,7 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.relational.core.conversion;
|
||||
package org.springframework.data.jdbc.core;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
@@ -29,6 +29,11 @@ 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;
|
||||
@@ -52,13 +57,15 @@ public class AggregateChangeIdGenerationUnitTests {
|
||||
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);
|
||||
|
||||
aggregateChange.executeWith(new IdSettingInterpreter(), context, converter);
|
||||
executor.execute(aggregateChange);
|
||||
|
||||
assertThat(entity.rootId).isEqualTo(1);
|
||||
}
|
||||
@@ -72,7 +79,7 @@ public class AggregateChangeIdGenerationUnitTests {
|
||||
aggregateChange.addAction(rootInsert);
|
||||
aggregateChange.addAction(createInsert("single", content, null));
|
||||
|
||||
aggregateChange.executeWith(new IdSettingInterpreter(), context, converter);
|
||||
executor.execute(aggregateChange);
|
||||
|
||||
SoftAssertions.assertSoftly(softly -> {
|
||||
|
||||
@@ -92,7 +99,7 @@ public class AggregateChangeIdGenerationUnitTests {
|
||||
aggregateChange.addAction(createInsert("contentList", content, 0));
|
||||
aggregateChange.addAction(createInsert("contentList", content2, 1));
|
||||
|
||||
aggregateChange.executeWith(new IdSettingInterpreter(), context, converter);
|
||||
executor.execute(aggregateChange);
|
||||
|
||||
SoftAssertions.assertSoftly(softly -> {
|
||||
|
||||
@@ -112,7 +119,7 @@ public class AggregateChangeIdGenerationUnitTests {
|
||||
aggregateChange.addAction(createInsert("contentMap", content, "a"));
|
||||
aggregateChange.addAction(createInsert("contentMap", content2, "b"));
|
||||
|
||||
aggregateChange.executeWith(new IdSettingInterpreter(), context, converter);
|
||||
executor.execute(aggregateChange);
|
||||
|
||||
assertThat(entity.rootId).isEqualTo(1);
|
||||
assertThat(entity.contentMap.values()).extracting(c -> c.id).containsExactly(2, 3);
|
||||
@@ -132,7 +139,7 @@ public class AggregateChangeIdGenerationUnitTests {
|
||||
aggregateChange.addAction(parentInsert);
|
||||
aggregateChange.addAction(insert);
|
||||
|
||||
aggregateChange.executeWith(new IdSettingInterpreter(), context, converter);
|
||||
executor.execute(aggregateChange);
|
||||
|
||||
assertThat(entity.rootId).isEqualTo(1);
|
||||
assertThat(entity.single.id).isEqualTo(2);
|
||||
@@ -156,7 +163,7 @@ public class AggregateChangeIdGenerationUnitTests {
|
||||
aggregateChange.addAction(insert1);
|
||||
aggregateChange.addAction(insert2);
|
||||
|
||||
aggregateChange.executeWith(new IdSettingInterpreter(), context, converter);
|
||||
executor.execute(aggregateChange);
|
||||
|
||||
SoftAssertions.assertSoftly(softly -> {
|
||||
|
||||
@@ -183,7 +190,7 @@ public class AggregateChangeIdGenerationUnitTests {
|
||||
aggregateChange.addAction(insert1);
|
||||
aggregateChange.addAction(insert2);
|
||||
|
||||
aggregateChange.executeWith(new IdSettingInterpreter(), context, converter);
|
||||
executor.execute(aggregateChange);
|
||||
|
||||
SoftAssertions.assertSoftly(softly -> {
|
||||
|
||||
@@ -218,7 +225,7 @@ public class AggregateChangeIdGenerationUnitTests {
|
||||
aggregateChange.addAction(insert1);
|
||||
aggregateChange.addAction(insert2);
|
||||
|
||||
aggregateChange.executeWith(new IdSettingInterpreter(), context, converter);
|
||||
executor.execute(aggregateChange);
|
||||
|
||||
SoftAssertions.assertSoftly(softly -> {
|
||||
|
||||
@@ -252,7 +259,7 @@ public class AggregateChangeIdGenerationUnitTests {
|
||||
aggregateChange.addAction(insert2);
|
||||
aggregateChange.addAction(insert3);
|
||||
|
||||
aggregateChange.executeWith(new IdSettingInterpreter(), context, converter);
|
||||
executor.execute(aggregateChange);
|
||||
|
||||
SoftAssertions.assertSoftly(softly -> {
|
||||
|
||||
@@ -290,7 +297,7 @@ public class AggregateChangeIdGenerationUnitTests {
|
||||
aggregateChange.addAction(insert2);
|
||||
aggregateChange.addAction(insert3);
|
||||
|
||||
aggregateChange.executeWith(new IdSettingInterpreter(), context, converter);
|
||||
executor.execute(aggregateChange);
|
||||
|
||||
SoftAssertions.assertSoftly(softly -> {
|
||||
|
||||
@@ -321,8 +328,9 @@ public class AggregateChangeIdGenerationUnitTests {
|
||||
DbAction.Insert<?> createDeepInsert(String propertyName, Object value, Object key,
|
||||
@Nullable DbAction.Insert<?> parentInsert) {
|
||||
|
||||
DbAction.Insert<Object> insert = new DbAction.Insert<>(value, toPath(entity, value), parentInsert);
|
||||
insert.getQualifiers().put(toPath(parentInsert.getPropertyPath().toDotPath() + "." + propertyName), key);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -335,14 +343,6 @@ public class AggregateChangeIdGenerationUnitTests {
|
||||
.orElseThrow(() -> new IllegalArgumentException("No matching path found"));
|
||||
}
|
||||
|
||||
PersistentPropertyPath<RelationalPersistentProperty> toPath(DummyEntity root, Object pathValue) {
|
||||
// DefaultPersistentPropertyPath is package-public
|
||||
return new WritingContext(context, entity, AggregateChange.forSave(root)).insert().stream()
|
||||
.filter(a -> a instanceof DbAction.Insert).map(DbAction.Insert.class::cast)
|
||||
.filter(a -> a.getEntity() == pathValue).map(DbAction.Insert::getPropertyPath).findFirst()
|
||||
.orElseThrow(() -> new IllegalArgumentException("No matching path found"));
|
||||
}
|
||||
|
||||
private static class DummyEntity {
|
||||
|
||||
@Id Integer rootId;
|
||||
@@ -15,24 +15,10 @@
|
||||
*/
|
||||
package org.springframework.data.relational.core.conversion;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
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 java.util.function.Consumer;
|
||||
|
||||
import org.springframework.data.mapping.PersistentProperty;
|
||||
import org.springframework.data.mapping.PersistentPropertyAccessor;
|
||||
import org.springframework.data.mapping.PersistentPropertyPath;
|
||||
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
|
||||
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
|
||||
import org.springframework.data.util.Pair;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
@@ -43,13 +29,13 @@ import org.springframework.util.ClassUtils;
|
||||
* @author Jens Schauder
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
@Getter
|
||||
public class AggregateChange<T> {
|
||||
|
||||
private final Kind kind;
|
||||
|
||||
/** Type of the aggregate root to be changed */
|
||||
private final Class<T> entityType;
|
||||
|
||||
private final List<DbAction<?>> actions = new ArrayList<>();
|
||||
/** Aggregate root, to which the change applies, if available */
|
||||
@Nullable private T entity;
|
||||
@@ -106,100 +92,84 @@ public class AggregateChange<T> {
|
||||
return new AggregateChange<>(Kind.DELETE, entityClass, entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an action to this {@code AggregateChange}.
|
||||
*
|
||||
* @param action must not be {@literal null}.
|
||||
*/
|
||||
public void addAction(DbAction<?> action) {
|
||||
|
||||
Assert.notNull(action, "Action must not be null.");
|
||||
|
||||
actions.add(action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the given consumer to each {@link DbAction} in this {@code AggregateChange}.
|
||||
*
|
||||
* @param consumer must not be {@literal null}.
|
||||
*/
|
||||
public void forEachAction(Consumer<? super DbAction<?>> consumer) {
|
||||
|
||||
Assert.notNull(consumer, "Consumer must not be null.");
|
||||
|
||||
actions.forEach(consumer);
|
||||
}
|
||||
|
||||
/**
|
||||
* All the actions contained in this {@code AggregateChange}.
|
||||
* <p>
|
||||
* The behavior when modifying this list might result in undesired behavior.
|
||||
* <p>
|
||||
* Use {@link #addAction(DbAction)} to add actions.
|
||||
*
|
||||
* @return Guaranteed to be not {@literal null}.
|
||||
*/
|
||||
public List<DbAction<?>> getActions() {
|
||||
return this.actions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link Kind} of {@code AggregateChange} this is.
|
||||
*
|
||||
* @return guaranteed to be not {@literal null}.
|
||||
*/
|
||||
public Kind getKind() {
|
||||
return this.kind;
|
||||
}
|
||||
|
||||
/**
|
||||
* The type of the root of this {@code AggregateChange}.
|
||||
*
|
||||
* @return Guaranteed to be not {@literal null}.
|
||||
*/
|
||||
public Class<T> getEntityType() {
|
||||
return this.entityType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the root object of the {@code AggregateChange}.
|
||||
*
|
||||
* @param aggregateRoot may be {@literal null} if the change refers to a list of aggregates or references it by id.
|
||||
*/
|
||||
public void setEntity(@Nullable T aggregateRoot) {
|
||||
// TODO: Check instanceOf compatibility to ensure type contract.
|
||||
|
||||
if (aggregateRoot != null) {
|
||||
Assert.isInstanceOf(entityType, aggregateRoot,
|
||||
String.format("AggregateRoot must be of type %s", entityType.getName()));
|
||||
}
|
||||
|
||||
entity = aggregateRoot;
|
||||
}
|
||||
|
||||
public void executeWith(Interpreter interpreter, RelationalMappingContext context, RelationalConverter converter) {
|
||||
|
||||
actions.forEach(action -> action.executeWith(interpreter));
|
||||
|
||||
T newRoot = populateIdsIfNecessary(context, converter);
|
||||
|
||||
if (newRoot != null) {
|
||||
entity = newRoot;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The entity to which this {@link AggregateChange} relates.
|
||||
*
|
||||
* @return may be {@literal null}.
|
||||
*/
|
||||
@Nullable
|
||||
private T populateIdsIfNecessary(RelationalMappingContext context, RelationalConverter converter) {
|
||||
|
||||
T newRoot = null;
|
||||
|
||||
// have the actions so that the inserts on the leaves come first.
|
||||
ArrayList<DbAction<?>> reverseActions = new ArrayList<>(actions);
|
||||
Collections.reverse(reverseActions);
|
||||
|
||||
StagedValues cascadingValues = new StagedValues();
|
||||
|
||||
for (DbAction<?> action : reverseActions) {
|
||||
|
||||
if (!(action instanceof DbAction.WithGeneratedId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
DbAction.WithGeneratedId<?> withGeneratedId = (DbAction.WithGeneratedId<?>) action;
|
||||
Object generatedId = withGeneratedId.getGeneratedId();
|
||||
Object newEntity = setIdAndCascadingProperties(context, converter, 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.dependingOn, insert.propertyPath,
|
||||
qualifier == null ? null : qualifier.getSecond(), newEntity);
|
||||
|
||||
} else if (action instanceof DbAction.InsertRoot) {
|
||||
newRoot = entityType.cast(newEntity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newRoot;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <S> Object setIdAndCascadingProperties(RelationalMappingContext context, RelationalConverter converter,
|
||||
DbAction.WithGeneratedId<S> action, @Nullable Object generatedId, 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).propertyPath);
|
||||
}
|
||||
|
||||
if (action instanceof DbAction.InsertRoot) {
|
||||
return pathToValue;
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException(String.format("DbAction of type %s is not supported.", action.getClass()));
|
||||
}
|
||||
|
||||
public void addAction(DbAction<?> action) {
|
||||
actions.add(action);
|
||||
public T getEntity() {
|
||||
return this.entity;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -218,182 +188,4 @@ public class AggregateChange<T> {
|
||||
DELETE
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @param dbAction
|
||||
* @param action
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user