From ee655f8befd08130d3740425894ee63bbe43c35e Mon Sep 17 00:00:00 2001 From: Gerrit Meier Date: Tue, 10 Dec 2019 21:48:12 +0100 Subject: [PATCH] Add relationship properties mapping. Provides the functionality of defining relationship with additional property classes represented as a map structure. Also the mapping and binder functions for loading or saving data were condensed into central anonymous functions and shifted their mapping logic to the Neo4jConverter. Required behaviour changes in the existing code base: For setting the Driver bean the Neo4jMappingContext must not be an implementation of a BeanDefinitionRegistryPostProcessor because this would require setting up the driver bean very early in the start process that e.g. configuration parameters that also rely on PostProcessor logic cannot be processed before setting up the driver. --- .../data/core/Neo4jTemplate.java | 2 +- .../data/core/convert/Neo4jConverter.java | 40 +- .../data/core/cypher/MapExpression.java | 8 + .../data/core/cypher/MapProjection.java | 4 +- .../data/core/cypher/Relationship.java | 2 +- .../mapping/DefaultNeo4jBinderFunction.java | 68 ---- .../core/mapping/DefaultNeo4jConverter.java | 341 ++++++++++++++++-- .../mapping/DefaultNeo4jMappingFunction.java | 321 ----------------- .../DefaultNeo4jPersistentProperty.java | 21 +- .../DefaultRelationshipDescription.java | 15 +- .../core/mapping/Neo4jMappingContext.java | 149 ++------ .../core/mapping/NodeDescriptionStore.java | 78 ++++ .../data/core/schema/CypherGenerator.java | 12 +- .../core/schema/RelationshipDescription.java | 20 + .../core/schema/RelationshipProperties.java | 42 +++ .../data/core/schema/Schema.java | 34 +- .../repository/query/Neo4jQuerySupport.java | 8 +- .../data/repository/support/Predicate.java | 2 +- .../mapping/DefaultNeo4jConverterTest.java | 8 +- .../core/mapping/Neo4jMappingContextTest.java | 9 +- ...hRelationshipWithPropertiesRepository.java | 34 ++ .../integration/imperative/RepositoryIT.java | 112 +++++- ...hRelationshipWithPropertiesRepository.java | 36 ++ .../reactive/ReactiveRepositoryIT.java | 116 +++++- .../data/integration/shared/Hobby.java | 7 + .../shared/LikesHobbyRelationship.java | 95 +++++ .../PersonWithRelationshipWithProperties.java | 52 +++ .../shared/TestSequenceGenerator.java | 11 + 28 files changed, 1043 insertions(+), 604 deletions(-) delete mode 100644 spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jBinderFunction.java delete mode 100644 spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jMappingFunction.java create mode 100644 spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/core/mapping/NodeDescriptionStore.java create mode 100644 spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/core/schema/RelationshipProperties.java create mode 100644 spring-data-neo4j-rx/src/test/java/org/neo4j/springframework/data/integration/imperative/PersonWithRelationshipWithPropertiesRepository.java create mode 100644 spring-data-neo4j-rx/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactivePersonWithRelationshipWithPropertiesRepository.java create mode 100644 spring-data-neo4j-rx/src/test/java/org/neo4j/springframework/data/integration/shared/LikesHobbyRelationship.java create mode 100644 spring-data-neo4j-rx/src/test/java/org/neo4j/springframework/data/integration/shared/PersonWithRelationshipWithProperties.java diff --git a/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/core/Neo4jTemplate.java b/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/core/Neo4jTemplate.java index ef50d268..1cbd58da 100644 --- a/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/core/Neo4jTemplate.java +++ b/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/core/Neo4jTemplate.java @@ -303,7 +303,7 @@ private ExecutableQuery createExecutableQuery(Class domainType, Statem PreparedQuery preparedQuery = PreparedQuery.queryFor(domainType) .withCypherQuery(renderer.render(statement)) .withParameters(parameters) - .usingMappingFunction(this.neo4jMappingContext.getRequiredMappingFunctionFor(domainType)) + .usingMappingFunction(neo4jMappingContext.getRequiredMappingFunctionFor(domainType)) .build(); return toExecutableQuery(preparedQuery); } diff --git a/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/core/convert/Neo4jConverter.java b/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/core/convert/Neo4jConverter.java index a9c715cd..362c9a19 100644 --- a/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/core/convert/Neo4jConverter.java +++ b/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/core/convert/Neo4jConverter.java @@ -18,12 +18,13 @@ */ package org.neo4j.springframework.data.core.convert; +import java.util.Map; + +import org.neo4j.driver.Record; import org.neo4j.driver.Value; -import org.neo4j.driver.types.TypeSystem; import org.springframework.dao.TypeMismatchDataAccessException; -import org.springframework.data.mapping.PersistentProperty; -import org.springframework.data.mapping.PersistentPropertyAccessor; -import org.springframework.data.mapping.model.ParameterValueProvider; +import org.springframework.data.convert.EntityReader; +import org.springframework.data.convert.EntityWriter; import org.springframework.data.util.TypeInformation; import org.springframework.lang.Nullable; @@ -34,7 +35,7 @@ * @soundtrack The Kleptones - A Night At The Hip-Hopera * @since 1.0 */ -public interface Neo4jConverter { +public interface Neo4jConverter extends EntityReader, EntityWriter> { /** * Reads a {@link Value} returned by the driver and converts it into a {@link Neo4jSimpleTypes simple type} supported @@ -43,34 +44,21 @@ public interface Neo4jConverter { * the failed conversion. * * @param value The value to be read, may be null. - * @param type The type information describing the target type + * @param type The type information describing the target type. * @return A simple type or null, if the value was {@literal null} or {@link org.neo4j.driver.Values#NULL}. * @throws TypeMismatchDataAccessException In case the value cannot be converted to the target type */ @Nullable - Object readValue(@Nullable Value value, TypeInformation type); - - @Nullable - Value writeValue(@Nullable Object value, TypeInformation type); + Object readValueForProperty(@Nullable Value value, TypeInformation type); /** - * Returns a {@link PersistentPropertyAccessor} that delegates to {@code targetPropertyAccessor} and applies - * all known conversions before returning a value. + * Converts an {@link Object} to a driver's value object. * - * @param targetPropertyAccessor The property accessor to delegate to, must not be {@code null}. - * @param The type of the entity to operate on. - * @return A {@link PersistentPropertyAccessor} guaranteed to be not {@code null}. + * @param value The value to get written, may be null. + * @param type The type information describing the target type. + * @return A driver compatible value object. */ - PersistentPropertyAccessor decoratePropertyAccessor(TypeSystem typeSystem, PersistentPropertyAccessor targetPropertyAccessor); + @Nullable + Value writeValueFromProperty(@Nullable Object value, TypeInformation type); - /** - * Returns a {@link ParameterValueProvider} that delegates to {@code targetParameterValueProvider} and applies - * all known conversions before returning a value. - * - * @param targetParameterValueProvider The parameter value provider to delegate to, must not be {@code null}. - * @param The type of the entity to operate on. - * @return A {@link ParameterValueProvider} guaranteed to be not {@code null}. - */ - > ParameterValueProvider decorateParameterValueProvider( - ParameterValueProvider targetParameterValueProvider); } diff --git a/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/core/cypher/MapExpression.java b/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/core/cypher/MapExpression.java index 8507351f..1683933f 100644 --- a/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/core/cypher/MapExpression.java +++ b/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/core/cypher/MapExpression.java @@ -18,6 +18,8 @@ */ package org.neo4j.springframework.data.core.cypher; +import static org.neo4j.springframework.data.core.cypher.Expressions.*; + import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -25,6 +27,7 @@ import org.apiguardian.api.API; import org.neo4j.springframework.data.core.cypher.support.TypedSubtree; +import org.neo4j.springframework.data.core.cypher.support.Visitable; import org.springframework.util.Assert; /** @@ -74,4 +77,9 @@ MapExpression addEntries(List entries) { newContent.addAll(entries); return new MapExpression<>(newContent); } + + @Override + protected Visitable prepareVisit(MapEntry child) { + return child instanceof Expression ? nameOrExpression((Expression) child) : child; + } } diff --git a/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/core/cypher/MapProjection.java b/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/core/cypher/MapProjection.java index 7eb4c2bc..fcb33d51 100644 --- a/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/core/cypher/MapProjection.java +++ b/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/core/cypher/MapProjection.java @@ -111,8 +111,8 @@ private static List createNewContent(Object... content) { Assert.isTrue(!knownKeys.contains(lastKey), "Duplicate key '" + lastKey + "'"); entry = new KeyValueMapEntry(lastKey, lastExpression); knownKeys.add(lastKey); - } else if (lastExpression instanceof PropertyLookup) { - entry = (PropertyLookup) lastExpression; + } else if (lastExpression instanceof MapEntry) { + entry = (MapEntry) lastExpression; } else { throw new IllegalArgumentException(lastExpression + " of type " + lastExpression.getClass() + " cannot be used with an implicit name as map entry."); diff --git a/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/core/cypher/Relationship.java b/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/core/cypher/Relationship.java index a14fde6f..2d32682e 100644 --- a/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/core/cypher/Relationship.java +++ b/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/core/cypher/Relationship.java @@ -37,7 +37,7 @@ */ @API(status = API.Status.INTERNAL, since = "1.0") public final class Relationship implements - PatternElement, Named, Expression, + PatternElement, Named, Expression, MapEntry, ExposesRelationships { /** diff --git a/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jBinderFunction.java b/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jBinderFunction.java deleted file mode 100644 index 27d4ad4f..00000000 --- a/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jBinderFunction.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright (c) 2019 "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.core.mapping; - -import static org.neo4j.springframework.data.core.schema.NodeDescription.*; - -import java.util.HashMap; -import java.util.Map; -import java.util.function.Function; - -import org.neo4j.springframework.data.core.convert.Neo4jConverter; -import org.springframework.data.mapping.PersistentPropertyAccessor; - -/** - * @author Michael J. Simons - * @param type that should get mapped by the binder function - * @since 1.0 - */ -final class DefaultNeo4jBinderFunction implements Function> { - - private final Neo4jPersistentEntity nodeDescription; - - private final Neo4jConverter converter; - - DefaultNeo4jBinderFunction(Neo4jPersistentEntity nodeDescription, Neo4jConverter converter) { - this.nodeDescription = nodeDescription; - this.converter = converter; - } - - @Override - public Map apply(T entity) { - Map properties = new HashMap<>(); - - PersistentPropertyAccessor propertyAccessor = nodeDescription.getPropertyAccessor(entity); - nodeDescription.doWithProperties((Neo4jPersistentProperty p) -> { - - // Skip the internal properties, we don't want them to end up stored as properties - if (p.isInternalIdProperty()) { - return; - } - - final Object value = converter.writeValue(propertyAccessor.getProperty(p), p.getTypeInformation()); - properties.put(p.getPropertyName(), value); - }); - - Map parameters = new HashMap<>(); - parameters.put(NAME_OF_PROPERTIES_PARAM, properties); - parameters.put(NAME_OF_ID_PARAM, propertyAccessor.getProperty(nodeDescription.getRequiredIdProperty())); - - return parameters; - } -} diff --git a/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jConverter.java b/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jConverter.java index 21e76ad6..5475231f 100644 --- a/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jConverter.java +++ b/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jConverter.java @@ -18,25 +18,47 @@ */ package org.neo4j.springframework.data.core.mapping; +import static java.util.stream.Collectors.*; +import static org.neo4j.springframework.data.core.schema.NodeDescription.*; +import static org.neo4j.springframework.data.core.schema.RelationshipDescription.*; + +import java.util.ArrayList; import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.StreamSupport; +import org.apache.commons.logging.LogFactory; +import org.neo4j.driver.Record; import org.neo4j.driver.Value; import org.neo4j.driver.Values; +import org.neo4j.driver.types.MapAccessor; +import org.neo4j.driver.types.Node; +import org.neo4j.driver.types.Relationship; import org.neo4j.driver.types.TypeSystem; import org.neo4j.springframework.data.core.convert.Neo4jConversions; import org.neo4j.springframework.data.core.convert.Neo4jConverter; +import org.neo4j.springframework.data.core.schema.RelationshipDescription; import org.springframework.core.CollectionFactory; import org.springframework.core.convert.ConversionService; -import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.support.ConfigurableConversionService; import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.core.log.LogAccessor; import org.springframework.dao.TypeMismatchDataAccessException; -import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.convert.EntityInstantiators; +import org.springframework.data.mapping.AssociationHandler; +import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.PreferredConstructor; -import org.springframework.data.mapping.model.ConvertingPropertyAccessor; +import org.springframework.data.mapping.PropertyHandler; import org.springframework.data.mapping.model.ParameterValueProvider; -import org.springframework.data.util.ClassTypeInformation; import org.springframework.data.util.TypeInformation; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -48,9 +70,21 @@ */ final class DefaultNeo4jConverter implements Neo4jConverter { + private static final LogAccessor log = new LogAccessor(LogFactory.getLog(DefaultNeo4jConverter.class)); + + /** + * The shared entity instantiators of this context. Those should not be recreated for each entity or even not for + * each query, as otherwise the cache of Spring's org.springframework.data.convert.ClassGeneratingEntityInstantiator + * won't apply + */ + private static final EntityInstantiators INSTANTIATORS = new EntityInstantiators(); + + private final NodeDescriptionStore nodeDescriptionStore; private final ConversionService conversionService; - DefaultNeo4jConverter(Neo4jConversions neo4jConversions) { + private TypeSystem typeSystem; + + DefaultNeo4jConverter(Neo4jConversions neo4jConversions, NodeDescriptionStore nodeDescriptionStore) { Assert.notNull(neo4jConversions, "Neo4jConversions must not be null!"); @@ -58,10 +92,54 @@ final class DefaultNeo4jConverter implements Neo4jConverter { neo4jConversions.registerConvertersIn(configurableConversionService); this.conversionService = configurableConversionService; + this.nodeDescriptionStore = nodeDescriptionStore; + } + + @Override + public R read(Class targetType, Record record) { + + Neo4jPersistentEntity rootNodeDescription = + (Neo4jPersistentEntity) nodeDescriptionStore.getNodeDescription(targetType); + + try { + List recordValues = record.values(); + String nodeLabel = rootNodeDescription.getPrimaryLabel(); + MapAccessor queryRoot = null; + for (Value value : recordValues) { + if (value.hasType(typeSystem.NODE()) && value.asNode().hasLabel(nodeLabel)) { + if (recordValues.size() > 1) { + queryRoot = mergeRootNodeWithRecord(value.asNode(), record); + } else { + queryRoot = value.asNode(); + } + break; + } + } + if (queryRoot == null) { + for (Value value : recordValues) { + if (value.hasType(typeSystem.MAP())) { + queryRoot = value; + break; + } + } + } + + if (queryRoot == null) { + log.warn(() -> String.format("Could not find mappable nodes or relationships inside %s for %s", record, + rootNodeDescription)); + return null; // todo should not be null because of the @nonnullapi annotation in the EntityReader. Fail? + } else { + Map knownObjects = new ConcurrentHashMap<>(); + return map(queryRoot, rootNodeDescription, knownObjects); + } + } catch (Exception e) { + throw new MappingException("Error mapping " + record.toString(), e); + } } + @Override @Nullable - public Object readValue(@Nullable Value value, TypeInformation type) { + public Object readValueForProperty(@Nullable Value value, TypeInformation type) { if (value == null || value == Values.NULL) { return null; @@ -86,7 +164,31 @@ public Object readValue(@Nullable Value value, TypeInformation type) { } @Override - public Value writeValue(@Nullable Object value, TypeInformation type) { + public void write(Object source, Map parameters) { + Map properties = new HashMap<>(); + + Neo4jPersistentEntity nodeDescription = + (Neo4jPersistentEntity) nodeDescriptionStore.getNodeDescription(source.getClass()); + + PersistentPropertyAccessor propertyAccessor = nodeDescription.getPropertyAccessor(source); + nodeDescription.doWithProperties((Neo4jPersistentProperty p) -> { + + // Skip the internal properties, we don't want them to end up stored as properties + if (p.isInternalIdProperty()) { + return; + } + + final Object value = writeValueFromProperty(propertyAccessor.getProperty(p), p.getTypeInformation()); + properties.put(p.getPropertyName(), value); + }); + + parameters.put(NAME_OF_PROPERTIES_PARAM, properties); + parameters.put(NAME_OF_ID_PARAM, propertyAccessor.getProperty(nodeDescription.getRequiredIdProperty())); + + } + + @Override + public Value writeValueFromProperty(@Nullable Object value, TypeInformation type) { if (value == null) { return Values.NULL; @@ -106,50 +208,221 @@ private static boolean isCollection(TypeInformation type) { return Collection.class.isAssignableFrom(type.getType()); } - @Override - public PersistentPropertyAccessor decoratePropertyAccessor(TypeSystem typeSystem, - PersistentPropertyAccessor targetPropertyAccessor) { + void setTypeSystem(TypeSystem typeSystem) { + this.typeSystem = typeSystem; + } + + /** + * Merges the root node of a query and the remaining record into one map, adding the internal ID of the node, too. + * Merge happens only when the record contains additional values. + * + * @param node Node whose attributes are about to be merged + * @param record Record that should be merged + * @return + */ + private static MapAccessor mergeRootNodeWithRecord(Node node, Record record) { + Map mergedAttributes = new HashMap<>(node.size() + record.size() + 1); + + mergedAttributes.put(NAME_OF_INTERNAL_ID, node.id()); + mergedAttributes.putAll(node.asMap(Function.identity())); + mergedAttributes.putAll(record.asMap(Function.identity())); - return new ConvertingPropertyAccessor<>(targetPropertyAccessor, new DelegatingConversionService()); + return Values.value(mergedAttributes); } - @Override - public > ParameterValueProvider decorateParameterValueProvider( - ParameterValueProvider targetParameterValueProvider) { + /** + * @param queryResult The original query result + * @param nodeDescription The node description of the current entity to be mapped from the result + * @param knownObjects The current list of known objects + * @param As in entity type + * @return + */ + private ET map(MapAccessor queryResult, + Neo4jPersistentEntity nodeDescription, + Map knownObjects) { + + ET instance = instantiate(nodeDescription, queryResult); + + PersistentPropertyAccessor propertyAccessor = nodeDescription.getPropertyAccessor(instance); + + if (nodeDescription.requiresPropertyPopulation()) { - return new ParameterValueProvider() { + // Fill simple properties + Predicate isConstructorParameter = nodeDescription + .getPersistenceConstructor()::isConstructorParameter; + nodeDescription.doWithProperties(populateFrom(queryResult, propertyAccessor, isConstructorParameter)); + + // Fill associations + Collection relationships = nodeDescription.getRelationships(); + nodeDescription.doWithAssociations( + populateFrom(queryResult, propertyAccessor, relationships, knownObjects)); + } + return instance; + } + + private ET instantiate(Neo4jPersistentEntity anotherNodeDescription, MapAccessor values) { + + ParameterValueProvider parameterValueProvider = new ParameterValueProvider() { @Override public Object getParameterValue(PreferredConstructor.Parameter parameter) { - Object originalValue = targetParameterValueProvider.getParameterValue(parameter); - Assert.isInstanceOf(Value.class, originalValue, "Decorated parameters other than of type Value are not supported."); - return readValue((Value) originalValue, parameter.getType()); + Neo4jPersistentProperty matchingProperty = anotherNodeDescription + .getRequiredPersistentProperty(parameter.getName()); + return readValueForProperty(extractValueOf(matchingProperty, values), parameter.getType()); } }; + + return INSTANTIATORS.getInstantiatorFor(anotherNodeDescription) + .createInstance(anotherNodeDescription, parameterValueProvider); } - class DelegatingConversionService implements ConversionService { + private PropertyHandler populateFrom( + MapAccessor queryResult, + PersistentPropertyAccessor propertyAccessor, + Predicate isConstructorParameter + ) { + return property -> { + if (isConstructorParameter.test(property)) { + return; + } - @Override - public boolean canConvert(Class sourceType, Class targetType) { - return sourceType == Value.class; - } + propertyAccessor.setProperty(property, + readValueForProperty(extractValueOf(property, queryResult), property.getTypeInformation())); + }; + } - @Override - public boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType) { - return sourceType.isAssignableTo(TypeDescriptor.valueOf(Value.class)); - } + private AssociationHandler populateFrom( + MapAccessor queryResult, + PersistentPropertyAccessor propertyAccessor, + Collection relationships, + Map knownObjects + ) { + return association -> { + Neo4jPersistentProperty inverse = association.getInverse(); - @Override @Nullable - public T convert(Object source, Class targetType) { + RelationshipDescription relationship = relationships.stream() + .filter(r -> r.getFieldName().equals(inverse.getName())) + .findFirst().get(); - return (T) readValue((Value) source, ClassTypeInformation.from(targetType)); - } + String relationshipType = relationship.getType(); + String targetLabel = relationship.getTarget().getPrimaryLabel(); + + Neo4jPersistentEntity targetNodeDescription = (Neo4jPersistentEntity) relationship.getTarget(); + + List value = new ArrayList<>(); + Map dynamicValue = new HashMap<>(); + + BiConsumer mappedObjectHandler = relationship.isDynamic() ? + dynamicValue::put : (type, mappedObject) -> value.add(mappedObject); + + Value list = queryResult.get(relationship.generateRelatedNodesCollectionName()); + + Map relationshipsAndProperties = new HashMap<>(); + + // if the list is null the mapping is based on a custom query + if (list == Values.NULL) { - @Override @Nullable - public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + Predicate isList = entry -> entry instanceof Value && typeSystem.LIST().isTypeOf(entry); + + Predicate containsOnlyRelationships = entry -> entry.asList(Function.identity()) + .stream() + .allMatch(listEntry -> typeSystem.RELATIONSHIP().isTypeOf(listEntry)); + + Predicate containsOnlyNodes = entry -> entry.asList(Function.identity()) + .stream() + .allMatch(listEntry -> typeSystem.NODE().isTypeOf(listEntry)); + + // find relationships in the result + List allMatchingTypeRelationshipsInResult = StreamSupport + .stream(queryResult.values().spliterator(), false) + .filter(isList.and(containsOnlyRelationships)) + .flatMap(entry -> entry.asList(Value::asRelationship).stream()) + .filter(r -> r.type().equals(relationshipType)) + .collect(toList()); + + List allNodesWithMatchingLabelInResult = StreamSupport + .stream(queryResult.values().spliterator(), false) + .filter(isList.and(containsOnlyNodes)) + .flatMap(entry -> entry.asList(Value::asNode).stream()) + .filter(n -> n.hasLabel(targetLabel)) + .collect(toList()); + + if (allNodesWithMatchingLabelInResult.isEmpty() && allMatchingTypeRelationshipsInResult.isEmpty()) { + return; + } + + for (Node possibleValueNode : allNodesWithMatchingLabelInResult) { + long nodeId = possibleValueNode.id(); + + for (Relationship possibleRelationship : allMatchingTypeRelationshipsInResult) { + if (possibleRelationship.endNodeId() == nodeId) { + Object mappedObject = map(possibleValueNode, targetNodeDescription, knownObjects); + if (relationship.hasRelationshipProperties()) { + + Class propertiesClass = relationship.getRelationshipPropertiesClass(); + + Object relationshipProperties = map(possibleRelationship, + (Neo4jPersistentEntity) nodeDescriptionStore.getNodeDescription(propertiesClass), + knownObjects); + relationshipsAndProperties.put(mappedObject, relationshipProperties); + } else { + mappedObjectHandler.accept(possibleRelationship.type(), mappedObject); + } + break; + } + } + } + } else { + for (Value relatedEntity : list.asList(Function.identity())) { + Neo4jPersistentProperty idProperty = targetNodeDescription.getRequiredIdProperty(); + + // internal (generated) id or external set + Object idValue = idProperty.isInternalIdProperty() + ? relatedEntity.get(NAME_OF_INTERNAL_ID) + : relatedEntity.get(idProperty.getName()); + Object valueEntry = knownObjects.computeIfAbsent(idValue, + (id) -> map(relatedEntity, targetNodeDescription, knownObjects)); + + if (relationship.hasRelationshipProperties()) { + Relationship relatedEntityRelationship = relatedEntity.get(NAME_OF_RELATIONSHIP).asRelationship(); + Class propertiesClass = relationship.getRelationshipPropertiesClass(); + + Object relationshipProperties = map(relatedEntityRelationship, + (Neo4jPersistentEntity) nodeDescriptionStore.getNodeDescription(propertiesClass), + knownObjects); + relationshipsAndProperties.put(valueEntry, relationshipProperties); + } else { + mappedObjectHandler.accept(relatedEntity.get(NAME_OF_RELATIONSHIP_TYPE).asString(), valueEntry); + } + } + } + + if (inverse.getTypeInformation().isCollectionLike()) { + if (inverse.getType().equals(Set.class)) { + propertyAccessor.setProperty(inverse, new HashSet(value)); + } else { + propertyAccessor.setProperty(inverse, value); + } + } else { + if (relationship.isDynamic()) { + propertyAccessor.setProperty(inverse, dynamicValue.isEmpty() ? null : dynamicValue); + } else if (relationship.hasRelationshipProperties()) { + propertyAccessor.setProperty(inverse, relationshipsAndProperties); + } else { + propertyAccessor.setProperty(inverse, value.isEmpty() ? null : value.get(0)); + } + } + }; + } - return readValue((Value) source, ClassTypeInformation.from(targetType.getType())); + private static Value extractValueOf(Neo4jPersistentProperty property, MapAccessor propertyContainer) { + if (property.isInternalIdProperty()) { + return propertyContainer instanceof Node ? + Values.value(((Node) propertyContainer).id()) : + propertyContainer.get(NAME_OF_INTERNAL_ID); + } else { + String graphPropertyName = property.getPropertyName(); + return propertyContainer.get(graphPropertyName); } } } diff --git a/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jMappingFunction.java b/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jMappingFunction.java deleted file mode 100644 index f234069a..00000000 --- a/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jMappingFunction.java +++ /dev/null @@ -1,321 +0,0 @@ -/* - * Copyright (c) 2019 "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.core.mapping; - -import static java.util.stream.Collectors.*; -import static org.neo4j.springframework.data.core.schema.NodeDescription.*; -import static org.neo4j.springframework.data.core.schema.RelationshipDescription.*; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.BiConsumer; -import java.util.function.BiFunction; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.stream.StreamSupport; - -import org.apache.commons.logging.LogFactory; -import org.neo4j.driver.Record; -import org.neo4j.driver.Value; -import org.neo4j.driver.Values; -import org.neo4j.driver.types.MapAccessor; -import org.neo4j.driver.types.Node; -import org.neo4j.driver.types.Relationship; -import org.neo4j.driver.types.TypeSystem; -import org.neo4j.springframework.data.core.convert.Neo4jConverter; -import org.neo4j.springframework.data.core.schema.RelationshipDescription; -import org.springframework.core.log.LogAccessor; -import org.springframework.data.convert.EntityInstantiators; -import org.springframework.data.mapping.AssociationHandler; -import org.springframework.data.mapping.MappingException; -import org.springframework.data.mapping.PersistentPropertyAccessor; -import org.springframework.data.mapping.PreferredConstructor; -import org.springframework.data.mapping.PropertyHandler; -import org.springframework.data.mapping.model.ParameterValueProvider; - -/** - * The central logic of mapping Neo4j's {@link org.neo4j.driver.Record records} to entities based on the Spring - * implementation of the {@link org.neo4j.springframework.data.core.schema.Schema}, - * represented by the {@link Neo4jMappingContext}. - * - * @author Gerrit Meier - * @author Michael J. Simons - * @since 1.0 - */ -final class DefaultNeo4jMappingFunction implements BiFunction { - - private static final LogAccessor log = new LogAccessor(LogFactory.getLog(DefaultNeo4jMappingFunction.class)); - - /** - * The shared entity instantiators of this context. Those should not be recreated for each entity or even not for - * each query, as otherwise the cache of Spring's org.springframework.data.convert.ClassGeneratingEntityInstantiator - * won't apply - */ - private static final EntityInstantiators INSTANTIATORS = new EntityInstantiators(); - - /** - * The description of the possible root node from where the mapping should start. - */ - private final Neo4jPersistentEntity rootNodeDescription; - - private final Neo4jConverter converter; - - DefaultNeo4jMappingFunction(Neo4jPersistentEntity rootNodeDescription, Neo4jConverter converter) { - - this.rootNodeDescription = rootNodeDescription; - this.converter = converter; - } - - @Override - public T apply(TypeSystem typeSystem, Record record) { - // That would be the place to call a custom converter for the whole object, if any such thing would be - // available (Converter - try { - List recordValues = record.values(); - String nodeLabel = rootNodeDescription.getPrimaryLabel(); - MapAccessor queryRoot = null; - for (Value value : recordValues) { - if (value.hasType(typeSystem.NODE()) && value.asNode().hasLabel(nodeLabel)) { - if (recordValues.size() > 1) { - queryRoot = mergeRootNodeWithRecord(value.asNode(), record); - } else { - queryRoot = value.asNode(); - } - break; - } - } - if (queryRoot == null) { - for (Value value : recordValues) { - if (value.hasType(typeSystem.MAP())) { - queryRoot = value; - break; - } - } - } - - if (queryRoot == null) { - log.warn(() -> String.format("Could not find mappable nodes or relationships inside %s for %s", record, - rootNodeDescription)); - return null; - } else { - Map knownObjects = new ConcurrentHashMap<>(); - return map(typeSystem, queryRoot, rootNodeDescription, knownObjects); - } - } catch (Exception e) { - throw new MappingException("Error mapping " + record.toString(), e); - } - } - - /** - * Merges the root node of a query and the remaining record into one map, adding the internal ID of the node, too. - * Merge happens only when the record contains additional values. - * - * @param node Node whose attributes are about to be merged - * @param record Record that should be merged - * @return - */ - private static MapAccessor mergeRootNodeWithRecord(Node node, Record record) { - Map mergedAttributes = new HashMap<>(node.size() + record.size() + 1); - - mergedAttributes.put(NAME_OF_INTERNAL_ID, node.id()); - mergedAttributes.putAll(node.asMap(Function.identity())); - mergedAttributes.putAll(record.asMap(Function.identity())); - - return Values.value(mergedAttributes); - } - - /** - * @param queryResult The original query result - * @param nodeDescription The node description of the current entity to be mapped from the result - * @param knownObjects The current list of known objects - * @param As in entity type - * @return - */ - private ET map(TypeSystem typeSystem, MapAccessor queryResult, - Neo4jPersistentEntity nodeDescription, - Map knownObjects) { - - ET instance = instantiate(nodeDescription, queryResult); - - PersistentPropertyAccessor propertyAccessor = converter - .decoratePropertyAccessor(typeSystem, nodeDescription.getPropertyAccessor(instance)); - if (nodeDescription.requiresPropertyPopulation()) { - - // Fill simple properties - Predicate isConstructorParameter = nodeDescription - .getPersistenceConstructor()::isConstructorParameter; - nodeDescription.doWithProperties(populateFrom(queryResult, propertyAccessor, isConstructorParameter)); - - // Fill associations - Collection relationships = nodeDescription.getRelationships(); - nodeDescription.doWithAssociations( - populateFrom(typeSystem, queryResult, propertyAccessor, relationships, knownObjects)); - } - return instance; - } - - private ET instantiate(Neo4jPersistentEntity anotherNodeDescription, MapAccessor values) { - - ParameterValueProvider parameterValueProvider = new ParameterValueProvider() { - @Override - public Value getParameterValue(PreferredConstructor.Parameter parameter) { - - Neo4jPersistentProperty matchingProperty = anotherNodeDescription - .getRequiredPersistentProperty(parameter.getName()); - return extractValueOf(matchingProperty, values); - } - }; - parameterValueProvider = converter.decorateParameterValueProvider(parameterValueProvider); - return INSTANTIATORS.getInstantiatorFor(anotherNodeDescription) - .createInstance(anotherNodeDescription, parameterValueProvider); - } - - private static PropertyHandler populateFrom( - MapAccessor queryResult, - PersistentPropertyAccessor propertyAccessor, - Predicate isConstructorParameter - ) { - return property -> { - if (isConstructorParameter.test(property)) { - return; - } - - Value value = extractValueOf(property, queryResult); - propertyAccessor.setProperty(property, value); - }; - } - - private AssociationHandler populateFrom( - TypeSystem typeSystem, - MapAccessor queryResult, - PersistentPropertyAccessor propertyAccessor, - Collection relationships, - Map knownObjects - ) { - return association -> { - Neo4jPersistentProperty inverse = association.getInverse(); - - RelationshipDescription relationship = relationships.stream() - .filter(r -> r.getFieldName().equals(inverse.getName())) - .findFirst().get(); - - String relationshipType = relationship.getType(); - String targetLabel = relationship.getTarget().getPrimaryLabel(); - - Neo4jPersistentEntity targetNodeDescription = (Neo4jPersistentEntity) relationship.getTarget(); - - List value = new ArrayList<>(); - Map dynamicValue = new HashMap<>(); - - BiConsumer mappedObjectHandler = relationship.isDynamic() ? - dynamicValue::put : (type, mappedObject) -> value.add(mappedObject); - - Value list = queryResult.get(relationship.generateRelatedNodesCollectionName()); - - // if the list is null the mapping is based on a custom query - if (list == Values.NULL) { - - Predicate isList = entry -> entry instanceof Value && typeSystem.LIST().isTypeOf(entry); - - Predicate containsOnlyRelationships = entry -> entry.asList(Function.identity()) - .stream() - .allMatch(listEntry -> typeSystem.RELATIONSHIP().isTypeOf(listEntry)); - - Predicate containsOnlyNodes = entry -> entry.asList(Function.identity()) - .stream() - .allMatch(listEntry -> typeSystem.NODE().isTypeOf(listEntry)); - - // find relationships in the result - List allMatchingTypeRelationshipsInResult = StreamSupport - .stream(queryResult.values().spliterator(), false) - .filter(isList.and(containsOnlyRelationships)) - .flatMap(entry -> entry.asList(Value::asRelationship).stream()) - .filter(r -> r.type().equals(relationshipType)) - .collect(toList()); - - List allNodesWithMatchingLabelInResult = StreamSupport - .stream(queryResult.values().spliterator(), false) - .filter(isList.and(containsOnlyNodes)) - .flatMap(entry -> entry.asList(Value::asNode).stream()) - .filter(n -> n.hasLabel(targetLabel)) - .collect(toList()); - - if (allNodesWithMatchingLabelInResult.isEmpty() && allMatchingTypeRelationshipsInResult.isEmpty()) { - return; - } - - for (Node possibleValueNode : allNodesWithMatchingLabelInResult) { - long nodeId = possibleValueNode.id(); - - for (Relationship possibleRelationship : allMatchingTypeRelationshipsInResult) { - if (possibleRelationship.endNodeId() == nodeId) { - Object mappedObject = map(typeSystem, possibleValueNode, targetNodeDescription, knownObjects); - mappedObjectHandler.accept(possibleRelationship.type(), mappedObject); - break; - } - } - } - } else { - for (Value relatedEntity : list.asList(Function.identity())) { - Neo4jPersistentProperty idProperty = targetNodeDescription.getRequiredIdProperty(); - - // internal (generated) id or external set - Object idValue = idProperty.isInternalIdProperty() - ? relatedEntity.get(NAME_OF_INTERNAL_ID) - : relatedEntity.get(idProperty.getName()); - Object valueEntry = knownObjects.computeIfAbsent(idValue, - (id) -> map(typeSystem, relatedEntity, targetNodeDescription, knownObjects)); - - mappedObjectHandler.accept(relatedEntity.get(NAME_OF_RELATIONSHIP_TYPE).asString(), valueEntry); - } - } - - if (inverse.getTypeInformation().isCollectionLike()) { - if (inverse.getType().equals(Set.class)) { - propertyAccessor.setProperty(inverse, new HashSet(value)); - } else { - propertyAccessor.setProperty(inverse, value); - } - } else { - if (relationship.isDynamic()) { - propertyAccessor.setProperty(inverse, dynamicValue.isEmpty() ? null : dynamicValue); - } else { - propertyAccessor.setProperty(inverse, value.isEmpty() ? null : value.get(0)); - } - } - }; - } - - private static Value extractValueOf(Neo4jPersistentProperty property, MapAccessor propertyContainer) { - if (property.isInternalIdProperty()) { - return propertyContainer instanceof Node ? - Values.value(((Node) propertyContainer).id()) : - propertyContainer.get(NAME_OF_INTERNAL_ID); - } else { - String graphPropertyName = property.getPropertyName(); - return propertyContainer.get(graphPropertyName); - } - } -} diff --git a/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jPersistentProperty.java b/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jPersistentProperty.java index 29db298a..2f81e2ca 100644 --- a/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jPersistentProperty.java +++ b/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jPersistentProperty.java @@ -20,6 +20,7 @@ import org.neo4j.springframework.data.core.schema.NodeDescription; import org.neo4j.springframework.data.core.schema.Relationship; +import org.neo4j.springframework.data.core.schema.RelationshipProperties; import org.springframework.data.mapping.Association; import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.PersistentEntity; @@ -68,8 +69,14 @@ class DefaultNeo4jPersistentProperty extends AnnotationBasedPersistentProperty createAssociation() { - Neo4jPersistentEntity obverseOwner = this.mappingContext - .getPersistentEntity(this.getAssociationTargetType()); + Neo4jPersistentEntity obverseOwner; + + // if the target is a relationship property always take the key type from the map instead of the value type. + if (this.hasActualTypeAnnotation(RelationshipProperties.class)) { + obverseOwner = this.mappingContext.getPersistentEntity(this.getComponentType()); + } else { + obverseOwner = this.mappingContext.getPersistentEntity(this.getAssociationTargetType()); + } Relationship outgoingRelationship = this.findAnnotation(Relationship.class); @@ -86,8 +93,14 @@ protected Association createAssociation() { } Neo4jPersistentProperty obverse = null; - return new DefaultRelationshipDescription(this, obverse, type, this.isDynamicAssociation(), - (NodeDescription) getOwner(), this.getName(), obverseOwner, direction); + boolean dynamicAssociation = this.isDynamicAssociation(); + + // Because a dynamic association is also represented as a Map, this ensures that the + // relationship properties class will only have a value if it's not a dynamic association. + Class relationshipPropertiesClass = dynamicAssociation ? null : getMapValueType(); + + return new DefaultRelationshipDescription(this, obverse, type, dynamicAssociation, (NodeDescription) getOwner(), + this.getName(), obverseOwner, direction, relationshipPropertiesClass); } @Override diff --git a/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultRelationshipDescription.java b/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultRelationshipDescription.java index 9ddcb1d0..ac5a3afe 100644 --- a/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultRelationshipDescription.java +++ b/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/core/mapping/DefaultRelationshipDescription.java @@ -24,6 +24,7 @@ import org.neo4j.springframework.data.core.schema.Relationship; import org.neo4j.springframework.data.core.schema.RelationshipDescription; import org.springframework.data.mapping.Association; +import org.springframework.lang.Nullable; /** * @author Michael J. Simons @@ -44,10 +45,12 @@ class DefaultRelationshipDescription extends Association relationshipPropertiesClass; + DefaultRelationshipDescription(Neo4jPersistentProperty inverse, Neo4jPersistentProperty obverse, String type, boolean dynamic, NodeDescription source, String fieldName, NodeDescription target, - Relationship.Direction direction) { + Relationship.Direction direction, @Nullable Class relationshipPropertiesClass) { super(inverse, obverse); @@ -57,6 +60,7 @@ class DefaultRelationshipDescription extends Association getRelationshipPropertiesClass() { + return relationshipPropertiesClass; + } + + @Override + public boolean hasRelationshipProperties() { + return getRelationshipPropertiesClass() != null; + } @Override public String toString() { diff --git a/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/core/mapping/Neo4jMappingContext.java b/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/core/mapping/Neo4jMappingContext.java index a1530f81..1662767b 100644 --- a/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/core/mapping/Neo4jMappingContext.java +++ b/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/core/mapping/Neo4jMappingContext.java @@ -18,43 +18,25 @@ */ package org.neo4j.springframework.data.core.mapping; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -import java.util.function.BiFunction; -import java.util.function.Function; -import java.util.function.Supplier; import org.apiguardian.api.API; -import org.neo4j.driver.Record; -import org.neo4j.driver.types.TypeSystem; +import org.neo4j.driver.Driver; import org.neo4j.springframework.data.core.convert.Neo4jConversions; import org.neo4j.springframework.data.core.convert.Neo4jConverter; import org.neo4j.springframework.data.core.convert.Neo4jSimpleTypes; -import org.neo4j.springframework.data.core.schema.IdDescription; import org.neo4j.springframework.data.core.schema.IdGenerator; import org.neo4j.springframework.data.core.schema.NodeDescription; -import org.neo4j.springframework.data.core.schema.RelationshipDescription; import org.neo4j.springframework.data.core.schema.Schema; -import org.neo4j.springframework.data.core.schema.UnknownEntityException; import org.springframework.beans.BeanUtils; import org.springframework.beans.BeansException; -import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.NoSuchBeanDefinitionException; -import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; -import org.springframework.beans.factory.support.BeanDefinitionReaderUtils; -import org.springframework.beans.factory.support.BeanDefinitionRegistry; -import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; -import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; import org.springframework.context.ApplicationContext; -import org.springframework.data.mapping.Association; import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.context.AbstractMappingContext; import org.springframework.data.mapping.model.Property; @@ -72,26 +54,25 @@ */ @API(status = API.Status.INTERNAL, since = "1.0") public final class Neo4jMappingContext - extends AbstractMappingContext, Neo4jPersistentProperty> implements Schema, - BeanDefinitionRegistryPostProcessor { + extends AbstractMappingContext, Neo4jPersistentProperty> implements Schema { /** - * A lookup of entities based on their primary label. We depend on the locking mechanism provided by the - * {@link AbstractMappingContext}, so this lookup is not synchronized further. + * A map of fallback id generators, that have not been added to the application context */ - private final Map> nodeDescriptionsByPrimaryLabel = new HashMap<>(); + private final Map>, IdGenerator> idGenerators = new ConcurrentHashMap<>(); /** - * A map of fallback id generators, that have not been added to the application context + * The {@link NodeDescriptionStore} is basically a {@link Map} and it is used to break the dependency + * cycle between this class and the {@link DefaultNeo4jConverter}. */ - private final Map>, IdGenerator> fallbackIdGenerators = new ConcurrentHashMap<>(); + private final NodeDescriptionStore nodeDescriptionStore = new NodeDescriptionStore(); /** * The converter used in this mapping context. */ private final Neo4jConverter converter; - private @Nullable ListableBeanFactory beanFactory; + private @Nullable AutowireCapableBeanFactory beanFactory; public Neo4jMappingContext() { @@ -101,7 +82,7 @@ public Neo4jMappingContext() { public Neo4jMappingContext(Neo4jConversions neo4jConversions) { super.setSimpleTypeHolder(Neo4jSimpleTypes.HOLDER); - this.converter = new DefaultNeo4jConverter(neo4jConversions); + this.converter = new DefaultNeo4jConverter(neo4jConversions, nodeDescriptionStore); } public Neo4jConverter getConverter() { @@ -118,7 +99,7 @@ protected Neo4jPersistentEntity createPersistentEntity(TypeInformation final DefaultNeo4jPersistentEntity newEntity = new DefaultNeo4jPersistentEntity<>(typeInformation); String primaryLabel = newEntity.getPrimaryLabel(); - if (this.nodeDescriptionsByPrimaryLabel.containsKey(primaryLabel)) { + if (this.nodeDescriptionStore.containsKey(primaryLabel)) { // @formatter:off throw new MappingException( String.format(Locale.ENGLISH, "The schema already contains a node description under the primary label %s", @@ -126,8 +107,8 @@ protected Neo4jPersistentEntity createPersistentEntity(TypeInformation // @formatter:on } - if (this.nodeDescriptionsByPrimaryLabel.containsValue(newEntity)) { - Optional label = this.nodeDescriptionsByPrimaryLabel.entrySet().stream() + if (this.nodeDescriptionStore.containsValue(newEntity)) { + Optional label = this.nodeDescriptionStore.entrySet().stream() .filter(e -> e.getValue().equals(newEntity)).map( Map.Entry::getKey).findFirst(); @@ -144,7 +125,7 @@ protected Neo4jPersistentEntity createPersistentEntity(TypeInformation newEntity.getUnderlyingClass().getName(), existingDescription.getPrimaryLabel())); } - this.nodeDescriptionsByPrimaryLabel.put(primaryLabel, newEntity); + this.nodeDescriptionStore.put(primaryLabel, newEntity); return newEntity; } @@ -163,39 +144,12 @@ protected Neo4jPersistentProperty createPersistentProperty(Property property, @Override @Nullable public NodeDescription getNodeDescription(String primaryLabel) { - return this.nodeDescriptionsByPrimaryLabel.get(primaryLabel); - } - - NodeDescription getRequiredNodeDescription(String primaryLabel) { - - NodeDescription nodeDescription = this.getNodeDescription(primaryLabel); - if (nodeDescription == null) { - throw new MappingException( - String.format("Required node description not found with primary label '%s'", primaryLabel)); - } - return nodeDescription; + return this.nodeDescriptionStore.get(primaryLabel); } @Override public NodeDescription getNodeDescription(Class underlyingClass) { - - for (NodeDescription nodeDescription : this.nodeDescriptionsByPrimaryLabel.values()) { - if (nodeDescription.getUnderlyingClass().equals(underlyingClass)) { - return nodeDescription; - } - } - return null; - } - - @Override - @Nullable - public BiFunction getMappingFunctionFor(Class targetClass) { - if (this.hasPersistentEntityFor(targetClass)) { - Neo4jPersistentEntity neo4jPersistentEntity = this.getPersistentEntity(targetClass); - return new DefaultNeo4jMappingFunction<>(neo4jPersistentEntity, this.converter); - } - - return null; + return this.nodeDescriptionStore.getNodeDescription(underlyingClass); } @Override @@ -203,42 +157,21 @@ public Optional> addPersistentEntity(Class type) { return super.addPersistentEntity(type); } - @Override - public Function> getRequiredBinderFunctionFor(Class sourceClass) { - - if (!this.hasPersistentEntityFor(sourceClass)) { - throw new UnknownEntityException(sourceClass); - } - - Neo4jPersistentEntity neo4jPersistentEntity = this.getPersistentEntity(sourceClass); - return new DefaultNeo4jBinderFunction(neo4jPersistentEntity, converter); - } - - private Collection computeRelationshipsOf(String primaryLabel) { - - NodeDescription nodeDescription = getRequiredNodeDescription(primaryLabel); - - final List relationships = new ArrayList<>(); - - Neo4jPersistentEntity entity = this.getPersistentEntity(nodeDescription.getUnderlyingClass()); - entity.doWithAssociations((Association association) -> - relationships.add((RelationshipDescription) association) - ); - - return Collections.unmodifiableCollection(relationships); - } - @Override public > T getOrCreateIdGeneratorOfType(Class idGeneratorType) { - Supplier fallbackSupplier = () -> (T) this.fallbackIdGenerators - .computeIfAbsent(idGeneratorType, BeanUtils::instantiateClass); - if (this.beanFactory == null) { - return fallbackSupplier.get(); + if (this.idGenerators.containsKey(idGeneratorType)) { + return (T) this.idGenerators.get(idGeneratorType); } else { - return this.beanFactory - .getBeanProvider(idGeneratorType) - .getIfUnique(fallbackSupplier); + T idGenerator; + if (this.beanFactory == null) { + idGenerator = BeanUtils.instantiateClass(idGeneratorType); + } else { + idGenerator = this.beanFactory.getBeanProvider(idGeneratorType) + .getIfUnique(() -> this.beanFactory.createBean(idGeneratorType)); + } + this.idGenerators.put(idGeneratorType, idGenerator); + return idGenerator; } } @@ -255,30 +188,8 @@ public > Optional getIdGenerator(String reference) { public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { super.setApplicationContext(applicationContext); - this.beanFactory = applicationContext; - } - - @Override - public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { - - // Register all the known id generators - this.getPersistentEntities().stream() - .map(Neo4jPersistentEntity::getIdDescription) - .filter(IdDescription::isExternallyGeneratedId).map(IdDescription::getIdGeneratorClass) - .filter(Optional::isPresent).map(Optional::get) - .distinct() - .map(generatorClass -> { - RootBeanDefinition definition = new RootBeanDefinition(generatorClass); - definition.setRole(RootBeanDefinition.ROLE_INFRASTRUCTURE); - return definition; - }) - .forEach(definition -> { - String beanName = BeanDefinitionReaderUtils.generateBeanName(definition, registry); - registry.registerBeanDefinition(beanName, definition); - }); - } - - @Override - public void postProcessBeanFactory(@SuppressWarnings({ "HiddenField" }) ConfigurableListableBeanFactory beanFactory) throws BeansException { + this.beanFactory = applicationContext.getAutowireCapableBeanFactory(); + Driver driver = this.beanFactory.getBean(Driver.class); + ((DefaultNeo4jConverter) this.converter).setTypeSystem(driver.defaultTypeSystem()); } } diff --git a/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/core/mapping/NodeDescriptionStore.java b/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/core/mapping/NodeDescriptionStore.java new file mode 100644 index 00000000..8832289a --- /dev/null +++ b/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/core/mapping/NodeDescriptionStore.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2019 "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.core.mapping; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import org.neo4j.springframework.data.core.schema.NodeDescription; +import org.springframework.data.mapping.context.AbstractMappingContext; +import org.springframework.lang.Nullable; + +/** + * This class is more or less just a wrapper around the node description lookup map. + * It ensures that there is no cyclic dependency between {@link Neo4jMappingContext} and {@link DefaultNeo4jConverter}. + * + * @author Gerrit Meier + */ +class NodeDescriptionStore { + + /** + * A lookup of entities based on their primary label. We depend on the locking mechanism provided by the + * {@link AbstractMappingContext}, so this lookup is not synchronized further. + */ + private final Map> nodeDescriptionsByPrimaryLabel = new HashMap<>(); + + public boolean containsKey(String primaryLabel) { + return nodeDescriptionsByPrimaryLabel.containsKey(primaryLabel); + } + + public boolean containsValue(DefaultNeo4jPersistentEntity newEntity) { + return nodeDescriptionsByPrimaryLabel.containsValue(newEntity); + } + + public void put(String primaryLabel, DefaultNeo4jPersistentEntity newEntity) { + nodeDescriptionsByPrimaryLabel.put(primaryLabel, newEntity); + } + + public Set>> entrySet() { + return nodeDescriptionsByPrimaryLabel.entrySet(); + } + + public Collection> values() { + return nodeDescriptionsByPrimaryLabel.values(); + } + + @Nullable + public NodeDescription get(String primaryLabel) { + return nodeDescriptionsByPrimaryLabel.get(primaryLabel); + } + + @Nullable + public NodeDescription getNodeDescription(Class targetType) { + for (NodeDescription nodeDescription : values()) { + if (nodeDescription.getUnderlyingClass().equals(targetType)) { + return nodeDescription; + } + } + return null; + } +} diff --git a/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/core/schema/CypherGenerator.java b/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/core/schema/CypherGenerator.java index 0052dd4f..acd0b15d 100644 --- a/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/core/schema/CypherGenerator.java +++ b/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/core/schema/CypherGenerator.java @@ -29,9 +29,9 @@ import org.apiguardian.api.API; import org.jetbrains.annotations.NotNull; +import org.neo4j.springframework.data.core.cypher.*; import org.neo4j.springframework.data.core.cypher.Node; import org.neo4j.springframework.data.core.cypher.Relationship; -import org.neo4j.springframework.data.core.cypher.*; import org.neo4j.springframework.data.core.mapping.Neo4jPersistentEntity; import org.springframework.data.mapping.MappingException; import org.springframework.lang.Nullable; @@ -198,8 +198,8 @@ public Statement createRelationshipRemoveQuery(Neo4jPersistentEntity neo4jPer boolean outgoing = relationshipDescription.isOutgoing(); String relationshipType = relationshipDescription.isDynamic() ? null : relationshipDescription.getType(); - String relationshipToRemoveName = "rel"; + String relationshipToRemoveName = "rel"; Relationship relationship = outgoing ? startNode.relationshipTo(endNode, relationshipType).named(relationshipToRemoveName) : startNode.relationshipFrom(endNode, relationshipType).named(relationshipToRemoveName); @@ -311,9 +311,15 @@ private List generateListsOf(Collection relatio ? startNode.relationshipTo(endNode, relationshipType) : startNode.relationshipFrom(endNode, relationshipType); + MapProjection mapProjection = projectAllPropertiesAndRelationships(endNodeDescription, fieldName); + if (relationshipDescription.hasRelationshipProperties()) { + relationship = relationship.named(RelationshipDescription.NAME_OF_RELATIONSHIP); + mapProjection = mapProjection.and(relationship); + } + generatedLists.add(relationshipTargetName); generatedLists.add(listBasedOn(relationship) - .returning(projectAllPropertiesAndRelationships(endNodeDescription, fieldName))); + .returning(mapProjection)); } } diff --git a/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/core/schema/RelationshipDescription.java b/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/core/schema/RelationshipDescription.java index 4741cf30..fd7a60cc 100644 --- a/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/core/schema/RelationshipDescription.java +++ b/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/core/schema/RelationshipDescription.java @@ -21,6 +21,7 @@ import org.apiguardian.api.API; import org.jetbrains.annotations.NotNull; import org.neo4j.springframework.data.core.schema.Relationship.Direction; +import org.springframework.lang.Nullable; /** * Description of a relationship. Those descriptions always describe outgoing relationships. The inverse direction @@ -33,6 +34,8 @@ @API(status = API.Status.INTERNAL, since = "1.0") public interface RelationshipDescription { + String NAME_OF_RELATIONSHIP = "__relationship__"; + String NAME_OF_RELATIONSHIP_TYPE = "__relationshipType__"; /** @@ -79,6 +82,23 @@ public interface RelationshipDescription { */ Direction getDirection(); + /** + * If this is a relationship with properties, the properties-defining class will get returned, + * otherwise {@literal null}. + * + * @return The type of the relationship property class for relationship with properties, otherwise {@literal null} + */ + @Nullable + Class getRelationshipPropertiesClass(); + + /** + * Tells if this relationship is a relationship with additional properties. + * In such cases {@code getRelationshipPropertiesClass} will return the type of the properties holding class. + * + * @return {@literal true} if an additional properties are available, otherwise {@literal false} + */ + boolean hasRelationshipProperties(); + default boolean isOutgoing() { return Direction.OUTGOING.equals(this.getDirection()); } diff --git a/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/core/schema/RelationshipProperties.java b/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/core/schema/RelationshipProperties.java new file mode 100644 index 00000000..5bf3f61c --- /dev/null +++ b/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/core/schema/RelationshipProperties.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2019 "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.core.schema; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.apiguardian.api.API; + +/** + * This marker interface is used on classes to mark that they represent additional relationship properties. + * A class that implements this interface must not be used as a or annotated with {@link Node}. + * + * @author Gerrit Meier + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Documented +@Inherited +@API(status = API.Status.STABLE, since = "1.0") +public @interface RelationshipProperties { +} diff --git a/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/core/schema/Schema.java b/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/core/schema/Schema.java index d8bdbc7f..1390ce66 100644 --- a/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/core/schema/Schema.java +++ b/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/core/schema/Schema.java @@ -18,6 +18,7 @@ */ package org.neo4j.springframework.data.core.schema; +import java.util.HashMap; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -27,6 +28,8 @@ import org.apiguardian.api.API; import org.neo4j.driver.Record; import org.neo4j.driver.types.TypeSystem; +import org.neo4j.springframework.data.core.convert.Neo4jConverter; +import org.springframework.data.mapping.MappingException; import org.springframework.lang.Nullable; /** @@ -74,6 +77,16 @@ default NodeDescription getRequiredNodeDescription(Class underlyingClass) return nodeDescription; } + default NodeDescription getRequiredNodeDescription(String primaryLabel) { + NodeDescription nodeDescription = getNodeDescription(primaryLabel); + if (nodeDescription == null) { + throw new MappingException( + String.format("Required node description not found with primary label '%s'", primaryLabel)); + } + return nodeDescription; + } + + /** * Retrieves a schema based mapping function for the {@code targetClass}. The mapping function will expect a * record containing all the nodes and relationships necessary to fully populate an instance of the given class. @@ -90,16 +103,27 @@ default NodeDescription getRequiredNodeDescription(Class underlyingClass) * @throws UnknownEntityException When {@code targetClass} is not a managed class */ default BiFunction getRequiredMappingFunctionFor(Class targetClass) { - BiFunction mappingFunction = getMappingFunctionFor(targetClass); - if (mappingFunction == null) { + NodeDescription nodeDescription = getNodeDescription(targetClass); + if (nodeDescription == null) { throw new UnknownEntityException(targetClass); } - return mappingFunction; + return (typeSystem, record) -> getConverter().read(targetClass, record); } - @Nullable BiFunction getMappingFunctionFor(Class targetClass); + Neo4jConverter getConverter(); + + default Function> getRequiredBinderFunctionFor(Class sourceClass) { - Function> getRequiredBinderFunctionFor(Class sourceClass); + if (getNodeDescription(sourceClass) == null) { + throw new UnknownEntityException(sourceClass); + } + + return t -> { + Map parameters = new HashMap<>(); + getConverter().write(t, parameters); + return parameters; + }; + } /** * Creates or retrieves an instance of the given id generator class. During the lifetime of the schema, diff --git a/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/repository/query/Neo4jQuerySupport.java b/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/repository/query/Neo4jQuerySupport.java index 71369f3f..298a3519 100644 --- a/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/repository/query/Neo4jQuerySupport.java +++ b/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/repository/query/Neo4jQuerySupport.java @@ -89,15 +89,15 @@ protected final ResultProcessor getResultProcessor(ParameterAccessor parameterAc } else if (resultProcessor.getReturnedType().isProjecting()) { if (returnedType.isInterface()) { - mappingFunction = this.mappingContext.getMappingFunctionFor(domainType); + mappingFunction = this.mappingContext.getRequiredMappingFunctionFor(domainType); } else if (this.mappingContext.hasPersistentEntityFor(returnedType)) { - mappingFunction = this.mappingContext.getMappingFunctionFor(returnedType); + mappingFunction = this.mappingContext.getRequiredMappingFunctionFor(returnedType); } else { this.mappingContext.addPersistentEntity(returnedType); - mappingFunction = this.mappingContext.getMappingFunctionFor(returnedType); + mappingFunction = this.mappingContext.getRequiredMappingFunctionFor(returnedType); } } else { - mappingFunction = this.mappingContext.getMappingFunctionFor(domainType); + mappingFunction = this.mappingContext.getRequiredMappingFunctionFor(domainType); } return mappingFunction; } diff --git a/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/repository/support/Predicate.java b/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/repository/support/Predicate.java index 5162a05e..f21ed068 100644 --- a/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/repository/support/Predicate.java +++ b/spring-data-neo4j-rx/src/main/java/org/neo4j/springframework/data/repository/support/Predicate.java @@ -141,7 +141,7 @@ static Predicate create(Neo4jMappingContext mappingContext, Example examp } predicate.add(mode, condition); predicate.parameters.put(propertyName, optionalValue - .map(v -> converter.writeValue(v, ((Neo4jPersistentProperty) graphProperty).getTypeInformation())) + .map(v -> converter.writeValueFromProperty(v, ((Neo4jPersistentProperty) graphProperty).getTypeInformation())) .get()); } } diff --git a/spring-data-neo4j-rx/src/test/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jConverterTest.java b/spring-data-neo4j-rx/src/test/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jConverterTest.java index 4557375a..9d9e3a97 100644 --- a/spring-data-neo4j-rx/src/test/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jConverterTest.java +++ b/spring-data-neo4j-rx/src/test/java/org/neo4j/springframework/data/core/mapping/DefaultNeo4jConverterTest.java @@ -42,7 +42,7 @@ */ class DefaultNeo4jConverterTest { - private final DefaultNeo4jConverter defaultNeo4jConverter = new DefaultNeo4jConverter(new Neo4jConversions()); + private final DefaultNeo4jConverter defaultNeo4jConverter = new DefaultNeo4jConverter(new Neo4jConversions(), null); @Nested class Reads { @@ -51,7 +51,7 @@ void shouldCatchConversionErrors() { Value value = Values.value("Das funktioniert nicht."); assertThatExceptionOfType(TypeMismatchDataAccessException.class) - .isThrownBy(() -> defaultNeo4jConverter.readValue(value, ClassTypeInformation.from(Date.class))) + .isThrownBy(() -> defaultNeo4jConverter.readValueForProperty(value, ClassTypeInformation.from(Date.class))) .withMessageStartingWith("Could not convert \"Das funktioniert nicht.\" into java.util.Date;") .withCauseInstanceOf(ConversionFailedException.class) .withRootCauseInstanceOf(DateTimeParseException.class); @@ -62,7 +62,7 @@ void shouldCatchUncoercibleErrors() { Value value = Values.value("Das funktioniert nicht."); assertThatExceptionOfType(TypeMismatchDataAccessException.class) - .isThrownBy(() -> defaultNeo4jConverter.readValue(value, ClassTypeInformation.from(LocalDate.class))) + .isThrownBy(() -> defaultNeo4jConverter.readValueForProperty(value, ClassTypeInformation.from(LocalDate.class))) .withMessageStartingWith("Could not convert \"Das funktioniert nicht.\" into java.time.LocalDate;") .withCauseInstanceOf(ConversionFailedException.class) .withRootCauseInstanceOf(Uncoercible.class); @@ -74,7 +74,7 @@ void shouldCatchUncoerfcibleErrors() { assertThatExceptionOfType(TypeMismatchDataAccessException.class) .isThrownBy( - () -> defaultNeo4jConverter.readValue(value, ClassTypeInformation.from(ReactiveNeo4jClient.class))) + () -> defaultNeo4jConverter.readValueForProperty(value, ClassTypeInformation.from(ReactiveNeo4jClient.class))) .withMessageStartingWith( "Could not convert \"Das funktioniert nicht.\" into org.neo4j.springframework.data.core.ReactiveNeo4jClient;") .withRootCauseInstanceOf(ConverterNotFoundException.class); diff --git a/spring-data-neo4j-rx/src/test/java/org/neo4j/springframework/data/core/mapping/Neo4jMappingContextTest.java b/spring-data-neo4j-rx/src/test/java/org/neo4j/springframework/data/core/mapping/Neo4jMappingContextTest.java index b1f36375..a2b45747 100644 --- a/spring-data-neo4j-rx/src/test/java/org/neo4j/springframework/data/core/mapping/Neo4jMappingContextTest.java +++ b/spring-data-neo4j-rx/src/test/java/org/neo4j/springframework/data/core/mapping/Neo4jMappingContextTest.java @@ -119,20 +119,13 @@ void shouldPreventIllegalIdTypes() { .withMessageMatching("Internally generated ids can only be assigned to one of .*"); } - @Test - void shouldNotProvideMappingForUnknownClasses() { - - Neo4jMappingContext schema = new Neo4jMappingContext(); - assertThat(schema.getMappingFunctionFor(UserNode.class)).isNull(); - } - @Test void targetTypeOfAssociationsShouldBeKnownToTheMappingContext() { Neo4jMappingContext schema = new Neo4jMappingContext(); Neo4jPersistentEntity bikeNodeEntity = schema.getPersistentEntity(BikeNode.class); bikeNodeEntity.doWithAssociations((Association association) -> - assertThat(schema.getMappingFunctionFor(association.getInverse().getAssociationTargetType())).isNotNull()); + assertThat(schema.getRequiredMappingFunctionFor(association.getInverse().getAssociationTargetType())).isNotNull()); } @Test diff --git a/spring-data-neo4j-rx/src/test/java/org/neo4j/springframework/data/integration/imperative/PersonWithRelationshipWithPropertiesRepository.java b/spring-data-neo4j-rx/src/test/java/org/neo4j/springframework/data/integration/imperative/PersonWithRelationshipWithPropertiesRepository.java new file mode 100644 index 00000000..52e31637 --- /dev/null +++ b/spring-data-neo4j-rx/src/test/java/org/neo4j/springframework/data/integration/imperative/PersonWithRelationshipWithPropertiesRepository.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2019 "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 org.neo4j.springframework.data.integration.shared.PersonWithRelationshipWithProperties; +import org.neo4j.springframework.data.repository.Neo4jRepository; +import org.neo4j.springframework.data.repository.query.Query; +import org.springframework.data.repository.query.Param; + +/** + * @author Gerrit Meier + */ +public interface PersonWithRelationshipWithPropertiesRepository extends Neo4jRepository { + + @Query("MATCH (p:PersonWithRelationshipWithProperties)-[l:LIKES]->(h:Hobby) return p, collect(l), collect(h)") + PersonWithRelationshipWithProperties loadFromCustomQuery(@Param("id") Long id); +} + diff --git a/spring-data-neo4j-rx/src/test/java/org/neo4j/springframework/data/integration/imperative/RepositoryIT.java b/spring-data-neo4j-rx/src/test/java/org/neo4j/springframework/data/integration/imperative/RepositoryIT.java index 715df017..47768ae8 100644 --- a/spring-data-neo4j-rx/src/test/java/org/neo4j/springframework/data/integration/imperative/RepositoryIT.java +++ b/spring-data-neo4j-rx/src/test/java/org/neo4j/springframework/data/integration/imperative/RepositoryIT.java @@ -36,6 +36,7 @@ import java.util.stream.IntStream; import java.util.stream.StreamSupport; +import org.assertj.core.data.MapEntry; import org.assertj.core.groups.Tuple; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -53,6 +54,7 @@ import org.neo4j.springframework.data.repository.config.EnableNeo4jRepositories; import org.neo4j.springframework.data.test.Neo4jExtension.Neo4jConnectionSupport; import org.neo4j.springframework.data.test.Neo4jIntegrationTest; +import org.neo4j.springframework.data.types.CartesianPoint2d; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -97,6 +99,7 @@ static PersonWithAllConstructor personExample(String sameValue) { private final PersonRepository repository; private final ThingRepository thingRepository; private final RelationshipRepository relationshipRepository; + private final PersonWithRelationshipWithPropertiesRepository relationshipWithPropertiesRepository; private final PetRepository petRepository; private final Driver driver; private Long id1; @@ -106,12 +109,14 @@ static PersonWithAllConstructor personExample(String sameValue) { @Autowired RepositoryIT(PersonRepository repository, ThingRepository thingRepository, - RelationshipRepository relationshipRepository, PetRepository petRepository, Driver driver) { + RelationshipRepository relationshipRepository, PetRepository petRepository, Driver driver, + PersonWithRelationshipWithPropertiesRepository relationshipWithPropertiesRepository) { this.repository = repository; this.relationshipRepository = relationshipRepository; this.thingRepository = thingRepository; this.petRepository = petRepository; + this.relationshipWithPropertiesRepository = relationshipWithPropertiesRepository; this.driver = driver; } @@ -408,6 +413,111 @@ void loadEntityWithRelationshipWithAssignedId() { assertThat(relatedThing.getName()).isEqualTo("Thing1"); } + @Test + void loadEntityWithRelationshipWithProperties() { + + long personId; + long hobbyNode1Id; + long hobbyNode2Id; + + try (Session session = driver.session()) { + Record record = session + .run("CREATE (n:PersonWithRelationshipWithProperties{name:'Freddie'})," + + " (n)-[l1:LIKES" + + "{since: 1995, active: true, localDate: date('1995-02-26'), myEnum: 'SOMETHING', point: point({x: 0, y: 1})}" + + "]->(h1:Hobby{name:'Music'})," + + " (n)-[l2:LIKES" + + "{since: 2000, active: false, localDate: date('2000-06-28'), myEnum: 'SOMETHING_DIFFERENT', point: point({x: 2, y: 3})}" + + "]->(h2:Hobby{name:'Something else'})" + + "RETURN n, h1, h2").single(); + + Node personNode = record.get("n").asNode(); + Node hobbyNode1 = record.get("h1").asNode(); + Node hobbyNode2 = record.get("h2").asNode(); + + personId = personNode.id(); + hobbyNode1Id = hobbyNode1.id(); + hobbyNode2Id = hobbyNode2.id(); + } + + Optional optionalPerson = relationshipWithPropertiesRepository.findById(personId); + assertThat(optionalPerson).isPresent(); + PersonWithRelationshipWithProperties person = optionalPerson.get(); + assertThat(person.getName()).isEqualTo("Freddie"); + + Hobby hobby1 = new Hobby(); + hobby1.setName("Music"); + hobby1.setId(hobbyNode1Id); + LikesHobbyRelationship rel1 = new LikesHobbyRelationship(1995); + rel1.setActive(true); + rel1.setLocalDate(LocalDate.of(1995, 2, 26)); + rel1.setMyEnum(LikesHobbyRelationship.MyEnum.SOMETHING); + rel1.setPoint(new CartesianPoint2d(0d, 1d)); + + Hobby hobby2 = new Hobby(); + hobby2.setName("Something else"); + hobby2.setId(hobbyNode2Id); + LikesHobbyRelationship rel2 = new LikesHobbyRelationship(2000); + rel2.setActive(false); + rel2.setLocalDate(LocalDate.of(2000, 6, 28)); + rel2.setMyEnum(LikesHobbyRelationship.MyEnum.SOMETHING_DIFFERENT); + rel2.setPoint(new CartesianPoint2d(2d, 3d)); + + assertThat(person.getHobbies()).contains(MapEntry.entry(hobby1, rel1), MapEntry.entry(hobby2, rel2)); + } + + + @Test + void loadEntityWithRelationshipWithPropertiesFromCustomQuery() { + + long personId; + long hobbyNode1Id; + long hobbyNode2Id; + + try (Session session = driver.session()) { + Record record = session + .run("CREATE (n:PersonWithRelationshipWithProperties{name:'Freddie'})," + + " (n)-[l1:LIKES" + + "{since: 1995, active: true, localDate: date('1995-02-26'), myEnum: 'SOMETHING', point: point({x: 0, y: 1})}" + + "]->(h1:Hobby{name:'Music'})," + + " (n)-[l2:LIKES" + + "{since: 2000, active: false, localDate: date('2000-06-28'), myEnum: 'SOMETHING_DIFFERENT', point: point({x: 2, y: 3})}" + + "]->(h2:Hobby{name:'Something else'})" + + "RETURN n, h1, h2").single(); + + Node personNode = record.get("n").asNode(); + Node hobbyNode1 = record.get("h1").asNode(); + Node hobbyNode2 = record.get("h2").asNode(); + + personId = personNode.id(); + hobbyNode1Id = hobbyNode1.id(); + hobbyNode2Id = hobbyNode2.id(); + } + + PersonWithRelationshipWithProperties person = relationshipWithPropertiesRepository.loadFromCustomQuery(personId); + assertThat(person.getName()).isEqualTo("Freddie"); + + Hobby hobby1 = new Hobby(); + hobby1.setName("Music"); + hobby1.setId(hobbyNode1Id); + LikesHobbyRelationship rel1 = new LikesHobbyRelationship(1995); + rel1.setActive(true); + rel1.setLocalDate(LocalDate.of(1995, 2, 26)); + rel1.setMyEnum(LikesHobbyRelationship.MyEnum.SOMETHING); + rel1.setPoint(new CartesianPoint2d(0d, 1d)); + + Hobby hobby2 = new Hobby(); + hobby2.setName("Something else"); + hobby2.setId(hobbyNode2Id); + LikesHobbyRelationship rel2 = new LikesHobbyRelationship(2000); + rel2.setActive(false); + rel2.setLocalDate(LocalDate.of(2000, 6, 28)); + rel2.setMyEnum(LikesHobbyRelationship.MyEnum.SOMETHING_DIFFERENT); + rel2.setPoint(new CartesianPoint2d(2d, 3d)); + + assertThat(person.getHobbies()).contains(MapEntry.entry(hobby1, rel1), MapEntry.entry(hobby2, rel2)); + } + @Test void existsById() { boolean exists = repository.existsById(id1); diff --git a/spring-data-neo4j-rx/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactivePersonWithRelationshipWithPropertiesRepository.java b/spring-data-neo4j-rx/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactivePersonWithRelationshipWithPropertiesRepository.java new file mode 100644 index 00000000..905fefdc --- /dev/null +++ b/spring-data-neo4j-rx/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactivePersonWithRelationshipWithPropertiesRepository.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2019 "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 reactor.core.publisher.Mono; + +import org.neo4j.springframework.data.integration.shared.PersonWithRelationshipWithProperties; +import org.neo4j.springframework.data.repository.ReactiveNeo4jRepository; +import org.neo4j.springframework.data.repository.query.Query; +import org.springframework.data.repository.query.Param; + +/** + * @author Gerrit Meier + */ +public interface ReactivePersonWithRelationshipWithPropertiesRepository + extends ReactiveNeo4jRepository { + + @Query("MATCH (p:PersonWithRelationshipWithProperties)-[l:LIKES]->(h:Hobby) return p, collect(l), collect(h)") + Mono loadFromCustomQuery(@Param("id") Long id); +} diff --git a/spring-data-neo4j-rx/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveRepositoryIT.java b/spring-data-neo4j-rx/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveRepositoryIT.java index d5f2ec69..76dfea0f 100644 --- a/spring-data-neo4j-rx/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveRepositoryIT.java +++ b/spring-data-neo4j-rx/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveRepositoryIT.java @@ -38,6 +38,7 @@ import java.util.Map; import java.util.stream.IntStream; +import org.assertj.core.data.MapEntry; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; @@ -54,13 +55,15 @@ import org.neo4j.springframework.data.integration.shared.AnotherThingWithAssignedId; import org.neo4j.springframework.data.integration.shared.Club; import org.neo4j.springframework.data.integration.shared.Hobby; +import org.neo4j.springframework.data.integration.shared.LikesHobbyRelationship; import org.neo4j.springframework.data.integration.shared.PersonWithAllConstructor; import org.neo4j.springframework.data.integration.shared.PersonWithRelationship; import org.neo4j.springframework.data.integration.shared.Pet; import org.neo4j.springframework.data.integration.shared.ThingWithAssignedId; import org.neo4j.springframework.data.repository.config.EnableReactiveNeo4jRepositories; -import org.neo4j.springframework.data.test.Neo4jExtension.*; import org.neo4j.springframework.data.test.Neo4jIntegrationTest; +import org.neo4j.springframework.data.test.Neo4jExtension.*; +import org.neo4j.springframework.data.types.CartesianPoint2d; import org.reactivestreams.Publisher; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; @@ -97,6 +100,7 @@ class ReactiveRepositoryIT { @Autowired private ReactiveThingRepository thingRepository; @Autowired private ReactiveRelationshipRepository relationshipRepository; @Autowired private ReactivePetRepository petRepository; + @Autowired private ReactivePersonWithRelationshipWithPropertiesRepository relationshipWithPropertiesRepository; @Autowired private Driver driver; @Autowired private ReactiveTransactionManager transactionManager; private long id1; @@ -379,6 +383,116 @@ void loadEntityWithRelationshipWithAssignedId() { .verifyComplete(); } + @Test + void loadEntityWithRelationshipWithProperties() { + + long personId; + long hobbyNode1Id; + long hobbyNode2Id; + + try (Session session = driver.session()) { + Record record = session + .run("CREATE (n:PersonWithRelationshipWithProperties{name:'Freddie'})," + + " (n)-[l1:LIKES" + + "{since: 1995, active: true, localDate: date('1995-02-26'), myEnum: 'SOMETHING', point: point({x: 0, y: 1})}" + + "]->(h1:Hobby{name:'Music'})," + + " (n)-[l2:LIKES" + + "{since: 2000, active: false, localDate: date('2000-06-28'), myEnum: 'SOMETHING_DIFFERENT', point: point({x: 2, y: 3})}" + + "]->(h2:Hobby{name:'Something else'})" + + "RETURN n, h1, h2").single(); + + Node personNode = record.get("n").asNode(); + Node hobbyNode1 = record.get("h1").asNode(); + Node hobbyNode2 = record.get("h2").asNode(); + + personId = personNode.id(); + hobbyNode1Id = hobbyNode1.id(); + hobbyNode2Id = hobbyNode2.id(); + } + + StepVerifier.create(relationshipWithPropertiesRepository.findById(personId)) + .assertNext(person -> { + assertThat(person.getName()).isEqualTo("Freddie"); + + Hobby hobby1 = new Hobby(); + hobby1.setName("Music"); + hobby1.setId(hobbyNode1Id); + LikesHobbyRelationship rel1 = new LikesHobbyRelationship(1995); + rel1.setActive(true); + rel1.setLocalDate(LocalDate.of(1995, 2, 26)); + rel1.setMyEnum(LikesHobbyRelationship.MyEnum.SOMETHING); + rel1.setPoint(new CartesianPoint2d(0d, 1d)); + + Hobby hobby2 = new Hobby(); + hobby2.setName("Something else"); + hobby2.setId(hobbyNode2Id); + LikesHobbyRelationship rel2 = new LikesHobbyRelationship(2000); + rel2.setActive(false); + rel2.setLocalDate(LocalDate.of(2000, 6, 28)); + rel2.setMyEnum(LikesHobbyRelationship.MyEnum.SOMETHING_DIFFERENT); + rel2.setPoint(new CartesianPoint2d(2d, 3d)); + + assertThat(person.getHobbies()).contains(MapEntry.entry(hobby1, rel1), MapEntry.entry(hobby2, rel2)); + }) + .verifyComplete(); + + } + + @Test + void loadEntityWithRelationshipWithPropertiesFromCustomQuery() { + + long personId; + long hobbyNode1Id; + long hobbyNode2Id; + + try (Session session = driver.session()) { + Record record = session + .run("CREATE (n:PersonWithRelationshipWithProperties{name:'Freddie'})," + + " (n)-[l1:LIKES" + + "{since: 1995, active: true, localDate: date('1995-02-26'), myEnum: 'SOMETHING', point: point({x: 0, y: 1})}" + + "]->(h1:Hobby{name:'Music'})," + + " (n)-[l2:LIKES" + + "{since: 2000, active: false, localDate: date('2000-06-28'), myEnum: 'SOMETHING_DIFFERENT', point: point({x: 2, y: 3})}" + + "]->(h2:Hobby{name:'Something else'})" + + "RETURN n, h1, h2").single(); + + Node personNode = record.get("n").asNode(); + Node hobbyNode1 = record.get("h1").asNode(); + Node hobbyNode2 = record.get("h2").asNode(); + + personId = personNode.id(); + hobbyNode1Id = hobbyNode1.id(); + hobbyNode2Id = hobbyNode2.id(); + } + + StepVerifier.create(relationshipWithPropertiesRepository.loadFromCustomQuery(personId)) + .assertNext(person -> { + assertThat(person.getName()).isEqualTo("Freddie"); + + Hobby hobby1 = new Hobby(); + hobby1.setName("Music"); + hobby1.setId(hobbyNode1Id); + LikesHobbyRelationship rel1 = new LikesHobbyRelationship(1995); + rel1.setActive(true); + rel1.setLocalDate(LocalDate.of(1995, 2, 26)); + rel1.setMyEnum(LikesHobbyRelationship.MyEnum.SOMETHING); + rel1.setPoint(new CartesianPoint2d(0d, 1d)); + + Hobby hobby2 = new Hobby(); + hobby2.setName("Something else"); + hobby2.setId(hobbyNode2Id); + LikesHobbyRelationship rel2 = new LikesHobbyRelationship(2000); + rel2.setActive(false); + rel2.setLocalDate(LocalDate.of(2000, 6, 28)); + rel2.setMyEnum(LikesHobbyRelationship.MyEnum.SOMETHING_DIFFERENT); + rel2.setPoint(new CartesianPoint2d(2d, 3d)); + + assertThat(person.getHobbies()).contains(MapEntry.entry(hobby1, rel1), MapEntry.entry(hobby2, rel2)); + }) + .verifyComplete(); + + } + @Test void findAllByIds() { List personList = Arrays.asList(person1, person2); diff --git a/spring-data-neo4j-rx/src/test/java/org/neo4j/springframework/data/integration/shared/Hobby.java b/spring-data-neo4j-rx/src/test/java/org/neo4j/springframework/data/integration/shared/Hobby.java index b614c23c..56412583 100644 --- a/spring-data-neo4j-rx/src/test/java/org/neo4j/springframework/data/integration/shared/Hobby.java +++ b/spring-data-neo4j-rx/src/test/java/org/neo4j/springframework/data/integration/shared/Hobby.java @@ -49,6 +49,13 @@ public void setName(String name) { this.name = name; } + @Override public String toString() { + return "Hobby{" + + "id=" + id + + ", name='" + name + '\'' + + '}'; + } + @Override public boolean equals(Object o) { if (this == o) { diff --git a/spring-data-neo4j-rx/src/test/java/org/neo4j/springframework/data/integration/shared/LikesHobbyRelationship.java b/spring-data-neo4j-rx/src/test/java/org/neo4j/springframework/data/integration/shared/LikesHobbyRelationship.java new file mode 100644 index 00000000..6b74c103 --- /dev/null +++ b/spring-data-neo4j-rx/src/test/java/org/neo4j/springframework/data/integration/shared/LikesHobbyRelationship.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2019 "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.time.LocalDate; +import java.util.Objects; + +import org.neo4j.springframework.data.core.schema.RelationshipProperties; +import org.neo4j.springframework.data.types.CartesianPoint2d; + +/** + * @author Gerrit Meier + */ +@RelationshipProperties +public class LikesHobbyRelationship { + + private final Integer since; + + private Boolean active; + + // use some properties that require conversion + // cypher type + private LocalDate localDate; + + // additional type + private MyEnum myEnum; + + // spatial type + private CartesianPoint2d point; + + + public LikesHobbyRelationship(Integer since) { + this.since = since; + } + + public void setActive(Boolean active) { + this.active = active; + } + + public void setLocalDate(LocalDate localDate) { + this.localDate = localDate; + } + + public void setMyEnum(MyEnum myEnum) { + this.myEnum = myEnum; + } + + public void setPoint(CartesianPoint2d point) { + this.point = point; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + LikesHobbyRelationship that = (LikesHobbyRelationship) o; + return since.equals(that.since) && + Objects.equals(active, that.active) && + Objects.equals(localDate, that.localDate) && + myEnum == that.myEnum && + Objects.equals(point, that.point); + } + + @Override + public int hashCode() { + return Objects.hash(since, active, localDate, myEnum, point); + } + + /** + * The missing javadoc + */ + public enum MyEnum { + SOMETHING, SOMETHING_DIFFERENT + } +} diff --git a/spring-data-neo4j-rx/src/test/java/org/neo4j/springframework/data/integration/shared/PersonWithRelationshipWithProperties.java b/spring-data-neo4j-rx/src/test/java/org/neo4j/springframework/data/integration/shared/PersonWithRelationshipWithProperties.java new file mode 100644 index 00000000..a42c10b4 --- /dev/null +++ b/spring-data-neo4j-rx/src/test/java/org/neo4j/springframework/data/integration/shared/PersonWithRelationshipWithProperties.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2019 "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.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; +import org.neo4j.springframework.data.core.schema.Relationship; + +/** + * @author Gerrit Meier + */ +@Node +public class PersonWithRelationshipWithProperties { + + @Id @GeneratedValue private Long id; + + private final String name; + + @Relationship("LIKES") + private Map hobbies; + + public PersonWithRelationshipWithProperties(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public Map getHobbies() { + return hobbies; + } +} diff --git a/spring-data-neo4j-rx/src/test/java/org/neo4j/springframework/data/integration/shared/TestSequenceGenerator.java b/spring-data-neo4j-rx/src/test/java/org/neo4j/springframework/data/integration/shared/TestSequenceGenerator.java index 166dc226..6f0435f8 100644 --- a/spring-data-neo4j-rx/src/test/java/org/neo4j/springframework/data/integration/shared/TestSequenceGenerator.java +++ b/spring-data-neo4j-rx/src/test/java/org/neo4j/springframework/data/integration/shared/TestSequenceGenerator.java @@ -20,6 +20,7 @@ import java.util.concurrent.atomic.AtomicInteger; +import org.neo4j.driver.Driver; import org.neo4j.springframework.data.core.schema.IdGenerator; import org.springframework.util.StringUtils; @@ -32,6 +33,16 @@ public class TestSequenceGenerator implements IdGenerator { private final AtomicInteger sequence = new AtomicInteger(0); + /** + * Use an instance of the {@link Driver} bean here to ensure that also injection works when the {@link IdGenerator} + * gets created. + **/ + private final Driver driver; + + public TestSequenceGenerator(Driver driver) { + this.driver = driver; + } + @Override public String generateId(String primaryLabel, Object entity) { return StringUtils.uncapitalize(primaryLabel) + "-" + sequence.incrementAndGet();