DATAGRAPH-1373 - Improve RelationshipProperties API.

The relationship with properties will now define the `TargetNode`
that points to the entity that should be referred to, either outgoing or
incoming.
The syntax with the `Map` usage was discarded because it has
limitations in the convinience usage of the derived finders and did not
allow the definition of mulitple relationships of the same type to the
same node.
This commit is contained in:
Gerrit Meier
2020-08-28 11:14:05 +02:00
parent 57d435490d
commit 4e2a18ba1e
30 changed files with 698 additions and 123 deletions

20
etc/adr/adr-005.adoc Normal file
View File

@@ -0,0 +1,20 @@
== ADR 5: Change relationship with properties from association to property
=== Status
rejected
=== Context
A definition of relationship with properties pointing to the current `@RelationshipProperties` annotated property class creates a Spring Data association.
Because the annotated class is not an entity this should not be an association in the first place but an entity property.
The contextual problem came up in the discussion of the refactoring of the relationship with properties (ADR-6).
=== Decision
Treat relationship with properties as properties on the defining entity.
=== Consequences
Requires refactoring of internal schema creation and mapping. There should not be any consequences user-facing.

20
etc/adr/adr-006.adoc Normal file
View File

@@ -0,0 +1,20 @@
== ADR 6: Refactor definition of relationship with properties
=== Status
accepted
=== Context
The current implementation of relationship with properties has two major flaws:
. No possibility to create derived finders reaching into the target entity.
. Impossible to create two relationships of the same type connecting the same target node.
=== Decision
Don't use the format of the `Map<Entity, RelationshipPropertiesClass>` but instead define the target node within the `@RelationshipProperties` class.
=== Consequences
Users will have to refactor the existing `@RelationshipProperties` based relationships.

View File

