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.
This commit is contained in:
Michael Simons
2020-06-05 12:06:52 +02:00
committed by GitHub
parent ff052d6297
commit ca3e14e1c6
13 changed files with 713 additions and 132 deletions

View File

@@ -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)

View File

@@ -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<ResultSummary> relationshipCreationMonoNested = neo4jClient

View File

@@ -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<String, Object> properties;
private RelationshipStatementHolder(@NonNull Statement relationshipCreationQuery) {
this.relationshipCreationQuery = relationshipCreationQuery;
this.properties = Collections.emptyMap();
this(relationshipCreationQuery, Collections.emptyMap());
}
private RelationshipStatementHolder(
@NonNull Statement relationshipCreationQuery,
@NonNull Map<String, Object> properties) {
@NonNull Map<String, Object> 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<String, ?>) relatedValue).getKey() : null,
relationshipType,
relatedInternalId);
return new RelationshipStatementHolder(relationshipCreationQuery);
}

View File

@@ -263,12 +263,15 @@ final class DefaultNeo4jConverter implements Neo4jConverter {
KnownObjects knownObjects) {
List<String> allLabels = getLabels(queryResult);
NodeDescriptionAndLabels nodeDescriptionAndLabels = nodeDescriptionStore.deriveConcreteNodeDescription(nodeDescription, allLabels);
Neo4jPersistentEntity<ET> concreteNodeDescription = (Neo4jPersistentEntity<ET>) nodeDescriptionAndLabels.getNodeDescription();
NodeDescriptionAndLabels nodeDescriptionAndLabels = nodeDescriptionStore
.deriveConcreteNodeDescription(nodeDescription, allLabels);
Neo4jPersistentEntity<ET> concreteNodeDescription = (Neo4jPersistentEntity<ET>) nodeDescriptionAndLabels
.getNodeDescription();
Collection<RelationshipDescription> relationships = concreteNodeDescription.getRelationships();
ET instance = instantiate(concreteNodeDescription, queryResult, knownObjects, relationships, nodeDescriptionAndLabels.getDynamicLabels());
ET instance = instantiate(concreteNodeDescription, queryResult, knownObjects, relationships,
nodeDescriptionAndLabels.getDynamicLabels());
PersistentPropertyAccessor<ET> 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<Neo4jPersistentProperty> 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<String> 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<Object> value = new ArrayList<>();
Map<String, Object> dynamicValue = new HashMap<>();
Map<Object, Object> dynamicValue = new HashMap<>();
BiConsumer<String, Object> mappedObjectHandler;
Function<String, ?> 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<Object> bucket = (List<Object>) dynamicValue.computeIfAbsent(type,
List<Object> bucket = (List<Object>) 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);
}

View File

