From 7d8c78e9ecd684e73774f809da8b1650ba0cab17 Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Thu, 17 Oct 2019 14:59:35 +0200 Subject: [PATCH] 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. --- .../jdbc/core/AggregateChangeExecutor.java | 330 ++++++++++++++++ .../data/jdbc/core/JdbcAggregateTemplate.java | 11 +- ...eChangeIdGenerationImmutableUnitTests.java | 49 ++- .../AggregateChangeIdGenerationUnitTests.java | 42 +- .../core/conversion/AggregateChange.java | 358 ++++-------------- 5 files changed, 458 insertions(+), 332 deletions(-) create mode 100644 spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/AggregateChangeExecutor.java rename {spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion => spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core}/AggregateChangeIdGenerationImmutableUnitTests.java (90%) rename {spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion => spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core}/AggregateChangeIdGenerationUnitTests.java (89%) diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/AggregateChangeExecutor.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/AggregateChangeExecutor.java new file mode 100644 index 00000000..1eb27495 --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/AggregateChangeExecutor.java @@ -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 RelationalPersistentProperty> context; + + AggregateChangeExecutor(Interpreter interpreter, RelationalConverter converter) { + + this.interpreter = interpreter; + this.converter = converter; + this.context = converter.getMappingContext(); + } + + @SuppressWarnings("unchecked") + void execute(AggregateChange aggregateChange) { + + List> 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> actions) { + + Object newRoot = null; + + // have the actions so that the inserts on the leaves come first. + List> 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 Object setIdAndCascadingProperties(DbAction.WithGeneratedId action, @Nullable Object generatedId, + AggregateChangeExecutor.StagedValues cascadingValues) { + + S originalEntity = action.getEntity(); + + RelationalPersistentEntity persistentEntity = (RelationalPersistentEntity) context + .getRequiredPersistentEntity(action.getEntityType()); + PersistentPropertyAccessor 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 aggregators = Arrays.asList(SetAggregator.INSTANCE, MapAggregator.INSTANCE, + ListAggregator.INSTANCE, SingleElementAggregator.INSTANCE); + + Map> 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") + void stage(DbAction action, PersistentPropertyPath path, @Nullable Object qualifier, Object value) { + + MultiValueAggregator aggregator = getAggregatorFor(path); + + Map 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 action) { + values.getOrDefault(dbAction, Collections.emptyMap()).forEach(action); + } + } + + interface MultiValueAggregator { + + default Class 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 { + + INSTANCE; + + @Override + public Class 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 { + + 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 { + + INSTANCE; + + @Override + public Class 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 { + + INSTANCE; + + @Override + @Nullable + public Object createEmptyInstance() { + return null; + } + + @Override + public Object add(@Nullable Object __null, @Nullable Object qualifier, Object value) { + return value; + } + } +} diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateTemplate.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateTemplate.java index 9707a6de..096ffb8b 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateTemplate.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateTemplate.java @@ -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 store(T aggregateRoot, Function> 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); } diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/AggregateChangeIdGenerationImmutableUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AggregateChangeIdGenerationImmutableUnitTests.java similarity index 90% rename from spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/AggregateChangeIdGenerationImmutableUnitTests.java rename to spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AggregateChangeIdGenerationImmutableUnitTests.java index 491a2487..bc2712c2 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/AggregateChangeIdGenerationImmutableUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AggregateChangeIdGenerationImmutableUnitTests.java @@ -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 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 insert = new DbAction.Insert<>(value, toPath(entity, value), parentInsert); - insert.getQualifiers().put(toPath(parentInsert.getPropertyPath().toDotPath() + "." + propertyName), key); + PersistentPropertyPath propertyPath = toPath( + parentInsert.getPropertyPath().toDotPath() + "." + propertyName); + DbAction.Insert 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 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 diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/AggregateChangeIdGenerationUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AggregateChangeIdGenerationUnitTests.java similarity index 89% rename from spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/AggregateChangeIdGenerationUnitTests.java rename to spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AggregateChangeIdGenerationUnitTests.java index b3c2377c..5eab98e9 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/AggregateChangeIdGenerationUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AggregateChangeIdGenerationUnitTests.java @@ -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 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 insert = new DbAction.Insert<>(value, toPath(entity, value), parentInsert); - insert.getQualifiers().put(toPath(parentInsert.getPropertyPath().toDotPath() + "." + propertyName), key); + PersistentPropertyPath propertyPath = toPath(parentInsert.getPropertyPath().toDotPath() + "." + propertyName); + DbAction.Insert 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 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; diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/AggregateChange.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/AggregateChange.java index 6319ba84..07baddbb 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/AggregateChange.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/AggregateChange.java @@ -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 { private final Kind kind; /** Type of the aggregate root to be changed */ private final Class entityType; + private final List> actions = new ArrayList<>(); /** Aggregate root, to which the change applies, if available */ @Nullable private T entity; @@ -106,100 +92,84 @@ public class AggregateChange { 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> consumer) { + + Assert.notNull(consumer, "Consumer must not be null."); + + actions.forEach(consumer); + } + + /** + * All the actions contained in this {@code AggregateChange}. + *

+ * The behavior when modifying this list might result in undesired behavior. + *

+ * Use {@link #addAction(DbAction)} to add actions. + * + * @return Guaranteed to be not {@literal null}. + */ + public List> 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 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> 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 Object setIdAndCascadingProperties(RelationalMappingContext context, RelationalConverter converter, - DbAction.WithGeneratedId action, @Nullable Object generatedId, StagedValues cascadingValues) { - - S originalEntity = action.getEntity(); - - RelationalPersistentEntity persistentEntity = (RelationalPersistentEntity) context - .getRequiredPersistentEntity(action.getEntityType()); - PersistentPropertyAccessor 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 { 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 aggregators = Arrays.asList(SetAggregator.INSTANCE, MapAggregator.INSTANCE, - ListAggregator.INSTANCE, SingleElementAggregator.INSTANCE); - - Map> 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") - void stage(DbAction action, PersistentPropertyPath path, @Nullable Object qualifier, Object value) { - - MultiValueAggregator aggregator = getAggregatorFor(path); - - Map 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 action) { - values.getOrDefault(dbAction, Collections.emptyMap()).forEach(action); - } - } - - interface MultiValueAggregator { - - default Class 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 { - - INSTANCE; - - @Override - public Class 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 { - - 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 { - - INSTANCE; - - @Override - public Class 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 { - - INSTANCE; - - @Override - @Nullable - public Object createEmptyInstance() { - return null; - } - - @Override - public Object add(@Nullable Object __null, @Nullable Object qualifier, Object value) { - return value; - } - } - }