@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.neo4j.core.support;
package org.springframework.data.neo4j.core;
import java.util.AbstractMap.SimpleEntry;
import java.util.Collection;
@@ -21,6 +21,7 @@ import java.util.Collections;
import java.util.Map;
import java.util.stream.Collectors;
import org.apiguardian.api.API;
import org.springframework.data.neo4j.core.mapping.Neo4jPersistentProperty;
import org.springframework.lang.Nullable;
@@ -28,7 +29,7 @@ import org.springframework.lang.Nullable;
* @author Michael J. Simons
* @author Philipp Tölle
*/
public final class Relationships {
final class MappingSupport {
/**
* The value for a relationship can be a scalar object (1:1), a collection (1:n), a map (1:n, but with dynamic
@@ -51,7 +52,7 @@ public final class Relationships {
unifiedValue = ((Map<String, Object>) rawValue).entrySet();
}
} else if (property.isRelationshipWithProperties()) {
unifiedValue = ((Map<Object, Object>) rawValue).entrySet();
unifiedValue = (Collection<Object>) rawValue;
} else if (property.isCollectionLike()) {
unifiedValue = (Collection<Object>) rawValue;
} else {
@@ -60,5 +61,28 @@ public final class Relationships {
return unifiedValue;
}
private Relationships() {}
private MappingSupport() {}
/**
* Class that defines a tuple of relationship with properties and the connected target entity.
*/
@API(status = API.Status.INTERNAL)
final static class RelationshipPropertiesWithEntityHolder {
private final Object relationshipProperties;
private final Object relatedEntity;
RelationshipPropertiesWithEntityHolder(Object relationshipProperties, Object relatedEntity) {
this.relationshipProperties = relationshipProperties;
this.relatedEntity = relatedEntity;
}
Object getRelationshipProperties() {
return relationshipProperties;
}
Object getRelatedEntity() {
return relatedEntity;
}
}
}

View File

@@ -53,7 +53,6 @@ import org.springframework.data.neo4j.core.schema.Constants;
import org.springframework.data.neo4j.core.schema.CypherGenerator;
import org.springframework.data.neo4j.core.schema.NodeDescription;
import org.springframework.data.neo4j.core.schema.RelationshipDescription;
import org.springframework.data.neo4j.core.support.Relationships;
import org.springframework.data.neo4j.repository.NoResultException;
import org.springframework.data.neo4j.repository.event.BeforeBindCallback;
import org.springframework.data.util.ClassTypeInformation;
@@ -404,7 +403,7 @@ public final class Neo4jTemplate implements Neo4jOperations, BeanFactoryAware {
NestedRelationshipContext relationshipContext = NestedRelationshipContext.of(association, propertyAccessor,
neo4jPersistentEntity);
Collection<?> relatedValuesToStore = Relationships.unifyRelationshipValue(relationshipContext.getInverse(),
Collection<?> relatedValuesToStore = MappingSupport.unifyRelationshipValue(relationshipContext.getInverse(),
relationshipContext.getValue());
RelationshipDescription relationshipDescription = relationshipContext.getRelationship();
@@ -439,13 +438,13 @@ public final class Neo4jTemplate implements Neo4jOperations, BeanFactoryAware {
for (Object relatedValueToStore : relatedValuesToStore) {
// here map entry is not always anymore a dynamic association
Object valueToBeSavedPreEvt = relationshipContext.identifyAndExtractRelationshipValue(relatedValueToStore);
valueToBeSavedPreEvt = eventSupport.maybeCallBeforeBind(valueToBeSavedPreEvt);
Object relatedNode = relationshipContext.identifyAndExtractRelationshipTargetNode(relatedValueToStore);
relatedNode = eventSupport.maybeCallBeforeBind(relatedNode);
Neo4jPersistentEntity<?> targetNodeDescription = neo4jMappingContext
.getPersistentEntity(valueToBeSavedPreEvt.getClass());
.getPersistentEntity(relatedNode.getClass());
Long relatedInternalId = saveRelatedNode(valueToBeSavedPreEvt, relationshipContext.getAssociationTargetType(),
Long relatedInternalId = saveRelatedNode(relatedNode, relationshipContext.getAssociationTargetType(),
targetNodeDescription, inDatabase);
RelationshipStatementHolder statementHolder = RelationshipStatementHolder.createStatement(neo4jMappingContext,
@@ -458,11 +457,11 @@ public final class Neo4jTemplate implements Neo4jOperations, BeanFactoryAware {
// if an internal id is used this must get set to link this entity in the next iteration
if (targetNodeDescription.isUsingInternalIds()) {
PersistentPropertyAccessor<?> targetPropertyAccessor = targetNodeDescription
.getPropertyAccessor(valueToBeSavedPreEvt);
.getPropertyAccessor(relatedNode);
targetPropertyAccessor.setProperty(targetNodeDescription.getRequiredIdProperty(), relatedInternalId);
}
if (processState != ProcessState.PROCESSED_ALL_VALUES) {
processNestedRelations(targetNodeDescription, valueToBeSavedPreEvt, inDatabase, stateMachine);
processNestedRelations(targetNodeDescription, relatedNode, inDatabase, stateMachine);
}
}
});

View File

@@ -15,6 +15,9 @@
*/
package org.springframework.data.neo4j.core;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import org.springframework.data.mapping.Association;
@@ -22,6 +25,7 @@ import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity;
import org.springframework.data.neo4j.core.mapping.Neo4jPersistentProperty;
import org.springframework.data.neo4j.core.schema.RelationshipDescription;
import org.springframework.data.neo4j.core.schema.TargetNode;
import org.springframework.lang.Nullable;
/**
@@ -75,17 +79,19 @@ final class NestedRelationshipContext {
return this.relationship.hasRelationshipProperties();
}
Object identifyAndExtractRelationshipValue(Object relatedValue) {
Object identifyAndExtractRelationshipTargetNode(Object relatedValue) {
Object valueToBeSaved = relatedValue;
if (relatedValue instanceof Map.Entry) {
Map.Entry relatedValueMapEntry = (Map.Entry) relatedValue;
if (this.getInverse().isDynamicAssociation()) {
valueToBeSaved = relatedValueMapEntry.getValue();
} else if (this.hasRelationshipWithProperties()) {
valueToBeSaved = relatedValueMapEntry.getKey();
}
}
if (this.hasRelationshipWithProperties()) {
// here comes the entity
valueToBeSaved = ((MappingSupport.RelationshipPropertiesWithEntityHolder) relatedValue).getRelatedEntity();
}
return valueToBeSaved;
}
@@ -96,15 +102,38 @@ final class NestedRelationshipContext {
Neo4jPersistentProperty inverse = handler.getInverse();
boolean inverseValueIsEmpty = propertyAccessor.getProperty(inverse) == null;
// value can be a collection or scalar of related notes, point to a relationship property (scalar or collection)
// or is a dynamic relationship (map)
Object value = propertyAccessor.getProperty(inverse);
RelationshipDescription relationship = neo4jPersistentEntity.getRelationships().stream()
.filter(r -> r.getFieldName().equals(inverse.getName())).findFirst().get();
// if we have a relationship with properties, the targetNodeType is the map key
Class<?> associationTargetType = relationship.hasRelationshipProperties() ? inverse.getComponentType()
: inverse.getAssociationTargetType();
Class<?> associationTargetType = inverse.getAssociationTargetType();
if (relationship.hasRelationshipProperties() && value != null) {
Neo4jPersistentEntity<?> relationshipPropertiesEntity = (Neo4jPersistentEntity<?>) relationship.getRelationshipPropertiesEntity();
List<MappingSupport.RelationshipPropertiesWithEntityHolder> relationshipProperties = new ArrayList<>();
for (Object relationshipProperty : ((Collection<Object>) value)) {
MappingSupport.RelationshipPropertiesWithEntityHolder oneOfThem =
new MappingSupport.RelationshipPropertiesWithEntityHolder(relationshipProperty,
getTargetNode(relationshipPropertiesEntity, relationshipProperty));
relationshipProperties.add(oneOfThem);
}
value = relationshipProperties;
}
return new NestedRelationshipContext(inverse, value, relationship, associationTargetType, inverseValueIsEmpty);
}
private static Object getTargetNode(Neo4jPersistentEntity<?> relationshipPropertiesEntity, Object object) {
PersistentPropertyAccessor<Object> propertyAccessor = relationshipPropertiesEntity.getPropertyAccessor(object);
return propertyAccessor.getProperty(relationshipPropertiesEntity.getPersistentProperty(TargetNode.class));
}
}

View File

@@ -57,7 +57,6 @@ import org.springframework.data.neo4j.core.schema.Constants;
import org.springframework.data.neo4j.core.schema.CypherGenerator;
import org.springframework.data.neo4j.core.schema.NodeDescription;
import org.springframework.data.neo4j.core.schema.RelationshipDescription;
import org.springframework.data.neo4j.core.support.Relationships;
import org.springframework.data.neo4j.repository.event.ReactiveBeforeBindCallback;
import org.springframework.data.util.ClassTypeInformation;
import org.springframework.lang.Nullable;
@@ -402,7 +401,7 @@ public final class ReactiveNeo4jTemplate implements ReactiveNeo4jOperations, Bea
NestedRelationshipContext relationshipContext = NestedRelationshipContext.of(association, propertyAccessor,
neo4jPersistentEntity);
Collection<?> relatedValuesToStore = Relationships.unifyRelationshipValue(relationshipContext.getInverse(),
Collection<?> relatedValuesToStore = MappingSupport.unifyRelationshipValue(relationshipContext.getInverse(),
relationshipContext.getValue());
RelationshipDescription relationshipDescription = relationshipContext.getRelationship();
@@ -436,7 +435,7 @@ public final class ReactiveNeo4jTemplate implements ReactiveNeo4jOperations, Bea
for (Object relatedValueToStore : relatedValuesToStore) {
Object valueToBeSavedPreEvt = relationshipContext.identifyAndExtractRelationshipValue(relatedValueToStore);
Object valueToBeSavedPreEvt = relationshipContext.identifyAndExtractRelationshipTargetNode(relatedValueToStore);
Mono<Void> createRelationship = eventSupport.maybeCallBeforeBind(valueToBeSavedPreEvt)
.flatMap(valueToBeSaved -> {

View File

@@ -64,7 +64,7 @@ final class RelationshipStatementHolder {
if (relationshipContext.hasRelationshipWithProperties()) {
return createStatementForRelationShipWithProperties(neo4jMappingContext, neo4jPersistentEntity,
relationshipContext, relatedInternalId, (Map.Entry) relatedValue);
relationshipContext, relatedInternalId, (MappingSupport.RelationshipPropertiesWithEntityHolder) relatedValue);
} else {
return createStatementForRelationshipWithoutProperties(neo4jMappingContext, neo4jPersistentEntity,
relationshipContext, relatedInternalId, relatedValue);
@@ -73,12 +73,13 @@ final class RelationshipStatementHolder {
private static RelationshipStatementHolder createStatementForRelationShipWithProperties(
Neo4jMappingContext neo4jMappingContext, Neo4jPersistentEntity<?> neo4jPersistentEntity,
NestedRelationshipContext relationshipContext, Long relatedInternalId, Map.Entry relatedValue) {
NestedRelationshipContext relationshipContext, Long relatedInternalId, MappingSupport.RelationshipPropertiesWithEntityHolder relatedValue) {
Statement relationshipCreationQuery = CypherGenerator.INSTANCE.createRelationshipWithPropertiesCreationQuery(
neo4jPersistentEntity, relationshipContext.getRelationship(), relatedInternalId);
Map<String, Object> propMap = new HashMap<>();
neo4jMappingContext.getConverter().write(relatedValue.getValue(), propMap);
// write relationship properties
neo4jMappingContext.getConverter().write(relatedValue.getRelationshipProperties(), propMap);
return new RelationshipStatementHolder(relationshipCreationQuery, propMap);
}

View File

@@ -57,6 +57,7 @@ import org.springframework.data.neo4j.core.convert.Neo4jConversions;
import org.springframework.data.neo4j.core.convert.Neo4jConverter;
import org.springframework.data.neo4j.core.schema.Constants;
import org.springframework.data.neo4j.core.schema.RelationshipDescription;
import org.springframework.data.neo4j.core.schema.TargetNode;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
@@ -179,7 +180,7 @@ final class DefaultNeo4jConverter implements Neo4jConverter {
nodeDescription.doWithProperties((Neo4jPersistentProperty p) -> {
// Skip the internal properties, we don't want them to end up stored as properties
if (p.isInternalIdProperty() || p.isDynamicLabels()) {
if (p.isInternalIdProperty() || p.isDynamicLabels() || p.isEntity()) {
return;
}
@@ -255,6 +256,19 @@ final class DefaultNeo4jConverter implements Neo4jConverter {
* @return
*/
private <ET> ET map(MapAccessor queryResult, Neo4jPersistentEntity<ET> nodeDescription, KnownObjects knownObjects) {
return map(queryResult, nodeDescription, knownObjects, null);
}
/**
* @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 lastMappedEntity Previous created entity for relationships, can be null.
* @param <ET> As in entity type
* @return
*/
private <ET> ET map(MapAccessor queryResult, Neo4jPersistentEntity<ET> nodeDescription, KnownObjects knownObjects,
@Nullable Object lastMappedEntity) {
List<String> allLabels = getLabels(queryResult);
NodeDescriptionAndLabels nodeDescriptionAndLabels = nodeDescriptionStore
@@ -274,8 +288,8 @@ final class DefaultNeo4jConverter implements Neo4jConverter {
// Fill simple properties
Predicate<Neo4jPersistentProperty> isConstructorParameter = concreteNodeDescription
.getPersistenceConstructor()::isConstructorParameter;
PropertyHandler<Neo4jPersistentProperty> handler = populateFrom(queryResult, propertyAccessor,
isConstructorParameter, nodeDescriptionAndLabels.getDynamicLabels());
PropertyHandler<Neo4jPersistentProperty> handler = populateFrom(queryResult, knownObjects, propertyAccessor,
isConstructorParameter, nodeDescriptionAndLabels.getDynamicLabels(), lastMappedEntity);
concreteNodeDescription.doWithProperties(handler);
// Fill associations
@@ -325,9 +339,9 @@ final class DefaultNeo4jConverter implements Neo4jConverter {
return INSTANTIATORS.getInstantiatorFor(nodeDescription).createInstance(nodeDescription, parameterValueProvider);
}
private PropertyHandler<Neo4jPersistentProperty> populateFrom(MapAccessor queryResult,
private PropertyHandler<Neo4jPersistentProperty> populateFrom(MapAccessor queryResult, KnownObjects knownObjects,
PersistentPropertyAccessor<?> propertyAccessor, Predicate<Neo4jPersistentProperty> isConstructorParameter,
Collection<String> surplusLabels) {
Collection<String> surplusLabels, Object targetNode) {
return property -> {
if (isConstructorParameter.test(property)) {
return;
@@ -336,6 +350,10 @@ final class DefaultNeo4jConverter implements Neo4jConverter {
if (property.isDynamicLabels()) {
propertyAccessor.setProperty(property,
createDynamicLabelsProperty(property.getTypeInformation(), surplusLabels));
} else if (property.isAnnotationPresent(TargetNode.class)) {
if (queryResult instanceof Relationship) {
propertyAccessor.setProperty(property, targetNode);
}
} else {
propertyAccessor.setProperty(property,
readValueForProperty(extractValueOf(property, queryResult), property.getTypeInformation()));
@@ -402,7 +420,7 @@ final class DefaultNeo4jConverter implements Neo4jConverter {
Value list = values.get(relationshipDescription.generateRelatedNodesCollectionName());
Map<Object, Object> relationshipsAndProperties = new HashMap<>();
List<Object> relationshipsAndProperties = new ArrayList<>();
// if the list is null the mapping is based on a custom query
if (Values.NULL.equals(list)) {
@@ -436,42 +454,37 @@ final class DefaultNeo4jConverter implements Neo4jConverter {
for (Relationship possibleRelationship : allMatchingTypeRelationshipsInResult) {
if (targetIdSelector.apply(possibleRelationship) == nodeId) {
Object mappedObject = map(possibleValueNode, concreteTargetNodeDescription, knownObjects);
Object mappedObject = knownObjects.computeIfAbsent(nodeId, () -> map(possibleValueNode, concreteTargetNodeDescription, knownObjects));
if (relationshipDescription.hasRelationshipProperties()) {
Class<?> propertiesClass = relationshipDescription.getRelationshipPropertiesClass();
Object relationshipProperties = map(possibleRelationship,
(Neo4jPersistentEntity) nodeDescriptionStore.getNodeDescription(propertiesClass), knownObjects);
relationshipsAndProperties.put(mappedObject, relationshipProperties);
(Neo4jPersistentEntity) relationshipDescription.getRelationshipPropertiesEntity(),
knownObjects, mappedObject);
relationshipsAndProperties.add(relationshipProperties);
} else {
mappedObjectHandler.accept(possibleRelationship.type(), mappedObject);
}
allMatchingTypeRelationshipsInResult.remove(possibleRelationship);
break;
}
}
}
} else {
for (Value relatedEntity : list.asList(Function.identity())) {
Neo4jPersistentProperty idProperty = concreteTargetNodeDescription.getRequiredIdProperty();
// internal (generated) id or external set
String relatedEntityIdKey = idProperty.isInternalIdProperty() ? Constants.NAME_OF_INTERNAL_ID
: concreteTargetNodeDescription.getIdDescription().getOptionalGraphPropertyName()
.orElse(idProperty.getName());
Object idValue = relatedEntity.get(relatedEntityIdKey);
Long internalIdValue = relatedEntity.get(Constants.NAME_OF_INTERNAL_ID).asLong();
Object valueEntry = knownObjects.computeIfAbsent(idValue,
Object valueEntry = knownObjects.computeIfAbsent(internalIdValue,
() -> map(relatedEntity, concreteTargetNodeDescription, knownObjects));
if (relationshipDescription.hasRelationshipProperties()) {
Relationship relatedEntityRelationship = relatedEntity.get(RelationshipDescription.NAME_OF_RELATIONSHIP)
.asRelationship();
Class<?> propertiesClass = relationshipDescription.getRelationshipPropertiesClass();
Object relationshipProperties = map(relatedEntityRelationship,
(Neo4jPersistentEntity) nodeDescriptionStore.getNodeDescription(propertiesClass), knownObjects);
relationshipsAndProperties.put(valueEntry, relationshipProperties);
(Neo4jPersistentEntity) relationshipDescription.getRelationshipPropertiesEntity(),
knownObjects, valueEntry);
relationshipsAndProperties.add(relationshipProperties);
} else {
mappedObjectHandler.accept(relatedEntity.get(RelationshipDescription.NAME_OF_RELATIONSHIP_TYPE).asString(),
valueEntry);
@@ -480,7 +493,9 @@ final class DefaultNeo4jConverter implements Neo4jConverter {
}
if (persistentProperty.getTypeInformation().isCollectionLike()) {
if (persistentProperty.getType().equals(Set.class)) {
if (relationshipDescription.hasRelationshipProperties()) {
return Optional.of(relationshipsAndProperties);
} else if (persistentProperty.getType().equals(Set.class)) {
return Optional.of(new HashSet(value));
} else {
return Optional.of(value);
@@ -489,7 +504,7 @@ final class DefaultNeo4jConverter implements Neo4jConverter {
if (relationshipDescription.isDynamic()) {
return Optional.ofNullable(dynamicValue.isEmpty() ? null : dynamicValue);
} else if (relationshipDescription.hasRelationshipProperties()) {
return Optional.of(relationshipsAndProperties);
return Optional.ofNullable(relationshipsAndProperties.isEmpty() ? null : relationshipsAndProperties.get(0));
} else {
return Optional.ofNullable(value.isEmpty() ? null : value.get(0));
}
@@ -513,14 +528,33 @@ final class DefaultNeo4jConverter implements Neo4jConverter {
private final Lock read = lock.readLock();
private final Lock write = lock.writeLock();
private Map<Object, Object> store = new HashMap<>();
private final Map<Long, Object> internalIdStore = new HashMap<>();
Object computeIfAbsent(Object key, Supplier<Object> entitySupplier) {
Object computeIfAbsent(Long internalId, Supplier<Object> entitySupplier) {
Object knownEntity = getObject(internalId);
// if it is not in the store, it has to get re-computed also for the internalIdStore
if (knownEntity != null) {
return knownEntity;
}
try {
write.lock();
Object computedEntity = entitySupplier.get();
internalIdStore.put(internalId, computedEntity);
return computedEntity;
} finally {
write.unlock();
}
}
@Nullable
private Object getObject(Long internalId) {
try {
read.lock();
Object knownEntity = store.get(key);
Object knownEntity = internalIdStore.get(internalId);
if (knownEntity != null) {
return knownEntity;
@@ -529,15 +563,7 @@ final class DefaultNeo4jConverter implements Neo4jConverter {
} finally {
read.unlock();
}
try {
write.lock();
Object computedEntity = entitySupplier.get();
store.put(key, computedEntity);
return computedEntity;
} finally {
write.unlock();
}
return null;
}
}
}

View File

@@ -42,6 +42,7 @@ import org.springframework.data.neo4j.core.schema.NodeDescription;
import org.springframework.data.neo4j.core.schema.Property;
import org.springframework.data.neo4j.core.schema.Relationship;
import org.springframework.data.neo4j.core.schema.RelationshipDescription;
import org.springframework.data.neo4j.core.schema.RelationshipProperties;
import org.springframework.data.support.IsNewStrategy;
import org.springframework.data.util.Lazy;
import org.springframework.data.util.TypeInformation;
@@ -87,6 +88,8 @@ class DefaultNeo4jPersistentEntity<T> extends BasicPersistentEntity<T, Neo4jPers
private final Lazy<Neo4jPersistentProperty> dynamicLabelsProperty;
private final Lazy<Boolean> isRelationshipPropertiesEntity;
DefaultNeo4jPersistentEntity(TypeInformation<T> information) {
super(information);
@@ -96,6 +99,7 @@ class DefaultNeo4jPersistentEntity<T> extends BasicPersistentEntity<T, Neo4jPers
this.graphProperties = Lazy.of(this::computeGraphProperties);
this.dynamicLabelsProperty = Lazy.of(() -> getGraphProperties().stream().map(Neo4jPersistentProperty.class::cast)
.filter(Neo4jPersistentProperty::isDynamicLabels).findFirst().orElse(null));
this.isRelationshipPropertiesEntity = Lazy.of(() -> isAnnotationPresent(RelationshipProperties.class));
}
/*
@@ -154,6 +158,11 @@ class DefaultNeo4jPersistentEntity<T> extends BasicPersistentEntity<T, Neo4jPers
return this.dynamicLabelsProperty.getOptional();
}
@Override
public boolean isRelationshipPropertiesEntity() {
return this.isRelationshipPropertiesEntity.get();
}
/*
* (non-Javadoc)
* @see BasicPersistentEntity#getFallbackIsNewStrategy()

View File

@@ -27,6 +27,7 @@ import org.springframework.data.neo4j.core.schema.NodeDescription;
import org.springframework.data.neo4j.core.schema.Relationship;
import org.springframework.data.neo4j.core.schema.RelationshipDescription;
import org.springframework.data.neo4j.core.schema.RelationshipProperties;
import org.springframework.data.neo4j.core.schema.TargetNode;
import org.springframework.data.util.Lazy;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable;
@@ -41,6 +42,7 @@ class DefaultNeo4jPersistentProperty extends AnnotationBasedPersistentProperty<N
private final Lazy<String> graphPropertyName;
private final Lazy<Boolean> isAssociation;
private final boolean isEntityInRelationshipWithProperties;
private final Neo4jMappingContext mappingContext;
@@ -52,15 +54,16 @@ class DefaultNeo4jPersistentProperty extends AnnotationBasedPersistentProperty<N
* @param simpleTypeHolder type holder
*/
DefaultNeo4jPersistentProperty(Property property, PersistentEntity<?, Neo4jPersistentProperty> owner,
Neo4jMappingContext mappingContext, SimpleTypeHolder simpleTypeHolder) {
Neo4jMappingContext mappingContext, SimpleTypeHolder simpleTypeHolder, boolean isEntityInRelationshipWithProperties) {
super(property, owner, simpleTypeHolder);
this.isEntityInRelationshipWithProperties = isEntityInRelationshipWithProperties;
this.graphPropertyName = Lazy.of(this::computeGraphPropertyName);
this.isAssociation = Lazy.of(() -> {
Class<?> targetType = getActualType();
return !(simpleTypeHolder.isSimpleType(targetType) || mappingContext.hasCustomWriteTarget(targetType));
return !(simpleTypeHolder.isSimpleType(targetType) || mappingContext.hasCustomWriteTarget(targetType) || isAnnotationPresent(TargetNode.class));
});
this.mappingContext = mappingContext;
}
@@ -72,7 +75,8 @@ class DefaultNeo4jPersistentProperty extends AnnotationBasedPersistentProperty<N
// 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());
Class<?> type = this.mappingContext.getPersistentEntity(getActualType()).getPersistentProperty(TargetNode.class).getType();
obverseOwner = this.mappingContext.getPersistentEntity(type);
} else {
obverseOwner = this.mappingContext.getPersistentEntity(this.getAssociationTargetType());
}
@@ -95,7 +99,10 @@ class DefaultNeo4jPersistentProperty extends AnnotationBasedPersistentProperty<N
// 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();
Neo4jPersistentEntity<?> relationshipPropertiesClass =
this.hasActualTypeAnnotation(RelationshipProperties.class)
? this.mappingContext.getPersistentEntity(getActualType())
: null;
// Try to determine if there is a relationship definition that expresses logically the same relationship
// on the other end.
@@ -135,7 +142,12 @@ class DefaultNeo4jPersistentProperty extends AnnotationBasedPersistentProperty<N
@Override
public boolean isEntity() {
return super.isEntity() && isAssociation();
return super.isEntity() && isAssociation() || (super.isEntity() && isEntityInRelationshipWithProperties());
}
@Override
public boolean isEntityInRelationshipWithProperties() {
return isEntityInRelationshipWithProperties;
}
/**

View File

@@ -42,13 +42,13 @@ class DefaultRelationshipDescription extends Association<Neo4jPersistentProperty
private final Relationship.Direction direction;
private Class<?> relationshipPropertiesClass;
private final NodeDescription<?> relationshipPropertiesClass;
private RelationshipDescription relationshipObverse;
DefaultRelationshipDescription(Neo4jPersistentProperty inverse, @Nullable RelationshipDescription relationshipObverse,
String type, boolean dynamic, NodeDescription<?> source, String fieldName, NodeDescription<?> target,
Relationship.Direction direction, @Nullable Class<?> relationshipPropertiesClass) {
Relationship.Direction direction, @Nullable NodeDescription<?> relationshipProperties) {
// the immutable obverse association-wise is always null because we cannot determine them on both sides
// if we consider to support bidirectional relationships.
@@ -61,7 +61,7 @@ class DefaultRelationshipDescription extends Association<Neo4jPersistentProperty
this.fieldName = fieldName;
this.target = target;
this.direction = direction;
this.relationshipPropertiesClass = relationshipPropertiesClass;
this.relationshipPropertiesClass = relationshipProperties;
}
@Override
@@ -95,13 +95,13 @@ class DefaultRelationshipDescription extends Association<Neo4jPersistentProperty
}
@Override
public Class<?> getRelationshipPropertiesClass() {
public NodeDescription<?> getRelationshipPropertiesEntity() {
return relationshipPropertiesClass;
}
@Override
public boolean hasRelationshipProperties() {
return getRelationshipPropertiesClass() != null;
return getRelationshipPropertiesEntity() != null;
}
@Override

View File

@@ -182,7 +182,7 @@ public final class Neo4jMappingContext extends AbstractMappingContext<Neo4jPersi
protected Neo4jPersistentProperty createPersistentProperty(Property property, Neo4jPersistentEntity<?> owner,
SimpleTypeHolder simpleTypeHolder) {
return new DefaultNeo4jPersistentProperty(property, owner, this, simpleTypeHolder);
return new DefaultNeo4jPersistentProperty(property, owner, this, simpleTypeHolder, owner.isRelationshipPropertiesEntity());
}
@Override

View File

@@ -42,4 +42,6 @@ public interface Neo4jPersistentEntity<T>
* "runtime managed" labels.
*/
Optional<Neo4jPersistentProperty> getDynamicLabelsProperty();
boolean isRelationshipPropertiesEntity();
}

View File

@@ -68,7 +68,8 @@ public interface Neo4jPersistentProperty extends PersistentProperty<Neo4jPersist
* @return True, if this association has properties
*/
default boolean isRelationshipWithProperties() {
return isAssociation() && isMap() && getMapValueType() != null
&& getMapValueType().isAnnotationPresent(RelationshipProperties.class);
return isAssociation() && isCollectionLike() && getActualType().isAnnotationPresent(RelationshipProperties.class);
}
boolean isEntityInRelationshipWithProperties();
}

View File

@@ -103,13 +103,11 @@ public enum CypherGenerator {
List<String> additionalLabels = nodeDescription.getAdditionalLabels();
Node rootNode = node(primaryLabel, additionalLabels).named(Constants.NAME_OF_ROOT_NODE);
IdDescription idDescription = nodeDescription.getIdDescription();
List<Expression> expressions = new ArrayList<>();
expressions.add(Constants.NAME_OF_ROOT_NODE);
if (idDescription.isInternallyGeneratedId()) {
expressions.add(Functions.id(rootNode).as(Constants.NAME_OF_INTERNAL_ID));
}
expressions.add(Functions.id(rootNode).as(Constants.NAME_OF_INTERNAL_ID));
return match(rootNode).where(conditionOrNoCondition(condition)).with(expressions.toArray(new Expression[] {}));
}
@@ -372,17 +370,15 @@ public enum CypherGenerator {
continue;
}
if (property.isInternalIdProperty()) {
nodePropertiesProjection.add(Constants.NAME_OF_INTERNAL_ID);
nodePropertiesProjection.add(Functions.id(node));
} else if (!((Neo4jPersistentProperty) property).isDynamicLabels()) {
if (!((Neo4jPersistentProperty) property).isDynamicLabels()) {
nodePropertiesProjection.add(property.getPropertyName());
}
}
nodePropertiesProjection.add(Constants.NAME_OF_LABELS);
nodePropertiesProjection.add(Functions.labels(node));
nodePropertiesProjection.add(Constants.NAME_OF_INTERNAL_ID);
nodePropertiesProjection.add(Functions.id(node));
return nodePropertiesProjection;
}

View File

@@ -85,7 +85,7 @@ public interface RelationshipDescription {
* @return The type of the relationship property class for relationship with properties, otherwise {@literal null}
*/
@Nullable
Class<?> getRelationshipPropertiesClass();
NodeDescription<?> getRelationshipPropertiesEntity();
/**
* Tells if this relationship is a relationship with additional properties. In such cases

View File

@@ -0,0 +1,33 @@
/*
* Copyright 2011-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.neo4j.core.schema;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Marks an entity in a {@link RelationshipProperties} as the target node.
*
* @author Gerrit Meier
* @soundtrack Goldfinger - Here in your bedroom
* @since 6.0
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface TargetNode {
}

View File

@@ -48,6 +48,10 @@ final class IdPopulator {
Neo4jPersistentEntity<?> nodeDescription = neo4jMappingContext.getRequiredPersistentEntity(entity.getClass());
IdDescription idDescription = nodeDescription.getIdDescription();
if (idDescription == null && nodeDescription.isRelationshipPropertiesEntity()) {
return entity;
}
// Filter in two steps to avoid unnecessary object creation.
if (!idDescription.isExternallyGeneratedId()) {
return entity;

View File

@@ -54,16 +54,16 @@ import org.springframework.data.geo.Box;
import org.springframework.data.geo.Circle;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.Polygon;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.PersistentPropertyPath;
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity;
import org.springframework.data.neo4j.core.mapping.Neo4jPersistentProperty;
import org.springframework.data.neo4j.core.schema.Constants;
import org.springframework.data.neo4j.core.schema.CypherGenerator;
import org.springframework.data.neo4j.core.schema.NodeDescription;
import org.springframework.data.neo4j.core.schema.RelationshipDescription;
import org.springframework.data.neo4j.core.schema.RelationshipProperties;
import org.springframework.data.neo4j.core.schema.TargetNode;
import org.springframework.data.repository.query.ParametersParameterAccessor;
import org.springframework.data.repository.query.parser.AbstractQueryCreator;
import org.springframework.data.repository.query.parser.Part;
@@ -186,29 +186,32 @@ final class CypherQueryCreator extends AbstractQueryCreator<QueryAndParameters,
break;
}
NodeDescription<?> relationshipPropertiesEntity = relationshipDescription.getRelationshipPropertiesEntity();
boolean hasTargetNode = hasTargetNode(relationshipPropertiesEntity);
NodeDescription<?> targetEntity = relationshipDescription.getTarget();
Node relatedNode = Cypher.node(targetEntity.getPrimaryLabel(), targetEntity.getAdditionalLabels());
boolean lastNode = isLastNode(persistentProperty);
if (lastNode) {
if (lastNode || hasTargetNode) {
relatedNode = relatedNode.named(getNodeName());
}
switch (relationshipDescription.getDirection()) {
case OUTGOING:
cypherRelationship = (RelationshipPattern) cypherRelationship.relationshipTo(relatedNode,
relationshipDescription.getType());
cypherRelationship = cypherRelationship
.relationshipTo(relatedNode, relationshipDescription.getType());
break;
case INCOMING:
cypherRelationship = (RelationshipPattern) cypherRelationship.relationshipFrom(relatedNode,
relationshipDescription.getType());
cypherRelationship = cypherRelationship
.relationshipFrom(relatedNode, relationshipDescription.getType());
break;
default:
cypherRelationship = (RelationshipPattern) cypherRelationship.relationshipBetween(relatedNode,
relationshipDescription.getType());
cypherRelationship = cypherRelationship
.relationshipBetween(relatedNode, relationshipDescription.getType());
}
if (lastNode) {
if (lastNode || hasTargetNode) {
cypherRelationship = ((RelationshipPattern) cypherRelationship).named(getRelationshipName());
}
}
@@ -216,6 +219,12 @@ final class CypherQueryCreator extends AbstractQueryCreator<QueryAndParameters,
return cypherRelationship;
}
private boolean hasTargetNode(@Nullable NodeDescription<?> relationshipPropertiesEntity) {
return relationshipPropertiesEntity != null
&& ((Neo4jPersistentEntity<?>) relationshipPropertiesEntity)
.getPersistentProperty(TargetNode.class) != null;
}
// if there is no direct property access, the list size is greater than 1 and as a consequence has to contain
// relationships.
private boolean hasRelationships() {
@@ -523,7 +532,7 @@ final class CypherQueryCreator extends AbstractQueryCreator<QueryAndParameters,
private Expression toCypherProperty(Neo4jPersistentProperty persistentProperty, boolean addToLower) {
PersistentEntity<?, Neo4jPersistentProperty> owner = persistentProperty.getOwner();
Neo4jPersistentEntity<?> owner = (Neo4jPersistentEntity<?>) persistentProperty.getOwner();
Expression expression;
if (owner.equals(this.nodeDescription)) {
@@ -534,7 +543,7 @@ final class CypherQueryCreator extends AbstractQueryCreator<QueryAndParameters,
String cypherElementName;
// this "entity" is a representation of a relationship with properties
if (owner.isAnnotationPresent(RelationshipProperties.class)) {
if (owner.isRelationshipPropertiesEntity()) {
cypherElementName = propertyPathWrapper.getRelationshipName();
} else {
cypherElementName = propertyPathWrapper.getNodeName();

View File

@@ -39,7 +39,6 @@ import java.util.stream.IntStream;
import java.util.stream.StreamSupport;
import org.assertj.core.api.Assertions;
import org.assertj.core.data.MapEntry;
import org.assertj.core.groups.Tuple;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
@@ -80,12 +79,17 @@ import org.springframework.data.neo4j.core.DatabaseSelectionProvider;
import org.springframework.data.neo4j.core.convert.Neo4jConversions;
import org.springframework.data.neo4j.integration.imperative.repositories.PersonRepository;
import org.springframework.data.neo4j.integration.imperative.repositories.ThingRepository;
import org.springframework.data.neo4j.integration.shared.AltHobby;
import org.springframework.data.neo4j.integration.shared.AltLikedByPersonRelationship;
import org.springframework.data.neo4j.integration.shared.AltPerson;
import org.springframework.data.neo4j.integration.shared.AnotherThingWithAssignedId;
import org.springframework.data.neo4j.integration.shared.BidirectionalEnd;
import org.springframework.data.neo4j.integration.shared.BidirectionalStart;
import org.springframework.data.neo4j.integration.shared.Club;
import org.springframework.data.neo4j.integration.shared.DeepRelationships;
import org.springframework.data.neo4j.integration.shared.EntityWithConvertedId;
import org.springframework.data.neo4j.integration.shared.Friend;
import org.springframework.data.neo4j.integration.shared.FriendshipRelationship;
import org.springframework.data.neo4j.integration.shared.Hobby;
import org.springframework.data.neo4j.integration.shared.ImmutablePerson;
import org.springframework.data.neo4j.integration.shared.Inheritance;
@@ -102,6 +106,7 @@ import org.springframework.data.neo4j.integration.shared.SimilarThing;
import org.springframework.data.neo4j.integration.shared.ThingWithAssignedId;
import org.springframework.data.neo4j.integration.shared.ThingWithCustomTypes;
import org.springframework.data.neo4j.integration.shared.ThingWithGeneratedId;
import org.springframework.data.neo4j.integration.shared.WorksInClubRelationship;
import org.springframework.data.neo4j.repository.Neo4jRepository;
import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories;
import org.springframework.data.neo4j.repository.query.BoundingBox;
@@ -961,7 +966,56 @@ class RepositoryIT {
rel2.setMyEnum(LikesHobbyRelationship.MyEnum.SOMETHING_DIFFERENT);
rel2.setPoint(new CartesianPoint2d(2d, 3d));
assertThat(person.getHobbies()).contains(MapEntry.entry(hobby1, rel1), MapEntry.entry(hobby2, rel2));
List<LikesHobbyRelationship> hobbies = person.getHobbies();
assertThat(hobbies).containsExactlyInAnyOrder(rel1, rel2);
assertThat(hobbies.get(hobbies.indexOf(rel1)).getHobby()).isEqualTo(hobby1);
assertThat(hobbies.get(hobbies.indexOf(rel2)).getHobby()).isEqualTo(hobby2);
}
@Test
void findEntityWithRelationshipWithPropertiesScalar(
@Autowired PersonWithRelationshipWithPropertiesRepository repository) {
long personId;
try (Session session = createSession()) {
Record record = session.run("CREATE (n:PersonWithRelationshipWithProperties{name:'Freddie'}),"
+ " (n)-[:WORKS_IN{since: 1995}]->(:Club{name:'Blubb'})"
+ "RETURN n").single();
Node personNode = record.get("n").asNode();
personId = personNode.id();
}
PersonWithRelationshipWithProperties person = repository.findById(personId).get();
WorksInClubRelationship loadedRelationship = person.getClub();
assertThat(loadedRelationship.getSince()).isEqualTo(1995);
assertThat(loadedRelationship.getClub().getName()).isEqualTo("Blubb");
}
@Test
void findEntityWithRelationshipWithPropertiesSameLabel(
@Autowired FriendRepository repository) {
long friendId;
try (Session session = createSession()) {
Record record = session.run("CREATE (n:Friend{name:'Freddie'}),"
+ " (n)-[:KNOWS{since: 1995}]->(:Friend{name:'Frank'})"
+ "RETURN n").single();
Node friendNode = record.get("n").asNode();
friendId = friendNode.id();
}
Friend person = repository.findById(friendId).get();
List<FriendshipRelationship> loadedRelationship = person.getFriends();
assertThat(loadedRelationship).allSatisfy(relationship -> {
assertThat(relationship.getSince()).isEqualTo(1995);
assertThat(relationship.getFriend().getName()).isEqualTo("Frank");
});
}
@Test
@@ -982,6 +1036,7 @@ class RepositoryIT {
rel1.setLocalDate(rel1LocalDate);
rel1.setMyEnum(rel1MyEnum);
rel1.setPoint(rel1Point);
rel1.setHobby(h1);
Hobby h2 = new Hobby();
h2.setName("Something else");
@@ -996,18 +1051,19 @@ class RepositoryIT {
rel2.setLocalDate(rel2LocalDate);
rel2.setMyEnum(rel2MyEnum);
rel2.setPoint(rel2Point);
rel2.setHobby(h2);
Map<Hobby, LikesHobbyRelationship> hobbies = new HashMap<>();
hobbies.put(h1, rel1);
hobbies.put(h2, rel2);
PersonWithRelationshipWithProperties clonePerson = new PersonWithRelationshipWithProperties("Freddie clone");
clonePerson.setHobbies(hobbies);
List<LikesHobbyRelationship> hobbies = new ArrayList<>();
PersonWithRelationshipWithProperties person = new PersonWithRelationshipWithProperties("Freddie clone");
hobbies.add(rel1);
hobbies.add(rel2);
person.setHobbies(hobbies);
// when
PersonWithRelationshipWithProperties shouldBeDifferentPerson = repository.save(clonePerson);
PersonWithRelationshipWithProperties shouldBeDifferentPerson = repository.save(person);
// then
assertThat(shouldBeDifferentPerson).isNotNull().isEqualToComparingOnlyGivenFields(clonePerson, "hobbies");
assertThat(shouldBeDifferentPerson).isNotNull().isEqualToComparingOnlyGivenFields(person, "hobbies");
assertThat(shouldBeDifferentPerson.getName()).isEqualToIgnoringCase("Freddie clone");
@@ -1072,6 +1128,7 @@ class RepositoryIT {
rel1.setLocalDate(LocalDate.of(1995, 2, 26));
rel1.setMyEnum(LikesHobbyRelationship.MyEnum.SOMETHING);
rel1.setPoint(new CartesianPoint2d(0d, 1d));
rel1.setHobby(hobby1);
Hobby hobby2 = new Hobby();
hobby2.setName("Something else");
@@ -1081,8 +1138,62 @@ class RepositoryIT {
rel2.setLocalDate(LocalDate.of(2000, 6, 28));
rel2.setMyEnum(LikesHobbyRelationship.MyEnum.SOMETHING_DIFFERENT);
rel2.setPoint(new CartesianPoint2d(2d, 3d));
rel2.setHobby(hobby2);
assertThat(person.getHobbies()).contains(MapEntry.entry(hobby1, rel1), MapEntry.entry(hobby2, rel2));
List<LikesHobbyRelationship> hobbies = person.getHobbies();
assertThat(hobbies).containsExactlyInAnyOrder(rel1, rel2);
assertThat(hobbies.get(hobbies.indexOf(rel1)).getHobby()).isEqualTo(hobby1);
assertThat(hobbies.get(hobbies.indexOf(rel2)).getHobby()).isEqualTo(hobby2);
}
@Test // DATAGRAPH-1350
void loadEntityWithRelationshipWithPropertiesFromCustomQueryIncoming(
@Autowired HobbyWithRelationshipWithPropertiesRepository repository) {
long personId;
try (Session session = createSession()) {
Record record = session.run("CREATE (n:AltPerson{name:'Freddie'}), (n)-[l1:LIKES {rating: 5}]->(h1:AltHobby{name:'Music'}) RETURN n, h1").single();
personId = record.get("n").asNode().id();
}
AltHobby hobby = repository.loadFromCustomQuery(personId);
assertThat(hobby.getName()).isEqualTo("Music");
assertThat(hobby.getLikedBy()).hasSize(1);
assertThat(hobby.getLikedBy()).first().satisfies(entry -> {
assertThat(entry.getAltPerson().getId()).isEqualTo(personId);
assertThat(entry.getRating()).isEqualTo(5);
});
}
@Test
void loadSameNodeWithDoubleRelationship(@Autowired HobbyWithRelationshipWithPropertiesRepository repository) {
long personId;
try (Session session = createSession()) {
Record record = session.run("CREATE (n:AltPerson{name:'Freddie'})," +
" (n)-[l1:LIKES {rating: 5}]->(h1:AltHobby{name:'Music'})," +
" (n)-[l2:LIKES {rating: 1}]->(h1)" +
" RETURN n, h1").single();
personId = record.get("n").asNode().id();
}
AltHobby hobby = repository.loadFromCustomQuery(personId);
assertThat(hobby.getName()).isEqualTo("Music");
List<AltLikedByPersonRelationship> likedBy = hobby.getLikedBy();
assertThat(likedBy).hasSize(2);
AltPerson altPerson = new AltPerson("Freddie");
altPerson.setId(personId);
AltLikedByPersonRelationship rel1 = new AltLikedByPersonRelationship();
rel1.setRating(5);
rel1.setAltPerson(altPerson);
AltLikedByPersonRelationship rel2 = new AltLikedByPersonRelationship();
rel2.setRating(1);
rel2.setAltPerson(altPerson);
assertThat(likedBy).containsExactlyInAnyOrder(rel1, rel2);
}
}
@@ -2694,6 +2805,17 @@ class RepositoryIT {
assertThat(repository.findByHobbiesSinceAndHobbiesActive(2019, true)).isNull();
assertThat(repository.findByHobbiesSinceAndHobbiesActive(2020, false)).isNull();
}
@Test
void findByPropertyOnRelationshipWithPropertiesRelatedEntity(
@Autowired PersonWithRelationshipWithPropertiesRepository repository) {
try (Session session = createSession()) {
session.run(
"CREATE (:PersonWithRelationshipWithProperties{name:'Freddie'})-[:LIKES{since: 2020, active: true}]->(:Hobby{name: 'Bowling'})");
}
assertThat(repository.findByHobbiesHobbyName("Bowling").getName()).isEqualTo("Freddie");
}
}
@Nested
@@ -2763,6 +2885,8 @@ class RepositoryIT {
PersonWithRelationshipWithProperties findByHobbiesSinceOrHobbiesActive(int since1, boolean active);
PersonWithRelationshipWithProperties findByHobbiesSinceAndHobbiesActive(int since1, boolean active);
PersonWithRelationshipWithProperties findByHobbiesHobbyName(String hobbyName);
}
interface PetRepository extends Neo4jRepository<Pet, Long> {}
@@ -2828,6 +2952,14 @@ class RepositoryIT {
@Param("customType") ThingWithCustomTypes.CustomType customType);
}
interface HobbyWithRelationshipWithPropertiesRepository extends Neo4jRepository<AltHobby, Long> {
@Query("MATCH (p:AltPerson)-[l:LIKES]->(h:AltHobby) WHERE id(p) = $personId RETURN h, collect(l), collect(p)")
AltHobby loadFromCustomQuery(@Param("personId") Long personId);
}
interface FriendRepository extends Neo4jRepository<Friend, Long> {}
@SpringJUnitConfig(Config.class)
static abstract class IntegrationTestBase {

View File

@@ -96,7 +96,7 @@ class TypeConversionIT extends Neo4jConversionsITBase {
assertThatExceptionOfType(MappingException.class)
.isThrownBy(() -> repository.findById(ID_OF_NON_EXISTING_PRIMITIVES_NODE))
.withMessageMatching(
"Error mapping Record<\\{n: \\{__internalNeo4jId__: \\d+, someBoolean: NULL, __nodeLabels__: \\[\"NonExistingPrimitives\"\\]\\}\\}>")
"Error mapping Record<\\{n: \\{__internalNeo4jId__: \\d+, id: NULL, someBoolean: NULL, __nodeLabels__: \\[\"NonExistingPrimitives\"\\]\\}\\}>")
.withStackTraceContaining(
"org.springframework.dao.TypeMismatchDataAccessException: Could not convert NULL into boolean; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [null] to type [boolean] for value 'null'; nested exception is java.lang.IllegalArgumentException: A null value cannot be assigned to a primitive type")
.withRootCauseInstanceOf(IllegalArgumentException.class);

View File

@@ -19,6 +19,8 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.tuple;
import org.springframework.data.neo4j.integration.shared.AltLikedByPersonRelationship;
import org.springframework.data.neo4j.integration.shared.AltPerson;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
@@ -37,7 +39,6 @@ import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.assertj.core.data.MapEntry;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Tag;
@@ -876,6 +877,7 @@ class ReactiveRepositoryIT {
rel1.setLocalDate(LocalDate.of(1995, 2, 26));
rel1.setMyEnum(LikesHobbyRelationship.MyEnum.SOMETHING);
rel1.setPoint(new CartesianPoint2d(0d, 1d));
rel1.setHobby(hobby1);
Hobby hobby2 = new Hobby();
hobby2.setName("Something else");
@@ -885,8 +887,12 @@ class ReactiveRepositoryIT {
rel2.setLocalDate(LocalDate.of(2000, 6, 28));
rel2.setMyEnum(LikesHobbyRelationship.MyEnum.SOMETHING_DIFFERENT);
rel2.setPoint(new CartesianPoint2d(2d, 3d));
rel2.setHobby(hobby2);
assertThat(person.getHobbies()).contains(MapEntry.entry(hobby1, rel1), MapEntry.entry(hobby2, rel2));
List<LikesHobbyRelationship> hobbies = person.getHobbies();
assertThat(hobbies).containsExactlyInAnyOrder(rel1, rel2);
assertThat(hobbies.get(hobbies.indexOf(rel1)).getHobby()).isEqualTo(hobby1);
assertThat(hobbies.get(hobbies.indexOf(rel2)).getHobby()).isEqualTo(hobby2);
}).verifyComplete();
}
@@ -909,6 +915,7 @@ class ReactiveRepositoryIT {
rel1.setLocalDate(rel1LocalDate);
rel1.setMyEnum(rel1MyEnum);
rel1.setPoint(rel1Point);
rel1.setHobby(h1);
Hobby h2 = new Hobby();
h2.setName("Something else");
@@ -923,10 +930,11 @@ class ReactiveRepositoryIT {
rel2.setLocalDate(rel2LocalDate);
rel2.setMyEnum(rel2MyEnum);
rel2.setPoint(rel2Point);
rel2.setHobby(h2);
Map<Hobby, LikesHobbyRelationship> hobbies = new HashMap<>();
hobbies.put(h1, rel1);
hobbies.put(h2, rel2);
List<LikesHobbyRelationship> hobbies = new ArrayList<>();
hobbies.add(rel1);
hobbies.add(rel2);
PersonWithRelationshipWithProperties clonePerson = new PersonWithRelationshipWithProperties("Freddie clone");
clonePerson.setHobbies(hobbies);
@@ -1008,6 +1016,7 @@ class ReactiveRepositoryIT {
rel1.setLocalDate(LocalDate.of(1995, 2, 26));
rel1.setMyEnum(LikesHobbyRelationship.MyEnum.SOMETHING);
rel1.setPoint(new CartesianPoint2d(0d, 1d));
rel1.setHobby(hobby1);
Hobby hobby2 = new Hobby();
hobby2.setName("Something else");
@@ -1017,15 +1026,19 @@ class ReactiveRepositoryIT {
rel2.setLocalDate(LocalDate.of(2000, 6, 28));
rel2.setMyEnum(LikesHobbyRelationship.MyEnum.SOMETHING_DIFFERENT);
rel2.setPoint(new CartesianPoint2d(2d, 3d));
rel2.setHobby(hobby2);
assertThat(person.getHobbies()).contains(MapEntry.entry(hobby1, rel1), MapEntry.entry(hobby2, rel2));
List<LikesHobbyRelationship> hobbies = person.getHobbies();
assertThat(hobbies).containsExactlyInAnyOrder(rel1, rel2);
assertThat(hobbies.get(hobbies.indexOf(rel1)).getHobby()).isEqualTo(hobby1);
assertThat(hobbies.get(hobbies.indexOf(rel2)).getHobby()).isEqualTo(hobby2);
}).verifyComplete();
}
@Test // DATAGRAPH-1350
void loadEntityWithRelationshipWithPropertiesFromCustomQueryIncoming(
@Autowired ReactiveHobbyithRelationshipWithPropertiesRepository repository) {
@Autowired ReactiveHobbyWithRelationshipWithPropertiesRepository repository) {
long personId;
@@ -1037,12 +1050,43 @@ class ReactiveRepositoryIT {
StepVerifier.create(repository.loadFromCustomQuery(personId)).assertNext(hobby -> {
assertThat(hobby.getName()).isEqualTo("Music");
assertThat(hobby.getLikedBy()).hasSize(1);
assertThat(hobby.getLikedBy().entrySet()).first().satisfies(entry -> {
assertThat(entry.getKey().getId()).isEqualTo(personId);
assertThat(entry.getValue().getRating()).isEqualTo(5);
assertThat(hobby.getLikedBy()).first().satisfies(entry -> {
assertThat(entry.getAltPerson().getId()).isEqualTo(personId);
assertThat(entry.getRating()).isEqualTo(5);
});
}).verifyComplete();
}
@Test
void loadSameNodeWithDoubleRelationship(@Autowired ReactiveHobbyWithRelationshipWithPropertiesRepository repository) {
long personId;
try (Session session = createSession()) {
Record record = session.run("CREATE (n:AltPerson{name:'Freddie'})," +
" (n)-[l1:LIKES {rating: 5}]->(h1:AltHobby{name:'Music'})," +
" (n)-[l2:LIKES {rating: 1}]->(h1)" +
" RETURN n, h1").single();
personId = record.get("n").asNode().id();
}
StepVerifier.create(repository.loadFromCustomQuery(personId)).assertNext(hobby -> {
assertThat(hobby.getName()).isEqualTo("Music");
List<AltLikedByPersonRelationship> likedBy = hobby.getLikedBy();
assertThat(likedBy).hasSize(2);
AltPerson altPerson = new AltPerson("Freddie");
altPerson.setId(personId);
AltLikedByPersonRelationship rel1 = new AltLikedByPersonRelationship();
rel1.setRating(5);
rel1.setAltPerson(altPerson);
AltLikedByPersonRelationship rel2 = new AltLikedByPersonRelationship();
rel2.setRating(1);
rel2.setAltPerson(altPerson);
assertThat(likedBy).containsExactlyInAnyOrder(rel1, rel2);
});
}
}
@Nested
@@ -2108,7 +2152,7 @@ class ReactiveRepositoryIT {
Mono<PersonWithRelationshipWithProperties> findByHobbiesSinceAndHobbiesActive(int since1, boolean active);
}
interface ReactiveHobbyithRelationshipWithPropertiesRepository
interface ReactiveHobbyWithRelationshipWithPropertiesRepository
extends ReactiveNeo4jRepository<AltHobby, Long> {
@Query("MATCH (p:AltPerson)-[l:LIKES]->(h:AltHobby) WHERE id(p) = $personId RETURN h, collect(l), collect(p)")

View File

@@ -15,8 +15,8 @@
*/
package org.springframework.data.neo4j.integration.shared;
import java.util.HashMap;
import java.util.Map;
import java.util.ArrayList;
import java.util.List;
import org.springframework.data.neo4j.core.schema.GeneratedValue;
import org.springframework.data.neo4j.core.schema.Id;
@@ -33,7 +33,7 @@ public class AltHobby {
private String name;
@Relationship(type = "LIKES", direction = Relationship.Direction.INCOMING)
private Map<AltPerson, AltLikedByPersonRelationship> likedBy = new HashMap<>();
private List<AltLikedByPersonRelationship> likedBy = new ArrayList<>();
public Long getId() {
return id;
@@ -51,7 +51,7 @@ public class AltHobby {
this.name = name;
}
public Map<AltPerson, AltLikedByPersonRelationship> getLikedBy() {
public List<AltLikedByPersonRelationship> getLikedBy() {
return likedBy;
}
}

View File

@@ -16,6 +16,9 @@
package org.springframework.data.neo4j.integration.shared;
import org.springframework.data.neo4j.core.schema.RelationshipProperties;
import org.springframework.data.neo4j.core.schema.TargetNode;
import java.util.Objects;
/**
* @@author Michael J. Simons
@@ -25,6 +28,9 @@ public class AltLikedByPersonRelationship {
private Integer rating;
@TargetNode
private AltPerson altPerson;
public Integer getRating() {
return rating;
}
@@ -32,4 +38,29 @@ public class AltLikedByPersonRelationship {
public void setRating(Integer rating) {
this.rating = rating;
}
public AltPerson getAltPerson() {
return altPerson;
}
public void setAltPerson(AltPerson altPerson) {
this.altPerson = altPerson;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
AltLikedByPersonRelationship that = (AltLikedByPersonRelationship) o;
return rating.equals(that.rating) && altPerson.equals(that.altPerson);
}
@Override
public int hashCode() {
return Objects.hash(rating, altPerson);
}
}

View File

@@ -19,6 +19,8 @@ import org.springframework.data.neo4j.core.schema.GeneratedValue;
import org.springframework.data.neo4j.core.schema.Id;
import org.springframework.data.neo4j.core.schema.Node;
import java.util.Objects;
/**
* @@author Michael J. Simons
*/
@@ -44,4 +46,21 @@ public class AltPerson {
public String getName() {
return name;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
AltPerson altPerson = (AltPerson) o;
return id.equals(altPerson.id) && name.equals(altPerson.name);
}
@Override
public int hashCode() {
return Objects.hash(id, name);
}
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2011-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.neo4j.integration.shared;
import org.springframework.data.neo4j.core.schema.GeneratedValue;
import org.springframework.data.neo4j.core.schema.Id;
import org.springframework.data.neo4j.core.schema.Node;
import org.springframework.data.neo4j.core.schema.Relationship;
import java.util.List;
/**
* @author Gerrit Meier
*/
@Node
public class Friend {
@Id @GeneratedValue private Long id;
private final String name;
@Relationship("KNOWS") private List<FriendshipRelationship> friends;
public Friend(String name) {
this.name = name;
}
public String getName() {
return name;
}
public List<FriendshipRelationship> getFriends() {
return friends;
}
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2011-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.neo4j.integration.shared;
import org.springframework.data.neo4j.core.schema.RelationshipProperties;
import org.springframework.data.neo4j.core.schema.TargetNode;
/**
* @author Gerrit Meier
*/
@RelationshipProperties
public class FriendshipRelationship {
private final Integer since;
@TargetNode
private Friend friend;
public FriendshipRelationship(Integer since) {
this.since = since;
}
public Integer getSince() {
return since;
}
public Friend getFriend() {
return friend;
}
public void setFriend(Friend friend) {
this.friend = friend;
}
}

View File

@@ -19,6 +19,7 @@ import java.time.LocalDate;
import java.util.Objects;
import org.springframework.data.neo4j.core.schema.RelationshipProperties;
import org.springframework.data.neo4j.core.schema.TargetNode;
import org.springframework.data.neo4j.types.CartesianPoint2d;
/**
@@ -41,6 +42,9 @@ public class LikesHobbyRelationship {
// spatial type
private CartesianPoint2d point;
@TargetNode
private Hobby hobby;
public LikesHobbyRelationship(Integer since) {
this.since = since;
}
@@ -79,6 +83,14 @@ public class LikesHobbyRelationship {
return Objects.hash(since, active, localDate, myEnum, point);
}
public Hobby getHobby() {
return hobby;
}
public void setHobby(Hobby hobby) {
this.hobby = hobby;
}
/**
* The missing javadoc
*/

View File

@@ -15,7 +15,7 @@
*/
package org.springframework.data.neo4j.integration.shared;
import java.util.Map;
import java.util.List;
import java.util.Set;
import org.springframework.data.neo4j.core.schema.GeneratedValue;
@@ -34,7 +34,9 @@ public class PersonWithRelationshipWithProperties {
private final String name;
@Relationship("LIKES") private Map<Hobby, LikesHobbyRelationship> hobbies;
@Relationship("LIKES") private List<LikesHobbyRelationship> hobbies;
@Relationship("WORKS_IN") private WorksInClubRelationship club;
@Relationship("OWNS") private Set<Pet> pets;
@@ -46,11 +48,20 @@ public class PersonWithRelationshipWithProperties {
return name;
}
public Map<Hobby, LikesHobbyRelationship> getHobbies() {
public List<LikesHobbyRelationship> getHobbies() {
return hobbies;
}
public void setHobbies(Map<Hobby, LikesHobbyRelationship> hobbies) {
public void setHobbies(List<LikesHobbyRelationship> hobbies) {
this.hobbies = hobbies;
}
public void setClub(WorksInClubRelationship club) {
this.club = club;
}
public WorksInClubRelationship getClub() {
return club;
}
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2011-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.neo4j.integration.shared;
import org.springframework.data.neo4j.core.schema.RelationshipProperties;
import org.springframework.data.neo4j.core.schema.TargetNode;
/**
* @author Gerrit Meier
*/
@RelationshipProperties
public class WorksInClubRelationship {
private final Integer since;
@TargetNode
private Club club;
public WorksInClubRelationship(Integer since) {
this.since = since;
}
public Integer getSince() {
return since;
}
public Club getClub() {
return club;
}
public void setClub(Club club) {
this.club = club;
}
}