From ca3e14e1c68dfd64690dec48022c1cd6ebd11674 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Fri, 5 Jun 2020 12:06:52 +0200 Subject: [PATCH] Add support for dynamic relationships with keys based on enums. This change closes #219 by allowing Enums to be used as `Map`-Keys in dynamic relationships. Conversion from and to strings is done via the registered conversions. --- .../data/core/Neo4jTemplate.java | 14 +- .../data/core/ReactiveNeo4jTemplate.java | 15 +- .../core/RelationshipStatementHolder.java | 43 +++- .../core/mapping/DefaultNeo4jConverter.java | 36 ++- .../core/mapping/Neo4jPersistentProperty.java | 7 +- .../core/mapping/Neo4jMappingContextTest.java | 49 +++- .../imperative/DynamicRelationshipsIT.java | 78 +++--- .../StringlyTypedDynamicRelationshipsIT.java | 196 +++++++++++++++ .../ReactiveDynamicRelationshipsIT.java | 78 +++--- ...iveStringlyTypeDynamicRelationshipsIT.java | 231 ++++++++++++++++++ .../shared/DynamicRelationshipsITBase.java | 13 +- .../shared/PersonWithRelatives.java | 22 +- .../PersonWithStringlyTypedRelatives.java | 63 +++++ 13 files changed, 713 insertions(+), 132 deletions(-) create mode 100644 spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/StringlyTypedDynamicRelationshipsIT.java create mode 100644 spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveStringlyTypeDynamicRelationshipsIT.java create mode 100644 spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/PersonWithStringlyTypedRelatives.java diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/Neo4jTemplate.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/Neo4jTemplate.java index fbaf045fb..dd9ea0e42 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/Neo4jTemplate.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/Neo4jTemplate.java @@ -20,7 +20,6 @@ package org.neo4j.springframework.data.core; import static java.util.Collections.*; import static java.util.stream.Collectors.*; -import static org.neo4j.springframework.data.core.RelationshipStatementHolder.*; import static org.neo4j.opencypherdsl.Cypher.*; import static org.neo4j.springframework.data.core.schema.Constants.*; @@ -461,17 +460,8 @@ public final class Neo4jTemplate implements Neo4jOperations, BeanFactoryAware { Long relatedInternalId = saveRelatedNode(valueToBeSaved, relationshipContext.getAssociationTargetType(), targetNodeDescription, inDatabase); - // handle creation of relationship depending on properties on relationship or not - RelationshipStatementHolder statementHolder = relationshipContext.hasRelationshipWithProperties() - ? createStatementForRelationShipWithProperties(neo4jMappingContext, - neo4jPersistentEntity, - relationshipContext, - relatedInternalId, - (Map.Entry) relatedValue) - : createStatementForRelationshipWithoutProperties(neo4jPersistentEntity, - relationshipContext, - relatedInternalId, - relatedValue); + RelationshipStatementHolder statementHolder = RelationshipStatementHolder.createStatement( + neo4jMappingContext, neo4jPersistentEntity, relationshipContext, relatedInternalId, relatedValue); neo4jClient.query(renderer.render(statementHolder.getRelationshipCreationQuery())) .in(inDatabase) diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/ReactiveNeo4jTemplate.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/ReactiveNeo4jTemplate.java index 2fd18b827..ea3bbe63d 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/ReactiveNeo4jTemplate.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/ReactiveNeo4jTemplate.java @@ -21,7 +21,6 @@ package org.neo4j.springframework.data.core; import static java.util.Collections.*; import static java.util.stream.Collectors.*; import static org.neo4j.springframework.data.core.DatabaseSelection.*; -import static org.neo4j.springframework.data.core.RelationshipStatementHolder.*; import static org.neo4j.opencypherdsl.Cypher.*; import static org.neo4j.springframework.data.core.schema.Constants.*; import static org.neo4j.springframework.data.core.support.Relationships.*; @@ -480,18 +479,8 @@ public final class ReactiveNeo4jTemplate implements ReactiveNeo4jOperations, Bea relatedInternalId); } - // handle creation of relationship depending on properties on relationship or not - RelationshipStatementHolder statementHolder = relationshipContext - .hasRelationshipWithProperties() - ? createStatementForRelationShipWithProperties(neo4jMappingContext, - neo4jPersistentEntity, - relationshipContext, - relatedInternalId, - (Map.Entry) relatedValue) - : createStatementForRelationshipWithoutProperties(neo4jPersistentEntity, - relationshipContext, - relatedInternalId, - relatedValue); + RelationshipStatementHolder statementHolder = RelationshipStatementHolder.createStatement( + neo4jMappingContext, neo4jPersistentEntity, relationshipContext, relatedInternalId, relatedValue); // in case of no properties the bind will just return an empty map Mono relationshipCreationMonoNested = neo4jClient diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/RelationshipStatementHolder.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/RelationshipStatementHolder.java index 6cebc7d47..20de93bea 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/RelationshipStatementHolder.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/RelationshipStatementHolder.java @@ -26,6 +26,7 @@ import org.neo4j.opencypherdsl.Statement; import org.neo4j.springframework.data.core.mapping.Neo4jMappingContext; import org.neo4j.springframework.data.core.mapping.Neo4jPersistentEntity; import org.neo4j.springframework.data.core.schema.CypherGenerator; +import org.springframework.data.util.TypeInformation; import org.springframework.lang.NonNull; /** @@ -35,6 +36,7 @@ import org.springframework.lang.NonNull; * {@link Neo4jTemplate} as well as in the {@link ReactiveNeo4jTemplate}. * * @author Philipp Tölle + * @author Michael J. Simons * @since 1.0 */ final class RelationshipStatementHolder { @@ -42,13 +44,13 @@ final class RelationshipStatementHolder { private final Map properties; private RelationshipStatementHolder(@NonNull Statement relationshipCreationQuery) { - this.relationshipCreationQuery = relationshipCreationQuery; - this.properties = Collections.emptyMap(); + this(relationshipCreationQuery, Collections.emptyMap()); } private RelationshipStatementHolder( @NonNull Statement relationshipCreationQuery, - @NonNull Map properties) { + @NonNull Map properties + ) { this.relationshipCreationQuery = relationshipCreationQuery; this.properties = properties; } @@ -61,7 +63,22 @@ final class RelationshipStatementHolder { return properties; } - static RelationshipStatementHolder createStatementForRelationShipWithProperties( + static RelationshipStatementHolder createStatement(Neo4jMappingContext neo4jMappingContext, + Neo4jPersistentEntity neo4jPersistentEntity, + NestedRelationshipContext relationshipContext, + Long relatedInternalId, + Object relatedValue) { + + if (relationshipContext.hasRelationshipWithProperties()) { + return createStatementForRelationShipWithProperties(neo4jMappingContext, neo4jPersistentEntity, + relationshipContext, relatedInternalId, (Map.Entry) relatedValue); + } else { + return createStatementForRelationshipWithoutProperties(neo4jMappingContext, neo4jPersistentEntity, + relationshipContext, relatedInternalId, relatedValue); + } + } + + private static RelationshipStatementHolder createStatementForRelationShipWithProperties( Neo4jMappingContext neo4jMappingContext, Neo4jPersistentEntity neo4jPersistentEntity, NestedRelationshipContext relationshipContext, @@ -80,16 +97,28 @@ final class RelationshipStatementHolder { return new RelationshipStatementHolder(relationshipCreationQuery, propMap); } - static RelationshipStatementHolder createStatementForRelationshipWithoutProperties( + private static RelationshipStatementHolder createStatementForRelationshipWithoutProperties( + Neo4jMappingContext neo4jMappingContext, Neo4jPersistentEntity neo4jPersistentEntity, NestedRelationshipContext relationshipContext, Long relatedInternalId, - Object relatedValue) { + Object relatedValue + ) { + + String relationshipType; + if (!relationshipContext.getRelationship().isDynamic()) { + relationshipType = null; + } else { + TypeInformation keyType = relationshipContext.getInverse().getTypeInformation() + .getRequiredComponentType(); + Object key = ((Map.Entry) relatedValue).getKey(); + relationshipType = neo4jMappingContext.getConverter().writeValueFromProperty(key, keyType).asString(); + } Statement relationshipCreationQuery = CypherGenerator.INSTANCE .createRelationshipCreationQuery(neo4jPersistentEntity, relationshipContext.getRelationship(), - relatedValue instanceof Map.Entry ? ((Map.Entry) relatedValue).getKey() : null, + relationshipType, relatedInternalId); return new RelationshipStatementHolder(relationshipCreationQuery); } diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jConverter.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jConverter.java index f83fbc138..e61522d38 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jConverter.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jConverter.java @@ -263,12 +263,15 @@ final class DefaultNeo4jConverter implements Neo4jConverter { KnownObjects knownObjects) { List allLabels = getLabels(queryResult); - NodeDescriptionAndLabels nodeDescriptionAndLabels = nodeDescriptionStore.deriveConcreteNodeDescription(nodeDescription, allLabels); - Neo4jPersistentEntity concreteNodeDescription = (Neo4jPersistentEntity) nodeDescriptionAndLabels.getNodeDescription(); + NodeDescriptionAndLabels nodeDescriptionAndLabels = nodeDescriptionStore + .deriveConcreteNodeDescription(nodeDescription, allLabels); + Neo4jPersistentEntity concreteNodeDescription = (Neo4jPersistentEntity) nodeDescriptionAndLabels + .getNodeDescription(); Collection relationships = concreteNodeDescription.getRelationships(); - ET instance = instantiate(concreteNodeDescription, queryResult, knownObjects, relationships, nodeDescriptionAndLabels.getDynamicLabels()); + ET instance = instantiate(concreteNodeDescription, queryResult, knownObjects, relationships, + nodeDescriptionAndLabels.getDynamicLabels()); PersistentPropertyAccessor propertyAccessor = concreteNodeDescription.getPropertyAccessor(instance); @@ -321,7 +324,8 @@ final class DefaultNeo4jConverter implements Neo4jConverter { .getRequiredPersistentProperty(parameter.getName()); if (matchingProperty.isRelationship()) { - return createInstanceOfRelationships(matchingProperty, values, knownObjects, relationships).orElse(null); + return createInstanceOfRelationships(matchingProperty, values, knownObjects, relationships) + .orElse(null); } else if (matchingProperty.isDynamicLabels()) { return createDynamicLabelsProperty(matchingProperty.getTypeInformation(), surplusLabels); } @@ -329,7 +333,8 @@ final class DefaultNeo4jConverter implements Neo4jConverter { } }; - return INSTANTIATORS.getInstantiatorFor(nodeDescription).createInstance(nodeDescription, parameterValueProvider); + return INSTANTIATORS.getInstantiatorFor(nodeDescription) + .createInstance(nodeDescription, parameterValueProvider); } private PropertyHandler populateFrom( @@ -344,7 +349,8 @@ final class DefaultNeo4jConverter implements Neo4jConverter { } if (property.isDynamicLabels()) { - propertyAccessor.setProperty(property, createDynamicLabelsProperty(property.getTypeInformation(), surplusLabels)); + propertyAccessor + .setProperty(property, createDynamicLabelsProperty(property.getTypeInformation(), surplusLabels)); } else { propertyAccessor.setProperty(property, readValueForProperty(extractValueOf(property, queryResult), property.getTypeInformation())); @@ -387,24 +393,32 @@ final class DefaultNeo4jConverter implements Neo4jConverter { (Neo4jPersistentEntity) relationshipDescription.getTarget(); List allLabels = getLabels(values); - NodeDescriptionAndLabels nodeDescriptionAndLabels = nodeDescriptionStore.deriveConcreteNodeDescription(genericTargetNodeDescription, allLabels); - Neo4jPersistentEntity concreteTargetNodeDescription = (Neo4jPersistentEntity) nodeDescriptionAndLabels.getNodeDescription(); + NodeDescriptionAndLabels nodeDescriptionAndLabels = nodeDescriptionStore + .deriveConcreteNodeDescription(genericTargetNodeDescription, allLabels); + Neo4jPersistentEntity concreteTargetNodeDescription = (Neo4jPersistentEntity) nodeDescriptionAndLabels + .getNodeDescription(); List value = new ArrayList<>(); - Map dynamicValue = new HashMap<>(); + Map dynamicValue = new HashMap<>(); BiConsumer mappedObjectHandler; + Function keyTransformer; + if (persistentProperty.isDynamicAssociation() && persistentProperty.getComponentType().isEnum()) { + keyTransformer = f -> conversionService.convert(f, persistentProperty.getComponentType()); + } else { + keyTransformer = Function.identity(); + } if (persistentProperty.isDynamicOneToManyAssociation()) { TypeInformation actualType = persistentProperty.getTypeInformation().getRequiredActualType(); mappedObjectHandler = (type, mappedObject) -> { - List bucket = (List) dynamicValue.computeIfAbsent(type, + List bucket = (List) dynamicValue.computeIfAbsent(keyTransformer.apply(type), s -> createCollection(actualType.getType(), persistentProperty.getAssociationTargetType(), values.size())); bucket.add(mappedObject); }; } else if (persistentProperty.isDynamicAssociation()) { - mappedObjectHandler = dynamicValue::put; + mappedObjectHandler = (type, mappedObject) -> dynamicValue.put(keyTransformer.apply(type), mappedObject); } else { mappedObjectHandler = (type, mappedObject) -> value.add(mappedObject); } diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/Neo4jPersistentProperty.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/Neo4jPersistentProperty.java index 3b1dc9136..219192eed 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/Neo4jPersistentProperty.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/mapping/Neo4jPersistentProperty.java @@ -19,9 +19,9 @@ package org.neo4j.springframework.data.core.mapping; import org.apiguardian.api.API; +import org.neo4j.springframework.data.core.schema.DynamicLabels; import org.neo4j.springframework.data.core.schema.GraphPropertyDescription; import org.neo4j.springframework.data.core.schema.RelationshipProperties; -import org.neo4j.springframework.data.core.schema.DynamicLabels; import org.springframework.data.mapping.PersistentProperty; /** @@ -36,12 +36,13 @@ public interface Neo4jPersistentProperty extends PersistentProperty, GraphPropertyDescription { /** - * Dynamic associations are associations to non-simple types stored in a map with a key type of {@literal java.lang.String}. + * Dynamic associations are associations to non-simple types stored in a map + * with a key type of {@literal java.lang.String} or enum. * * @return True, if this association is a dynamic association. */ default boolean isDynamicAssociation() { - return isAssociation() && isMap() && getComponentType() == String.class; + return isAssociation() && isMap() && (getComponentType() == String.class || getComponentType().isEnum()); } /** diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/mapping/Neo4jMappingContextTest.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/mapping/Neo4jMappingContextTest.java index 1b7fb9449..5e766ab2c 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/mapping/Neo4jMappingContextTest.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/core/mapping/Neo4jMappingContextTest.java @@ -114,7 +114,8 @@ class Neo4jMappingContextTest { schema.setInitialEntitySet(new HashSet<>(Arrays.asList(InvalidId.class))); assertThatIllegalArgumentException() .isThrownBy(() -> schema.initialize()) - .withMessageMatching("Cannot use internal id strategy with custom property getMappingFunctionFor on entity .*"); + .withMessageMatching( + "Cannot use internal id strategy with custom property getMappingFunctionFor on entity .*"); } @Test @@ -142,7 +143,8 @@ class Neo4jMappingContextTest { Neo4jMappingContext schema = new Neo4jMappingContext(); Neo4jPersistentEntity bikeNodeEntity = schema.getPersistentEntity(BikeNode.class); bikeNodeEntity.doWithAssociations((Association association) -> - assertThat(schema.getRequiredMappingFunctionFor(association.getInverse().getAssociationTargetType())).isNotNull()); + assertThat(schema.getRequiredMappingFunctionFor(association.getInverse().getAssociationTargetType())) + .isNotNull()); } @Test @@ -186,7 +188,8 @@ class Neo4jMappingContextTest { } } - Neo4jMappingContext schema = new Neo4jMappingContext(new Neo4jConversions(singleton(new ConvertibleTypeConverter()))); + Neo4jMappingContext schema = new Neo4jMappingContext( + new Neo4jConversions(singleton(new ConvertibleTypeConverter()))); Neo4jPersistentEntity entity = schema.getPersistentEntity(EntityWithConvertibleProperty.class); assertThat(entity.getPersistentProperty("convertibleType").isRelationship()).isFalse(); @@ -217,6 +220,19 @@ class Neo4jMappingContextTest { assertThat(associations).containsOnly("bikes"); } + @Test + void enumMapKeys() { + + Neo4jMappingContext schema = new Neo4jMappingContext(); + Neo4jPersistentEntity enumRelNodeEntity = schema.getPersistentEntity(EnumRelNode.class); + + List associations = new ArrayList<>(); + enumRelNodeEntity + .doWithAssociations((Association a) -> associations.add(a.getInverse())); + + assertThat(associations).hasSize(2); + } + static class DummyIdGenerator implements IdGenerator { @Override @@ -250,6 +266,23 @@ class Neo4jMappingContextTest { } + enum A { + A1, A2 + } + + enum ExtendedA { + + EA1, EA2 { + @Override + public void doNothing() { + } + }; + + public void doNothing() { + + } + } + static class BikeNode { @Id @@ -267,6 +300,16 @@ class Neo4jMappingContextTest { Map funnyDynamicProperties; } + static class EnumRelNode { + + @Id + private String id; + + Map relA; + + Map relEA; + } + static class TripNode { @Id diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/DynamicRelationshipsIT.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/DynamicRelationshipsIT.java index 2928a4985..c816e6b82 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/DynamicRelationshipsIT.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/DynamicRelationshipsIT.java @@ -34,6 +34,8 @@ import org.neo4j.springframework.data.config.AbstractNeo4jConfig; import org.neo4j.springframework.data.integration.shared.DynamicRelationshipsITBase; import org.neo4j.springframework.data.integration.shared.Person; import org.neo4j.springframework.data.integration.shared.PersonWithRelatives; +import org.neo4j.springframework.data.integration.shared.PersonWithRelatives.TypeOfPet; +import org.neo4j.springframework.data.integration.shared.PersonWithRelatives.TypeOfRelative; import org.neo4j.springframework.data.integration.shared.Pet; import org.neo4j.springframework.data.repository.config.EnableNeo4jRepositories; import org.springframework.beans.factory.annotation.Autowired; @@ -44,13 +46,11 @@ import org.springframework.test.util.ReflectionTestUtils; import org.springframework.transaction.annotation.EnableTransactionManagement; /** - * * @author Michael J. Simons */ -class DynamicRelationshipsIT extends DynamicRelationshipsITBase { +class DynamicRelationshipsIT extends DynamicRelationshipsITBase { - @Autowired - DynamicRelationshipsIT(Driver driver) { + @Autowired DynamicRelationshipsIT(Driver driver) { super(driver); } @@ -61,10 +61,10 @@ class DynamicRelationshipsIT extends DynamicRelationshipsITBase { assertThat(person).isNotNull(); assertThat(person.getName()).isEqualTo("A"); - Map relatives = person.getRelatives(); - assertThat(relatives).containsOnlyKeys("HAS_WIFE", "HAS_DAUGHTER"); - assertThat(relatives.get("HAS_WIFE").getFirstName()).isEqualTo("B"); - assertThat(relatives.get("HAS_DAUGHTER").getFirstName()).isEqualTo("C"); + Map relatives = person.getRelatives(); + assertThat(relatives).containsOnlyKeys(TypeOfRelative.HAS_WIFE, TypeOfRelative.HAS_DAUGHTER); + assertThat(relatives.get(TypeOfRelative.HAS_WIFE).getFirstName()).isEqualTo("B"); + assertThat(relatives.get(TypeOfRelative.HAS_DAUGHTER).getFirstName()).isEqualTo("C"); } @Test // GH-216 @@ -74,10 +74,10 @@ class DynamicRelationshipsIT extends DynamicRelationshipsITBase { assertThat(person).isNotNull(); assertThat(person.getName()).isEqualTo("A"); - Map> pets = person.getPets(); - assertThat(pets).containsOnlyKeys("CATS", "DOGS"); - assertThat(pets.get("CATS")).extracting(Pet::getName).containsExactlyInAnyOrder("Tom", "Garfield"); - assertThat(pets.get("DOGS")).extracting(Pet::getName).containsExactlyInAnyOrder("Benji", "Lassie"); + Map> pets = person.getPets(); + assertThat(pets).containsOnlyKeys(TypeOfPet.CATS, TypeOfPet.DOGS); + assertThat(pets.get(TypeOfPet.CATS)).extracting(Pet::getName).containsExactlyInAnyOrder("Tom", "Garfield"); + assertThat(pets.get(TypeOfPet.DOGS)).extracting(Pet::getName).containsExactlyInAnyOrder("Benji", "Lassie"); } @Test @@ -87,20 +87,20 @@ class DynamicRelationshipsIT extends DynamicRelationshipsITBase { assumeThat(person).isNotNull(); assumeThat(person.getName()).isEqualTo("A"); - Map relatives = person.getRelatives(); - assumeThat(relatives).containsOnlyKeys("HAS_WIFE", "HAS_DAUGHTER"); + Map relatives = person.getRelatives(); + assumeThat(relatives).containsOnlyKeys(TypeOfRelative.HAS_WIFE, TypeOfRelative.HAS_DAUGHTER); - relatives.remove("HAS_WIFE"); + relatives.remove(TypeOfRelative.HAS_WIFE); Person d = new Person(); ReflectionTestUtils.setField(d, "firstName", "D"); - relatives.put("HAS_SON", d); - ReflectionTestUtils.setField(relatives.get("HAS_DAUGHTER"), "firstName", "C2"); + relatives.put(TypeOfRelative.HAS_SON, d); + ReflectionTestUtils.setField(relatives.get(TypeOfRelative.HAS_DAUGHTER), "firstName", "C2"); person = repository.save(person); relatives = person.getRelatives(); - assertThat(relatives).containsOnlyKeys("HAS_DAUGHTER", "HAS_SON"); - assertThat(relatives.get("HAS_DAUGHTER").getFirstName()).isEqualTo("C2"); - assertThat(relatives.get("HAS_SON").getFirstName()).isEqualTo("D"); + assertThat(relatives).containsOnlyKeys(TypeOfRelative.HAS_DAUGHTER, TypeOfRelative.HAS_SON); + assertThat(relatives.get(TypeOfRelative.HAS_DAUGHTER).getFirstName()).isEqualTo("C2"); + assertThat(relatives.get(TypeOfRelative.HAS_SON).getFirstName()).isEqualTo("D"); } @Test // GH-216 @@ -110,42 +110,43 @@ class DynamicRelationshipsIT extends DynamicRelationshipsITBase { assertThat(person).isNotNull(); assertThat(person.getName()).isEqualTo("A"); - Map> pets = person.getPets(); - assertThat(pets).containsOnlyKeys("CATS", "DOGS"); + Map> pets = person.getPets(); + assertThat(pets).containsOnlyKeys(TypeOfPet.CATS, TypeOfPet.DOGS); - pets.remove("DOGS"); - pets.get("CATS").add(new Pet("Delilah")); + pets.remove(TypeOfPet.DOGS); + pets.get(TypeOfPet.CATS).add(new Pet("Delilah")); - pets.put("FISH", Collections.singletonList(new Pet("Nemo"))); + pets.put(TypeOfPet.FISH, Collections.singletonList(new Pet("Nemo"))); person = repository.save(person); pets = person.getPets(); - assertThat(pets).containsOnlyKeys("CATS", "FISH"); - assertThat(pets.get("CATS")).extracting(Pet::getName).containsExactlyInAnyOrder("Tom", "Garfield", "Delilah"); - assertThat(pets.get("FISH")).extracting(Pet::getName).containsExactlyInAnyOrder("Nemo"); + assertThat(pets).containsOnlyKeys(TypeOfPet.CATS, TypeOfPet.FISH); + assertThat(pets.get(TypeOfPet.CATS)).extracting(Pet::getName) + .containsExactlyInAnyOrder("Tom", "Garfield", "Delilah"); + assertThat(pets.get(TypeOfPet.FISH)).extracting(Pet::getName).containsExactlyInAnyOrder("Nemo"); } @Test void shouldWriteDynamicRelationships(@Autowired PersonWithRelativesRepository repository) { PersonWithRelatives newPerson = new PersonWithRelatives("Test"); - Map relatives = newPerson.getRelatives(); + Map relatives = newPerson.getRelatives(); Person d = new Person(); ReflectionTestUtils.setField(d, "firstName", "R1"); - relatives.put("RELATIVE_1", d); + relatives.put(TypeOfRelative.RELATIVE_1, d); d = new Person(); ReflectionTestUtils.setField(d, "firstName", "R2"); - relatives.put("RELATIVE_2", d); + relatives.put(TypeOfRelative.RELATIVE_2, d); newPerson = repository.save(newPerson); relatives = newPerson.getRelatives(); - assertThat(relatives).containsOnlyKeys("RELATIVE_1", "RELATIVE_2"); + assertThat(relatives).containsOnlyKeys(TypeOfRelative.RELATIVE_1, TypeOfRelative.RELATIVE_2); try (Transaction transaction = driver.session().beginTransaction()) { long numberOfRelations = transaction.run("" - + "MATCH (t:PersonWithRelatives) WHERE id(t) = $id " + + "MATCH (t:" + labelOfTestSubject + ") WHERE id(t) = $id " + "RETURN size((t)-->(:Person))" + " as numberOfRelations", Values.parameters("id", newPerson.getId())) .single().get("numberOfRelations").asLong(); @@ -157,22 +158,22 @@ class DynamicRelationshipsIT extends DynamicRelationshipsITBase { void shouldWriteDynamicCollectionRelationships(@Autowired PersonWithRelativesRepository repository) { PersonWithRelatives newPerson = new PersonWithRelatives("Test"); - Map> pets = newPerson.getPets(); + Map> pets = newPerson.getPets(); - List monsters = pets.computeIfAbsent("MONSTERS", s -> new ArrayList<>()); + List monsters = pets.computeIfAbsent(TypeOfPet.MONSTERS, s -> new ArrayList<>()); monsters.add(new Pet("Godzilla")); monsters.add(new Pet("King Kong")); - List fish = pets.computeIfAbsent("FISH", s -> new ArrayList<>()); + List fish = pets.computeIfAbsent(TypeOfPet.FISH, s -> new ArrayList<>()); fish.add(new Pet("Nemo")); newPerson = repository.save(newPerson); pets = newPerson.getPets(); - assertThat(pets).containsOnlyKeys("MONSTERS", "FISH"); + assertThat(pets).containsOnlyKeys(TypeOfPet.MONSTERS, TypeOfPet.FISH); try (Transaction transaction = driver.session().beginTransaction()) { long numberOfRelations = transaction.run("" - + "MATCH (t:PersonWithRelatives) WHERE id(t) = $id " + + "MATCH (t:" + labelOfTestSubject + ") WHERE id(t) = $id " + "RETURN size((t)-->(:Pet))" + " as numberOfRelations", Values.parameters("id", newPerson.getId())) .single().get("numberOfRelations").asLong(); @@ -192,6 +193,5 @@ class DynamicRelationshipsIT extends DynamicRelationshipsITBase { public Driver driver() { return neo4jConnectionSupport.getDriver(); } - } } diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/StringlyTypedDynamicRelationshipsIT.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/StringlyTypedDynamicRelationshipsIT.java new file mode 100644 index 000000000..d91dc9384 --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/StringlyTypedDynamicRelationshipsIT.java @@ -0,0 +1,196 @@ +/* + * Copyright (c) 2019-2020 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * 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.neo4j.springframework.data.integration.imperative; + +import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.Assumptions.*; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.neo4j.driver.Driver; +import org.neo4j.driver.Transaction; +import org.neo4j.driver.Values; +import org.neo4j.springframework.data.config.AbstractNeo4jConfig; +import org.neo4j.springframework.data.integration.shared.DynamicRelationshipsITBase; +import org.neo4j.springframework.data.integration.shared.Person; +import org.neo4j.springframework.data.integration.shared.PersonWithStringlyTypedRelatives; +import org.neo4j.springframework.data.integration.shared.Pet; +import org.neo4j.springframework.data.repository.config.EnableNeo4jRepositories; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.repository.CrudRepository; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +/** + * @author Michael J. Simons + */ +class StringlyTypedDynamicRelationshipsIT extends DynamicRelationshipsITBase { + + @Autowired + StringlyTypedDynamicRelationshipsIT(Driver driver) { + super(driver); + } + + @Test + void shouldReadDynamicRelationships(@Autowired PersonWithRelativesRepository repository) { + + PersonWithStringlyTypedRelatives person = repository.findById(idOfExistingPerson).get(); + assertThat(person).isNotNull(); + assertThat(person.getName()).isEqualTo("A"); + + Map relatives = person.getRelatives(); + assertThat(relatives).containsOnlyKeys("HAS_WIFE", "HAS_DAUGHTER"); + assertThat(relatives.get("HAS_WIFE").getFirstName()).isEqualTo("B"); + assertThat(relatives.get("HAS_DAUGHTER").getFirstName()).isEqualTo("C"); + } + + @Test // GH-216 + void shouldReadDynamicCollectionRelationships(@Autowired PersonWithRelativesRepository repository) { + + PersonWithStringlyTypedRelatives person = repository.findById(idOfExistingPerson).get(); + assertThat(person).isNotNull(); + assertThat(person.getName()).isEqualTo("A"); + + Map> pets = person.getPets(); + assertThat(pets).containsOnlyKeys("CATS", "DOGS"); + assertThat(pets.get("CATS")).extracting(Pet::getName).containsExactlyInAnyOrder("Tom", "Garfield"); + assertThat(pets.get("DOGS")).extracting(Pet::getName).containsExactlyInAnyOrder("Benji", "Lassie"); + } + + @Test + void shouldUpdateDynamicRelationships(@Autowired PersonWithRelativesRepository repository) { + + PersonWithStringlyTypedRelatives person = repository.findById(idOfExistingPerson).get(); + assumeThat(person).isNotNull(); + assumeThat(person.getName()).isEqualTo("A"); + + Map relatives = person.getRelatives(); + assumeThat(relatives).containsOnlyKeys("HAS_WIFE", "HAS_DAUGHTER"); + + relatives.remove("HAS_WIFE"); + Person d = new Person(); + ReflectionTestUtils.setField(d, "firstName", "D"); + relatives.put("HAS_SON", d); + ReflectionTestUtils.setField(relatives.get("HAS_DAUGHTER"), "firstName", "C2"); + + person = repository.save(person); + relatives = person.getRelatives(); + assertThat(relatives).containsOnlyKeys("HAS_DAUGHTER", "HAS_SON"); + assertThat(relatives.get("HAS_DAUGHTER").getFirstName()).isEqualTo("C2"); + assertThat(relatives.get("HAS_SON").getFirstName()).isEqualTo("D"); + } + + @Test // GH-216 + void shouldUpdateDynamicCollectionRelationships(@Autowired PersonWithRelativesRepository repository) { + + PersonWithStringlyTypedRelatives person = repository.findById(idOfExistingPerson).get(); + assertThat(person).isNotNull(); + assertThat(person.getName()).isEqualTo("A"); + + Map> pets = person.getPets(); + assertThat(pets).containsOnlyKeys("CATS", "DOGS"); + + pets.remove("DOGS"); + pets.get("CATS").add(new Pet("Delilah")); + + pets.put("FISH", Collections.singletonList(new Pet("Nemo"))); + + person = repository.save(person); + pets = person.getPets(); + assertThat(pets).containsOnlyKeys("CATS", "FISH"); + assertThat(pets.get("CATS")).extracting(Pet::getName).containsExactlyInAnyOrder("Tom", "Garfield", "Delilah"); + assertThat(pets.get("FISH")).extracting(Pet::getName).containsExactlyInAnyOrder("Nemo"); + } + + @Test + void shouldWriteDynamicRelationships(@Autowired PersonWithRelativesRepository repository) { + + PersonWithStringlyTypedRelatives newPerson = new PersonWithStringlyTypedRelatives("Test"); + Map relatives = newPerson.getRelatives(); + + Person d = new Person(); + ReflectionTestUtils.setField(d, "firstName", "R1"); + relatives.put("RELATIVE_1", d); + + d = new Person(); + ReflectionTestUtils.setField(d, "firstName", "R2"); + relatives.put("RELATIVE_2", d); + + newPerson = repository.save(newPerson); + relatives = newPerson.getRelatives(); + assertThat(relatives).containsOnlyKeys("RELATIVE_1", "RELATIVE_2"); + + try (Transaction transaction = driver.session().beginTransaction()) { + long numberOfRelations = transaction.run("" + + "MATCH (t:" + labelOfTestSubject + ") WHERE id(t) = $id " + + "RETURN size((t)-->(:Person))" + + " as numberOfRelations", Values.parameters("id", newPerson.getId())) + .single().get("numberOfRelations").asLong(); + assertThat(numberOfRelations).isEqualTo(2L); + } + } + + @Test // GH-216 + void shouldWriteDynamicCollectionRelationships(@Autowired PersonWithRelativesRepository repository) { + + PersonWithStringlyTypedRelatives newPerson = new PersonWithStringlyTypedRelatives("Test"); + Map> pets = newPerson.getPets(); + + List monsters = pets.computeIfAbsent("MONSTERS", s -> new ArrayList<>()); + monsters.add(new Pet("Godzilla")); + monsters.add(new Pet("King Kong")); + + List fish = pets.computeIfAbsent("FISH", s -> new ArrayList<>()); + fish.add(new Pet("Nemo")); + + newPerson = repository.save(newPerson); + pets = newPerson.getPets(); + assertThat(pets).containsOnlyKeys("MONSTERS", "FISH"); + + try (Transaction transaction = driver.session().beginTransaction()) { + long numberOfRelations = transaction.run("" + + "MATCH (t:" + labelOfTestSubject + ") WHERE id(t) = $id " + + "RETURN size((t)-->(:Pet))" + + " as numberOfRelations", Values.parameters("id", newPerson.getId())) + .single().get("numberOfRelations").asLong(); + assertThat(numberOfRelations).isEqualTo(3L); + } + } + + interface PersonWithRelativesRepository extends CrudRepository { + } + + @Configuration + @EnableTransactionManagement + @EnableNeo4jRepositories(considerNestedRepositories = true) + static class Config extends AbstractNeo4jConfig { + + @Bean + public Driver driver() { + return neo4jConnectionSupport.getDriver(); + } + + } +} diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveDynamicRelationshipsIT.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveDynamicRelationshipsIT.java index 0936aa0a4..a50f79652 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveDynamicRelationshipsIT.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveDynamicRelationshipsIT.java @@ -38,6 +38,8 @@ import org.neo4j.springframework.data.config.AbstractReactiveNeo4jConfig; import org.neo4j.springframework.data.integration.shared.DynamicRelationshipsITBase; import org.neo4j.springframework.data.integration.shared.Person; import org.neo4j.springframework.data.integration.shared.PersonWithRelatives; +import org.neo4j.springframework.data.integration.shared.PersonWithRelatives.TypeOfPet; +import org.neo4j.springframework.data.integration.shared.PersonWithRelatives.TypeOfRelative; import org.neo4j.springframework.data.integration.shared.Pet; import org.neo4j.springframework.data.repository.ReactiveNeo4jRepository; import org.neo4j.springframework.data.repository.config.EnableReactiveNeo4jRepositories; @@ -51,7 +53,7 @@ import org.springframework.transaction.annotation.EnableTransactionManagement; * @author Michael J. Simons */ @Tag(NEEDS_REACTIVE_SUPPORT) -class ReactiveDynamicRelationshipsIT extends DynamicRelationshipsITBase { +class ReactiveDynamicRelationshipsIT extends DynamicRelationshipsITBase { @Autowired ReactiveDynamicRelationshipsIT(Driver driver) { super(driver); @@ -66,10 +68,10 @@ class ReactiveDynamicRelationshipsIT extends DynamicRelationshipsITBase { assertThat(person).isNotNull(); assertThat(person.getName()).isEqualTo("A"); - Map relatives = person.getRelatives(); - assertThat(relatives).containsOnlyKeys("HAS_WIFE", "HAS_DAUGHTER"); - assertThat(relatives.get("HAS_WIFE").getFirstName()).isEqualTo("B"); - assertThat(relatives.get("HAS_DAUGHTER").getFirstName()).isEqualTo("C"); + Map relatives = person.getRelatives(); + assertThat(relatives).containsOnlyKeys(TypeOfRelative.HAS_WIFE, TypeOfRelative.HAS_DAUGHTER); + assertThat(relatives.get(TypeOfRelative.HAS_WIFE).getFirstName()).isEqualTo("B"); + assertThat(relatives.get(TypeOfRelative.HAS_DAUGHTER).getFirstName()).isEqualTo("C"); }) .verifyComplete(); } @@ -83,10 +85,10 @@ class ReactiveDynamicRelationshipsIT extends DynamicRelationshipsITBase { assertThat(person).isNotNull(); assertThat(person.getName()).isEqualTo("A"); - Map> pets = person.getPets(); - assertThat(pets).containsOnlyKeys("CATS", "DOGS"); - assertThat(pets.get("CATS")).extracting(Pet::getName).containsExactlyInAnyOrder("Tom", "Garfield"); - assertThat(pets.get("DOGS")).extracting(Pet::getName).containsExactlyInAnyOrder("Benji", "Lassie"); + Map> pets = person.getPets(); + assertThat(pets).containsOnlyKeys(TypeOfPet.CATS, TypeOfPet.DOGS); + assertThat(pets.get(TypeOfPet.CATS)).extracting(Pet::getName).containsExactlyInAnyOrder("Tom", "Garfield"); + assertThat(pets.get(TypeOfPet.DOGS)).extracting(Pet::getName).containsExactlyInAnyOrder("Benji", "Lassie"); }) .verifyComplete(); } @@ -99,23 +101,23 @@ class ReactiveDynamicRelationshipsIT extends DynamicRelationshipsITBase { assumeThat(person).isNotNull(); assumeThat(person.getName()).isEqualTo("A"); - Map relatives = person.getRelatives(); - assumeThat(relatives).containsOnlyKeys("HAS_WIFE", "HAS_DAUGHTER"); + Map relatives = person.getRelatives(); + assumeThat(relatives).containsOnlyKeys(TypeOfRelative.HAS_WIFE, TypeOfRelative.HAS_DAUGHTER); - relatives.remove("HAS_WIFE"); + relatives.remove(TypeOfRelative.HAS_WIFE); Person d = new Person(); ReflectionTestUtils.setField(d, "firstName", "D"); - relatives.put("HAS_SON", d); - ReflectionTestUtils.setField(relatives.get("HAS_DAUGHTER"), "firstName", "C2"); + relatives.put(TypeOfRelative.HAS_SON, d); + ReflectionTestUtils.setField(relatives.get(TypeOfRelative.HAS_DAUGHTER), "firstName", "C2"); return person; }) .flatMap(repository::save) .as(StepVerifier::create) .consumeNextWith(person -> { - Map relatives = person.getRelatives(); - assertThat(relatives).containsOnlyKeys("HAS_DAUGHTER", "HAS_SON"); - assertThat(relatives.get("HAS_DAUGHTER").getFirstName()).isEqualTo("C2"); - assertThat(relatives.get("HAS_SON").getFirstName()).isEqualTo("D"); + Map relatives = person.getRelatives(); + assertThat(relatives).containsOnlyKeys(TypeOfRelative.HAS_DAUGHTER, TypeOfRelative.HAS_SON); + assertThat(relatives.get(TypeOfRelative.HAS_DAUGHTER).getFirstName()).isEqualTo("C2"); + assertThat(relatives.get(TypeOfRelative.HAS_SON).getFirstName()).isEqualTo("D"); }) .verifyComplete(); } @@ -128,23 +130,23 @@ class ReactiveDynamicRelationshipsIT extends DynamicRelationshipsITBase { assumeThat(person).isNotNull(); assumeThat(person.getName()).isEqualTo("A"); - Map> pets = person.getPets(); - assertThat(pets).containsOnlyKeys("CATS", "DOGS"); + Map> pets = person.getPets(); + assertThat(pets).containsOnlyKeys(TypeOfPet.CATS, TypeOfPet.DOGS); - pets.remove("DOGS"); - pets.get("CATS").add(new Pet("Delilah")); + pets.remove(TypeOfPet.DOGS); + pets.get(TypeOfPet.CATS).add(new Pet("Delilah")); - pets.put("FISH", Collections.singletonList(new Pet("Nemo"))); + pets.put(TypeOfPet.FISH, Collections.singletonList(new Pet("Nemo"))); return person; }) .flatMap(repository::save) .as(StepVerifier::create) .consumeNextWith(person -> { - Map> pets = person.getPets(); - assertThat(pets).containsOnlyKeys("CATS", "FISH"); - assertThat(pets.get("CATS")).extracting(Pet::getName).containsExactlyInAnyOrder("Tom", "Garfield", "Delilah"); - assertThat(pets.get("FISH")).extracting(Pet::getName).containsExactlyInAnyOrder("Nemo"); + Map> pets = person.getPets(); + assertThat(pets).containsOnlyKeys(TypeOfPet.CATS, TypeOfPet.FISH); + assertThat(pets.get(TypeOfPet.CATS)).extracting(Pet::getName).containsExactlyInAnyOrder("Tom", "Garfield", "Delilah"); + assertThat(pets.get(TypeOfPet.FISH)).extracting(Pet::getName).containsExactlyInAnyOrder("Nemo"); }) .verifyComplete(); } @@ -155,24 +157,24 @@ class ReactiveDynamicRelationshipsIT extends DynamicRelationshipsITBase { PersonWithRelatives newPerson = new PersonWithRelatives("Test"); Person d = new Person(); ReflectionTestUtils.setField(d, "firstName", "R1"); - newPerson.getRelatives().put("RELATIVE_1", d); + newPerson.getRelatives().put(TypeOfRelative.RELATIVE_1, d); d = new Person(); ReflectionTestUtils.setField(d, "firstName", "R2"); - newPerson.getRelatives().put("RELATIVE_2", d); + newPerson.getRelatives().put(TypeOfRelative.RELATIVE_2, d); List recorded = new ArrayList<>(); repository.save(newPerson) .as(StepVerifier::create) .recordWith(() -> recorded) .consumeNextWith(personWithRelatives -> { - Map relatives = personWithRelatives.getRelatives(); - assertThat(relatives).containsOnlyKeys("RELATIVE_1", "RELATIVE_2"); + Map relatives = personWithRelatives.getRelatives(); + assertThat(relatives).containsOnlyKeys(TypeOfRelative.RELATIVE_1, TypeOfRelative.RELATIVE_2); }) .verifyComplete(); try (Transaction transaction = driver.session().beginTransaction()) { long numberOfRelations = transaction.run("" - + "MATCH (t:PersonWithRelatives) WHERE id(t) = $id " + + "MATCH (t:" + labelOfTestSubject + ") WHERE id(t) = $id " + "RETURN size((t)-->(:Person))" + " as numberOfRelations", Values.parameters("id", recorded.get(0).getId())) @@ -185,13 +187,13 @@ class ReactiveDynamicRelationshipsIT extends DynamicRelationshipsITBase { void shouldWriteDynamicCollectionRelationships(@Autowired PersonWithRelativesRepository repository) { PersonWithRelatives newPerson = new PersonWithRelatives("Test"); - Map> pets = newPerson.getPets(); + Map> pets = newPerson.getPets(); - List monsters = pets.computeIfAbsent("MONSTERS", s -> new ArrayList<>()); + List monsters = pets.computeIfAbsent(TypeOfPet.MONSTERS, s -> new ArrayList<>()); monsters.add(new Pet("Godzilla")); monsters.add(new Pet("King Kong")); - List fish = pets.computeIfAbsent("FISH", s -> new ArrayList<>()); + List fish = pets.computeIfAbsent(TypeOfPet.FISH, s -> new ArrayList<>()); fish.add(new Pet("Nemo")); List recorded = new ArrayList<>(); @@ -199,14 +201,14 @@ class ReactiveDynamicRelationshipsIT extends DynamicRelationshipsITBase { .as(StepVerifier::create) .recordWith(() -> recorded) .consumeNextWith(person -> { - Map> writtenPets = person.getPets(); - assertThat(writtenPets).containsOnlyKeys("MONSTERS", "FISH"); + Map> writtenPets = person.getPets(); + assertThat(writtenPets).containsOnlyKeys(TypeOfPet.MONSTERS, TypeOfPet.FISH); }) .verifyComplete(); try (Transaction transaction = driver.session().beginTransaction()) { long numberOfRelations = transaction.run("" - + "MATCH (t:PersonWithRelatives) WHERE id(t) = $id " + + "MATCH (t:" + labelOfTestSubject + ") WHERE id(t) = $id " + "RETURN size((t)-->(:Pet))" + " as numberOfRelations", Values.parameters("id", recorded.get(0).getId())) .single().get("numberOfRelations").asLong(); diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveStringlyTypeDynamicRelationshipsIT.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveStringlyTypeDynamicRelationshipsIT.java new file mode 100644 index 000000000..35125518c --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveStringlyTypeDynamicRelationshipsIT.java @@ -0,0 +1,231 @@ +/* + * Copyright (c) 2019-2020 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * 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.neo4j.springframework.data.integration.reactive; + +import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.Assumptions.*; +import static org.neo4j.springframework.data.test.Neo4jExtension.*; + +import reactor.test.StepVerifier; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.neo4j.driver.Driver; +import org.neo4j.driver.Transaction; +import org.neo4j.driver.Values; +import org.neo4j.springframework.data.config.AbstractReactiveNeo4jConfig; +import org.neo4j.springframework.data.integration.shared.DynamicRelationshipsITBase; +import org.neo4j.springframework.data.integration.shared.Person; +import org.neo4j.springframework.data.integration.shared.PersonWithStringlyTypedRelatives; +import org.neo4j.springframework.data.integration.shared.Pet; +import org.neo4j.springframework.data.repository.ReactiveNeo4jRepository; +import org.neo4j.springframework.data.repository.config.EnableReactiveNeo4jRepositories; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +/** + * @author Michael J. Simons + */ +@Tag(NEEDS_REACTIVE_SUPPORT) +class ReactiveStringlyTypeDynamicRelationshipsIT extends DynamicRelationshipsITBase { + + @Autowired ReactiveStringlyTypeDynamicRelationshipsIT(Driver driver) { + super(driver); + } + + @Test + void shouldReadDynamicRelationships(@Autowired PersonWithRelativesRepository repository) { + + repository.findById(idOfExistingPerson) + .as(StepVerifier::create) + .consumeNextWith(person -> { + assertThat(person).isNotNull(); + assertThat(person.getName()).isEqualTo("A"); + + Map relatives = person.getRelatives(); + assertThat(relatives).containsOnlyKeys("HAS_WIFE", "HAS_DAUGHTER"); + assertThat(relatives.get("HAS_WIFE").getFirstName()).isEqualTo("B"); + assertThat(relatives.get("HAS_DAUGHTER").getFirstName()).isEqualTo("C"); + }) + .verifyComplete(); + } + + @Test // GH-216 + void shouldReadDynamicCollectionRelationships(@Autowired PersonWithRelativesRepository repository) { + + repository.findById(idOfExistingPerson) + .as(StepVerifier::create) + .consumeNextWith(person -> { + assertThat(person).isNotNull(); + assertThat(person.getName()).isEqualTo("A"); + + Map> pets = person.getPets(); + assertThat(pets).containsOnlyKeys("CATS", "DOGS"); + assertThat(pets.get("CATS")).extracting(Pet::getName).containsExactlyInAnyOrder("Tom", "Garfield"); + assertThat(pets.get("DOGS")).extracting(Pet::getName).containsExactlyInAnyOrder("Benji", "Lassie"); + }) + .verifyComplete(); + } + + @Test + void shouldUpdateDynamicRelationships(@Autowired PersonWithRelativesRepository repository) { + + repository.findById(idOfExistingPerson) + .map(person -> { + assumeThat(person).isNotNull(); + assumeThat(person.getName()).isEqualTo("A"); + + Map relatives = person.getRelatives(); + assumeThat(relatives).containsOnlyKeys("HAS_WIFE", "HAS_DAUGHTER"); + + relatives.remove("HAS_WIFE"); + Person d = new Person(); + ReflectionTestUtils.setField(d, "firstName", "D"); + relatives.put("HAS_SON", d); + ReflectionTestUtils.setField(relatives.get("HAS_DAUGHTER"), "firstName", "C2"); + return person; + }) + .flatMap(repository::save) + .as(StepVerifier::create) + .consumeNextWith(person -> { + Map relatives = person.getRelatives(); + assertThat(relatives).containsOnlyKeys("HAS_DAUGHTER", "HAS_SON"); + assertThat(relatives.get("HAS_DAUGHTER").getFirstName()).isEqualTo("C2"); + assertThat(relatives.get("HAS_SON").getFirstName()).isEqualTo("D"); + }) + .verifyComplete(); + } + + @Test // GH-216 + void shouldUpdateDynamicCollectionRelationships(@Autowired PersonWithRelativesRepository repository) { + + repository.findById(idOfExistingPerson) + .map(person -> { + assumeThat(person).isNotNull(); + assumeThat(person.getName()).isEqualTo("A"); + + Map> pets = person.getPets(); + assertThat(pets).containsOnlyKeys("CATS", "DOGS"); + + pets.remove("DOGS"); + pets.get("CATS").add(new Pet("Delilah")); + + pets.put("FISH", Collections.singletonList(new Pet("Nemo"))); + + return person; + }) + .flatMap(repository::save) + .as(StepVerifier::create) + .consumeNextWith(person -> { + Map> pets = person.getPets(); + assertThat(pets).containsOnlyKeys("CATS", "FISH"); + assertThat(pets.get("CATS")).extracting(Pet::getName).containsExactlyInAnyOrder("Tom", "Garfield", "Delilah"); + assertThat(pets.get("FISH")).extracting(Pet::getName).containsExactlyInAnyOrder("Nemo"); + }) + .verifyComplete(); + } + + @Test + void shouldWriteDynamicRelationships(@Autowired PersonWithRelativesRepository repository) { + + PersonWithStringlyTypedRelatives newPerson = new PersonWithStringlyTypedRelatives("Test"); + Person d = new Person(); + ReflectionTestUtils.setField(d, "firstName", "R1"); + newPerson.getRelatives().put("RELATIVE_1", d); + d = new Person(); + ReflectionTestUtils.setField(d, "firstName", "R2"); + newPerson.getRelatives().put("RELATIVE_2", d); + + List recorded = new ArrayList<>(); + repository.save(newPerson) + .as(StepVerifier::create) + .recordWith(() -> recorded) + .consumeNextWith(personWithRelatives -> { + Map relatives = personWithRelatives.getRelatives(); + assertThat(relatives).containsOnlyKeys("RELATIVE_1", "RELATIVE_2"); + }) + .verifyComplete(); + + try (Transaction transaction = driver.session().beginTransaction()) { + long numberOfRelations = transaction.run("" + + "MATCH (t:" + labelOfTestSubject + ") WHERE id(t) = $id " + + "RETURN size((t)-->(:Person))" + + " as numberOfRelations", + Values.parameters("id", recorded.get(0).getId())) + .single().get("numberOfRelations").asLong(); + assertThat(numberOfRelations).isEqualTo(2L); + } + } + + @Test // GH-216 + void shouldWriteDynamicCollectionRelationships(@Autowired PersonWithRelativesRepository repository) { + + PersonWithStringlyTypedRelatives newPerson = new PersonWithStringlyTypedRelatives("Test"); + Map> pets = newPerson.getPets(); + + List monsters = pets.computeIfAbsent("MONSTERS", s -> new ArrayList<>()); + monsters.add(new Pet("Godzilla")); + monsters.add(new Pet("King Kong")); + + List fish = pets.computeIfAbsent("FISH", s -> new ArrayList<>()); + fish.add(new Pet("Nemo")); + + List recorded = new ArrayList<>(); + repository.save(newPerson) + .as(StepVerifier::create) + .recordWith(() -> recorded) + .consumeNextWith(person -> { + Map> writtenPets = person.getPets(); + assertThat(writtenPets).containsOnlyKeys("MONSTERS", "FISH"); + }) + .verifyComplete(); + + try (Transaction transaction = driver.session().beginTransaction()) { + long numberOfRelations = transaction.run("" + + "MATCH (t:" + labelOfTestSubject + ") WHERE id(t) = $id " + + "RETURN size((t)-->(:Pet))" + + " as numberOfRelations", Values.parameters("id", recorded.get(0).getId())) + .single().get("numberOfRelations").asLong(); + assertThat(numberOfRelations).isEqualTo(3L); + } + } + + interface PersonWithRelativesRepository extends ReactiveNeo4jRepository { + } + + @Configuration + @EnableTransactionManagement + @EnableReactiveNeo4jRepositories(considerNestedRepositories = true) + static class Config extends AbstractReactiveNeo4jConfig { + + @Bean + public Driver driver() { + return neo4jConnectionSupport.getDriver(); + } + + } +} diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/DynamicRelationshipsITBase.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/DynamicRelationshipsITBase.java index 7a3274536..b84ef411f 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/DynamicRelationshipsITBase.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/DynamicRelationshipsITBase.java @@ -18,6 +18,9 @@ */ package org.neo4j.springframework.data.integration.shared; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + import org.junit.jupiter.api.BeforeEach; import org.neo4j.driver.Driver; import org.neo4j.driver.Session; @@ -28,11 +31,12 @@ import org.neo4j.springframework.data.test.Neo4jIntegrationTest; /** * Make sure that dynamic relationships can be loaded and stored. * + * @param Type of the person with relatives * @author Michael J. Simons * @soundtrack Helge Schneider - Live At The Grugahalle */ @Neo4jIntegrationTest -public abstract class DynamicRelationshipsITBase { +public abstract class DynamicRelationshipsITBase { protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; @@ -40,8 +44,13 @@ public abstract class DynamicRelationshipsITBase { protected long idOfExistingPerson; + protected final String labelOfTestSubject; + protected DynamicRelationshipsITBase(Driver driver) { this.driver = driver; + Type type = getClass().getGenericSuperclass(); + String typeName = ((ParameterizedType) type).getActualTypeArguments()[0].getTypeName(); + this.labelOfTestSubject = typeName.substring(typeName.lastIndexOf(".") + 1); } @BeforeEach @@ -52,7 +61,7 @@ public abstract class DynamicRelationshipsITBase { ) { transaction.run("MATCH (n) detach delete n"); idOfExistingPerson = transaction.run("" - + "CREATE (t:PersonWithRelatives {name: 'A'}) WITH t " + + "CREATE (t:" + labelOfTestSubject + " {name: 'A'}) WITH t " + "CREATE (t) - [:HAS_WIFE] -> (w:Person {firstName: 'B'}) " + "CREATE (t) - [:HAS_DAUGHTER] -> (d:Person {firstName: 'C'}) " + "WITH t " diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/PersonWithRelatives.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/PersonWithRelatives.java index 58000d95f..0c27c7367 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/PersonWithRelatives.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/PersonWithRelatives.java @@ -32,14 +32,28 @@ import org.neo4j.springframework.data.core.schema.Node; @Node public class PersonWithRelatives { + /** + * Some enum representing relatives. + */ + public enum TypeOfRelative { + HAS_WIFE, HAS_DAUGHTER, HAS_SON, RELATIVE_1, RELATIVE_2 + } + + /** + * Some enum representing pets. + */ + public enum TypeOfPet { + CATS, DOGS, FISH, MONSTERS + } + @Id @GeneratedValue private Long id; private final String name; - private Map relatives = new HashMap<>(); + private Map relatives = new HashMap<>(); - private Map> pets = new HashMap<>(); + private Map> pets = new HashMap<>(); public PersonWithRelatives(String name) { this.name = name; @@ -53,11 +67,11 @@ public class PersonWithRelatives { return name; } - public Map getRelatives() { + public Map getRelatives() { return relatives; } - public Map> getPets() { + public Map> getPets() { return pets; } } diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/PersonWithStringlyTypedRelatives.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/PersonWithStringlyTypedRelatives.java new file mode 100644 index 000000000..4e2b31de9 --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/PersonWithStringlyTypedRelatives.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2019-2020 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * 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.neo4j.springframework.data.integration.shared; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.neo4j.springframework.data.core.schema.GeneratedValue; +import org.neo4j.springframework.data.core.schema.Id; +import org.neo4j.springframework.data.core.schema.Node; + +/** + * @author Michael J. Simons + */ +@Node +public class PersonWithStringlyTypedRelatives { + + @Id @GeneratedValue + private Long id; + + private final String name; + + private Map relatives = new HashMap<>(); + + private Map> pets = new HashMap<>(); + + public PersonWithStringlyTypedRelatives(String name) { + this.name = name; + } + + public long getId() { + return id; + } + + public String getName() { + return name; + } + + public Map getRelatives() { + return relatives; + } + + public Map> getPets() { + return pets; + } +}