From feb60c85600d2c9bf198720a6fa19f42ee8c4bf5 Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Wed, 11 Sep 2019 10:44:48 +0200 Subject: [PATCH] DATAJDBC-291 - Setting DB-generated ids now works across collections. Original pull request: #173. --- ...AggregateTemplateHsqlIntegrationTests.java | 45 +- ...egateTemplateHsqlIntegrationTests-hsql.sql | 28 +- .../core/conversion/AggregateChange.java | 404 +++++++----- .../relational/core/conversion/DbAction.java | 19 + ...eChangeIdGenerationImmutableUnitTests.java | 583 ++++++++++++++++++ .../AggregateChangeIdGenerationUnitTests.java | 437 +++++++++++++ .../conversion/AggregateChangeUnitTests.java | 266 -------- 7 files changed, 1363 insertions(+), 419 deletions(-) create mode 100644 spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/AggregateChangeIdGenerationImmutableUnitTests.java create mode 100644 spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/AggregateChangeIdGenerationUnitTests.java delete mode 100644 spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/AggregateChangeUnitTests.java diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/ImmutableAggregateTemplateHsqlIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/ImmutableAggregateTemplateHsqlIntegrationTests.java index 3095a7ec..df7a72bb 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/ImmutableAggregateTemplateHsqlIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/ImmutableAggregateTemplateHsqlIntegrationTests.java @@ -97,6 +97,29 @@ public class ImmutableAggregateTemplateHsqlIntegrationTests { softly.assertAll(); } + @Test // DATAJDBC-291 + public void saveAndLoadAnEntityWithTwoReferencedEntitiesById() { + + LegoSet saved = template.save(createLegoSet(createManual(), new Author(null, "Alfred E. Neumann"))); + + assertThat(saved.manual.id).describedAs("id of stored manual").isNotNull(); + assertThat(saved.author.id).describedAs("id of stored author").isNotNull(); + + LegoSet reloadedLegoSet = template.findById(saved.getId(), LegoSet.class); + + assertThat(reloadedLegoSet.manual).isNotNull(); + + SoftAssertions softly = new SoftAssertions(); + + softly.assertThat(reloadedLegoSet.manual.getId()) // + .isEqualTo(saved.getManual().getId()) // + .isNotNull(); + softly.assertThat(reloadedLegoSet.manual.getContent()).isEqualTo(saved.getManual().getContent()); + softly.assertThat(reloadedLegoSet.author.getName()).isEqualTo(saved.getAuthor().getName()); + + softly.assertAll(); + } + @Test // DATAJDBC-241 public void saveAndLoadManyEntitiesWithReferencedEntity() { @@ -168,7 +191,7 @@ public class ImmutableAggregateTemplateHsqlIntegrationTests { LegoSet saved = template.save(createLegoSet(null)); - LegoSet changedLegoSet = new LegoSet(saved.id, saved.name, new Manual(23L, "Some content")); + LegoSet changedLegoSet = new LegoSet(saved.id, saved.name, new Manual(23L, "Some content"), null); template.save(changedLegoSet); @@ -182,7 +205,7 @@ public class ImmutableAggregateTemplateHsqlIntegrationTests { LegoSet saved = template.save(createLegoSet(null)); - LegoSet changedLegoSet = new LegoSet(saved.id, saved.name, null); + LegoSet changedLegoSet = new LegoSet(saved.id, saved.name, null, null); template.save(changedLegoSet); @@ -201,7 +224,7 @@ public class ImmutableAggregateTemplateHsqlIntegrationTests { LegoSet saved = template.save(createLegoSet(null)); - LegoSet changedLegoSet = new LegoSet(saved.id, saved.name, new Manual(null, "other content")); + LegoSet changedLegoSet = new LegoSet(saved.id, saved.name, new Manual(null, "other content"), null); template.save(changedLegoSet); @@ -233,7 +256,12 @@ public class ImmutableAggregateTemplateHsqlIntegrationTests { private static LegoSet createLegoSet(Manual manual) { - return new LegoSet(null, "Star Destroyer", manual); + return new LegoSet(null, "Star Destroyer", manual, null); + } + + private static LegoSet createLegoSet(Manual manual, Author author) { + + return new LegoSet(null, "Star Destroyer", manual, author); } private static Manual createManual() { @@ -248,6 +276,7 @@ public class ImmutableAggregateTemplateHsqlIntegrationTests { @Id Long id; String name; Manual manual; + Author author; } @Value @@ -258,6 +287,14 @@ public class ImmutableAggregateTemplateHsqlIntegrationTests { String content; } + @Value + @Wither + static class Author { + + @Id Long id; + String name; + } + @Configuration @Import(TestConfiguration.class) static class Config { diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/ImmutableAggregateTemplateHsqlIntegrationTests-hsql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/ImmutableAggregateTemplateHsqlIntegrationTests-hsql.sql index 20a0a290..3b2810e7 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/ImmutableAggregateTemplateHsqlIntegrationTests-hsql.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/ImmutableAggregateTemplateHsqlIntegrationTests-hsql.sql @@ -1,5 +1,25 @@ -CREATE TABLE LEGO_SET ( id BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1) PRIMARY KEY, NAME VARCHAR(30)); -CREATE TABLE MANUAL ( id BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1) PRIMARY KEY, LEGO_SET BIGINT, CONTENT VARCHAR(2000)); +CREATE TABLE LEGO_SET +( + id BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, + NAME VARCHAR(30) +); -ALTER TABLE MANUAL ADD FOREIGN KEY (LEGO_SET) -REFERENCES LEGO_SET(id); +CREATE TABLE MANUAL +( + id BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, + LEGO_SET BIGINT, + CONTENT VARCHAR(2000) +); +ALTER TABLE MANUAL + ADD FOREIGN KEY (LEGO_SET) + REFERENCES LEGO_SET (id); + +CREATE TABLE AUTHOR +( + id BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, + LEGO_SET BIGINT, + NAME VARCHAR(2000) +); +ALTER TABLE AUTHOR + ADD FOREIGN KEY (LEGO_SET) + REFERENCES LEGO_SET (id); 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 9cdbdbd3..6ef32fd2 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 @@ -18,16 +18,20 @@ 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 org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.PersistentPropertyPath; -import org.springframework.data.relational.core.mapping.PersistentPropertyPathExtension; import org.springframework.data.relational.core.mapping.RelationalMappingContext; 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; @@ -55,164 +59,108 @@ public class AggregateChange { this.entity = entity; } - @SuppressWarnings("unchecked") - static void setIdOfNonRootEntity(RelationalMappingContext context, RelationalConverter converter, - PersistentPropertyAccessor propertyAccessor, DbAction.WithDependingOn action, Object generatedId) { - - PersistentPropertyPath propertyPathToEntity = action.getPropertyPath(); - PersistentPropertyPathExtension extPath = new PersistentPropertyPathExtension(context, propertyPathToEntity); - - RelationalPersistentProperty leafProperty = propertyPathToEntity.getRequiredLeafProperty(); - - Object currentPropertyValue = propertyAccessor.getProperty(propertyPathToEntity); - Assert.notNull(currentPropertyValue, "Trying to set an ID for an element that does not exist"); - - if (leafProperty.isQualified()) { - - Object keyObject = action.getQualifiers().get(propertyPathToEntity); - - if (List.class.isAssignableFrom(leafProperty.getType())) { - setIdInElementOfList(converter, action, generatedId, (List) currentPropertyValue, (int) keyObject); - } else if (Map.class.isAssignableFrom(leafProperty.getType())) { - setIdInElementOfMap(converter, action, generatedId, (Map) currentPropertyValue, keyObject); - } else { - throw new IllegalStateException("Can't handle " + currentPropertyValue); - } - } else if (leafProperty.isCollectionLike()) { - - if (Set.class.isAssignableFrom(leafProperty.getType())) { - setIdInElementOfSet(converter, action, generatedId, (Set) currentPropertyValue); - } else { - throw new IllegalStateException("Can't handle " + currentPropertyValue); - } - } else if (extPath.hasIdProperty()) { - - RelationalPersistentProperty requiredIdProperty = context - .getRequiredPersistentEntity(propertyPathToEntity.getRequiredLeafProperty().getActualType()) - .getRequiredIdProperty(); - - PersistentPropertyPath pathToId = context.getPersistentPropertyPath( - propertyPathToEntity.toDotPath() + '.' + requiredIdProperty.getName(), - propertyPathToEntity.getBaseProperty().getOwner().getType()); - - propertyAccessor.setProperty(pathToId, generatedId); - } - } - - public void setEntity(@Nullable T aggregateRoot) { entity = aggregateRoot; } - @SuppressWarnings("unchecked") - private static void setIdInElementOfSet(RelationalConverter converter, DbAction.WithDependingOn action, - Object generatedId, Set set) { - - PersistentPropertyAccessor intermediateAccessor = setId(converter, action, generatedId); - - // this currently only works on the standard collections - // no support for immutable collections, nor specialized ones. - set.remove((T) action.getEntity()); - set.add((T) intermediateAccessor.getBean()); - } - - @SuppressWarnings("unchecked") - private static void setIdInElementOfMap(RelationalConverter converter, DbAction.WithDependingOn action, - Object generatedId, Map map, K keyObject) { - - PersistentPropertyAccessor intermediateAccessor = setId(converter, action, generatedId); - - // this currently only works on the standard collections - // no support for immutable collections, nor specialized ones. - map.put(keyObject, (V) intermediateAccessor.getBean()); - } - - @SuppressWarnings("unchecked") - private static void setIdInElementOfList(RelationalConverter converter, DbAction.WithDependingOn action, - Object generatedId, List list, int index) { - - PersistentPropertyAccessor intermediateAccessor = setId(converter, action, generatedId); - - // this currently only works on the standard collections - // no support for immutable collections, nor specialized ones. - list.set(index, (T) intermediateAccessor.getBean()); - } - - /** - * Sets the id of the entity referenced in the action and uses the {@link PersistentPropertyAccessor} used for that. - */ - private static PersistentPropertyAccessor setId(RelationalConverter converter, - DbAction.WithDependingOn action, Object generatedId) { - - T originalElement = action.getEntity(); - - RelationalPersistentEntity persistentEntity = (RelationalPersistentEntity) converter.getMappingContext() - .getRequiredPersistentEntity(action.getEntityType()); - PersistentPropertyAccessor intermediateAccessor = converter.getPropertyAccessor(persistentEntity, - originalElement); - - RelationalPersistentProperty idProperty = persistentEntity.getIdProperty(); - if (idProperty != null) { - intermediateAccessor.setProperty(idProperty, generatedId); - } - - return intermediateAccessor; - } - - @SuppressWarnings("unchecked") public void executeWith(Interpreter interpreter, RelationalMappingContext context, RelationalConverter converter) { - RelationalPersistentEntity persistentEntity = entity != null - ? (RelationalPersistentEntity) context.getRequiredPersistentEntity(entity.getClass()) - : null; + actions.forEach(action -> action.executeWith(interpreter)); - PersistentPropertyAccessor propertyAccessor = // - persistentEntity != null // - ? converter.getPropertyAccessor(persistentEntity, entity) // - : null; + T newRoot = setGeneratedIds(context, converter); - actions.forEach(action -> { - - action.executeWith(interpreter); - - processGeneratedId(context, converter, persistentEntity, propertyAccessor, action); - }); - - if (propertyAccessor != null) { - entity = propertyAccessor.getBean(); + if (newRoot != null) { + entity = newRoot; } } + @SuppressWarnings("unchecked") + @Nullable + private T setGeneratedIds(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); + + CascadingValuesLookup cascadingValues = new CascadingValuesLookup(); + + for (DbAction action : reverseActions) { + + if (action instanceof DbAction.WithGeneratedId) { + + 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.add(insert.dependingOn, insert.propertyPath, newEntity, + qualifier == null ? null : qualifier.getSecond()); + + } else if (action instanceof DbAction.InsertRoot) { + newRoot = (T) newEntity; + } + } + } + } + + return newRoot; + } + + @SuppressWarnings("unchecked") + private Object setIdAndCascadingProperties(RelationalMappingContext context, RelationalConverter converter, + DbAction.WithGeneratedId action, @Nullable Object generatedId, CascadingValuesLookup 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 + Map cascadingValue = cascadingValues.get(action); + for (Map.Entry pathValuePair : cascadingValue.entrySet()) { + propertyAccessor.setProperty(getRelativePath(action, pathValuePair), pathValuePair.getValue()); + } + + return propertyAccessor.getBean(); + } + + @SuppressWarnings("unchecked") + private PersistentPropertyPath getRelativePath(DbAction action, + Map.Entry pathValuePair) { + + PersistentPropertyPath pathToValue = pathValuePair.getKey(); + + 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); } - private void processGeneratedId(RelationalMappingContext context, RelationalConverter converter, - @Nullable RelationalPersistentEntity persistentEntity, - @Nullable PersistentPropertyAccessor propertyAccessor, DbAction action) { - - if (!(action instanceof DbAction.WithGeneratedId)) { - return; - } - - Assert.notNull(persistentEntity, - "For statements triggering database side id generation a RelationalPersistentEntity must be provided."); - Assert.notNull(propertyAccessor, "propertyAccessor must not be null"); - - Object generatedId = ((DbAction.WithGeneratedId) action).getGeneratedId(); - - if (generatedId == null) { - return; - } - - if (action instanceof DbAction.InsertRoot && action.getEntityType().equals(entityType)) { - propertyAccessor.setProperty(persistentEntity.getRequiredIdProperty(), generatedId); - } else if (action instanceof DbAction.WithDependingOn) { - - setIdOfNonRootEntity(context, converter, propertyAccessor, (DbAction.WithDependingOn) action, generatedId); - } - } - /** * The kind of action to be performed on an aggregate. */ @@ -228,4 +176,170 @@ public class AggregateChange { */ DELETE } + + /** + * Gathers and holds information about immutable properties in an aggregate that need updating. + */ + private static class CascadingValuesLookup { + + static final List aggregators = Arrays.asList(new SetAggregator(), new MapAggregator(), + new ListAggregator(), new SingleElementAggregator()); + + 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 value The value to be set. Must not be {@literal null}. + * @param qualifier If {@code path} is a qualified multivalued properties this parameter contains the qualifier. May + * be {@literal null}. + */ + @SuppressWarnings("unchecked") + public void add(DbAction action, PersistentPropertyPath path, Object value, @Nullable Object qualifier) { + + MultiValueAggregator aggregator = getAggregatorFor(path); + + Map valuesForPath = this.values.get(action); + if (valuesForPath == null) { + valuesForPath = new HashMap<>(); + values.put(action, valuesForPath); + } + + T currentValue = (T) valuesForPath.get(path); + if (currentValue == null) { + currentValue = aggregator.createEmptyInstance(); + } + + Object newValue = aggregator.add(currentValue, value, qualifier); + + 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)); + } + + public Map get(DbAction action) { + return values.getOrDefault(action, Collections.emptyMap()); + } + } + + 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, Object value, @Nullable Object qualifier); + + } + + static private class SetAggregator implements MultiValueAggregator { + + @Override + public Class handledType() { + return Set.class; + } + + @Override + public Set createEmptyInstance() { + return new HashSet(); + } + + @SuppressWarnings("unchecked") + @Override + public Set add(@Nullable Set set, Object value, @Nullable Object qualifier) { + + Assert.notNull(set, "Set must not be null"); + + set.add(value); + return set; + } + } + + static private class ListAggregator implements MultiValueAggregator { + + @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, Object value, @Nullable Object qualifier) { + + 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; + } + } + + static private class MapAggregator implements MultiValueAggregator { + + @Override + public Class handledType() { + return Map.class; + } + + @Override + public Map createEmptyInstance() { + return new HashMap(); + } + + @SuppressWarnings("unchecked") + @Override + public Map add(@Nullable Map map, Object value, @Nullable Object qualifier) { + + Assert.notNull(map, "Map must not be null."); + + map.put(qualifier, value); + return map; + } + } + + static private class SingleElementAggregator implements MultiValueAggregator { + + @Override + @Nullable + public Object createEmptyInstance() { + return null; + } + + @Override + public Object add(@Nullable Object __null, Object value, @Nullable Object qualifier) { + return value; + } + } + } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/DbAction.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/DbAction.java index 4e8093d8..7d092f74 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/DbAction.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/DbAction.java @@ -25,6 +25,7 @@ import java.util.Map; import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; +import org.springframework.data.util.Pair; import org.springframework.lang.Nullable; /** @@ -261,6 +262,24 @@ public interface DbAction { */ Map, Object> getQualifiers(); + @Nullable + default Pair, Object> getQualifier() { + Map, Object> qualifiers = getQualifiers(); + if (qualifiers.size() == 0) + return null; + + if (qualifiers.size() > 1) { + throw new IllegalStateException("Can't handle more then on qualifier"); + } + + Map.Entry, Object> entry = qualifiers.entrySet().iterator().next(); + if (entry.getValue() == null) { + return null; + } + return Pair.of(entry.getKey(), entry.getValue()); + }; + + @Override default Class getEntityType() { return WithEntity.super.getEntityType(); diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/AggregateChangeIdGenerationImmutableUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/AggregateChangeIdGenerationImmutableUnitTests.java new file mode 100644 index 00000000..bf2bcb63 --- /dev/null +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/AggregateChangeIdGenerationImmutableUnitTests.java @@ -0,0 +1,583 @@ +/* + * 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.relational.core.conversion; + +import static java.util.Arrays.*; +import static java.util.Collections.*; +import static org.assertj.core.api.Assertions.*; + +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.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.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); + + @Test // DATAJDBC-291 + public void singleRoot() { + + AggregateChange aggregateChange = new AggregateChange<>(AggregateChange.Kind.SAVE, DummyEntity.class, + entity); + aggregateChange.addAction(rootInsert); + + aggregateChange.executeWith(new IdSettingInterpreter(), context, converter); + + entity = aggregateChange.getEntity(); + + assertThat(entity.rootId).isEqualTo(1); + } + + @Test // DATAJDBC-291 + public void simpleReference() { + + entity = entity.withSingle(content); + + AggregateChange aggregateChange = new AggregateChange<>(AggregateChange.Kind.SAVE, DummyEntity.class, + entity); + aggregateChange.addAction(rootInsert); + aggregateChange.addAction(createInsert("single", content, null)); + + aggregateChange.executeWith(new IdSettingInterpreter(), context, converter); + + entity = aggregateChange.getEntity(); + + SoftAssertions.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 aggregateChange = new AggregateChange<>(AggregateChange.Kind.SAVE, DummyEntity.class, + entity); + aggregateChange.addAction(rootInsert); + aggregateChange.addAction(createInsert("contentList", content, 0)); + aggregateChange.addAction(createInsert("contentList", content2, 1)); + + aggregateChange.executeWith(new IdSettingInterpreter(), context, converter); + + entity = aggregateChange.getEntity(); + + 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 = entity.withContentMap(createContentMap("a", content, "b", content2)); + + AggregateChange aggregateChange = new AggregateChange<>(AggregateChange.Kind.SAVE, DummyEntity.class, + entity); + aggregateChange.addAction(rootInsert); + aggregateChange.addAction(createInsert("contentMap", content, "a")); + aggregateChange.addAction(createInsert("contentMap", content2, "b")); + + aggregateChange.executeWith(new IdSettingInterpreter(), context, converter); + + 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 aggregateChange = new AggregateChange<>(AggregateChange.Kind.SAVE, DummyEntity.class, + entity); + aggregateChange.addAction(rootInsert); + aggregateChange.addAction(parentInsert); + aggregateChange.addAction(insert); + + aggregateChange.executeWith(new IdSettingInterpreter(), context, converter); + + 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 aggregateChange = new AggregateChange<>(AggregateChange.Kind.SAVE, DummyEntity.class, + entity); + aggregateChange.addAction(rootInsert); + aggregateChange.addAction(parentInsert); + aggregateChange.addAction(insert1); + aggregateChange.addAction(insert2); + + aggregateChange.executeWith(new IdSettingInterpreter(), context, converter); + + entity = aggregateChange.getEntity(); + + 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 = 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 aggregateChange = new AggregateChange<>(AggregateChange.Kind.SAVE, DummyEntity.class, + entity); + aggregateChange.addAction(rootInsert); + aggregateChange.addAction(parentInsert); + aggregateChange.addAction(insert1); + aggregateChange.addAction(insert2); + + aggregateChange.executeWith(new IdSettingInterpreter(), context, converter); + + entity = aggregateChange.getEntity(); + + SoftAssertions.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 aggregateChange = new AggregateChange<>(AggregateChange.Kind.SAVE, DummyEntity.class, + entity); + aggregateChange.addAction(rootInsert); + aggregateChange.addAction(parentInsert1); + aggregateChange.addAction(parentInsert2); + aggregateChange.addAction(insert1); + aggregateChange.addAction(insert2); + + aggregateChange.executeWith(new IdSettingInterpreter(), context, converter); + + entity = aggregateChange.getEntity(); + + 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 = 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 aggregateChange = new AggregateChange<>(AggregateChange.Kind.SAVE, DummyEntity.class, + entity); + aggregateChange.addAction(rootInsert); + aggregateChange.addAction(parentInsert1); + aggregateChange.addAction(parentInsert2); + aggregateChange.addAction(insert1); + aggregateChange.addAction(insert2); + aggregateChange.addAction(insert3); + + aggregateChange.executeWith(new IdSettingInterpreter(), context, converter); + + entity = aggregateChange.getEntity(); + + 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 = 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 aggregateChange = new AggregateChange<>(AggregateChange.Kind.SAVE, DummyEntity.class, + entity); + aggregateChange.addAction(rootInsert); + aggregateChange.addAction(parentInsert1); + aggregateChange.addAction(parentInsert2); + aggregateChange.addAction(insert1); + aggregateChange.addAction(insert2); + aggregateChange.addAction(insert3); + + aggregateChange.executeWith(new IdSettingInterpreter(), context, converter); + + entity = aggregateChange.getEntity(); + + 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) // + ); // + }); + } + + @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 aggregateChange = new AggregateChange<>(AggregateChange.Kind.SAVE, DummyEntity.class, + entity); + aggregateChange.addAction(rootInsert); + aggregateChange.addAction(parentInsert1); + aggregateChange.addAction(parentInsert2); + aggregateChange.addAction(insert1); + aggregateChange.addAction(insert2); + + aggregateChange.executeWith(new IdSettingInterpreter(), context, converter); + + entity = aggregateChange.getEntity(); + + SoftAssertions.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 aggregateChange = new AggregateChange<>(AggregateChange.Kind.SAVE, DummyEntity.class, + entity); + aggregateChange.addAction(rootInsert); + aggregateChange.addAction(parentInsert); + + aggregateChange.executeWith(new IdSettingInterpreter(), context, converter); + + entity = aggregateChange.getEntity(); + + assertThat(entity.rootId).isEqualTo(1); + assertThat(entity.embedded.single.id).isEqualTo(2); + } + + private static Map createContentMap(Object... keysAndValues) { + + Map 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 createTagMap(Object... keysAndValues) { + + Map 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 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) { + + DbAction.Insert insert = new DbAction.Insert<>(value, toPath(entity, value), parentInsert); + insert.getQualifiers().put(toPath(parentInsert.getPropertyPath().toDotPath() + "." + propertyName), key); + return insert; + } + + PersistentPropertyPath toPath(String path) { + + PersistentPropertyPaths persistentPropertyPaths = context + .findPersistentPropertyPaths(DummyEntity.class, p -> true); + + return persistentPropertyPaths.filter(p -> p.toDotPath().equals(path)).stream().findFirst() + .orElseThrow(() -> new IllegalArgumentException("No matching path found")); + } + + PersistentPropertyPath toPath(DummyEntity root, Object pathValue) { + // DefaultPersistentPropertyPath is package-public + return new WritingContext(context, entity, + new AggregateChange<>(AggregateChange.Kind.SAVE, DummyEntity.class, 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 + private static class DummyEntity { + + @Id Integer rootId; + Content single; + Set contentSet; + List contentList; + Map contentMap; + List 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 tagSet; + List tagList; + Map tagMap; + + Content() { + + id = null; + single = null; + tagSet = emptySet(); + tagList = emptyList(); + tagMap = emptyMap(); + } + } + + @Value + @Wither + @AllArgsConstructor + private static class ContentNoId { + + Tag single; + Set tagSet; + List tagList; + Map 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 void interpret(DbAction.Insert insert) { + + if (insert.getEntityType().getSimpleName().endsWith("NoId")) { + return; + } + insert.setGeneratedId(++id); + } + + @Override + public void interpret(DbAction.InsertRoot insert) { + insert.setGeneratedId(++id); + } + + @Override + public void interpret(DbAction.Update update) { + throw new UnsupportedOperationException(); + } + + @Override + public void interpret(DbAction.UpdateRoot update) { + throw new UnsupportedOperationException(); + } + + @Override + public void interpret(DbAction.Merge update) { + throw new UnsupportedOperationException(); + } + + @Override + public void interpret(DbAction.Delete delete) { + throw new UnsupportedOperationException(); + } + + @Override + public void interpret(DbAction.DeleteRoot deleteRoot) { + throw new UnsupportedOperationException(); + } + + @Override + public void interpret(DbAction.DeleteAll delete) { + throw new UnsupportedOperationException(); + } + + @Override + public void interpret(DbAction.DeleteAllRoot DeleteAllRoot) { + throw new UnsupportedOperationException(); + } + } +} diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/AggregateChangeIdGenerationUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/AggregateChangeIdGenerationUnitTests.java new file mode 100644 index 00000000..c9fd5b34 --- /dev/null +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/AggregateChangeIdGenerationUnitTests.java @@ -0,0 +1,437 @@ +/* + * 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.relational.core.conversion; + +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.mapping.RelationalMappingContext; +import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; +import org.springframework.lang.Nullable; + +/** + * Unit tests for the {@link AggregateChange}. + * + * @author Jens Schauder + * 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); + + @Test // DATAJDBC-291 + public void singleRoot() { + + AggregateChange aggregateChange = new AggregateChange<>(AggregateChange.Kind.SAVE, DummyEntity.class, + entity); + aggregateChange.addAction(rootInsert); + + aggregateChange.executeWith(new IdSettingInterpreter(), context, converter); + + assertThat(entity.rootId).isEqualTo(1); + } + + @Test // DATAJDBC-291 + public void simpleReference() { + + entity.single = content; + + AggregateChange aggregateChange = new AggregateChange<>(AggregateChange.Kind.SAVE, DummyEntity.class, + entity); + aggregateChange.addAction(rootInsert); + aggregateChange.addAction(createInsert("single", content, null)); + + aggregateChange.executeWith(new IdSettingInterpreter(), context, converter); + + 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 aggregateChange = new AggregateChange<>(AggregateChange.Kind.SAVE, DummyEntity.class, + entity); + aggregateChange.addAction(rootInsert); + aggregateChange.addAction(createInsert("contentList", content, 0)); + aggregateChange.addAction(createInsert("contentList", content2, 1)); + + aggregateChange.executeWith(new IdSettingInterpreter(), context, converter); + + 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 aggregateChange = new AggregateChange<>(AggregateChange.Kind.SAVE, DummyEntity.class, + entity); + aggregateChange.addAction(rootInsert); + aggregateChange.addAction(createInsert("contentMap", content, "a")); + aggregateChange.addAction(createInsert("contentMap", content2, "b")); + + aggregateChange.executeWith(new IdSettingInterpreter(), context, converter); + + 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 aggregateChange = new AggregateChange<>(AggregateChange.Kind.SAVE, DummyEntity.class, + entity); + aggregateChange.addAction(rootInsert); + aggregateChange.addAction(parentInsert); + aggregateChange.addAction(insert); + + aggregateChange.executeWith(new IdSettingInterpreter(), context, converter); + + 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 aggregateChange = new AggregateChange<>(AggregateChange.Kind.SAVE, DummyEntity.class, + entity); + aggregateChange.addAction(rootInsert); + aggregateChange.addAction(parentInsert); + aggregateChange.addAction(insert1); + aggregateChange.addAction(insert2); + + aggregateChange.executeWith(new IdSettingInterpreter(), context, converter); + + 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 aggregateChange = new AggregateChange<>(AggregateChange.Kind.SAVE, DummyEntity.class, + entity); + aggregateChange.addAction(rootInsert); + aggregateChange.addAction(parentInsert); + aggregateChange.addAction(insert1); + aggregateChange.addAction(insert2); + + aggregateChange.executeWith(new IdSettingInterpreter(), context, converter); + + 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 aggregateChange = new AggregateChange<>(AggregateChange.Kind.SAVE, DummyEntity.class, + entity); + aggregateChange.addAction(rootInsert); + aggregateChange.addAction(parentInsert1); + aggregateChange.addAction(parentInsert2); + aggregateChange.addAction(insert1); + aggregateChange.addAction(insert2); + + aggregateChange.executeWith(new IdSettingInterpreter(), context, converter); + + 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 aggregateChange = new AggregateChange<>(AggregateChange.Kind.SAVE, DummyEntity.class, + entity); + aggregateChange.addAction(rootInsert); + aggregateChange.addAction(parentInsert1); + aggregateChange.addAction(parentInsert2); + aggregateChange.addAction(insert1); + aggregateChange.addAction(insert2); + aggregateChange.addAction(insert3); + + aggregateChange.executeWith(new IdSettingInterpreter(), context, converter); + + 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 aggregateChange = new AggregateChange<>(AggregateChange.Kind.SAVE, DummyEntity.class, + entity); + aggregateChange.addAction(rootInsert); + aggregateChange.addAction(parentInsert1); + aggregateChange.addAction(parentInsert2); + aggregateChange.addAction(insert1); + aggregateChange.addAction(insert2); + aggregateChange.addAction(insert3); + + aggregateChange.executeWith(new IdSettingInterpreter(), context, converter); + + 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 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) { + + DbAction.Insert insert = new DbAction.Insert<>(value, toPath(entity, value), parentInsert); + insert.getQualifiers().put(toPath(parentInsert.getPropertyPath().toDotPath() + "." + propertyName), key); + return insert; + } + + PersistentPropertyPath toPath(String path) { + + PersistentPropertyPaths persistentPropertyPaths = context + .findPersistentPropertyPaths(DummyEntity.class, p -> true); + + return persistentPropertyPaths.filter(p -> p.toDotPath().equals(path)).stream().findFirst() + .orElseThrow(() -> new IllegalArgumentException("No matching path found")); + } + + PersistentPropertyPath toPath(DummyEntity root, Object pathValue) { + // DefaultPersistentPropertyPath is package-public + return new WritingContext(context, entity, + new AggregateChange<>(AggregateChange.Kind.SAVE, DummyEntity.class, 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; + + Content single; + + Set contentSet = new HashSet<>(); + + List contentList = new ArrayList<>(); + + Map contentMap = new HashMap<>(); + } + + private static class Content { + + @Id Integer id; + + Tag single; + + Set tagSet = new HashSet<>(); + + List tagList = new ArrayList<>(); + + Map tagMap = new HashMap<>(); + } + + private static class Tag { + @Id Integer id; + } + + private static class IdSettingInterpreter implements Interpreter { + int id = 0; + + @Override + public void interpret(DbAction.Insert insert) { + insert.setGeneratedId(++id); + } + + @Override + public void interpret(DbAction.InsertRoot insert) { + insert.setGeneratedId(++id); + + } + + @Override + public void interpret(DbAction.Update update) { + throw new UnsupportedOperationException(); + } + + @Override + public void interpret(DbAction.UpdateRoot update) { + throw new UnsupportedOperationException(); + } + + @Override + public void interpret(DbAction.Merge update) { + throw new UnsupportedOperationException(); + } + + @Override + public void interpret(DbAction.Delete delete) { + throw new UnsupportedOperationException(); + } + + @Override + public void interpret(DbAction.DeleteRoot deleteRoot) { + throw new UnsupportedOperationException(); + } + + @Override + public void interpret(DbAction.DeleteAll delete) { + throw new UnsupportedOperationException(); + } + + @Override + public void interpret(DbAction.DeleteAllRoot DeleteAllRoot) { + throw new UnsupportedOperationException(); + } + } +} diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/AggregateChangeUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/AggregateChangeUnitTests.java deleted file mode 100644 index d9034e52..00000000 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/AggregateChangeUnitTests.java +++ /dev/null @@ -1,266 +0,0 @@ -/* - * 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.relational.core.conversion; - -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.junit.Test; -import org.springframework.data.annotation.Id; -import org.springframework.data.mapping.PersistentPropertyAccessor; -import org.springframework.data.mapping.PersistentPropertyPath; -import org.springframework.data.mapping.PersistentPropertyPaths; -import org.springframework.data.relational.core.mapping.RelationalMappingContext; -import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; - -/** - * Unit tests for the {@link AggregateChange}. - * - * @author Jens Schauder - */ -public class AggregateChangeUnitTests { - - DummyEntity entity = new DummyEntity(); - Content content = new Content(); - Tag tag = new Tag(); - - RelationalMappingContext context = new RelationalMappingContext(); - RelationalConverter converter = new BasicRelationalConverter(context); - - PersistentPropertyAccessor propertyAccessor = context.getRequiredPersistentEntity(DummyEntity.class) - .getPropertyAccessor(entity); - PersistentPropertyAccessor contentPropertyAccessor = context.getRequiredPersistentEntity(Content.class) - .getPropertyAccessor(content); - Object id = 23; - - DbAction.WithEntity rootInsert = new DbAction.InsertRoot<>(entity); - - DbAction.Insert createInsert(String propertyName, Object value, Object key) { - - DbAction.Insert insert = new DbAction.Insert<>(value, - context.getPersistentPropertyPath(propertyName, DummyEntity.class), rootInsert); - insert.getQualifiers().put(toPath(propertyName, DummyEntity.class), key); - - return insert; - } - - DbAction.Insert createDeepInsert(String propertyName, Object value, Object key, DbAction.Insert parentInsert) { - DbAction.Insert insert = new DbAction.Insert<>(value, toPath(entity, value), parentInsert); - insert.getQualifiers().put(toPath(parentInsert.getPropertyPath().toDotPath() + "." + propertyName, DummyEntity.class), key); - return insert; - } - - @Test // DATAJDBC-241 - public void setIdForSimpleReference() { - - entity.single = content; - - DbAction.Insert insert = createInsert("single", content, null); - - AggregateChange.setIdOfNonRootEntity(context, converter, propertyAccessor, insert, id); - - DummyEntity result = propertyAccessor.getBean(); - - assertThat(result.single.id).isEqualTo(id); - } - - @Test // DATAJDBC-241 - public void setIdForSingleElementSet() { - - entity.contentSet.add(content); - - DbAction.Insert insert = createInsert("contentSet", content, null); - - AggregateChange.setIdOfNonRootEntity(context, converter, propertyAccessor, insert, id); - - DummyEntity result = propertyAccessor.getBean(); - assertThat(result.contentSet).isNotNull(); - assertThat(result.contentSet).extracting(c -> c == null ? "null" : c.id).containsExactlyInAnyOrder(23); - } - - @Test // DATAJDBC-241 - public void setIdForSingleElementList() { - - entity.contentList.add(content); - - DbAction.Insert insert = createInsert("contentList", content, 0); - - AggregateChange.setIdOfNonRootEntity(context, converter, propertyAccessor, insert, id); - - DummyEntity result = propertyAccessor.getBean(); - assertThat(result.contentList).extracting(c -> c.id).containsExactlyInAnyOrder(23); - } - - @Test // DATAJDBC-241 - public void setIdForSingleElementMap() { - - entity.contentMap.put("one", content); - - DbAction.Insert insert = createInsert("contentMap", content, "one"); - - AggregateChange.setIdOfNonRootEntity(context, converter, propertyAccessor, insert, id); - - DummyEntity result = propertyAccessor.getBean(); - assertThat(result.contentMap.entrySet()).extracting(e -> e.getKey(), e -> e.getValue().id) - .containsExactlyInAnyOrder(tuple("one", 23)); - } - - @Test // DATAJDBC-291 - public void setIdForDeepReference() { - - content.single = tag; - entity.single = content; - - DbAction.Insert parentInsert = createInsert("single", content, null); - DbAction.Insert insert = createDeepInsert("single", tag, null, parentInsert); - - AggregateChange.setIdOfNonRootEntity(context, converter, propertyAccessor, insert, id); - - Content result = contentPropertyAccessor.getBean(); - - assertThat(result.single.id).isEqualTo(id); - } - - @Test // DATAJDBC-291 - public void setIdForDeepReferenceElementList() { - - content.tagList.add(tag); - entity.single = content; - - DbAction.Insert parentInsert = createInsert("single", content, null); - DbAction.Insert insert = createDeepInsert("tagList", tag, 0, parentInsert); - - AggregateChange.setIdOfNonRootEntity(context, converter, propertyAccessor, insert, id); - - Content result = contentPropertyAccessor.getBean(); - assertThat(result.tagList).extracting(c -> c.id).containsExactlyInAnyOrder(23); - } - - @Test // DATAJDBC-291 - public void setIdForDeepElementSetElementSet() { - - content.tagSet.add(tag); - entity.contentSet.add(content); - - DbAction.Insert parentInsert = createInsert("contentSet", content, null); - DbAction.Insert insert = createDeepInsert("tagSet", tag, null, parentInsert); - - AggregateChange.setIdOfNonRootEntity(context, converter, propertyAccessor, insert, id); - - Content result = contentPropertyAccessor.getBean(); - assertThat(result.tagSet).isNotNull(); - assertThat(result.tagSet).extracting(c -> c == null ? "null" : c.id).containsExactlyInAnyOrder(23); - } - - @Test // DATAJDBC-291 - public void setIdForDeepElementListSingleReference() { - - content.single = tag; - entity.contentList.add(content); - - DbAction.Insert parentInsert = createInsert("contentList", content, 0); - DbAction.Insert insert = createDeepInsert("single", tag, null, parentInsert); - - AggregateChange.setIdOfNonRootEntity(context, converter, propertyAccessor, insert, id); - - Content result = contentPropertyAccessor.getBean(); - assertThat(result.single.id).isEqualTo(id); - } - - @Test // DATAJDBC-291 - public void setIdForDeepElementListElementList() { - - content.tagList.add(tag); - entity.contentList.add(content); - - DbAction.Insert parentInsert = createInsert("contentList", content, 0); - DbAction.Insert insert = createDeepInsert("tagList", tag, 0, parentInsert); - - AggregateChange.setIdOfNonRootEntity(context, converter, propertyAccessor, insert, id); - - Content result = contentPropertyAccessor.getBean(); - assertThat(result.tagList).extracting(c -> c.id).containsExactlyInAnyOrder(23); - } - - @Test // DATAJDBC-291 - public void setIdForDeepElementMapElementMap() { - - content.tagMap.put("one", tag); - entity.contentMap.put("one", content); - - DbAction.Insert parentInsert = createInsert("contentMap", content, "one"); - DbAction.Insert insert = createDeepInsert("tagMap", tag, "one", parentInsert); - - AggregateChange.setIdOfNonRootEntity(context, converter, propertyAccessor, insert, id); - - Content result = contentPropertyAccessor.getBean(); - assertThat(result.tagMap.entrySet()).extracting(e -> e.getKey(), e -> e.getValue().id) - .containsExactlyInAnyOrder(tuple("one", 23)); - } - - PersistentPropertyPath toPath(String path, Class source) { - - PersistentPropertyPaths persistentPropertyPaths = context - .findPersistentPropertyPaths(source, p -> true); - - return persistentPropertyPaths.filter(p -> p.toDotPath().equals(path)).stream().findFirst().orElse(null); - } - - PersistentPropertyPath toPath(DummyEntity root, Object pathValue) { - // DefaultPersistentPropertyPath is package-public - return new WritingContext(context, entity, new AggregateChange<>(AggregateChange.Kind.SAVE, DummyEntity.class, root)) - .insert().stream().filter(a -> a instanceof DbAction.Insert).map(DbAction.Insert.class::cast) - .filter(a -> a.getEntity() == pathValue) - .map(DbAction.Insert::getPropertyPath) - .findFirst().orElse(null); - } - - private static class DummyEntity { - - @Id Integer rootId; - - Content single; - - Set contentSet = new HashSet<>(); - - List contentList = new ArrayList<>(); - - Map contentMap = new HashMap<>(); - } - - private static class Content { - - @Id Integer id; - - Tag single; - - Set tagSet = new HashSet<>(); - - List tagList = new ArrayList<>(); - - Map tagMap = new HashMap<>(); - } - - private static class Tag { - @Id Integer id; - } -}