@@ -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<Neo4jPersistentProperty>, 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());
}
/**

View File

@@ -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<Neo4jPersistentProperty> 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<Neo4jPersistentProperty> associations = new ArrayList<>();
enumRelNodeEntity
.doWithAssociations((Association<Neo4jPersistentProperty> a) -> associations.add(a.getInverse()));
assertThat(associations).hasSize(2);
}
static class DummyIdGenerator implements IdGenerator<Void> {
@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<String, Object> funnyDynamicProperties;
}
static class EnumRelNode {
@Id
private String id;
Map<A, UserNode> relA;
Map<ExtendedA, BikeNode> relEA;
}
static class TripNode {
@Id

View File

@@ -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<PersonWithRelatives> {
@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<String, Person> 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<TypeOfRelative, Person> 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<String, List<Pet>> 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<TypeOfPet, List<Pet>> 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<String, Person> relatives = person.getRelatives();
assumeThat(relatives).containsOnlyKeys("HAS_WIFE", "HAS_DAUGHTER");
Map<TypeOfRelative, Person> 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<String, List<Pet>> pets = person.getPets();
assertThat(pets).containsOnlyKeys("CATS", "DOGS");
Map<TypeOfPet, List<Pet>> 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<String, Person> relatives = newPerson.getRelatives();
Map<TypeOfRelative, Person> 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<String, List<Pet>> pets = newPerson.getPets();
Map<TypeOfPet, List<Pet>> pets = newPerson.getPets();
List<Pet> monsters = pets.computeIfAbsent("MONSTERS", s -> new ArrayList<>());
List<Pet> monsters = pets.computeIfAbsent(TypeOfPet.MONSTERS, s -> new ArrayList<>());
monsters.add(new Pet("Godzilla"));
monsters.add(new Pet("King Kong"));
List<Pet> fish = pets.computeIfAbsent("FISH", s -> new ArrayList<>());
List<Pet> 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();
}
}
}

View File

@@ -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<PersonWithStringlyTypedRelatives> {
@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<String, Person> 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<String, List<Pet>> 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<String, Person> 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<String, List<Pet>> 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<String, Person> 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<String, List<Pet>> pets = newPerson.getPets();
List<Pet> monsters = pets.computeIfAbsent("MONSTERS", s -> new ArrayList<>());
monsters.add(new Pet("Godzilla"));
monsters.add(new Pet("King Kong"));
List<Pet> 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<PersonWithStringlyTypedRelatives, Long> {
}
@Configuration
@EnableTransactionManagement
@EnableNeo4jRepositories(considerNestedRepositories = true)
static class Config extends AbstractNeo4jConfig {
@Bean
public Driver driver() {
return neo4jConnectionSupport.getDriver();
}
}
}

View File

@@ -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<PersonWithRelatives> {
@Autowired ReactiveDynamicRelationshipsIT(Driver driver) {
super(driver);
@@ -66,10 +68,10 @@ class ReactiveDynamicRelationshipsIT extends DynamicRelationshipsITBase {
assertThat(person).isNotNull();
assertThat(person.getName()).isEqualTo("A");
Map<String, Person> 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<TypeOfRelative, Person> 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<String, List<Pet>> 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<TypeOfPet, List<Pet>> 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<String, Person> relatives = person.getRelatives();
assumeThat(relatives).containsOnlyKeys("HAS_WIFE", "HAS_DAUGHTER");
Map<TypeOfRelative, Person> 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<String, 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");
Map<TypeOfRelative, Person> 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<String, List<Pet>> pets = person.getPets();
assertThat(pets).containsOnlyKeys("CATS", "DOGS");
Map<TypeOfPet, List<Pet>> 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<String, List<Pet>> 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<TypeOfPet, List<Pet>> 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<PersonWithRelatives> recorded = new ArrayList<>();
repository.save(newPerson)
.as(StepVerifier::create)
.recordWith(() -> recorded)
.consumeNextWith(personWithRelatives -> {
Map<String, Person> relatives = personWithRelatives.getRelatives();
assertThat(relatives).containsOnlyKeys("RELATIVE_1", "RELATIVE_2");
Map<TypeOfRelative, Person> 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<String, List<Pet>> pets = newPerson.getPets();
Map<TypeOfPet, List<Pet>> pets = newPerson.getPets();
List<Pet> monsters = pets.computeIfAbsent("MONSTERS", s -> new ArrayList<>());
List<Pet> monsters = pets.computeIfAbsent(TypeOfPet.MONSTERS, s -> new ArrayList<>());
monsters.add(new Pet("Godzilla"));
monsters.add(new Pet("King Kong"));
List<Pet> fish = pets.computeIfAbsent("FISH", s -> new ArrayList<>());
List<Pet> fish = pets.computeIfAbsent(TypeOfPet.FISH, s -> new ArrayList<>());
fish.add(new Pet("Nemo"));
List<PersonWithRelatives> recorded = new ArrayList<>();
@@ -199,14 +201,14 @@ class ReactiveDynamicRelationshipsIT extends DynamicRelationshipsITBase {
.as(StepVerifier::create)
.recordWith(() -> recorded)
.consumeNextWith(person -> {
Map<String, List<Pet>> writtenPets = person.getPets();
assertThat(writtenPets).containsOnlyKeys("MONSTERS", "FISH");
Map<TypeOfPet, List<Pet>> 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();

View File

@@ -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<PersonWithStringlyTypedRelatives> {
@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<String, Person> 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<String, List<Pet>> 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<String, Person> 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<String, 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");
})
.verifyComplete();
}
@Test // GH-216
void shouldUpdateDynamicCollectionRelationships(@Autowired PersonWithRelativesRepository repository) {
repository.findById(idOfExistingPerson)
.map(person -> {
assumeThat(person).isNotNull();
assumeThat(person.getName()).isEqualTo("A");
Map<String, List<Pet>> 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<String, List<Pet>> 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<PersonWithStringlyTypedRelatives> recorded = new ArrayList<>();
repository.save(newPerson)
.as(StepVerifier::create)
.recordWith(() -> recorded)
.consumeNextWith(personWithRelatives -> {
Map<String, Person> 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<String, List<Pet>> pets = newPerson.getPets();
List<Pet> monsters = pets.computeIfAbsent("MONSTERS", s -> new ArrayList<>());
monsters.add(new Pet("Godzilla"));
monsters.add(new Pet("King Kong"));
List<Pet> fish = pets.computeIfAbsent("FISH", s -> new ArrayList<>());
fish.add(new Pet("Nemo"));
List<PersonWithStringlyTypedRelatives> recorded = new ArrayList<>();
repository.save(newPerson)
.as(StepVerifier::create)
.recordWith(() -> recorded)
.consumeNextWith(person -> {
Map<String, List<Pet>> 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<PersonWithStringlyTypedRelatives, Long> {
}
@Configuration
@EnableTransactionManagement
@EnableReactiveNeo4jRepositories(considerNestedRepositories = true)
static class Config extends AbstractReactiveNeo4jConfig {
@Bean
public Driver driver() {
return neo4jConnectionSupport.getDriver();
}
}
}

View File

@@ -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 <T> 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<T> {
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 "

View File

@@ -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<String, Person> relatives = new HashMap<>();
private Map<TypeOfRelative, Person> relatives = new HashMap<>();
private Map<String, List<Pet>> pets = new HashMap<>();
private Map<TypeOfPet, List<Pet>> pets = new HashMap<>();
public PersonWithRelatives(String name) {
this.name = name;
@@ -53,11 +67,11 @@ public class PersonWithRelatives {
return name;
}
public Map<String, Person> getRelatives() {
public Map<TypeOfRelative, Person> getRelatives() {
return relatives;
}
public Map<String, List<Pet>> getPets() {
public Map<TypeOfPet, List<Pet>> getPets() {
return pets;
}
}

View File

@@ -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<String, Person> relatives = new HashMap<>();
private Map<String, List<Pet>> pets = new HashMap<>();
public PersonWithStringlyTypedRelatives(String name) {
this.name = name;
}
public long getId() {
return id;
}
public String getName() {
return name;
}
public Map<String, Person> getRelatives() {
return relatives;
}
public Map<String, List<Pet>> getPets() {
return pets;
}
}