refactor: Avoid all usage of Neo4j id() function unless required by the users datamodel.

This change makes use of the Cypher-DSL dialect capatibilities and uses a String identifier internally for all objects. This will be the `elementId()` on Neo4j 5 and higher and `toString(id())` on Neo4j 4 and lower.

Unless explicitly configured by the user through having `@Id @GeneratedValue long id`, we don’t use the `id()` function anymore. This function has been deprecated in Neo4j 5.

Users are encouraged to use `@Id @GeneratedValue String id` for assigning the element id as object id or using custom unique identifiers or `@Id @GeneratedValue UUID id` or  `@Id @GeneratedValue(value = UUIDStringGenerator.class) String id` for UUIDs.

We marked the long ids as deprecated, but don’t have any plans for now to remove them, so that future SDN versions will still be compatible with older Neo4j versions if the data model contains long ids.

We opted for using String’s directly to avoid tons of additional instances and rely on the JVMs excellent String caching capatiblities, thus, the `ElementId` interface - not usable so far anyhow - has been removed without current or planned replacement.

See #2716 and other tickets, closes #2718.
This commit is contained in:
Michael Simons
2023-05-04 16:01:33 +02:00
parent fca0a9af69
commit d8bff18b22
31 changed files with 1756 additions and 579 deletions

View File

@@ -0,0 +1,15 @@
# Run with
# ./mvnw org.openrewrite.maven:rewrite-maven-plugin:dryRun \
# -Drewrite.checkstyleDetectionEnabled=false \
# -Drewrite.configLocation=etc/migrate-to-element-id.yml \
# -Drewrite.activeRecipes=sdn.elementId.rewriteIdCalls
---
type: specs.openrewrite.org/v1beta/recipe
name: sdn.elementId.rewriteIdCalls
displayName: Change calls to Functions.id to Functions.elemenetId
recipeList:
- org.openrewrite.java.ChangeMethodName:
methodPattern: org.neo4j.cypherdsl.core.Functions id(..)
newMethodName: elementId
ignoreDefinition: true

View File

@@ -409,23 +409,21 @@ public final class Neo4jTemplate implements
throw new IllegalStateException("Could not retrieve an internal id while saving");
}
Long internalId = newOrUpdatedNode.map(IdentitySupport::getInternalId).get();
String elementId = newOrUpdatedNode.map(IdentitySupport::getElementId).get();
PersistentPropertyAccessor<T> propertyAccessor = entityMetaData.getPropertyAccessor(entityToBeSaved);
if (entityMetaData.isUsingInternalIds()) {
propertyAccessor.setProperty(entityMetaData.getRequiredIdProperty(), internalId);
}
TemplateSupport.setGeneratedIdIfNecessary(entityMetaData, propertyAccessor, elementId, newOrUpdatedNode);
TemplateSupport.updateVersionPropertyIfPossible(entityMetaData, propertyAccessor, newOrUpdatedNode.get());
if (stateMachine == null) {
stateMachine = new NestedRelationshipProcessingStateMachine(neo4jMappingContext, instance, internalId);
stateMachine = new NestedRelationshipProcessingStateMachine(neo4jMappingContext, instance, elementId);
}
stateMachine.markValueAsProcessed(instance, internalId);
stateMachine.markEntityAsProcessed(instance, elementId);
processRelations(entityMetaData, propertyAccessor, isEntityNew, stateMachine, binderFunction.filter);
T bean = propertyAccessor.getBean();
stateMachine.markValueAsProcessedAs(instance, bean);
stateMachine.markAsAliased(instance, bean);
return bean;
}
@@ -510,21 +508,21 @@ public final class Neo4jTemplate implements
binderFunction = TemplateSupport.createAndApplyPropertyFilter(pps, entityMetaData, binderFunction);
List<Map<String, Object>> entityList = entitiesToBeSaved.stream().map(h -> h.modifiedInstance).map(binderFunction)
.collect(Collectors.toList());
Map<Value, Long> idToInternalIdMapping = neo4jClient
Map<Value, String> idToInternalIdMapping = neo4jClient
.query(() -> renderer.render(cypherGenerator.prepareSaveOfMultipleInstancesOf(entityMetaData)))
.bind(entityList).to(Constants.NAME_OF_ENTITY_LIST_PARAM)
.fetchAs(Map.Entry.class)
.mappedBy((t, r) -> new AbstractMap.SimpleEntry<>(r.get(Constants.NAME_OF_ID), r.get(Constants.NAME_OF_INTERNAL_ID).asLong()))
.mappedBy((t, r) -> new AbstractMap.SimpleEntry<>(r.get(Constants.NAME_OF_ID), r.get(Constants.NAME_OF_ELEMENT_ID).asString()))
.all()
.stream()
.collect(Collectors.toMap(m -> (Value) m.getKey(), m -> (Long) m.getValue()));
.collect(Collectors.toMap(m -> (Value) m.getKey(), m -> (String) m.getValue()));
// Save related
return entitiesToBeSaved.stream().map(t -> {
PersistentPropertyAccessor<T> propertyAccessor = entityMetaData.getPropertyAccessor(t.modifiedInstance);
Neo4jPersistentProperty idProperty = entityMetaData.getRequiredIdProperty();
Object id = convertIdValues(idProperty, propertyAccessor.getProperty(idProperty));
Long internalId = idToInternalIdMapping.get(id);
String internalId = idToInternalIdMapping.get(id);
return this.<T>processRelations(entityMetaData, propertyAccessor, t.wasNew, new NestedRelationshipProcessingStateMachine(neo4jMappingContext, t.originalInstance, internalId), TemplateSupport.computeIncludePropertyPredicate(pps, entityMetaData));
}).collect(Collectors.toList());
}
@@ -757,14 +755,14 @@ public final class Neo4jTemplate implements
// This avoids the usage of cache but might have significant impact on overall performance
if (!isParentObjectNew && !stateMachine.hasProcessedRelationship(fromId, relationshipDescription)) {
List<Long> knownRelationshipsIds = new ArrayList<>();
List<Object> knownRelationshipsIds = new ArrayList<>();
if (idProperty != null) {
for (Object relatedValueToStore : relatedValuesToStore) {
if (relatedValueToStore == null) {
continue;
}
Long id = (Long) relationshipContext.getRelationshipPropertiesPropertyAccessor(relatedValueToStore).getProperty(idProperty);
Object id = relationshipContext.getRelationshipPropertiesPropertyAccessor(relatedValueToStore).getProperty(idProperty);
if (id != null) {
knownRelationshipsIds.add(id);
}
@@ -809,37 +807,30 @@ public final class Neo4jTemplate implements
? stateMachine.getProcessedAs(relatedObjectBeforeCallbacksApplied)
: eventSupport.maybeCallBeforeBind(relatedObjectBeforeCallbacksApplied);
Long relatedInternalId;
String relatedInternalId;
Entity savedEntity = null;
// No need to save values if processed
if (stateMachine.hasProcessedValue(relatedValueToStore)) {
relatedInternalId = stateMachine.getInternalId(relatedValueToStore);
relatedInternalId = stateMachine.getObjectId(relatedValueToStore);
} else {
savedEntity = saveRelatedNode(newRelatedObject, targetEntity, includeProperty, currentPropertyPath);
relatedInternalId = IdentitySupport.getInternalId(savedEntity);
stateMachine.markValueAsProcessed(relatedValueToStore, relatedInternalId);
relatedInternalId = IdentitySupport.getElementId(savedEntity);
stateMachine.markEntityAsProcessed(relatedValueToStore, relatedInternalId);
if (relatedValueToStore instanceof MappingSupport.RelationshipPropertiesWithEntityHolder) {
Object entity = ((MappingSupport.RelationshipPropertiesWithEntityHolder) relatedValueToStore).getRelatedEntity();
stateMachine.markValueAsProcessedAs(entity, relatedInternalId);
stateMachine.markAsAliased(entity, relatedInternalId);
}
}
Neo4jPersistentProperty requiredIdProperty = targetEntity.getRequiredIdProperty();
PersistentPropertyAccessor<?> targetPropertyAccessor = targetEntity.getPropertyAccessor(newRelatedObject);
Object actualRelatedId = targetPropertyAccessor.getProperty(requiredIdProperty);
// if an internal id is used this must be set to link this entity in the next iteration
if (targetEntity.isUsingInternalIds()) {
if (relatedInternalId == null && actualRelatedId != null) {
relatedInternalId = (Long) targetPropertyAccessor.getProperty(requiredIdProperty);
} else if (actualRelatedId == null) {
targetPropertyAccessor.setProperty(requiredIdProperty, relatedInternalId);
}
}
Object possibleInternalLongId = targetPropertyAccessor.getProperty(requiredIdProperty);
relatedInternalId = TemplateSupport.retrieveOrSetRelatedId(targetEntity, targetPropertyAccessor, Optional.ofNullable(savedEntity), relatedInternalId);
if (savedEntity != null) {
TemplateSupport.updateVersionPropertyIfPossible(targetEntity, targetPropertyAccessor, savedEntity);
}
stateMachine.markValueAsProcessedAs(relatedObjectBeforeCallbacksApplied, targetPropertyAccessor.getBean());
stateMachine.markRelationshipAsProcessed(actualRelatedId == null ? relatedInternalId : actualRelatedId,
stateMachine.markAsAliased(relatedObjectBeforeCallbacksApplied, targetPropertyAccessor.getBean());
stateMachine.markRelationshipAsProcessed(possibleInternalLongId == null ? relatedInternalId : possibleInternalLongId,
relationshipDescription.getRelationshipObverse());
Object idValue = idProperty != null
@@ -854,42 +845,43 @@ public final class Neo4jTemplate implements
boolean isNewRelationship = idValue == null;
if (relationshipDescription.isDynamic()) {
// create new dynamic relationship properties
if (relationshipDescription.hasRelationshipProperties() && isNewRelationship && idProperty != null) {
CreateRelationshipStatementHolder statementHolder = neo4jMappingContext.createStatementForSingleRelationship(
sourceEntity, relationshipDescription, relatedValueToStore, true);
if (relationshipDescription.hasRelationshipProperties() && isNewRelationship && idProperty != null) {
CreateRelationshipStatementHolder statementHolder = neo4jMappingContext.createStatementForSingleRelationship(
sourceEntity, relationshipDescription, relatedValueToStore, true);
List<Object> row = Collections.singletonList(properties);
statementHolder = statementHolder.addProperty(Constants.NAME_OF_RELATIONSHIP_LIST_PARAM, row);
List<Object> row = Collections.singletonList(properties);
statementHolder = statementHolder.addProperty(Constants.NAME_OF_RELATIONSHIP_LIST_PARAM, row);
Optional<Long> relationshipInternalId = neo4jClient.query(renderer.render(statementHolder.getStatement()))
.bind(convertIdValues(sourceEntity.getRequiredIdProperty(), fromId)) //
.to(Constants.FROM_ID_PARAMETER_NAME) //
.bind(relatedInternalId) //
.to(Constants.TO_ID_PARAMETER_NAME) //
.bind(idValue) //
.to(Constants.NAME_OF_KNOWN_RELATIONSHIP_PARAM) //
.bindAll(statementHolder.getProperties())
.fetchAs(Long.class)
.mappedBy(IdentitySupport::getInternalId)
.one();
Optional<Object> relationshipInternalId = neo4jClient.query(renderer.render(statementHolder.getStatement()))
.bind(convertIdValues(sourceEntity.getRequiredIdProperty(), fromId)) //
.to(Constants.FROM_ID_PARAMETER_NAME) //
.bind(relatedInternalId) //
.to(Constants.TO_ID_PARAMETER_NAME) //
.bind(idValue) //
.to(Constants.NAME_OF_KNOWN_RELATIONSHIP_PARAM) //
.bindAll(statementHolder.getProperties())
.fetchAs(Object.class)
.mappedBy((t, r) -> IdentitySupport.mapperForRelatedIdValues(idProperty).apply(r))
.one();
assignIdToRelationshipProperties(relationshipContext, relatedValueToStore, idProperty, relationshipInternalId.get());
} else { // plain (new or to update) dynamic relationship or dynamic relationships with properties to update
CreateRelationshipStatementHolder statementHolder = neo4jMappingContext.createStatementForSingleRelationship(
sourceEntity, relationshipDescription, relatedValueToStore, false);
assignIdToRelationshipProperties(relationshipContext, relatedValueToStore, idProperty, relationshipInternalId.orElseThrow());
} else { // plain (new or to update) dynamic relationship or dynamic relationships with properties to update
List<Object> row = Collections.singletonList(properties);
statementHolder = statementHolder.addProperty(Constants.NAME_OF_RELATIONSHIP_LIST_PARAM, row);
neo4jClient.query(renderer.render(statementHolder.getStatement()))
.bind(convertIdValues(sourceEntity.getRequiredIdProperty(), fromId)) //
.to(Constants.FROM_ID_PARAMETER_NAME) //
.bind(relatedInternalId) //
.to(Constants.TO_ID_PARAMETER_NAME) //
.bind(idValue)
.to(Constants.NAME_OF_KNOWN_RELATIONSHIP_PARAM) //
.bindAll(statementHolder.getProperties())
.run();
}
CreateRelationshipStatementHolder statementHolder = neo4jMappingContext.createStatementForSingleRelationship(
sourceEntity, relationshipDescription, relatedValueToStore, false);
List<Object> row = Collections.singletonList(properties);
statementHolder = statementHolder.addProperty(Constants.NAME_OF_RELATIONSHIP_LIST_PARAM, row);
neo4jClient.query(renderer.render(statementHolder.getStatement()))
.bind(convertIdValues(sourceEntity.getRequiredIdProperty(), fromId)) //
.to(Constants.FROM_ID_PARAMETER_NAME) //
.bind(relatedInternalId) //
.to(Constants.TO_ID_PARAMETER_NAME) //
.bind(idValue)
.to(Constants.NAME_OF_KNOWN_RELATIONSHIP_PARAM) //
.bindAll(statementHolder.getProperties())
.run();
}
} else if (relationshipDescription.hasRelationshipProperties() && isNewRelationship && idProperty != null) {
newRelationshipPropertiesRows.add(properties);
newRelatedValuesToStore.add(relatedValueToStore);
@@ -913,7 +905,6 @@ public final class Neo4jTemplate implements
relationshipProperty.isDynamicAssociation(),
relatedValueToStore,
targetPropertyAccessor);
relationshipHandler.handle(relatedValueToStore, relatedObjectBeforeCallbacksApplied, potentiallyRecreatedNewRelatedObject);
}
// batch operations
@@ -937,14 +928,15 @@ public final class Neo4jTemplate implements
if (!newRelatedValuesToStore.isEmpty()) {
CreateRelationshipStatementHolder statementHolder = neo4jMappingContext.createStatementForImperativeRelationshipsWithPropertiesBatch(true,
sourceEntity, relationshipDescription, newRelatedValuesToStore, newRelationshipPropertiesRows);
List<Long> all = new ArrayList<>(neo4jClient.query(renderer.render(statementHolder.getStatement()))
List<Object> all = new ArrayList<>(neo4jClient.query(renderer.render(statementHolder.getStatement()))
.bindAll(statementHolder.getProperties())
.fetchAs(Long.class).mappedBy(IdentitySupport::getInternalId).all());
.fetchAs(Object.class)
.mappedBy((t, r) -> IdentitySupport.mapperForRelatedIdValues(idProperty).apply(r))
.all());
// assign new ids
for (int i = 0; i < all.size(); i++) {
Long aLong = all.get(i);
assignIdToRelationshipProperties(relationshipContext, newRelatedValuesToStore.get(i), idProperty, aLong);
Object anId = all.get(i);
assignIdToRelationshipProperties(relationshipContext, newRelatedValuesToStore.get(i), idProperty, anId);
}
}
}
@@ -957,7 +949,12 @@ public final class Neo4jTemplate implements
return finalSubgraphRoot;
}
private void assignIdToRelationshipProperties(NestedRelationshipContext relationshipContext, Object relatedValueToStore, Neo4jPersistentProperty idProperty, Long relationshipInternalId) {
private void assignIdToRelationshipProperties(
NestedRelationshipContext relationshipContext,
Object relatedValueToStore,
Neo4jPersistentProperty idProperty,
Object relationshipInternalId
) {
relationshipContext
.getRelationshipPropertiesPropertyAccessor(relatedValueToStore)
.setProperty(idProperty, relationshipInternalId);
@@ -1171,12 +1168,12 @@ public final class Neo4jTemplate implements
Map<String, Object> usedParameters = new HashMap<>(parameters);
usedParameters.putAll(rootNodesStatement.getCatalog().getParameters());
final Collection<Long> rootNodeIds = new HashSet<>(neo4jClient
final Collection<String> rootNodeIds = new HashSet<>(neo4jClient
.query(renderer.render(rootNodesStatement))
.bindAll(usedParameters)
.fetchAs(Value.class).mappedBy((t, r) -> r.get(Constants.NAME_OF_SYNTHESIZED_ROOT_NODE))
.one()
.map(value -> value.asList(Value::asLong))
.map(value -> value.asList(Value::asString))
.get());
if (rootNodeIds.isEmpty()) {
@@ -1184,8 +1181,8 @@ public final class Neo4jTemplate implements
return NodesAndRelationshipsByIdStatementProvider.EMPTY;
}
// load first level relationships
final Set<Long> relationshipIds = new HashSet<>();
final Set<Long> relatedNodeIds = new HashSet<>();
final Set<String> relationshipIds = new HashSet<>();
final Set<String> relatedNodeIds = new HashSet<>();
for (RelationshipDescription relationshipDescription : entityMetaData.getRelationshipsInHierarchy(queryFragments::includeField)) {
@@ -1205,8 +1202,8 @@ public final class Neo4jTemplate implements
return new NodesAndRelationshipsByIdStatementProvider(rootNodeIds, relationshipIds, relatedNodeIds, queryFragments);
}
private void iterateNextLevel(Collection<Long> nodeIds, RelationshipDescription sourceRelationshipDescription, Set<Long> relationshipIds,
Set<Long> relatedNodeIds, PropertyPathWalkStep currentPathStep) {
private void iterateNextLevel(Collection<String> nodeIds, RelationshipDescription sourceRelationshipDescription, Set<String> relationshipIds,
Set<String> relatedNodeIds, PropertyPathWalkStep currentPathStep) {
Neo4jPersistentEntity<?> target = (Neo4jPersistentEntity<?>) sourceRelationshipDescription.getTarget();
@@ -1233,7 +1230,7 @@ public final class Neo4jTemplate implements
Statement statement = cypherGenerator
.prepareMatchOf(target, relationshipDescription, null,
Functions.id(node).in(Cypher.parameter(Constants.NAME_OF_IDS)))
Functions.elementId(node).in(Cypher.parameter(Constants.NAME_OF_IDS)))
.returning(cypherGenerator.createGenericReturnStatement()).build();
neo4jClient.query(renderer.render(statement))
@@ -1245,20 +1242,20 @@ public final class Neo4jTemplate implements
}
@NonNull
private Consumer<Map<String, Object>> iterateAndMapNextLevel(Set<Long> relationshipIds,
Set<Long> relatedNodeIds,
private Consumer<Map<String, Object>> iterateAndMapNextLevel(Set<String> relationshipIds,
Set<String> relatedNodeIds,
RelationshipDescription relationshipDescription,
PropertyPathWalkStep currentPathStep) {
return record -> {
@SuppressWarnings("unchecked")
List<Long> newRelationshipIds = (List<Long>) record.get(Constants.NAME_OF_SYNTHESIZED_RELATIONS);
List<String> newRelationshipIds = (List<String>) record.get(Constants.NAME_OF_SYNTHESIZED_RELATIONS);
relationshipIds.addAll(newRelationshipIds);
@SuppressWarnings("unchecked")
List<Long> newRelatedNodeIds = (List<Long>) record.get(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES);
List<String> newRelatedNodeIds = (List<String>) record.get(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES);
Set<Long> relatedIds = new HashSet<>(newRelatedNodeIds);
Set<String> relatedIds = new HashSet<>(newRelatedNodeIds);
// use this list to get down the road
// 1. remove already visited ones;
relatedIds.removeAll(relatedNodeIds);

View File

@@ -36,6 +36,7 @@ import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;
@@ -446,10 +447,11 @@ public final class ReactiveNeo4jTemplate implements
PersistentPropertyAccessor<T> propertyAccessor = entityMetaData.getPropertyAccessor(entityToBeSaved);
return idMono.doOnNext(newOrUpdatedNode -> {
IdentitySupport.updateInternalId(entityMetaData, propertyAccessor, newOrUpdatedNode);
var elementId = IdentitySupport.getElementId(newOrUpdatedNode);
TemplateSupport.setGeneratedIdIfNecessary(entityMetaData, propertyAccessor, elementId, Optional.of(newOrUpdatedNode));
TemplateSupport.updateVersionPropertyIfPossible(entityMetaData, propertyAccessor, newOrUpdatedNode);
finalStateMachine.markValueAsProcessed(instance, IdentitySupport.getInternalId(newOrUpdatedNode));
}).map(IdentitySupport::getInternalId)
finalStateMachine.markEntityAsProcessed(instance, elementId);
}).map(IdentitySupport::getElementId)
.flatMap(internalId -> processRelations(entityMetaData, propertyAccessor, isNewEntity, finalStateMachine, binderFunction.filter));
});
}
@@ -582,15 +584,15 @@ public final class ReactiveNeo4jTemplate implements
.query(() -> renderer.render(cypherGenerator.prepareSaveOfMultipleInstancesOf(entityMetaData)))
.bind(boundedEntityList).to(Constants.NAME_OF_ENTITY_LIST_PARAM)
.fetchAs(Tuple2.class)
.mappedBy((t, r) -> Tuples.of(r.get(Constants.NAME_OF_ID), r.get(Constants.NAME_OF_INTERNAL_ID).asLong()))
.mappedBy((t, r) -> Tuples.of(r.get(Constants.NAME_OF_ID), r.get(Constants.NAME_OF_ELEMENT_ID).asString()))
.all()
.collectMap(m -> (Value) m.getT1(), m -> (Long) m.getT2());
.collectMap(m -> (Value) m.getT1(), m -> (String) m.getT2());
}).flatMapMany(idToInternalIdMapping -> Flux.fromIterable(entitiesToBeSaved)
.flatMap(t -> {
PersistentPropertyAccessor<T> propertyAccessor = entityMetaData.getPropertyAccessor(t.getT3());
Neo4jPersistentProperty idProperty = entityMetaData.getRequiredIdProperty();
Object id = convertIdValues(idProperty, propertyAccessor.getProperty(idProperty));
Long internalId = idToInternalIdMapping.get(id);
String internalId = idToInternalIdMapping.get(id);
return processRelations(entityMetaData, propertyAccessor, t.getT2(), new NestedRelationshipProcessingStateMachine(neo4jMappingContext, t.getT1(), internalId),
TemplateSupport.computeIncludePropertyPredicate(pps, entityMetaData));
}))
@@ -709,9 +711,9 @@ public final class ReactiveNeo4jTemplate implements
return Mono.deferContextual(ctx -> {
Class<?> rootClass = entityMetaData.getUnderlyingClass();
Set<Long> rootNodeIds = ctx.get("rootNodes");
Set<Long> processedRelationshipIds = ctx.get("processedRelationships");
Set<Long> processedNodeIds = ctx.get("processedNodes");
Set<String> rootNodeIds = ctx.get("rootNodes");
Set<String> processedRelationshipIds = ctx.get("processedRelationships");
Set<String> processedNodeIds = ctx.get("processedNodes");
return Flux.fromIterable(entityMetaData.getRelationshipsInHierarchy(queryFragments::includeField))
.flatMap(relationshipDescription -> {
@@ -723,16 +725,19 @@ public final class ReactiveNeo4jTemplate implements
usedParameters.putAll(statement.getCatalog().getParameters());
return neo4jClient.query(renderer.render(statement))
.bindAll(usedParameters)
.fetchAs(TupleOfLongsHolder.class)
.fetchAs(Tuple2.class)
.mappedBy((t, r) -> {
Collection<Long> rootIds = r.get(Constants.NAME_OF_SYNTHESIZED_ROOT_NODE).asList(Value::asLong);
Collection<String> rootIds = r.get(Constants.NAME_OF_SYNTHESIZED_ROOT_NODE).asList(Value::asString);
rootNodeIds.addAll(rootIds);
Collection<Long> newRelationshipIds = r.get(Constants.NAME_OF_SYNTHESIZED_RELATIONS).asList(Value::asLong);
Collection<Long> newRelatedNodeIds = r.get(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES).asList(Value::asLong);
return TupleOfLongsHolder.with(Tuples.of(newRelationshipIds, newRelatedNodeIds));
Collection<String> newRelationshipIds = r.get(Constants.NAME_OF_SYNTHESIZED_RELATIONS).asList(Value::asString);
Collection<String> newRelatedNodeIds = r.get(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES).asList(Value::asString);
return Tuples.of(newRelationshipIds, newRelatedNodeIds);
})
.one()
.map(TupleOfLongsHolder::get)
.map((t) -> {
//noinspection unchecked
return (Tuple2<Collection<String>, Collection<String>>) t;
})
.expand(iterateAndMapNextLevel(relationshipDescription, queryFragments, rootClass, PropertyPathWalkStep.empty()));
})
.then(Mono.fromSupplier(() -> new NodesAndRelationshipsByIdStatementProvider(rootNodeIds, processedRelationshipIds, processedNodeIds, queryFragments)));
@@ -744,23 +749,7 @@ public final class ReactiveNeo4jTemplate implements
}
static class TupleOfLongsHolder {
private final Tuple2<Collection<Long>, Collection<Long>> content;
static TupleOfLongsHolder with(Tuple2<Collection<Long>, Collection<Long>> content) {
return new TupleOfLongsHolder(content);
}
private TupleOfLongsHolder(Tuple2<Collection<Long>, Collection<Long>> content) {
this.content = content;
}
Tuple2<Collection<Long>, Collection<Long>> get() {
return content;
}
}
private Flux<Tuple2<Collection<Long>, Collection<Long>>> iterateNextLevel(Collection<Long> relatedNodeIds,
private Flux<Tuple2<Collection<String>, Collection<String>>> iterateNextLevel(Collection<String> relatedNodeIds,
RelationshipDescription sourceRelationshipDescription, QueryFragments queryFragments,
Class<?> rootClass, PropertyPathWalkStep currentPathStep) {
@@ -786,43 +775,46 @@ public final class ReactiveNeo4jTemplate implements
Statement statement = cypherGenerator
.prepareMatchOf(target, relDe, null,
Functions.id(node).in(Cypher.parameter(Constants.NAME_OF_ID)))
Functions.elementId(node).in(Cypher.parameter(Constants.NAME_OF_ID)))
.returning(cypherGenerator.createGenericReturnStatement()).build();
return neo4jClient.query(renderer.render(statement))
.bindAll(Collections.singletonMap(Constants.NAME_OF_ID, relatedNodeIds))
.fetchAs(TupleOfLongsHolder.class)
.fetchAs(Tuple2.class)
.mappedBy((t, r) -> {
Collection<Long> newRelationshipIds = r.get(Constants.NAME_OF_SYNTHESIZED_RELATIONS).asList(Value::asLong);
Collection<Long> newRelatedNodeIds = r.get(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES).asList(Value::asLong);
Collection<String> newRelationshipIds = r.get(Constants.NAME_OF_SYNTHESIZED_RELATIONS).asList(Value::asString);
Collection<String> newRelatedNodeIds = r.get(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES).asList(Value::asString);
return TupleOfLongsHolder.with(Tuples.of(newRelationshipIds, newRelatedNodeIds));
return Tuples.of(newRelationshipIds, newRelatedNodeIds);
})
.one()
.map(TupleOfLongsHolder::get)
.map((t) -> {
//noinspection unchecked
return (Tuple2<Collection<String>, Collection<String>>) t;
})
.expand(object -> iterateAndMapNextLevel(relDe, queryFragments, rootClass, nextPathStep).apply(object));
});
}
@NonNull
private Function<Tuple2<Collection<Long>, Collection<Long>>,
Publisher<Tuple2<Collection<Long>, Collection<Long>>>> iterateAndMapNextLevel(
private Function<Tuple2<Collection<String>, Collection<String>>,
Publisher<Tuple2<Collection<String>, Collection<String>>>> iterateAndMapNextLevel(
RelationshipDescription relationshipDescription, QueryFragments queryFragments, Class<?> rootClass, PropertyPathWalkStep currentPathStep) {
return newRelationshipAndRelatedNodeIds ->
Flux.deferContextual(ctx -> {
Set<Long> relationshipIds = ctx.get("processedRelationships");
Set<Long> processedNodeIds = ctx.get("processedNodes");
Set<String> relationshipIds = ctx.get("processedRelationships");
Set<String> processedNodeIds = ctx.get("processedNodes");
Collection<Long> newRelationshipIds = newRelationshipAndRelatedNodeIds.getT1();
Set<Long> tmpProcessedRels = ConcurrentHashMap.newKeySet(newRelationshipIds.size());
Collection<String> newRelationshipIds = newRelationshipAndRelatedNodeIds.getT1();
Set<String> tmpProcessedRels = ConcurrentHashMap.newKeySet(newRelationshipIds.size());
tmpProcessedRels.addAll(newRelationshipIds);
tmpProcessedRels.removeAll(relationshipIds);
relationshipIds.addAll(newRelationshipIds);
Collection<Long> newRelatedNodeIds = newRelationshipAndRelatedNodeIds.getT2();
Set<Long> tmpProcessedNodes = ConcurrentHashMap.newKeySet(newRelatedNodeIds.size());
Collection<String> newRelatedNodeIds = newRelationshipAndRelatedNodeIds.getT2();
Set<String> tmpProcessedNodes = ConcurrentHashMap.newKeySet(newRelatedNodeIds.size());
tmpProcessedNodes.addAll(newRelatedNodeIds);
tmpProcessedNodes.removeAll(processedNodeIds);
processedNodeIds.addAll(newRelatedNodeIds);
@@ -904,14 +896,14 @@ public final class ReactiveNeo4jTemplate implements
// This avoids the usage of cache but might have significant impact on overall performance
if (!isParentObjectNew && !stateMachine.hasProcessedRelationship(fromId, relationshipDescription)) {
List<Long> knownRelationshipsIds = new ArrayList<>();
List<Object> knownRelationshipsIds = new ArrayList<>();
if (idProperty != null) {
for (Object relatedValueToStore : relatedValuesToStore) {
if (relatedValueToStore == null) {
continue;
}
Long id = (Long) relationshipContext
Object id = relationshipContext
.getRelationshipPropertiesPropertyAccessor(relatedValueToStore)
.getProperty(idProperty);
if (id != null) {
@@ -950,44 +942,38 @@ public final class ReactiveNeo4jTemplate implements
.flatMap(newRelatedObject -> {
Neo4jPersistentEntity<?> targetEntity = neo4jMappingContext.getRequiredPersistentEntity(relatedObjectBeforeCallbacksApplied.getClass());
Mono<Tuple2<AtomicReference<Long>, AtomicReference<Entity>>> queryOrSave;
Mono<Tuple2<AtomicReference<String>, AtomicReference<Entity>>> queryOrSave;
if (stateMachine.hasProcessedValue(relatedValueToStore)) {
AtomicReference<Long> relatedInternalId = new AtomicReference<>();
Long possibleValue = stateMachine.getInternalId(relatedValueToStore);
AtomicReference<String> relatedInternalId = new AtomicReference<>();
String possibleValue = stateMachine.getObjectId(relatedValueToStore);
if (possibleValue != null) {
relatedInternalId.set(possibleValue);
}
queryOrSave = Mono.just(Tuples.of(relatedInternalId, new AtomicReference<>()));
} else {
queryOrSave = saveRelatedNode(newRelatedObject, targetEntity, includeProperty, currentPropertyPath)
.map(entity -> Tuples.of(new AtomicReference<>(IdentitySupport.getInternalId(entity)), new AtomicReference<>(entity)))
.doOnNext(entity -> {
stateMachine.markValueAsProcessed(relatedValueToStore, entity.getT1().get());
.map(entity -> Tuples.of(new AtomicReference<>(IdentitySupport.getElementId(entity)), new AtomicReference<>(entity)))
.doOnNext(t -> {
var relatedInternalId = t.getT1().get();
stateMachine.markEntityAsProcessed(relatedValueToStore, relatedInternalId);
if (relatedValueToStore instanceof MappingSupport.RelationshipPropertiesWithEntityHolder) {
Object value = ((MappingSupport.RelationshipPropertiesWithEntityHolder) relatedValueToStore).getRelatedEntity();
stateMachine.markValueAsProcessedAs(value, entity.getT1().get());
Object entity = ((MappingSupport.RelationshipPropertiesWithEntityHolder) relatedValueToStore).getRelatedEntity();
stateMachine.markAsAliased(entity, relatedInternalId);
}
});
}
return queryOrSave.flatMap(idAndEntity -> {
Long relatedInternalId = idAndEntity.getT1().get();
String relatedInternalId = idAndEntity.getT1().get();
Entity savedEntity = idAndEntity.getT2().get();
Neo4jPersistentProperty requiredIdProperty = targetEntity.getRequiredIdProperty();
PersistentPropertyAccessor<?> targetPropertyAccessor = targetEntity.getPropertyAccessor(newRelatedObject);
Object actualRelatedId = targetPropertyAccessor.getProperty(requiredIdProperty);
// if an internal id is used this must be set to link this entity in the next iteration
if (targetEntity.isUsingInternalIds()) {
if (relatedInternalId == null && actualRelatedId != null) {
relatedInternalId = (Long) targetPropertyAccessor.getProperty(requiredIdProperty);
} else if (actualRelatedId == null) {
targetPropertyAccessor.setProperty(requiredIdProperty, relatedInternalId);
}
}
Object possibleInternalLongId = targetPropertyAccessor.getProperty(requiredIdProperty);
relatedInternalId = TemplateSupport.retrieveOrSetRelatedId(targetEntity, targetPropertyAccessor, Optional.ofNullable(savedEntity), relatedInternalId);
if (savedEntity != null) {
TemplateSupport.updateVersionPropertyIfPossible(targetEntity, targetPropertyAccessor, savedEntity);
}
stateMachine.markValueAsProcessedAs(relatedObjectBeforeCallbacksApplied, targetPropertyAccessor.getBean());
stateMachine.markRelationshipAsProcessed(actualRelatedId == null ? relatedInternalId : actualRelatedId,
stateMachine.markAsAliased(relatedObjectBeforeCallbacksApplied, targetPropertyAccessor.getBean());
stateMachine.markRelationshipAsProcessed(possibleInternalLongId == null ? relatedInternalId : possibleInternalLongId,
relationshipDescription.getRelationshipObverse());
Object idValue = idProperty != null
@@ -1016,8 +1002,8 @@ public final class ReactiveNeo4jTemplate implements
.bind(idValue) //
.to(Constants.NAME_OF_KNOWN_RELATIONSHIP_PARAM) //
.bindAll(statementHolder.getProperties())
.fetchAs(Long.class)
.mappedBy(IdentitySupport::getInternalId)
.fetchAs(Object.class)
.mappedBy((t, r) -> IdentitySupport.mapperForRelatedIdValues(idProperty).apply(r))
.one()
.flatMap(relationshipInternalId -> {
if (idProperty != null && isNewRelationship) {

View File

@@ -22,6 +22,8 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.BiPredicate;
@@ -45,6 +47,7 @@ import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.neo4j.core.mapping.Constants;
import org.springframework.data.neo4j.core.mapping.EntityInstanceWithSource;
import org.springframework.data.neo4j.core.mapping.IdDescription;
import org.springframework.data.neo4j.core.mapping.IdentitySupport;
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity;
import org.springframework.data.neo4j.core.mapping.Neo4jPersistentProperty;
@@ -179,10 +182,10 @@ public final class TemplateSupport {
final static NodesAndRelationshipsByIdStatementProvider EMPTY =
new NodesAndRelationshipsByIdStatementProvider(Collections.emptySet(), Collections.emptySet(), Collections.emptySet(), new QueryFragments());
private final Map<String, Collection<Long>> parameters = new HashMap<>(3);
private final Map<String, Collection<String>> parameters = new HashMap<>(3);
private final QueryFragments queryFragments;
NodesAndRelationshipsByIdStatementProvider(Collection<Long> rootNodeIds, Collection<Long> relationshipsIds, Collection<Long> relatedNodeIds, QueryFragments queryFragments) {
NodesAndRelationshipsByIdStatementProvider(Collection<String> rootNodeIds, Collection<String> relationshipsIds, Collection<String> relatedNodeIds, QueryFragments queryFragments) {
this.parameters.put(ROOT_NODE_IDS, rootNodeIds);
this.parameters.put(RELATIONSHIP_IDS, relationshipsIds);
@@ -207,13 +210,13 @@ public final class TemplateSupport {
Node relatedNodes = Cypher.anyNode(relatedNodeIds);
Relationship relationships = Cypher.anyNode().relationshipBetween(Cypher.anyNode()).named(relationshipIds);
return Cypher.match(rootNodes)
.where(Functions.id(rootNodes).in(Cypher.parameter(rootNodeIds)))
.where(Functions.elementId(rootNodes).in(Cypher.parameter(rootNodeIds)))
.with(Functions.collect(rootNodes).as(Constants.NAME_OF_ROOT_NODE))
.optionalMatch(relationships)
.where(Functions.id(relationships).in(Cypher.parameter(relationshipIds)))
.where(Functions.elementId(relationships).in(Cypher.parameter(relationshipIds)))
.with(Constants.NAME_OF_ROOT_NODE, Functions.collectDistinct(relationships).as(Constants.NAME_OF_SYNTHESIZED_RELATIONS))
.optionalMatch(relatedNodes)
.where(Functions.id(relatedNodes).in(Cypher.parameter(relatedNodeIds)))
.where(Functions.elementId(relatedNodes).in(Cypher.parameter(relatedNodeIds)))
.with(
Constants.NAME_OF_ROOT_NODE,
Cypher.name(Constants.NAME_OF_SYNTHESIZED_RELATIONS).as(Constants.NAME_OF_SYNTHESIZED_RELATIONS),
@@ -337,6 +340,76 @@ public final class TemplateSupport {
}
}
/**
* Uses the given {@link PersistentPropertyAccessor propertyAccessor} to set the value of the generated id.
*
* @param entityMetaData The type information from SDN
* @param propertyAccessor An accessor tied to a concrete instance
* @param elementId The element id to store
* @param databaseEntity A fallback entity to retrieve the deprecated internal long id
* @param <T> The type of the entity
*/
static <T> void setGeneratedIdIfNecessary(
Neo4jPersistentEntity<?> entityMetaData,
PersistentPropertyAccessor<T> propertyAccessor,
String elementId,
Optional<Entity> databaseEntity
) {
if (!entityMetaData.isUsingInternalIds()) {
return;
}
var requiredIdProperty = entityMetaData.getRequiredIdProperty();
var idPropertyType = requiredIdProperty.getType();
if (entityMetaData.isUsingDeprecatedInternalId()) {
propertyAccessor.setProperty(requiredIdProperty, databaseEntity.map(IdentitySupport::getInternalId).orElseThrow());
} else if (idPropertyType.equals(String.class)) {
propertyAccessor.setProperty(requiredIdProperty, elementId);
} else {
throw new IllegalArgumentException("Unsupported generated id property " + idPropertyType);
}
}
/**
* Retrieves the object id for a related object if no has been found so far or updates the object with the id of a previously
* processed object.
*
* @param entityMetadata Needed for determining the type of ids
* @param propertyAccessor Bound to the currently processed entity
* @param databaseEntity Source for the old neo4j internal id
* @param relatedInternalId The element id or the string version of the old id
* @param <T> The type of the entity
* @return The actual related internal id being used.
*/
static <T> String retrieveOrSetRelatedId(
Neo4jPersistentEntity<?> entityMetadata,
PersistentPropertyAccessor<T> propertyAccessor,
Optional<Entity> databaseEntity,
@Nullable String relatedInternalId
) {
if (!entityMetadata.isUsingInternalIds()) {
return Objects.requireNonNull(relatedInternalId);
}
var requiredIdProperty = entityMetadata.getRequiredIdProperty();
var current = propertyAccessor.getProperty(requiredIdProperty);
if (entityMetadata.isUsingDeprecatedInternalId()) {
if (relatedInternalId == null && current != null) {
relatedInternalId = current.toString();
} else if (current == null) {
long internalId = databaseEntity.map(Entity::id).orElseThrow();
propertyAccessor.setProperty(requiredIdProperty, internalId);
}
} else {
if (relatedInternalId == null && current != null) {
relatedInternalId = (String) current;
} else if (current == null) {
propertyAccessor.setProperty(requiredIdProperty, relatedInternalId);
}
}
return Objects.requireNonNull(relatedInternalId);
}
private TemplateSupport() {
}
}

View File

@@ -51,7 +51,6 @@ import org.springframework.data.convert.ConverterBuilder;
import org.springframework.data.convert.ReadingConverter;
import org.springframework.data.convert.WritingConverter;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.neo4j.core.schema.ElementId;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -104,24 +103,11 @@ final class AdditionalTypes {
hlp.add(ConverterBuilder.reading(Value.class, Entity.class, Value::asEntity));
hlp.add(ConverterBuilder.reading(Value.class, Node.class, Value::asNode));
hlp.add(ConverterBuilder.reading(Value.class, Relationship.class, Value::asRelationship));
hlp.add(ConverterBuilder.reading(Value.class, ElementId.class, AdditionalTypes::asElementId).andWriting(AdditionalTypes::value));
hlp.add(ConverterBuilder.reading(Value.class, Map.class, Value::asMap).andWriting(AdditionalTypes::value));
CONVERTERS = Collections.unmodifiableList(hlp);
}
static ElementId asElementId(Value value) {
return ElementId.of(value.asString());
}
static Value value(ElementId elementId) {
if (elementId == null) {
return Values.NULL;
}
return Values.value(elementId.value());
}
static Value value(Map<?, ?> map) {
return Values.value(map);
}

View File

@@ -30,6 +30,7 @@ import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.UnaryOperator;
import java.util.regex.Pattern;
@@ -41,7 +42,6 @@ import org.neo4j.cypherdsl.core.Condition;
import org.neo4j.cypherdsl.core.Conditions;
import org.neo4j.cypherdsl.core.Cypher;
import org.neo4j.cypherdsl.core.Expression;
import org.neo4j.cypherdsl.core.FunctionInvocation;
import org.neo4j.cypherdsl.core.Functions;
import org.neo4j.cypherdsl.core.IdentifiableElement;
import org.neo4j.cypherdsl.core.MapProjection;
@@ -117,7 +117,9 @@ public enum CypherGenerator {
List<IdentifiableElement> expressions = new ArrayList<>();
expressions.add(rootNode.getRequiredSymbolicName());
expressions.add(Functions.id(rootNode).as(Constants.NAME_OF_INTERNAL_ID));
if (nodeDescription instanceof Neo4jPersistentEntity<?> entity && entity.isUsingDeprecatedInternalId()) {
expressions.add(Functions.id(rootNode).as(Constants.NAME_OF_INTERNAL_ID));
}
expressions.add(Functions.elementId(rootNode).as(Constants.NAME_OF_ELEMENT_ID));
return match(rootNode).where(conditionOrNoCondition(condition)).with(expressions.toArray(IdentifiableElement[]::new));
@@ -131,7 +133,7 @@ public enum CypherGenerator {
StatementBuilder.OngoingReadingWithoutWhere match = prepareMatchOfRootNode(rootNode, initialMatchOn);
List<IdentifiableElement> expressions = new ArrayList<>();
expressions.add(Functions.collect(Functions.id(rootNode)).as(Constants.NAME_OF_SYNTHESIZED_ROOT_NODE));
expressions.add(Functions.collect(Functions.elementId(rootNode)).as(Constants.NAME_OF_SYNTHESIZED_ROOT_NODE));
return match
.where(conditionOrNoCondition(condition))
@@ -168,9 +170,9 @@ public enum CypherGenerator {
relationship = relationship.named(Constants.NAME_OF_SYNTHESIZED_RELATIONS);
List<IdentifiableElement> expressions = new ArrayList<>();
expressions.add(Functions.collect(Functions.id(rootNode)).as(Constants.NAME_OF_SYNTHESIZED_ROOT_NODE));
expressions.add(Functions.collect(Functions.id(targetNode)).as(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES));
expressions.add(Functions.collect(Functions.id(relationship)).as(Constants.NAME_OF_SYNTHESIZED_RELATIONS));
expressions.add(Functions.collect(Functions.elementId(rootNode)).as(Constants.NAME_OF_SYNTHESIZED_ROOT_NODE));
expressions.add(Functions.collect(Functions.elementId(targetNode)).as(Constants.NAME_OF_SYNTHESIZED_RELATED_NODES));
expressions.add(Functions.collect(Functions.elementId(relationship)).as(Constants.NAME_OF_SYNTHESIZED_RELATIONS));
return match
.where(conditionOrNoCondition(condition))
@@ -341,11 +343,14 @@ public enum CypherGenerator {
Statement createIfNew;
Statement updateIfExists;
if (((Neo4jPersistentEntity<?>) nodeDescription).hasVersionProperty()) {
Property versionProperty = rootNode.property(((Neo4jPersistentEntity<?>) nodeDescription).getRequiredVersionProperty().getName());
var neo4jPersistentEntity = (Neo4jPersistentEntity<?>) nodeDescription;
var nodeIdFunction = getNodeIdFunction(neo4jPersistentEntity);
if (neo4jPersistentEntity.hasVersionProperty()) {
Property versionProperty = rootNode.property(neo4jPersistentEntity.getRequiredVersionProperty().getName());
createIfNew = updateDecorator.apply(optionalMatch(possibleExistingNode)
.where(Functions.id(possibleExistingNode).isEqualTo(idParameter))
.where(nodeIdFunction.apply(possibleExistingNode).isEqualTo(idParameter))
.with(possibleExistingNode)
.where(possibleExistingNode.isNull())
.create(rootNode.withProperties(versionProperty, literalOf(0)))
@@ -355,7 +360,7 @@ public enum CypherGenerator {
.build();
updateIfExists = updateDecorator.apply(match(rootNode)
.where(Functions.id(rootNode).isEqualTo(idParameter))
.where(nodeIdFunction.apply(rootNode).isEqualTo(idParameter))
.and(versionProperty.isEqualTo(parameter(Constants.NAME_OF_VERSION_PARAM))) // Initial check
.set(versionProperty.to(versionProperty.add(literalOf(1)))) // Acquire lock
.with(rootNode)
@@ -365,12 +370,12 @@ public enum CypherGenerator {
.returning(rootNode).build();
} else {
createIfNew = updateDecorator
.apply(optionalMatch(possibleExistingNode).where(Functions.id(possibleExistingNode).isEqualTo(idParameter))
.apply(optionalMatch(possibleExistingNode).where(nodeIdFunction.apply(possibleExistingNode).isEqualTo(idParameter))
.with(possibleExistingNode).where(possibleExistingNode.isNull()).create(rootNode)
.set(rootNode, parameter(Constants.NAME_OF_PROPERTIES_PARAM)))
.returning(rootNode).build();
updateIfExists = updateDecorator.apply(match(rootNode).where(Functions.id(rootNode).isEqualTo(idParameter))
updateIfExists = updateDecorator.apply(match(rootNode).where(nodeIdFunction.apply(rootNode).isEqualTo(idParameter))
.mutate(rootNode, parameter(Constants.NAME_OF_PROPERTIES_PARAM))).returning(rootNode).build();
}
@@ -391,15 +396,18 @@ public enum CypherGenerator {
String nameOfIdProperty = idDescription.getOptionalGraphPropertyName()
.orElseThrow(() -> new MappingException("External id does not correspond to a graph property"));
List<Expression> expressions = new ArrayList<>();
if (nodeDescription instanceof Neo4jPersistentEntity<?> entity && entity.isUsingDeprecatedInternalId()) {
Functions.id(rootNode).as(Constants.NAME_OF_INTERNAL_ID);
}
expressions.add(Functions.elementId(rootNode).as(Constants.NAME_OF_ELEMENT_ID));
expressions.add(rootNode.property(nameOfIdProperty).as(Constants.NAME_OF_ID));
String row = "entity";
return Cypher.unwind(parameter(Constants.NAME_OF_ENTITY_LIST_PARAM)).as(row)
.merge(rootNode.withProperties(nameOfIdProperty, Cypher.property(row, Constants.NAME_OF_ID)))
.mutate(rootNode, Cypher.property(row, Constants.NAME_OF_PROPERTIES_PARAM))
.returning(
Functions.id(rootNode).as(Constants.NAME_OF_INTERNAL_ID),
Functions.elementId(rootNode).as(Constants.NAME_OF_ELEMENT_ID),
rootNode.property(nameOfIdProperty).as(Constants.NAME_OF_ID)
)
.returning(expressions)
.build();
}
@@ -412,7 +420,6 @@ public enum CypherGenerator {
.named(START_NODE_NAME);
final Node endNode = anyNode(END_NODE_NAME);
String idPropertyName = neo4jPersistentEntity.getRequiredIdProperty().getPropertyName();
Parameter<?> idParameter = parameter(Constants.FROM_ID_PARAMETER_NAME);
String type = relationship.isDynamic() ? dynamicRelationshipType : relationship.getType();
@@ -420,17 +427,45 @@ public enum CypherGenerator {
startNode.relationshipTo(endNode, type) :
startNode.relationshipFrom(endNode, type)).named(RELATIONSHIP_NAME);
var startNodeIdFunction = getNodeIdFunction(neo4jPersistentEntity);
return match(startNode)
.where(neo4jPersistentEntity.isUsingInternalIds() ? Functions.id(startNode).isEqualTo(idParameter)
: startNode.property(idPropertyName).isEqualTo(idParameter))
.match(endNode).where(Functions.id(endNode).isEqualTo(parameter(Constants.TO_ID_PARAMETER_NAME)))
.where(startNodeIdFunction.apply(startNode).isEqualTo(idParameter))
.match(endNode).where(Functions.elementId(endNode).isEqualTo(parameter(Constants.TO_ID_PARAMETER_NAME)))
.merge(relationshipFragment)
.returning(
Functions.id(relationshipFragment).as(Constants.NAME_OF_INTERNAL_ID),
Functions.elementId(relationshipFragment).as(Constants.NAME_OF_ELEMENT_ID)
)
.returning(getReturnedIdExpressionsForRelationship(relationship, relationshipFragment))
.build();
}
private static Function<Node, Expression> getNodeIdFunction(Neo4jPersistentEntity<?> entity) {
Function<Node, Expression> startNodeIdFunction;
var idProperty = entity.getRequiredIdProperty();
if (entity.isUsingInternalIds()) {
if (entity.isUsingDeprecatedInternalId()) {
startNodeIdFunction = Functions::id;
} else {
startNodeIdFunction = Functions::elementId;
}
} else {
startNodeIdFunction = node -> node.property(idProperty.getPropertyName());
}
return startNodeIdFunction;
}
private static Function<Relationship, Expression> getRelationshipIdFunction(RelationshipDescription relationshipDescription) {
Function<Relationship, Expression> result = Functions::elementId;
if (relationshipDescription.hasRelationshipProperties()) {
Neo4jPersistentEntity<?> entity = (Neo4jPersistentEntity<?>) relationshipDescription.getRelationshipPropertiesEntity();
if (entity != null && entity.isUsingDeprecatedInternalId()) {
result = Functions::id;
} else {
result = Functions::elementId;
}
}
return result;
}
@NonNull
public Statement prepareSaveOfRelationships(Neo4jPersistentEntity<?> neo4jPersistentEntity,
RelationshipDescription relationship, @Nullable String dynamicRelationshipType) {
@@ -441,7 +476,6 @@ public enum CypherGenerator {
.named(START_NODE_NAME);
final Node endNode = anyNode(END_NODE_NAME);
String idPropertyName = neo4jPersistentEntity.getRequiredIdProperty().getPropertyName();
String type = relationship.isDynamic() ? dynamicRelationshipType : relationship.getType();
Relationship relationshipFragment = (relationship.isOutgoing() ?
@@ -453,15 +487,10 @@ public enum CypherGenerator {
return Cypher.unwind(parameter(Constants.NAME_OF_RELATIONSHIP_LIST_PARAM)).as(row)
.with(row)
.match(startNode)
.where(neo4jPersistentEntity.isUsingInternalIds()
? internalId(startNode).isEqualTo(idProperty)
: startNode.property(idPropertyName).isEqualTo(idProperty))
.match(endNode).where(internalId(endNode).isEqualTo(Cypher.property(row, Constants.TO_ID_PARAMETER_NAME)))
.where(getNodeIdFunction(neo4jPersistentEntity).apply(startNode).isEqualTo(idProperty))
.match(endNode).where(Functions.elementId(endNode).isEqualTo(Cypher.property(row, Constants.TO_ID_PARAMETER_NAME)))
.merge(relationshipFragment)
.returning(
Functions.id(relationshipFragment).as(Constants.NAME_OF_INTERNAL_ID),
Functions.elementId(relationshipFragment).as(Constants.NAME_OF_ELEMENT_ID)
)
.returning(getReturnedIdExpressionsForRelationship(relationship, relationshipFragment))
.build();
}
@@ -476,7 +505,6 @@ public enum CypherGenerator {
Node startNode = node(neo4jPersistentEntity.getPrimaryLabel(), neo4jPersistentEntity.getAdditionalLabels()).named(START_NODE_NAME);
Node endNode = anyNode(END_NODE_NAME);
String idPropertyName = neo4jPersistentEntity.getRequiredIdProperty().getPropertyName();
Parameter<?> idParameter = parameter(Constants.FROM_ID_PARAMETER_NAME);
Parameter<?> relationshipProperties = parameter(Constants.NAME_OF_PROPERTIES_PARAM);
@@ -488,21 +516,20 @@ public enum CypherGenerator {
startNode.relationshipFrom(endNode, type))
.named(RELATIONSHIP_NAME);
var nodeIdFunction = getNodeIdFunction(neo4jPersistentEntity);
var relationshipIdFunction = getRelationshipIdFunction(relationship);
StatementBuilder.OngoingReadingWithWhere startAndEndNodeMatch = match(startNode)
.where(neo4jPersistentEntity.isUsingInternalIds() ? Functions.id(startNode).isEqualTo(idParameter)
: startNode.property(idPropertyName).isEqualTo(idParameter))
.match(endNode).where(Functions.id(endNode).isEqualTo(parameter(Constants.TO_ID_PARAMETER_NAME)));
.where(nodeIdFunction.apply(startNode).isEqualTo(idParameter))
.match(endNode).where(Functions.elementId(endNode).isEqualTo(parameter(Constants.TO_ID_PARAMETER_NAME)));
StatementBuilder.ExposesSet createOrMatch = isNew
? startAndEndNodeMatch.create(relationshipFragment)
: startAndEndNodeMatch.match(relationshipFragment)
.where(Functions.id(relationshipFragment).isEqualTo(Cypher.parameter(Constants.NAME_OF_KNOWN_RELATIONSHIP_PARAM)));
.where(relationshipIdFunction.apply(relationshipFragment).isEqualTo(Cypher.parameter(Constants.NAME_OF_KNOWN_RELATIONSHIP_PARAM)));
return createOrMatch
.mutate(RELATIONSHIP_NAME, relationshipProperties)
.returning(
Functions.id(relationshipFragment).as(Constants.NAME_OF_INTERNAL_ID),
Functions.elementId(relationshipFragment).as(Constants.NAME_OF_ELEMENT_ID)
)
.returning(getReturnedIdExpressionsForRelationship(relationship, relationshipFragment))
.build();
}
@@ -515,7 +542,6 @@ public enum CypherGenerator {
Node startNode = node(neo4jPersistentEntity.getPrimaryLabel(), neo4jPersistentEntity.getAdditionalLabels()).named(START_NODE_NAME);
Node endNode = anyNode(END_NODE_NAME);
String idPropertyName = neo4jPersistentEntity.getRequiredIdProperty().getPropertyName();
String type = relationship.getType();
@@ -532,27 +558,37 @@ public enum CypherGenerator {
.as(row)
.with(row);
var nodeIdFunction = getNodeIdFunction(neo4jPersistentEntity);
var relationshipIdFunction = getRelationshipIdFunction(relationship);
// we only need start and end node querying if we have to create a new relationship...
if (isNew) {
return cypherUnwind
.match(startNode)
.where(neo4jPersistentEntity.isUsingInternalIds() ? internalId(startNode).isEqualTo(idProperty)
: startNode.property(idPropertyName).isEqualTo(idProperty))
.match(endNode).where(internalId(endNode).isEqualTo(Cypher.property(row, Constants.TO_ID_PARAMETER_NAME)))
.where(nodeIdFunction.apply(startNode).isEqualTo(idProperty))
.match(endNode)
.where(endNode.elementId().isEqualTo(Cypher.property(row, Constants.TO_ID_PARAMETER_NAME)))
.create(relationshipFragment)
.mutate(RELATIONSHIP_NAME, relationshipProperties).returning(
Functions.id(relationshipFragment).as(Constants.NAME_OF_INTERNAL_ID),
Functions.elementId(relationshipFragment).as(Constants.NAME_OF_ELEMENT_ID)
).build();
.mutate(RELATIONSHIP_NAME, relationshipProperties)
.returning(getReturnedIdExpressionsForRelationship(relationship, relationshipFragment))
.build();
}
// ... otherwise we can just fetch the existing relationship by known id
return cypherUnwind.match(relationshipFragment)
.where(Functions.id(relationshipFragment).isEqualTo(Cypher.property(row, Constants.NAME_OF_KNOWN_RELATIONSHIP_PARAM)))
.where(relationshipIdFunction.apply(relationshipFragment).isEqualTo(Cypher.property(row, Constants.NAME_OF_KNOWN_RELATIONSHIP_PARAM)))
.mutate(RELATIONSHIP_NAME, relationshipProperties).build();
}
private static List<Expression> getReturnedIdExpressionsForRelationship(RelationshipDescription relationship, Relationship relationshipFragment) {
List<Expression> result = new ArrayList<>();
if (relationship.hasRelationshipProperties() && relationship.getRelationshipPropertiesEntity() instanceof Neo4jPersistentEntity<?> entity && entity.isUsingDeprecatedInternalId()) {
result.add(Functions.id(relationshipFragment).as(Constants.NAME_OF_INTERNAL_ID));
}
result.add(Functions.elementId(relationshipFragment).as(Constants.NAME_OF_ELEMENT_ID));
return result;
}
@NonNull
public Statement prepareDeleteOf(
Neo4jPersistentEntity<?> neo4jPersistentEntity,
@@ -565,7 +601,6 @@ public enum CypherGenerator {
NodeDescription<?> target = relationshipDescription.getTarget();
Node endNode = node(target.getPrimaryLabel(), target.getAdditionalLabels());
String idPropertyName = neo4jPersistentEntity.getRequiredIdProperty().getPropertyName();
boolean outgoing = relationshipDescription.isOutgoing();
String relationshipType = relationshipDescription.isDynamic() ? null : relationshipDescription.getType();
@@ -577,9 +612,8 @@ public enum CypherGenerator {
Parameter<?> idParameter = parameter(Constants.FROM_ID_PARAMETER_NAME);
return match(relationship)
.where(neo4jPersistentEntity.isUsingInternalIds() ? Functions.id(startNode).isEqualTo(idParameter)
: startNode.property(idPropertyName).isEqualTo(idParameter))
.and(Functions.id(relationship).in(Cypher.parameter(Constants.NAME_OF_KNOWN_RELATIONSHIPS_PARAM)).not())
.where(getNodeIdFunction(neo4jPersistentEntity).apply(startNode).isEqualTo(idParameter))
.and(getRelationshipIdFunction(relationshipDescription).apply(relationship).in(Cypher.parameter(Constants.NAME_OF_KNOWN_RELATIONSHIPS_PARAM)).not())
.delete(relationship.getRequiredSymbolicName())
.build();
}
@@ -677,11 +711,6 @@ public enum CypherGenerator {
return returnExpressions;
}
@SuppressWarnings("deprecation")
private FunctionInvocation internalId(Node node) {
return node.internalId();
}
private MapProjection projectPropertiesAndRelationships(PropertyFilter.RelaxedPropertyPath parentPath, Neo4jPersistentEntity<?> nodeDescription, SymbolicName nodeName,
Predicate<PropertyFilter.RelaxedPropertyPath> includedProperties, @Nullable RelationshipDescription relationshipDescription, List<RelationshipDescription> processedRelationships) {
@@ -749,8 +778,10 @@ public enum CypherGenerator {
nodePropertiesProjection.add(Constants.NAME_OF_LABELS);
nodePropertiesProjection.add(Functions.labels(node));
nodePropertiesProjection.add(Constants.NAME_OF_INTERNAL_ID);
nodePropertiesProjection.add(Functions.id(node));
if (nodeDescription instanceof Neo4jPersistentEntity<?> entity && entity.isUsingDeprecatedInternalId()) {
nodePropertiesProjection.add(Constants.NAME_OF_INTERNAL_ID);
nodePropertiesProjection.add(Functions.id(node));
}
nodePropertiesProjection.add(Constants.NAME_OF_ELEMENT_ID);
nodePropertiesProjection.add(Functions.elementId(node));
return nodePropertiesProjection;

View File

@@ -54,7 +54,6 @@ import org.springframework.data.mapping.model.EntityInstantiators;
import org.springframework.data.mapping.model.ParameterValueProvider;
import org.springframework.data.neo4j.core.convert.Neo4jConversionService;
import org.springframework.data.neo4j.core.mapping.callback.EventSupport;
import org.springframework.data.neo4j.core.schema.ElementId;
import org.springframework.data.neo4j.core.schema.TargetNode;
import org.springframework.data.util.ReflectionUtils;
import org.springframework.data.util.TypeInformation;
@@ -148,7 +147,7 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter {
Node node = value.asNode();
if (primaryLabels.stream().anyMatch(node::hasLabel)) { // it has a matching label
// We haven't seen this node yet, so we take it
if (knownObjects.getObject("N" + IdentitySupport.getInternalId(node)) == null) {
if (knownObjects.getObject("N" + IdentitySupport.getElementId(node)) == null) {
matchingNodes.add(node);
} else {
seenMatchingNodes.add(node);
@@ -281,7 +280,7 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter {
Map<String, Object> mergedAttributes = new HashMap<>(node.size() + record.size() + 1);
mergedAttributes.put(Constants.NAME_OF_INTERNAL_ID, IdentitySupport.getInternalId(node));
mergedAttributes.put(Constants.NAME_OF_ELEMENT_ID, ElementId.of(node).value());
mergedAttributes.put(Constants.NAME_OF_ELEMENT_ID, node.elementId());
mergedAttributes.put(Constants.NAME_OF_LABELS, node.labels());
mergedAttributes.putAll(node.asMap(Function.identity()));
mergedAttributes.putAll(record.asMap(Function.identity()));
@@ -310,7 +309,7 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter {
// losing the central access. The behaviour of knowObjects should take different sources of ids into account,
// as relationships and nodes might have overlapping values
String direction = relationshipDescription != null ? relationshipDescription.getDirection().name() : null;
String internalId = IdentitySupport.getInternalId(queryResult, direction);
String internalId = IdentitySupport.getPrefixedElementId(queryResult, direction);
Supplier<ET> mappedObjectSupplier = () -> {
knownObjects.setInCreation(internalId);
@@ -656,11 +655,18 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter {
List<Object> relationshipsAndProperties = new ArrayList<>();
if (Values.NULL.equals(list)) {
Long sourceNodeId = IdentitySupport.getInternalId(values);
//Long sourceNodeId = getInternalIdAsLong(values);
Function<Relationship, Long> sourceIdSelector = relationshipDescription.isIncoming() ? IdentitySupport::getEndId : IdentitySupport::getStartId;
Function<Relationship, Long> targetIdSelector = relationshipDescription.isIncoming() ? IdentitySupport::getStartId : IdentitySupport::getEndId;
String sourceNodeId;
Function<Relationship, String> sourceIdSelector;
Function<Relationship, String> targetIdSelector = relationshipDescription.isIncoming() ? Relationship::startNodeElementId : Relationship::endNodeElementId;
if (IdentitySupport.getElementId(values) == null) {
// this can happen when someone used dto mapping and added the "classical" approach
sourceNodeId = Optional.ofNullable(IdentitySupport.getInternalId(values)).map(l -> Long.toString(l)).orElseThrow();
Function<Relationship, Long> hlp = relationshipDescription.isIncoming() ? Relationship::endNodeId : Relationship::startNodeId;
sourceIdSelector = hlp.andThen(l -> Long.toString(l));
} else {
sourceNodeId = IdentitySupport.getElementId(values);
sourceIdSelector = relationshipDescription.isIncoming() ? Relationship::endNodeElementId : Relationship::startNodeElementId;
}
// Retrieve all matching relationships from the result's list(s)
Collection<Relationship> allMatchingTypeRelationshipsInResult =
@@ -671,21 +677,21 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter {
Collection<Node> allNodesWithMatchingLabelInResult = extractMatchingNodes(nodesFromResult, targetLabel);
for (Node possibleValueNode : allNodesWithMatchingLabelInResult) {
long targetNodeId = IdentitySupport.getInternalId(possibleValueNode);
String targetNodeId = IdentitySupport.getElementId(possibleValueNode);
Neo4jPersistentEntity<?> concreteTargetNodeDescription =
getMostConcreteTargetNodeDescription(genericTargetNodeDescription, possibleValueNode);
Set<Relationship> relationshipsProcessed = new HashSet<>();
for (Relationship possibleRelationship : allMatchingTypeRelationshipsInResult) {
if (targetIdSelector.apply(possibleRelationship) == targetNodeId) {
if (targetIdSelector.apply(possibleRelationship).equals(targetNodeId)) {
// Reduce the amount of relationships in the candidate list.
// If this relationship got processed twice (OUTGOING, INCOMING), it is never needed again
// and therefor should not be in the list.
// Otherwise, for highly linked data it could potentially cause a StackOverflowError.
String direction = relationshipDescription.getDirection().name();
if (knownObjects.hasProcessedRelationshipCompletely("R" + direction + IdentitySupport.getInternalId(possibleRelationship))) {
if (knownObjects.hasProcessedRelationshipCompletely("R" + direction + IdentitySupport.getElementId(possibleRelationship))) {
relationshipsFromResult.remove(possibleRelationship);
}
// If the target is the same(equal) node, get the related object from the cache.
@@ -711,7 +717,7 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter {
if (fetchMore) {
relationshipProperties = map(possibleRelationship, relationshipPropertiesEntity, relationshipPropertiesEntity, mappedObject, relationshipDescription, relationshipsFromResult, nodesFromResult);
} else {
Object objectFromStore = knownObjects.getObject(IdentitySupport.getInternalId(possibleRelationship, relationshipDescription.getDirection().name()));
Object objectFromStore = knownObjects.getObject(IdentitySupport.getPrefixedElementId(possibleRelationship, relationshipDescription.getDirection().name()));
relationshipProperties = objectFromStore != null
? objectFromStore
: map(possibleRelationship, relationshipPropertiesEntity, relationshipPropertiesEntity, mappedObject, relationshipDescription, relationshipsFromResult, nodesFromResult);
@@ -736,7 +742,7 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter {
if (fetchMore) {
valueEntry = map(relatedEntity, concreteTargetNodeDescription, genericNodeDescription, null, null, relationshipsFromResult, nodesFromResult);
} else {
Object objectFromStore = knownObjects.getObject(IdentitySupport.getInternalId(relatedEntity, null));
Object objectFromStore = knownObjects.getObject(IdentitySupport.getPrefixedElementId(relatedEntity, null));
valueEntry = objectFromStore != null
? objectFromStore
: map(relatedEntity, concreteTargetNodeDescription, genericNodeDescription, null, null, relationshipsFromResult, nodesFromResult);
@@ -754,7 +760,7 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter {
if (fetchMore) {
relationshipProperties = map(relatedEntityRelationship, relationshipPropertiesEntity, relationshipPropertiesEntity, valueEntry, relationshipDescription, relationshipsFromResult, nodesFromResult);
} else {
Object objectFromStore = knownObjects.getObject(IdentitySupport.getInternalId(relatedEntityRelationship, relationshipDescription.getDirection().name()));
Object objectFromStore = knownObjects.getObject(IdentitySupport.getPrefixedElementId(relatedEntityRelationship, relationshipDescription.getDirection().name()));
relationshipProperties = objectFromStore != null
? objectFromStore
: map(relatedEntityRelationship, relationshipPropertiesEntity, relationshipPropertiesEntity, valueEntry, relationshipDescription, relationshipsFromResult, nodesFromResult);
@@ -832,13 +838,15 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter {
.filter(this.relationshipType::isTypeOf)
.map(Value::asRelationship)
.forEach(allRelationshipsInResult::add);
return allRelationshipsInResult;
}
private static Value extractValueOf(Neo4jPersistentProperty property, MapAccessor propertyContainer) {
if (property.isInternalIdProperty()) {
return Values.value(IdentitySupport.getInternalId(propertyContainer));
if (Neo4jPersistentEntity.DEPRECATED_GENERATED_ID_TYPES.contains(property.getType())) {
return Values.value(IdentitySupport.getInternalId(propertyContainer));
}
return Values.value(IdentitySupport.getElementId(propertyContainer));
} else if (property.isComposite()) {
String prefix = property.computePrefixWithDelimiter();
@@ -924,7 +932,7 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter {
try {
read.lock();
return internalIdStore.containsKey(IdentitySupport.getInternalId(node));
return internalIdStore.containsKey(IdentitySupport.getElementId(node));
} finally {
read.unlock();
}

View File

@@ -38,7 +38,6 @@ import org.springframework.data.annotation.Persistent;
import org.springframework.data.mapping.Association;
import org.springframework.data.mapping.model.BasicPersistentEntity;
import org.springframework.data.neo4j.core.schema.DynamicLabels;
import org.springframework.data.neo4j.core.schema.ElementId;
import org.springframework.data.neo4j.core.schema.GeneratedValue;
import org.springframework.data.neo4j.core.schema.IdGenerator;
import org.springframework.data.neo4j.core.schema.Node;
@@ -62,8 +61,7 @@ import org.springframework.util.StringUtils;
final class DefaultNeo4jPersistentEntity<T> extends BasicPersistentEntity<T, Neo4jPersistentProperty>
implements Neo4jPersistentEntity<T> {
private static final Set<Class<?>> DEPRECATED_GENERATED_ID_TYPES = Set.of(Long.class, long.class);
private static final Set<Class<?>> VALID_GENERATED_ID_TYPES = Stream.concat(Stream.of(ElementId.class), DEPRECATED_GENERATED_ID_TYPES.stream()).collect(Collectors.toUnmodifiableSet());
private static final Set<Class<?>> VALID_GENERATED_ID_TYPES = Stream.concat(Stream.of(String.class), DEPRECATED_GENERATED_ID_TYPES.stream()).collect(Collectors.toUnmodifiableSet());
private static final LogAccessor log = new LogAccessor(LogFactory.getLog(Neo4jPersistentEntity.class));
/**
@@ -444,7 +442,8 @@ final class DefaultNeo4jPersistentEntity<T> extends BasicPersistentEntity<T, Neo
"Internally generated ids can only be assigned to one of " + VALID_GENERATED_ID_TYPES);
}
if (DEPRECATED_GENERATED_ID_TYPES.contains(idProperty.getActualType())) {
var isDeprecated = DEPRECATED_GENERATED_ID_TYPES.contains(idProperty.getActualType());
if (isDeprecated) {
Supplier<CharSequence> messageSupplier = () -> String.format(""
+ "The entity %s is using a Long value for storing internally generated Neo4j ids. "
+ "The Neo4j internal Long Ids are deprecated, please consider using an external ID generator.",
@@ -452,7 +451,7 @@ final class DefaultNeo4jPersistentEntity<T> extends BasicPersistentEntity<T, Neo
log.warn(messageSupplier);
}
return IdDescription.forInternallyGeneratedIds(Constants.NAME_OF_TYPED_ROOT_NODE.apply(this));
return IdDescription.forInternallyGeneratedIds(Constants.NAME_OF_TYPED_ROOT_NODE.apply(this), isDeprecated);
}
// Externally generated ids.

View File

@@ -58,11 +58,15 @@ public final class IdDescription {
public static IdDescription forAssignedIds(SymbolicName symbolicName, String graphPropertyName) {
Assert.notNull(graphPropertyName, "Graph property name is required");
return new IdDescription(symbolicName, null, null, graphPropertyName);
return new IdDescription(symbolicName, null, null, graphPropertyName, false);
}
public static IdDescription forInternallyGeneratedIds(SymbolicName symbolicName) {
return new IdDescription(symbolicName, GeneratedValue.InternalIdGenerator.class, null, null);
return forInternallyGeneratedIds(symbolicName, false);
}
public static IdDescription forInternallyGeneratedIds(SymbolicName symbolicName, boolean isDeprecated) {
return new IdDescription(symbolicName, GeneratedValue.InternalIdGenerator.class, null, null, isDeprecated);
}
public static IdDescription forExternallyGeneratedIds(SymbolicName symbolicName,
@@ -73,18 +77,18 @@ public final class IdDescription {
try {
Assert.hasText(idGeneratorRef, "Reference to an ID generator has precedence");
return new IdDescription(symbolicName, null, idGeneratorRef, graphPropertyName);
return new IdDescription(symbolicName, null, idGeneratorRef, graphPropertyName, false);
} catch (IllegalArgumentException e) {
Assert.notNull(idGeneratorClass, "Class of id generator is required");
Assert.isTrue(idGeneratorClass != GeneratedValue.InternalIdGenerator.class,
"Cannot use InternalIdGenerator for externally generated ids");
return new IdDescription(symbolicName, idGeneratorClass, null, graphPropertyName);
return new IdDescription(symbolicName, idGeneratorClass, null, graphPropertyName, false);
}
}
private IdDescription(SymbolicName symbolicName, @Nullable Class<? extends IdGenerator<?>> idGeneratorClass,
@Nullable String idGeneratorRef, @Nullable String graphPropertyName) {
@Nullable String idGeneratorRef, @Nullable String graphPropertyName, boolean isDeprecated) {
this.idGeneratorClass = idGeneratorClass;
this.idGeneratorRef = idGeneratorRef != null && idGeneratorRef.isEmpty() ? null : idGeneratorRef;
@@ -93,7 +97,7 @@ public final class IdDescription {
this.idExpression = Lazy.of(() -> {
final Node rootNode = Cypher.anyNode(symbolicName);
if (this.isInternallyGeneratedId()) {
return Functions.id(rootNode);
return isDeprecated ? Functions.id(rootNode) : Functions.elementId(rootNode);
} else {
return this.getOptionalGraphPropertyName()
.map(propertyName -> Cypher.property(symbolicName, propertyName)).get();

View File

@@ -17,14 +17,13 @@ package org.springframework.data.neo4j.core.mapping;
import static org.apiguardian.api.API.Status.INTERNAL;
import java.util.function.Function;
import org.apiguardian.api.API;
import org.neo4j.driver.Record;
import org.neo4j.driver.types.Entity;
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.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
@@ -32,7 +31,7 @@ import org.springframework.lang.Nullable;
* This class is <strong>not</strong> part of any public API and will be changed without further notice as needed. It's
* primary goal is to mitigate the changes in Neo4j5, which introduces the notion of an {@literal element id} for both nodes
* and relationships while deprecating {@literal id} at the same time. The identity support allows to isolate our calls
* deprecated API in one central place and will exists for SDN 7 only to make SDN 7 work with both Neo4j 4.4 and Neo4j 5.x.
* deprecated API in one central place and will exist for SDN 7 only to make SDN 7 work with both Neo4j 4.4 and Neo4j 5.x.
*
* @author Michael J. Simons
* @soundtrack Buckethead - SIGIL Soundtrack
@@ -44,31 +43,12 @@ public final class IdentitySupport {
private IdentitySupport() {
}
/**
* Updates the internal id of a given client side entity from a server side entity using a property accessor.
* Does nothing if the local entity does not use internally generated ids.
*
* @param entityMetaData The entity's meta data
* @param propertyAccessor An accessor to the entity
* @param entity As received via the driver
*/
public static void updateInternalId(Neo4jPersistentEntity<?> entityMetaData,
PersistentPropertyAccessor<?> propertyAccessor, Entity entity) {
if (!entityMetaData.isUsingInternalIds()) {
return;
}
propertyAccessor.setProperty(entityMetaData.getRequiredIdProperty(), getInternalId(entity));
}
/**
* @param entity The entity container as received from the server.
* @return The internal id
*/
@SuppressWarnings("deprecation")
public static Long getInternalId(Entity entity) {
return entity.id();
public static String getElementId(Entity entity) {
return entity.elementId();
}
/**
@@ -78,9 +58,24 @@ public final class IdentitySupport {
* @return An internal id
*/
@Nullable
public static String getElementId(@NonNull MapAccessor row) {
if (row instanceof Entity entity) {
return getElementId(entity);
}
var columnToUse = Constants.NAME_OF_ELEMENT_ID;
if (row.get(columnToUse) == null || row.get(columnToUse).isNull()) {
return null;
}
return row.get(columnToUse).asString();
}
@Nullable
@Deprecated
public static Long getInternalId(@NonNull MapAccessor row) {
if (row instanceof Entity entity) {
return getInternalId(entity);
return entity.id();
}
var columnToUse = Constants.NAME_OF_INTERNAL_ID;
@@ -92,41 +87,20 @@ public final class IdentitySupport {
}
@Nullable
public static String getInternalId(@NonNull MapAccessor queryResult, @Nullable String seed) {
public static String getPrefixedElementId(@NonNull MapAccessor queryResult, @Nullable String seed) {
if (queryResult instanceof Node) {
return "N" + getInternalId(queryResult);
return "N" + getElementId(queryResult);
} else if (queryResult instanceof Relationship) {
return "R" + seed + getInternalId(queryResult);
} else if (!(queryResult.get(Constants.NAME_OF_INTERNAL_ID) == null || queryResult.get(Constants.NAME_OF_INTERNAL_ID).isNull())) {
return "N" + queryResult.get(Constants.NAME_OF_INTERNAL_ID).asLong();
return "R" + seed + getElementId(queryResult);
} else if (!(queryResult.get(Constants.NAME_OF_ELEMENT_ID) == null || queryResult.get(Constants.NAME_OF_ELEMENT_ID).isNull())) {
return "N" + queryResult.get(Constants.NAME_OF_ELEMENT_ID).asString();
}
return null;
}
/**
* Returns the start id of the relationship
*
* @param relationship A relationship to retrieve the start identity
* @return An internal id
*/
@SuppressWarnings("deprecation")
public static Long getStartId(Relationship relationship) {
return relationship.startNodeId();
}
/**
* Returns the end id of the relationship
*
* @param relationship A relationship to retrieve the end identity
* @return An internal id
*/
@SuppressWarnings("deprecation")
public static Long getEndId(Relationship relationship) {
return relationship.endNodeId();
}
public static Long getInternalId(TypeSystem typeSystem, Record row) {
return getInternalId(row);
public static Function<MapAccessor, Object> mapperForRelatedIdValues(@Nullable Neo4jPersistentProperty idProperty) {
boolean deprecatedHolder = idProperty != null && Neo4jPersistentEntity.DEPRECATED_GENERATED_ID_TYPES.contains(idProperty.getType());
return deprecatedHolder ? IdentitySupport::getInternalId : IdentitySupport::getElementId;
}
}

View File

@@ -16,6 +16,7 @@
package org.springframework.data.neo4j.core.mapping;
import java.util.Optional;
import java.util.Set;
import org.apiguardian.api.API;
import org.springframework.data.mapping.AssociationHandler;
@@ -40,6 +41,8 @@ import org.springframework.data.mapping.model.MutablePersistentEntity;
public interface Neo4jPersistentEntity<T>
extends MutablePersistentEntity<T, Neo4jPersistentProperty>, NodeDescription<T> {
Set<Class<?>> DEPRECATED_GENERATED_ID_TYPES = Set.of(Long.class, long.class);
/**
* @return An optional property pointing to a {@link java.util.Collection Collection&lt;String&gt;} containing dynamic
* "runtime managed" labels.
@@ -53,4 +56,10 @@ public interface Neo4jPersistentEntity<T>
*/
boolean isRelationshipPropertiesEntity();
/**
* @return True if the underlying domain classes uses {@code id()} to compute internally generated ids.
*/
default boolean isUsingDeprecatedInternalId() {
return isUsingInternalIds() && Neo4jPersistentEntity.DEPRECATED_GENERATED_ID_TYPES.contains(getRequiredIdProperty().getType());
}
}

View File

@@ -19,11 +19,9 @@ import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.locks.StampedLock;
import java.util.stream.Collectors;
import org.apiguardian.api.API;
import org.springframework.lang.NonNull;
@@ -66,7 +64,7 @@ public final class NestedRelationshipProcessingStateMachine {
* A map pointing from a processed object to the internal id.
* This will be useful during the persistence to avoid another DB network round-trip.
*/
private final Map<Integer, Long> processedObjectsIds = new HashMap<>();
private final Map<Integer, String> processedObjectsIds = new HashMap<>();
public NestedRelationshipProcessingStateMachine(final Neo4jMappingContext mappingContext) {
@@ -75,22 +73,24 @@ public final class NestedRelationshipProcessingStateMachine {
this.mappingContext = mappingContext;
}
public NestedRelationshipProcessingStateMachine(final Neo4jMappingContext mappingContext, Object initialObject, Long internalId) {
public NestedRelationshipProcessingStateMachine(final Neo4jMappingContext mappingContext, Object initialObject, String elementId) {
this(mappingContext);
Assert.notNull(initialObject, "Initial object must not be null");
Assert.notNull(internalId, "The initial objects internal ID must not be null");
Assert.notNull(elementId, "The initial objects element ID must not be null");
storeHashedVersionInProcessedObjectsIds(initialObject, internalId);
storeHashedVersionInProcessedObjectsIds(initialObject, elementId);
}
/**
* @param relationshipDescription Check whether this relationship description has been processed
* @param valuesToStore Check whether all the values in the collection have been processed
* @param valuesToStore Check whether all the values in the collection have been processed
* @return The state of things processed
*/
public ProcessState getStateOf(Object fromId, RelationshipDescription relationshipDescription, @Nullable Collection<?> valuesToStore) {
public ProcessState getStateOf(@Nullable Object fromId, RelationshipDescription relationshipDescription, @Nullable Collection<?> valuesToStore) {
if (fromId == null) {
return ProcessState.PROCESSED_BOTH;
}
final long stamp = lock.readLock();
try {
boolean hasProcessedRelationship = hasProcessedRelationship(fromId, relationshipDescription);
@@ -116,31 +116,7 @@ public final class NestedRelationshipProcessingStateMachine {
* can get processed for different objects of the same entity.
* One could say that this is a Tuple but it has a nicer name.
*/
private static class RelationshipDescriptionWithSourceId {
private final Object id;
private final RelationshipDescription relationshipDescription;
RelationshipDescriptionWithSourceId(Object id, RelationshipDescription relationshipDescription) {
this.id = id;
this.relationshipDescription = relationshipDescription;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
RelationshipDescriptionWithSourceId that = (RelationshipDescriptionWithSourceId) o;
return id.equals(that.id) && relationshipDescription.equals(that.relationshipDescription);
}
@Override
public int hashCode() {
return Objects.hash(id, relationshipDescription);
}
private record RelationshipDescriptionWithSourceId(Object id, RelationshipDescription relationshipDescription) {
}
/**
@@ -165,24 +141,24 @@ public final class NestedRelationshipProcessingStateMachine {
* Marks the passed objects as processed
*
* @param valueToStore If not {@literal null}, all non-null values will be marked as processed
* @param internalId The internal id of the value processed
* @param elementId The internal id of the value processed
*/
public void markValueAsProcessed(Object valueToStore, Long internalId) {
public void markEntityAsProcessed(Object valueToStore, String elementId) {
final long stamp = lock.writeLock();
try {
doMarkValueAsProcessed(valueToStore, internalId);
doMarkValueAsProcessed(valueToStore, elementId);
storeProcessedInAlias(valueToStore, valueToStore);
} finally {
lock.unlock(stamp);
}
}
private void doMarkValueAsProcessed(Object valueToStore, Long internalId) {
private void doMarkValueAsProcessed(Object valueToStore, String elementId) {
Object value = extractRelatedValueFromRelationshipProperties(valueToStore);
storeHashedVersionInProcessedObjectsIds(valueToStore, internalId);
storeHashedVersionInProcessedObjectsIds(value, internalId);
storeHashedVersionInProcessedObjectsIds(valueToStore, elementId);
storeHashedVersionInProcessedObjectsIds(value, elementId);
}
/**
@@ -204,6 +180,7 @@ public final class NestedRelationshipProcessingStateMachine {
Neo4jPersistentEntity<?> entity = mappingContext.getRequiredPersistentEntity(typeOfValue);
Neo4jPersistentProperty idProperty = entity.getIdProperty();
Object id = idProperty == null ? null : entity.getPropertyAccessor(valueToCheck).getProperty(idProperty);
// After the lookup by system.identityHashCode failed for a processed object alias,
// we must traverse or iterate over all value with the matching type and compare the domain ids
// to figure out if the logical object has already been processed through a different object instance.
@@ -214,7 +191,7 @@ public final class NestedRelationshipProcessingStateMachine {
.findAny();
if (alreadyProcessedObject.isPresent()) { // Skip the show the next time around.
processed = true;
Long internalId = getInternalId(alreadyProcessedObject.get());
String internalId = getObjectId(alreadyProcessedObject.get());
if (internalId != null) {
stamp = lock.tryConvertToWriteLock(stamp);
doMarkValueAsProcessed(valueToCheck, internalId);
@@ -245,26 +222,32 @@ public final class NestedRelationshipProcessingStateMachine {
return false;
}
public void markValueAsProcessedAs(Object valueToStore, Object bean) {
public void markAsAliased(Object aliasEntity, Object entityOrId) {
final long stamp = lock.writeLock();
try {
storeProcessedInAlias(valueToStore, bean);
storeProcessedInAlias(aliasEntity, entityOrId);
} finally {
lock.unlock(stamp);
}
}
/**
* This returns an id for the given object. We deliberate use the wording of a generic object id as that might either be
* the Neo4j 5+ {@literal elementId()} or on older Neo4j versions or with older data modules {@code toString(id())}.
*
* @param object The object for which an id is requested
* @return The objects id
*/
@Nullable
public Long getInternalId(Object object) {
public String getObjectId(Object object) {
final long stamp = lock.readLock();
try {
Object valueToCheck = extractRelatedValueFromRelationshipProperties(object);
Long possibleId = getProcessedObjectIds(valueToCheck);
String possibleId = getProcessedObjectIds(valueToCheck);
return possibleId != null ? possibleId : getProcessedObjectIds(getProcessedAs(valueToCheck));
} finally {
lock.unlock(stamp);
}
}
public Object getProcessedAs(Object entity) {
@@ -278,7 +261,7 @@ public final class NestedRelationshipProcessingStateMachine {
}
@Nullable
private Long getProcessedObjectIds(@Nullable Object entity) {
private String getProcessedObjectIds(@Nullable Object entity) {
if (entity == null) {
return null;
}
@@ -299,27 +282,27 @@ public final class NestedRelationshipProcessingStateMachine {
/*
* Convenience wrapper functions to avoid exposing the System.identityHashCode "everywhere" in this class.
*/
private void storeHashedVersionInProcessedObjectsIds(Object initialObject, Long internalId) {
processedObjectsIds.put(System.identityHashCode(initialObject), internalId);
private void storeHashedVersionInProcessedObjectsIds(Object initialObject, String elementId) {
processedObjectsIds.put(System.identityHashCode(initialObject), elementId);
}
private void storeProcessedInAlias(Object valueToStore, Object bean) {
processedObjectsAlias.put(System.identityHashCode(valueToStore), bean);
private void storeProcessedInAlias(Object aliasEntity, Object targetEntity) {
processedObjectsAlias.put(System.identityHashCode(aliasEntity), targetEntity);
}
private Object getProcessedAsWithDefaults(Object entity) {
return processedObjectsAlias.getOrDefault(System.identityHashCode(entity), entity);
}
private boolean hasProcessed(Object valueToCheck) {
return processedObjectsAlias.containsKey(System.identityHashCode(valueToCheck));
private boolean hasProcessed(Object entity) {
return processedObjectsAlias.containsKey(System.identityHashCode(entity));
}
private boolean hasProcessedAllOf(@Nullable Collection<?> valuesToStore) {
private boolean hasProcessedAllOf(@Nullable Collection<?> entities) {
// there can be null elements in the unified collection of values to store.
if (valuesToStore == null) {
if (entities == null) {
return false;
}
return processedObjectsIds.keySet().containsAll(valuesToStore.stream().map(System::identityHashCode).collect(Collectors.toList()));
return processedObjectsIds.keySet().containsAll(entities.stream().map(System::identityHashCode).toList());
}
}

View File

@@ -134,6 +134,7 @@ public interface NodeDescription<T> {
@Nullable
NodeDescription<?> getParentNodeDescription();
/**
* @return An expression that represents the right identifier type.
*/

View File

@@ -26,7 +26,7 @@ import java.util.Optional;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import org.junit.Assert;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
@@ -35,6 +35,8 @@ import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.Mockito;
import org.neo4j.cypherdsl.core.Cypher;
import org.neo4j.cypherdsl.core.Statement;
import org.neo4j.cypherdsl.core.renderer.Configuration;
import org.neo4j.cypherdsl.core.renderer.Dialect;
import org.neo4j.cypherdsl.core.renderer.Renderer;
import org.springframework.data.domain.Sort;
import org.springframework.data.neo4j.core.schema.Id;
@@ -57,8 +59,8 @@ class CypherGeneratorTest {
relationshipDescription, "REL");
String expectedQuery = "MATCH (startNode:`Entity1`) WHERE startNode.id = $fromId MATCH (endNode)"
+ " WHERE id(endNode) = $toId MERGE (startNode)<-[relProps:`REL`]-(endNode) RETURN id(relProps) AS __internalNeo4jId__, toString(id(relProps)) AS __elementId__";
Assert.assertEquals(expectedQuery, Renderer.getDefaultRenderer().render(statement));
+ " WHERE elementId(endNode) = $toId MERGE (startNode)<-[relProps:`REL`]-(endNode) RETURN elementId(relProps) AS __elementId__";
Assertions.assertEquals(expectedQuery, Renderer.getRenderer(Configuration.newConfig().withDialect(Dialect.NEO4J_5).build()).render(statement));
}
@Test
@@ -73,8 +75,8 @@ class CypherGeneratorTest {
String expectedQuery =
"MATCH (startNode:`Entity1`:`MultipleLabel`) WHERE startNode.id = $fromId MATCH (endNode)"
+ " WHERE id(endNode) = $toId MERGE (startNode)<-[relProps:`REL`]-(endNode) RETURN id(relProps) AS __internalNeo4jId__, toString(id(relProps)) AS __elementId__";
Assert.assertEquals(expectedQuery, Renderer.getDefaultRenderer().render(statement));
+ " WHERE elementId(endNode) = $toId MERGE (startNode)<-[relProps:`REL`]-(endNode) RETURN elementId(relProps) AS __elementId__";
Assertions.assertEquals(expectedQuery, Renderer.getRenderer(Configuration.newConfig().withDialect(Dialect.NEO4J_5).build()).render(statement));
}
@Test
@@ -83,16 +85,18 @@ class CypherGeneratorTest {
Neo4jPersistentEntity<?> persistentEntity = Mockito.mock(Neo4jPersistentEntity.class);
Neo4jPersistentProperty persistentProperty = Mockito.mock(Neo4jPersistentProperty.class);
doReturn(Long.class).when(persistentProperty).getType();
when(relationshipDescription.isDynamic()).thenReturn(true);
when(persistentEntity.isUsingInternalIds()).thenReturn(true);
when(persistentEntity.getRequiredIdProperty()).thenReturn(persistentProperty);
when(persistentEntity.isUsingDeprecatedInternalId()).thenReturn(true);
Statement statement = CypherGenerator.INSTANCE.prepareSaveOfRelationship(persistentEntity,
relationshipDescription, "REL");
String expectedQuery = "MATCH (startNode) WHERE id(startNode) = $fromId MATCH (endNode)"
+ " WHERE id(endNode) = $toId MERGE (startNode)<-[relProps:`REL`]-(endNode) RETURN id(relProps) AS __internalNeo4jId__, toString(id(relProps)) AS __elementId__";
Assert.assertEquals(expectedQuery, Renderer.getDefaultRenderer().render(statement));
+ " WHERE elementId(endNode) = $toId MERGE (startNode)<-[relProps:`REL`]-(endNode) RETURN elementId(relProps) AS __elementId__";
Assertions.assertEquals(expectedQuery, Renderer.getRenderer(Configuration.newConfig().withDialect(Dialect.NEO4J_5).build()).render(statement));
}
@Test
@@ -104,8 +108,8 @@ class CypherGeneratorTest {
Statement statement = CypherGenerator.INSTANCE.prepareDeleteOf(persistentEntity, relationshipDescription);
String expectedQuery = "MATCH (startNode:`Entity1`)<-[rel]-(:`Entity2`) WHERE (startNode.id = $fromId AND NOT (id(rel) IN $__knownRelationShipIds__)) DELETE rel";
Assert.assertEquals(expectedQuery, Renderer.getDefaultRenderer().render(statement));
String expectedQuery = "MATCH (startNode:`Entity1`)<-[rel]-(:`Entity2`) WHERE (startNode.id = $fromId AND NOT (elementId(rel) IN $__knownRelationShipIds__)) DELETE rel";
Assertions.assertEquals(expectedQuery, Renderer.getRenderer(Configuration.newConfig().withDialect(Dialect.NEO4J_5).build()).render(statement));
}
@Test
@@ -119,8 +123,8 @@ class CypherGeneratorTest {
Statement statement = CypherGenerator.INSTANCE.prepareDeleteOf(persistentEntity, relationshipDescription);
String expectedQuery = "MATCH (startNode:`Entity1`:`MultipleLabel`)<-[rel]-(:`Entity2`:`MultipleLabel`) WHERE (startNode.id = $fromId AND NOT (id(rel) IN $__knownRelationShipIds__)) DELETE rel";
Assert.assertEquals(expectedQuery, Renderer.getDefaultRenderer().render(statement));
String expectedQuery = "MATCH (startNode:`Entity1`:`MultipleLabel`)<-[rel]-(:`Entity2`:`MultipleLabel`) WHERE (startNode.id = $fromId AND NOT (elementId(rel) IN $__knownRelationShipIds__)) DELETE rel";
Assertions.assertEquals(expectedQuery, Renderer.getRenderer(Configuration.newConfig().withDialect(Dialect.NEO4J_5).build()).render(statement));
}
@Test
@@ -131,16 +135,18 @@ class CypherGeneratorTest {
RelationshipDescription relationshipDescription = Mockito.mock(RelationshipDescription.class);
Neo4jPersistentEntity<?> persistentEntity = Mockito.mock(Neo4jPersistentEntity.class);
Neo4jPersistentProperty persistentProperty = Mockito.mock(Neo4jPersistentProperty.class);
doReturn(Long.class).when(persistentProperty).getType();
doReturn(relatedEntity).when(relationshipDescription).getTarget();
when(relationshipDescription.isDynamic()).thenReturn(true);
when(persistentEntity.isUsingInternalIds()).thenReturn(true);
when(persistentEntity.getRequiredIdProperty()).thenReturn(persistentProperty);
when(persistentEntity.isUsingDeprecatedInternalId()).thenReturn(true);
Statement statement = CypherGenerator.INSTANCE.prepareDeleteOf(persistentEntity, relationshipDescription);
String expectedQuery = "MATCH (startNode)<-[rel]-(:`Entity2`) WHERE (id(startNode) = $fromId AND NOT (id(rel) IN $__knownRelationShipIds__)) DELETE rel";
Assert.assertEquals(expectedQuery, Renderer.getDefaultRenderer().render(statement));
String expectedQuery = "MATCH (startNode)<-[rel]-(:`Entity2`) WHERE (id(startNode) = $fromId AND NOT (elementId(rel) IN $__knownRelationShipIds__)) DELETE rel";
Assertions.assertEquals(expectedQuery, Renderer.getRenderer(Configuration.newConfig().withDialect(Dialect.NEO4J_5).build()).render(statement));
}
private static Stream<Arguments> pageables() {

View File

@@ -904,7 +904,7 @@ class Neo4jMappingContextTest {
static class InvalidIdType {
@Id @GeneratedValue @SuppressWarnings("unused")
private String id;
private Double id;
}
@Node

View File

@@ -58,7 +58,7 @@ class IdPopulatorTest {
@Test
void shouldIgnoreInternalIdGenerator() {
IdDescription toBeReturned = IdDescription.forInternallyGeneratedIds(Constants.NAME_OF_ROOT_NODE);
IdDescription toBeReturned = IdDescription.forInternallyGeneratedIds(Constants.NAME_OF_ROOT_NODE, true);
doReturn(toBeReturned).when(nodeDescription).getIdDescription();
doReturn(nodeDescription).when(neo4jMappingContext).getRequiredPersistentEntity(Sample.class);

View File

@@ -35,8 +35,6 @@ import org.neo4j.driver.types.Node;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.neo4j.core.mapping.IdentitySupport;
import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration;
import org.springframework.data.neo4j.core.DatabaseSelectionProvider;
import org.springframework.data.neo4j.core.Neo4jTemplate;
import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager;
@@ -52,6 +50,7 @@ import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories;
import org.springframework.data.neo4j.repository.query.Query;
import org.springframework.data.neo4j.test.BookmarkCapture;
import org.springframework.data.neo4j.test.Neo4jExtension;
import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration;
import org.springframework.data.neo4j.test.Neo4jIntegrationTest;
import org.springframework.data.repository.query.Param;
import org.springframework.transaction.PlatformTransactionManager;
@@ -371,8 +370,8 @@ public class InheritanceMappingIT {
Record record = createRelationsToDifferentImplementations();
Optional<Inheritance.ParentModel2> optionalDivision = repository.findById(
IdentitySupport.getInternalId(record.get(0).asNode()));
@SuppressWarnings("deprecation")
Optional<Inheritance.ParentModel2> optionalDivision = repository.findById(record.get(0).asNode().id());
assertThat(optionalDivision).isPresent();
assertThat(optionalDivision).hasValueSatisfying(twoDifferentInterfacesHaveBeenLoaded());
}

View File

@@ -34,8 +34,6 @@ import org.neo4j.driver.Transaction;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.neo4j.core.mapping.IdentitySupport;
import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration;
import org.springframework.data.neo4j.core.DatabaseSelectionProvider;
import org.springframework.data.neo4j.core.Neo4jClient;
import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager;
@@ -43,7 +41,9 @@ import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager;
import org.springframework.data.neo4j.integration.shared.common.PersonWithAllConstructor;
import org.springframework.data.neo4j.test.BookmarkCapture;
import org.springframework.data.neo4j.test.Neo4jExtension.Neo4jConnectionSupport;
import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration;
import org.springframework.data.neo4j.test.Neo4jIntegrationTest;
import org.springframework.data.neo4j.test.TestIdentitySupport;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.support.TransactionTemplate;
@@ -85,7 +85,7 @@ class Neo4jClientIT {
.first().extracting(r -> r.get("n").get("value").asLong()).isEqualTo(42L);
transactionStatus.setRollbackOnly();
return IdentitySupport.getInternalId(records.get(0).get("n").asNode());
return TestIdentitySupport.getInternalId(records.get(0).get("n").asNode());
});
// Make sure we actually interacted with the managed transaction (that had been rolled back)

View File

@@ -98,7 +98,6 @@ import org.springframework.data.neo4j.core.Neo4jTemplate;
import org.springframework.data.neo4j.core.UserSelection;
import org.springframework.data.neo4j.core.UserSelectionProvider;
import org.springframework.data.neo4j.core.convert.Neo4jConversions;
import org.springframework.data.neo4j.core.mapping.IdentitySupport;
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager;
import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager;
@@ -162,6 +161,7 @@ import org.springframework.data.neo4j.test.BookmarkCapture;
import org.springframework.data.neo4j.test.Neo4jExtension;
import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration;
import org.springframework.data.neo4j.test.ServerVersion;
import org.springframework.data.neo4j.test.TestIdentitySupport;
import org.springframework.data.neo4j.types.CartesianPoint2d;
import org.springframework.data.neo4j.types.GeographicPoint2d;
import org.springframework.data.repository.query.FluentQuery;
@@ -736,9 +736,9 @@ class RepositoryIT {
Record record = doWithSession(session -> session.run(
"CREATE (p1:Pet{name: 'Pet1'})-[:Has]->(p2:Pet{name: 'Pet2'}), (p2)-[:Has]->(p3:Pet{name: 'Pet3'}) RETURN p1, p2, p3").single());
long petNode1Id = IdentitySupport.getInternalId(record.get("p1").asNode());
long petNode2Id = IdentitySupport.getInternalId(record.get("p2").asNode());
long petNode3Id = IdentitySupport.getInternalId(record.get("p3").asNode());
long petNode1Id = TestIdentitySupport.getInternalId(record.get("p1").asNode());
long petNode2Id = TestIdentitySupport.getInternalId(record.get("p2").asNode());
long petNode3Id = TestIdentitySupport.getInternalId(record.get("p3").asNode());
Pet loadedPet = repository.customQueryWithDeepRelationshipMapping(petNode1Id);
@@ -910,12 +910,12 @@ class RepositoryIT {
Node petNode1 = record.get("p1").asNode();
Node petNode2 = record.get("p2").asNode();
long personId = IdentitySupport.getInternalId(personNode);
long clubId = IdentitySupport.getInternalId(clubNode);
long hobbyNode1Id = IdentitySupport.getInternalId(hobbyNode1);
long hobbyNode2Id = IdentitySupport.getInternalId(hobbyNode2);
long petNode1Id = IdentitySupport.getInternalId(petNode1);
long petNode2Id = IdentitySupport.getInternalId(petNode2);
long personId = TestIdentitySupport.getInternalId(personNode);
long clubId = TestIdentitySupport.getInternalId(clubNode);
long hobbyNode1Id = TestIdentitySupport.getInternalId(hobbyNode1);
long hobbyNode2Id = TestIdentitySupport.getInternalId(hobbyNode2);
long petNode1Id = TestIdentitySupport.getInternalId(petNode1);
long petNode2Id = TestIdentitySupport.getInternalId(petNode2);
PersonWithRelationship loadedPerson = repository.findById(personId).get();
assertThat(loadedPerson.getName()).isEqualTo("Freddie");
@@ -950,9 +950,9 @@ class RepositoryIT {
Record record = doWithSession(session -> session.run(
"CREATE (p1:Pet{name: 'Pet1'})-[:Has]->(p2:Pet{name: 'Pet2'}), (p2)-[:Has]->(p3:Pet{name: 'Pet3'}) RETURN p1, p2, p3").single());
long petNode1Id = IdentitySupport.getInternalId(record.get("p1").asNode());
long petNode2Id = IdentitySupport.getInternalId(record.get("p2").asNode());
long petNode3Id = IdentitySupport.getInternalId(record.get("p3").asNode());
long petNode1Id = TestIdentitySupport.getInternalId(record.get("p1").asNode());
long petNode2Id = TestIdentitySupport.getInternalId(record.get("p2").asNode());
long petNode3Id = TestIdentitySupport.getInternalId(record.get("p3").asNode());
Pet loadedPet = repository.findById(petNode1Id).get();
Pet comparisonPet2 = new Pet(petNode2Id, "Pet2");
@@ -998,7 +998,7 @@ class RepositoryIT {
@Test
void findLoopingDeepRelationships(@Autowired LoopingRelationshipRepository loopingRelationshipRepository) {
long type1Id = IdentitySupport.getInternalId(doWithSession(session -> session.run("""
long type1Id = TestIdentitySupport.getInternalId(doWithSession(session -> session.run("""
CREATE (t1:LoopingType1)-[:NEXT_TYPE]->(:LoopingType2)-[:NEXT_TYPE]->(:LoopingType3)-[:NEXT_TYPE]->
(:LoopingType1)-[:NEXT_TYPE]->(:LoopingType2)-[:NEXT_TYPE]->(:LoopingType3)-[:NEXT_TYPE]->
(:LoopingType1)-[:NEXT_TYPE]->(:LoopingType2)-[:NEXT_TYPE]->(:LoopingType3)-[:NEXT_TYPE]->
@@ -1049,9 +1049,9 @@ class RepositoryIT {
Node hobbyNode1 = record.get("h1").asNode();
Node petNode1 = record.get("p1").asNode();
long personId = IdentitySupport.getInternalId(personNode);
long hobbyNode1Id = IdentitySupport.getInternalId(hobbyNode1);
long petNode1Id = IdentitySupport.getInternalId(petNode1);
long personId = TestIdentitySupport.getInternalId(personNode);
long hobbyNode1Id = TestIdentitySupport.getInternalId(hobbyNode1);
long petNode1Id = TestIdentitySupport.getInternalId(petNode1);
PersonWithRelationship loadedPerson = repository.findById(personId).get();
assertThat(loadedPerson.getName()).isEqualTo("Freddie");
@@ -1075,17 +1075,17 @@ class RepositoryIT {
@Test
void findEntityWithBidirectionalRelationshipInConstructorThrowsException(@Autowired BidirectionalStartRepository repository) {
long startId = IdentitySupport.getInternalId(doWithSession(session -> session
var node = doWithSession(session -> session
.run("""
CREATE
(n:BidirectionalStart{name:'Ernie'})-[:CONNECTED]->(e:BidirectionalEnd{name:'Bert'}),
(e)<-[:ANOTHER_CONNECTION]-(anotherStart:BidirectionalStart{name:'Elmo'})
RETURN n"""
)
.single().get("n").asNode()));
.single().get("n").asNode());
assertThatThrownBy(() -> repository.findById(startId))
.hasRootCauseMessage("The node with id " + startId + " has a logical cyclic mapping dependency; " +
assertThatThrownBy(() -> repository.findById(TestIdentitySupport.getInternalId(node)))
.hasRootCauseMessage("The node with id " + node.elementId() + " has a logical cyclic mapping dependency; " +
"its creation caused the creation of another node that has a reference to this")
.hasRootCauseInstanceOf(MappingException.class);
@@ -1133,7 +1133,7 @@ class RepositoryIT {
@Test
void findEntityWithBidirectionalRelationshipFromIncomingSide(@Autowired BidirectionalEndRepository repository) {
long endId = IdentitySupport.getInternalId(doWithSession(session -> session.run(
long endId = TestIdentitySupport.getInternalId(doWithSession(session -> session.run(
"CREATE (n:BidirectionalStart{name:'Ernie'})-[:CONNECTED]->(e:BidirectionalEnd{name:'Bert'}) RETURN e")
.single().get("e").asNode()));
@@ -1151,14 +1151,14 @@ class RepositoryIT {
.run("CREATE (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(h:Hobby{name:'Music'}), (n)-[:Has]->(p:Pet{name: 'Jerry'}) RETURN n, h, p")
.single());
long hobbyNode1Id = IdentitySupport.getInternalId(record.get("h").asNode());
long petNode1Id = IdentitySupport.getInternalId(record.get("p").asNode());
long hobbyNode1Id = TestIdentitySupport.getInternalId(record.get("h").asNode());
long petNode1Id = TestIdentitySupport.getInternalId(record.get("p").asNode());
record = doWithSession(session -> session.run(
"CREATE (n:PersonWithRelationship{name:'SomeoneElse'})-[:Has]->(h:Hobby{name:'Music2'}), (n)-[:Has]->(p:Pet{name: 'Jerry2'}) RETURN n, h, p").single());
long hobbyNode2Id = IdentitySupport.getInternalId(record.get("h").asNode());
long petNode2Id = IdentitySupport.getInternalId(record.get("p").asNode());
long hobbyNode2Id = TestIdentitySupport.getInternalId(record.get("h").asNode());
long petNode2Id = TestIdentitySupport.getInternalId(record.get("p").asNode());
List<PersonWithRelationship> loadedPersons = repository.findAll();
@@ -1190,10 +1190,10 @@ class RepositoryIT {
Node petNode1 = record.get("p1").asNode();
Node petNode2 = record.get("p2").asNode();
long personId = IdentitySupport.getInternalId(personNode);
long hobbyNodeId = IdentitySupport.getInternalId(hobbyNode1);
long petNode1Id = IdentitySupport.getInternalId(petNode1);
long petNode2Id = IdentitySupport.getInternalId(petNode2);
long personId = TestIdentitySupport.getInternalId(personNode);
long hobbyNodeId = TestIdentitySupport.getInternalId(hobbyNode1);
long petNode1Id = TestIdentitySupport.getInternalId(petNode1);
long petNode2Id = TestIdentitySupport.getInternalId(petNode2);
PersonWithRelationship loadedPerson = repository.getPersonWithRelationshipsViaQuery();
assertThat(loadedPerson.getName()).isEqualTo("Freddie");
@@ -1228,10 +1228,10 @@ class RepositoryIT {
Node petNode1 = record.get("p1").asNode();
Node petNode2 = record.get("p2").asNode();
long personId = IdentitySupport.getInternalId(personNode);
long hobbyNodeId = IdentitySupport.getInternalId(hobbyNode1);
long petNode1Id = IdentitySupport.getInternalId(petNode1);
long petNode2Id = IdentitySupport.getInternalId(petNode2);
long personId = TestIdentitySupport.getInternalId(personNode);
long hobbyNodeId = TestIdentitySupport.getInternalId(hobbyNode1);
long petNode1Id = TestIdentitySupport.getInternalId(petNode1);
long petNode2Id = TestIdentitySupport.getInternalId(petNode2);
PersonWithRelationship loadedPerson = repository.getPersonWithRelationshipsViaPathQuery();
assertThat(loadedPerson.getName()).isEqualTo("Freddie");
@@ -1251,7 +1251,7 @@ class RepositoryIT {
@Test
void findEntityWithRelationshipWithAssignedId(@Autowired PetRepository repository) {
long petNodeId = IdentitySupport.getInternalId(doWithSession(session -> session
long petNodeId = TestIdentitySupport.getInternalId(doWithSession(session -> session
.run("CREATE (p:Pet{name:'Jerry'})-[:Has]->(t:Thing{theId:'t1', name:'Thing1'}) RETURN p, t").single()
.get("p").asNode()));
@@ -1438,7 +1438,7 @@ class RepositoryIT {
+ "{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().get("n").asNode());
long personId = IdentitySupport.getInternalId(hlp);
long personId = TestIdentitySupport.getInternalId(hlp);
Optional<PersonWithRelationshipWithProperties2> optionalPerson = template.findById(personId, PersonWithRelationshipWithProperties2.class);
assertThat(optionalPerson).hasValueSatisfying(person -> {
assertThat(person.getName()).isEqualTo("Freddie");
@@ -1466,9 +1466,9 @@ class RepositoryIT {
Node hobbyNode1 = record.get("h1").asNode();
Node hobbyNode2 = record.get("h2").asNode();
long personId = IdentitySupport.getInternalId(personNode);
long hobbyNode1Id = IdentitySupport.getInternalId(hobbyNode1);
long hobbyNode2Id = IdentitySupport.getInternalId(hobbyNode2);
long personId = TestIdentitySupport.getInternalId(personNode);
long hobbyNode1Id = TestIdentitySupport.getInternalId(hobbyNode1);
long hobbyNode2Id = TestIdentitySupport.getInternalId(hobbyNode2);
Optional<PersonWithRelationshipWithProperties> optionalPerson = repository.findById(personId);
assertThat(optionalPerson).isPresent();
@@ -1507,7 +1507,7 @@ class RepositoryIT {
@Test
void findEntityWithRelationshipWithPropertiesScalar(@Autowired PersonWithRelationshipWithPropertiesRepository repository) {
long personId = IdentitySupport.getInternalId(doWithSession(session -> session.run("CREATE (n:PersonWithRelationshipWithProperties{name:'Freddie'}),"
long personId = TestIdentitySupport.getInternalId(doWithSession(session -> session.run("CREATE (n:PersonWithRelationshipWithProperties{name:'Freddie'}),"
+ " (n)-[:WORKS_IN{since: 1995}]->(:Club{name:'Blubb'}),"
+ "(n) - [:OWNS {place: 'The place to be'}] -> (c1:Club {name: 'Berlin Mitte'}), "
+ "(n) - [:OWNS {place: 'Whatever'}] -> (c2:Club {name: 'Schachklub'}) "
@@ -1524,7 +1524,7 @@ class RepositoryIT {
void findEntityWithRelationshipWithPropertiesSameLabel(
@Autowired FriendRepository repository) {
long friendId = IdentitySupport.getInternalId(doWithSession(session -> session.run("CREATE (n:Friend{name:'Freddie'}),"
long friendId = TestIdentitySupport.getInternalId(doWithSession(session -> session.run("CREATE (n:Friend{name:'Freddie'}),"
+ " (n)-[:KNOWS{since: 1995}]->(:Friend{name:'Frank'})"
+ "RETURN n").single().get("n").asNode()));
@@ -1634,9 +1634,9 @@ class RepositoryIT {
Node hobbyNode1 = record.get("h1").asNode();
Node hobbyNode2 = record.get("h2").asNode();
long personId = IdentitySupport.getInternalId(personNode);
long hobbyNode1Id = IdentitySupport.getInternalId(hobbyNode1);
long hobbyNode2Id = IdentitySupport.getInternalId(hobbyNode2);
long personId = TestIdentitySupport.getInternalId(personNode);
long hobbyNode1Id = TestIdentitySupport.getInternalId(hobbyNode1);
long hobbyNode2Id = TestIdentitySupport.getInternalId(hobbyNode2);
PersonWithRelationshipWithProperties person = repository.loadFromCustomQuery(personId);
assertThat(person.getName()).isEqualTo("Freddie");
@@ -1671,7 +1671,7 @@ class RepositoryIT {
void loadEntityWithRelationshipWithPropertiesFromCustomQueryIncoming(
@Autowired HobbyWithRelationshipWithPropertiesRepository repository) {
long personId = IdentitySupport.getInternalId(doWithSession(
long personId = TestIdentitySupport.getInternalId(doWithSession(
session -> session.run("CREATE (n:AltPerson{name:'Freddie'}), (n)-[l1:LIKES {rating: 5}]->(h1:AltHobby{name:'Music'}) RETURN n, h1").single().get("n").asNode()));
AltHobby hobby = repository.loadFromCustomQuery(personId);
@@ -1686,7 +1686,7 @@ class RepositoryIT {
@Test
void loadSameNodeWithDoubleRelationship(@Autowired HobbyWithRelationshipWithPropertiesRepository repository) {
long personId = IdentitySupport.getInternalId(doWithSession(session -> session.run("CREATE (n:AltPerson{name:'Freddie'})," +
long personId = TestIdentitySupport.getInternalId(doWithSession(session -> 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().get("n").asNode()));
@@ -1736,9 +1736,8 @@ class RepositoryIT {
@Test
void updateAndCreateRelationshipProperties(@Autowired HobbyWithRelationshipWithPropertiesRepository repository) {
@SuppressWarnings("deprecation")
long hobbyId = doWithSession(
session -> session.run("CREATE (n:AltPerson{name:'Freddie'}), (n)-[l1:LIKES {rating: 5}]->(h1:AltHobby{name:'Music'}) RETURN n, h1").single().get("h1").asNode().id());
session -> TestIdentitySupport.getInternalId(session.run("CREATE (n:AltPerson{name:'Freddie'}), (n)-[l1:LIKES {rating: 5}]->(h1:AltHobby{name:'Music'}) RETURN n, h1").single().get("h1").asNode()));
AltHobby hobby = repository.findById(hobbyId).get();
assertThat(hobby.getName()).isEqualTo("Music");
@@ -1746,13 +1745,13 @@ class RepositoryIT {
AltLikedByPersonRelationship liked = new AltLikedByPersonRelationship();
liked.setAltPerson(new AltPerson("SomethingElse"));
liked.setRating(2);
hobby.getLikedBy().add(liked);
repository.save(hobby);
AltHobby savedHobby = repository.findById(hobbyId).get();
assertThat(savedHobby.getLikedBy()).hasSize(2);
}
}
@@ -1792,7 +1791,7 @@ class RepositoryIT {
assertThat(record.containsKey("n")).isTrue();
Node node = record.get("n").asNode();
assertThat(savedPerson.getId()).isEqualTo(IdentitySupport.getInternalId(node));
assertThat(savedPerson.getId()).isEqualTo(TestIdentitySupport.getInternalId(node));
assertThat(node.get("things").asList()).containsExactly("b", "a");
});
}
@@ -1939,7 +1938,7 @@ class RepositoryIT {
assertThat(record.containsKey("n")).isTrue();
Node node = record.get("n").asNode();
assertThat(IdentitySupport.getInternalId(node)).isEqualTo(savedPerson.getId());
assertThat(TestIdentitySupport.getInternalId(node)).isEqualTo(savedPerson.getId());
assertThat(node.get("first_name").asString()).isEqualTo(savedPerson.getFirstName());
assertThat(node.get("nullable").asString()).isEqualTo(savedPerson.getNullable());
assertThat(node.get("things").asList()).isEmpty();
@@ -2145,7 +2144,7 @@ class RepositoryIT {
assertThat(record.containsKey("n")).isTrue();
Node rootNode = record.get("n").asNode();
assertThat(savedPerson.getId()).isEqualTo(IdentitySupport.getInternalId(rootNode));
assertThat(savedPerson.getId()).isEqualTo(TestIdentitySupport.getInternalId(rootNode));
assertThat(savedPerson.getName()).isEqualTo("Freddie");
List<List<Object>> petsWithHobbies = record.get("petsWithHobbies").asList(Value::asList);
@@ -2206,7 +2205,7 @@ class RepositoryIT {
assertThat(record.containsKey("n")).isTrue();
Node rootNode = record.get("n").asNode();
assertThat(savedPerson.getId()).isEqualTo(IdentitySupport.getInternalId(rootNode));
assertThat(savedPerson.getId()).isEqualTo(TestIdentitySupport.getInternalId(rootNode));
assertThat(savedPerson.getName()).isEqualTo("Freddie");
List<List<Object>> petsWithHobbies = record.get("petsWithHobbies").asList(Value::asList);
@@ -2264,7 +2263,7 @@ class RepositoryIT {
assertThat(record.containsKey("n")).isTrue();
Node rootNode = record.get("n").asNode();
assertThat(savedPerson.getId()).isEqualTo(IdentitySupport.getInternalId(rootNode));
assertThat(savedPerson.getId()).isEqualTo(TestIdentitySupport.getInternalId(rootNode));
assertThat(savedPerson.getName()).isEqualTo("Freddie");
assertThat(record.get("hobbies").asList(entry -> entry.asNode().get("name").asString()))
@@ -2307,7 +2306,7 @@ class RepositoryIT {
assertThat(record.containsKey("n")).isTrue();
Node rootNode = record.get("n").asNode();
assertThat(savedPerson.getId()).isEqualTo(IdentitySupport.getInternalId(rootNode));
assertThat(savedPerson.getId()).isEqualTo(TestIdentitySupport.getInternalId(rootNode));
assertThat(savedPerson.getName()).isEqualTo("Freddie");
assertThat(record.get("hobbies").asList(entry -> entry.asNode().get("name").asString()))
@@ -3052,10 +3051,10 @@ class RepositoryIT {
Node petNode1 = record.get("p1").asNode();
Node petNode2 = record.get("p2").asNode();
long personId = IdentitySupport.getInternalId(personNode);
long hobbyNodeId = IdentitySupport.getInternalId(hobbyNode1);
long petNode1Id = IdentitySupport.getInternalId(petNode1);
long petNode2Id = IdentitySupport.getInternalId(petNode2);
long personId = TestIdentitySupport.getInternalId(personNode);
long hobbyNodeId = TestIdentitySupport.getInternalId(hobbyNode1);
long petNode1Id = TestIdentitySupport.getInternalId(petNode1);
long petNode2Id = TestIdentitySupport.getInternalId(petNode2);
PersonWithRelationship probe = new PersonWithRelationship();
probe.setName("Freddie");
@@ -3092,10 +3091,10 @@ class RepositoryIT {
Node petNode1 = record.get("p1").asNode();
Node petNode2 = record.get("p2").asNode();
long personId = IdentitySupport.getInternalId(personNode);
long hobbyNodeId = IdentitySupport.getInternalId(hobbyNode1);
long petNode1Id = IdentitySupport.getInternalId(petNode1);
long petNode2Id = IdentitySupport.getInternalId(petNode2);
long personId = TestIdentitySupport.getInternalId(personNode);
long hobbyNodeId = TestIdentitySupport.getInternalId(hobbyNode1);
long petNode1Id = TestIdentitySupport.getInternalId(petNode1);
long petNode2Id = TestIdentitySupport.getInternalId(petNode2);
PersonWithRelationship probe = new PersonWithRelationship();
probe.setName("Freddie");
@@ -3142,10 +3141,10 @@ class RepositoryIT {
Node petNode1 = record.get("p1").asNode();
Node petNode2 = record.get("p2").asNode();
long personId = IdentitySupport.getInternalId(personNode);
long hobbyNodeId = IdentitySupport.getInternalId(hobbyNode1);
long petNode1Id = IdentitySupport.getInternalId(petNode1);
long petNode2Id = IdentitySupport.getInternalId(petNode2);
long personId = TestIdentitySupport.getInternalId(personNode);
long hobbyNodeId = TestIdentitySupport.getInternalId(hobbyNode1);
long petNode1Id = TestIdentitySupport.getInternalId(petNode1);
long petNode2Id = TestIdentitySupport.getInternalId(petNode2);
PersonWithRelationship probe = new PersonWithRelationship();
probe.setName("Freddie");
@@ -3181,10 +3180,10 @@ class RepositoryIT {
Node petNode1 = record.get("p1").asNode();
Node petNode2 = record.get("p2").asNode();
long personId = IdentitySupport.getInternalId(personNode);
long hobbyNodeId = IdentitySupport.getInternalId(hobbyNode1);
long petNode1Id = IdentitySupport.getInternalId(petNode1);
long petNode2Id = IdentitySupport.getInternalId(petNode2);
long personId = TestIdentitySupport.getInternalId(personNode);
long hobbyNodeId = TestIdentitySupport.getInternalId(hobbyNode1);
long petNode1Id = TestIdentitySupport.getInternalId(petNode1);
long petNode2Id = TestIdentitySupport.getInternalId(petNode2);
PersonWithRelationship probe = new PersonWithRelationship();
probe.setName("Freddie");
@@ -3773,9 +3772,9 @@ class RepositoryIT {
void findNodeWithMultipleLabels(@Autowired MultipleLabelRepository multipleLabelRepository) {
Record record = doWithSession(session -> session.run("CREATE (n1:A:B:C), (n2:B:C), (n3:A) return n1, n2, n3").single());
long n1Id = IdentitySupport.getInternalId(record.get("n1").asNode());
long n2Id = IdentitySupport.getInternalId(record.get("n2").asNode());
long n3Id = IdentitySupport.getInternalId(record.get("n3").asNode());
long n1Id = TestIdentitySupport.getInternalId(record.get("n1").asNode());
long n2Id = TestIdentitySupport.getInternalId(record.get("n2").asNode());
long n3Id = TestIdentitySupport.getInternalId(record.get("n3").asNode());
Assertions.assertThat(multipleLabelRepository.findById(n1Id)).isPresent();
Assertions.assertThat(multipleLabelRepository.findById(n2Id)).isNotPresent();
@@ -3786,9 +3785,9 @@ class RepositoryIT {
void deleteNodeWithMultipleLabels(@Autowired MultipleLabelRepository multipleLabelRepository) {
Record record = doWithSession(session -> session.run("CREATE (n1:A:B:C), (n2:B:C), (n3:A) return n1, n2, n3").single());
long n1Id = IdentitySupport.getInternalId(record.get("n1").asNode());
long n2Id = IdentitySupport.getInternalId(record.get("n2").asNode());
long n3Id = IdentitySupport.getInternalId(record.get("n3").asNode());
long n1Id = TestIdentitySupport.getInternalId(record.get("n1").asNode());
long n2Id = TestIdentitySupport.getInternalId(record.get("n2").asNode());
long n3Id = TestIdentitySupport.getInternalId(record.get("n3").asNode());
multipleLabelRepository.deleteById(n1Id);
multipleLabelRepository.deleteById(n2Id);

View File

@@ -0,0 +1,69 @@
/*
* Copyright 2011-2023 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.issues.pure_element_id;
import java.util.Objects;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import org.junit.jupiter.api.BeforeEach;
import org.neo4j.driver.Driver;
import org.neo4j.driver.Session;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.neo4j.test.BookmarkCapture;
import org.springframework.data.neo4j.test.Neo4jExtension;
abstract class AbstractElementIdTestBase {
protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport;
@BeforeEach
void setupData(@Autowired Driver driver, @Autowired BookmarkCapture bookmarkCapture) {
try (Session session = driver.session()) {
session.run("MATCH (n) DETACH DELETE n").consume();
bookmarkCapture.seedWith(session.lastBookmarks());
}
}
private static final Pattern NEO5J_ELEMENT_ID_PATTERN = Pattern.compile("\\d+:.+:\\d+");
Predicate<String> validIdForCurrentNeo4j() {
Predicate<String> nonNull = Objects::nonNull;
if (neo4jConnectionSupport.isCypher5SyntaxCompatible()) {
return nonNull.and(NEO5J_ELEMENT_ID_PATTERN.asMatchPredicate());
}
// Must be a valid long, and yes, I know how to write a pattern for that, too
return nonNull.and(v -> {
try {
Long.parseLong(v);
} catch (NumberFormatException e) {
return false;
}
return true;
});
}
static String adaptQueryTo44IfNecessary(String query) {
if (!neo4jConnectionSupport.isCypher5SyntaxCompatible()) {
query = query.replaceAll("elementId\\((.+?)\\)", "toString(id($1))");
}
return query;
}
}

View File

@@ -0,0 +1,362 @@
/*
* Copyright 2011-2023 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.issues.pure_element_id;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.neo4j.driver.Driver;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.neo4j.core.DatabaseSelectionProvider;
import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager;
import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager;
import org.springframework.data.neo4j.repository.Neo4jRepository;
import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories;
import org.springframework.data.neo4j.test.BookmarkCapture;
import org.springframework.data.neo4j.test.LogbackCapture;
import org.springframework.data.neo4j.test.LogbackCapturingExtension;
import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration;
import org.springframework.data.neo4j.test.Neo4jIntegrationTest;
import org.springframework.lang.NonNull;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
/**
* Assertions that no {@code id()} calls are generated when no deprecated id types are present
*
* @author Michael J. Simons
*/
@Neo4jIntegrationTest
@ExtendWith(LogbackCapturingExtension.class)
public class ImperativeElementIdIT extends AbstractElementIdTestBase {
interface Repo1 extends Neo4jRepository<NodeWithGeneratedId1, String> {
}
interface Repo2 extends Neo4jRepository<NodeWithGeneratedId2, String> {
}
interface Repo3 extends Neo4jRepository<NodeWithGeneratedId3, String> {
}
interface Repo4 extends Neo4jRepository<NodeWithGeneratedId4, String> {
}
@Test
void simpleNodeCreationShouldFillIdAndNotUseIdFunction(LogbackCapture logbackCapture, @Autowired Repo1 repo1) {
var node = repo1.save(new NodeWithGeneratedId1("from-sdn-repo"));
assertThat(node.getId())
.matches(validIdForCurrentNeo4j());
assertThatLogMessageDoNotIndicateIDUsage(logbackCapture);
}
@Test
void simpleNodeAllCreationShouldFillIdAndNotUseIdFunction(LogbackCapture logbackCapture, @Autowired Repo1 repo1) {
var nodes = repo1.saveAll(List.of(new NodeWithGeneratedId1("from-sdn-repo")));
assertThat(nodes).isNotEmpty()
.extracting(NodeWithGeneratedId1::getId)
.allMatch(validIdForCurrentNeo4j());
assertThatLogMessageDoNotIndicateIDUsage(logbackCapture);
}
@Test
void findByIdMustNotCallIdFunction(LogbackCapture logbackCapture, @Autowired Repo1 repo1, @Autowired BookmarkCapture bookmarkCapture, @Autowired Driver driver) {
String id;
try (var session = driver.session(bookmarkCapture.createSessionConfig())) {
id = session.run("CREATE (n:NodeWithGeneratedId1 {value: 'whatever'}) RETURN n").single().get("n").asNode().elementId();
}
var optionalNode = repo1.findById(id);
assertThat(optionalNode).map(NodeWithGeneratedId1::getValue)
.hasValue("whatever");
assertThatLogMessageDoNotIndicateIDUsage(logbackCapture);
}
@Test
void findAllMustNotCallIdFunction(LogbackCapture logbackCapture, @Autowired Repo1 repo1, @Autowired BookmarkCapture bookmarkCapture, @Autowired Driver driver) {
try (var session = driver.session(bookmarkCapture.createSessionConfig())) {
session.run("CREATE (n:NodeWithGeneratedId1 {value: 'whatever'}) RETURN n").single().get("n").asNode().elementId();
}
var nodes = repo1.findAll();
assertThat(nodes).isNotEmpty()
.extracting(NodeWithGeneratedId1::getId)
.allMatch(validIdForCurrentNeo4j());
assertThatLogMessageDoNotIndicateIDUsage(logbackCapture);
}
@Test
void updateMustNotCallIdFunction(LogbackCapture logbackCapture, @Autowired Repo1 repo1, @Autowired BookmarkCapture bookmarkCapture, @Autowired Driver driver) {
NodeWithGeneratedId1 node;
try (var session = driver.session(bookmarkCapture.createSessionConfig())) {
var dbNode = session.run("CREATE (n:NodeWithGeneratedId1 {value: 'whatever'}) RETURN n").single().get("n").asNode();
node = new NodeWithGeneratedId1(dbNode.get("value").asString() + "_edited");
node.setId(dbNode.elementId());
}
node = repo1.save(node);
assertThat(node).extracting(NodeWithGeneratedId1::getValue)
.isEqualTo("whatever_edited");
assertThatLogMessageDoNotIndicateIDUsage(logbackCapture);
}
@Test
void updateAllMustNotCallIdFunction(LogbackCapture logbackCapture, @Autowired Repo1 repo1, @Autowired BookmarkCapture bookmarkCapture, @Autowired Driver driver) {
NodeWithGeneratedId1 node;
try (var session = driver.session(bookmarkCapture.createSessionConfig())) {
var dbNode = session.run("CREATE (n:NodeWithGeneratedId1 {value: 'whatever'}) RETURN n").single().get("n").asNode();
node = new NodeWithGeneratedId1(dbNode.get("value").asString() + "_edited");
node.setId(dbNode.elementId());
}
var nodes = repo1.saveAll(List.of(node));
assertThat(nodes).isNotEmpty()
.extracting(NodeWithGeneratedId1::getId)
.allMatch(validIdForCurrentNeo4j());
assertThatLogMessageDoNotIndicateIDUsage(logbackCapture);
}
@Test
void nodeAndRelationshipsWithoutPropsAndIdsMustNotUseIdFunctionWhileCreating(LogbackCapture logbackCapture, @Autowired Repo2 repo2, @Autowired BookmarkCapture bookmarkCapture, @Autowired Driver driver) {
var owner = new NodeWithGeneratedId2("owner");
owner.setRelatedNodes(List.of(new NodeWithGeneratedId1("child1"), new NodeWithGeneratedId1("child2")));
owner = repo2.save(owner);
assertThat(owner.getId()).isNotNull();
assertThat(owner.getRelatedNodes())
.allSatisfy(owned -> assertThat(owned.getId()).matches(validIdForCurrentNeo4j()));
assertThatLogMessageDoNotIndicateIDUsage(logbackCapture);
}
@Test
void nodeAndRelationshipsWithoutPropsAndIdsMustNotUseIdFunctionWhileUpdating(LogbackCapture logbackCapture, @Autowired Repo2 repo2, @Autowired BookmarkCapture bookmarkCapture, @Autowired Driver driver) {
String ownerId;
String ownedId;
try (var session = driver.session(bookmarkCapture.createSessionConfig())) {
var row = session.run("CREATE (n:NodeWithGeneratedId2 {value: 'owner'}) -[r:RELATED_NODES] -> (m:NodeWithGeneratedId1 {value:'owned'}) RETURN *").single();
ownerId = row.get("n").asNode().elementId();
ownedId = row.get("m").asNode().elementId();
}
var owner = repo2.findById(ownerId).orElseThrow();
assertThat(owner.getRelatedNodes())
.hasSize(1)
.first()
.extracting(NodeWithGeneratedId1::getId)
.isEqualTo(ownedId);
owner.getRelatedNodes().get(0).setValue("owned_changed");
owner.setValue("owner_changed");
repo2.save(owner);
assertThatLogMessageDoNotIndicateIDUsage(logbackCapture);
try (var session = driver.session(bookmarkCapture.createSessionConfig())) {
var count = session.run(adaptQueryTo44IfNecessary("""
MATCH (n:NodeWithGeneratedId2 {value: $v1}) -[r:RELATED_NODES] -> (m:NodeWithGeneratedId1 {value: $v2})
WHERE elementId(n) = $id1 AND elementId(m) = $id2
RETURN count(*)"""),
Map.of("v1", "owner_changed", "v2", "owned_changed", "id1", ownerId, "id2", ownedId)).single().get(0).asLong();
assertThat(count).isOne();
}
}
@Test
void relsWithPropOnCreation(LogbackCapture logbackCapture, @Autowired Repo3 repo3, @Autowired BookmarkCapture bookmarkCapture, @Autowired Driver driver) {
var owner = new NodeWithGeneratedId3("owner");
var target1 = new NodeWithGeneratedId1("target1");
var target2 = new NodeWithGeneratedId1("target2");
owner.setRelatedNodes(
List.of(
new RelWithProps(target1, "vr1"),
new RelWithProps(target2, "vr2"))
);
owner = repo3.save(owner);
assertThat(owner.getId()).matches(validIdForCurrentNeo4j());
assertThat(owner.getRelatedNodes())
.hasSize(2)
.allSatisfy(r -> assertThat(r.getTarget().getId()).isNotNull())
.extracting(RelWithProps::getRelValue)
.containsExactlyInAnyOrder("vr1", "vr2");
assertThatLogMessageDoNotIndicateIDUsage(logbackCapture);
try (var session = driver.session(bookmarkCapture.createSessionConfig())) {
var count = session.run(adaptQueryTo44IfNecessary("""
MATCH (n:NodeWithGeneratedId3 {value: $v1}) -[r:RELATED_NODES] -> (m:NodeWithGeneratedId1)
WHERE elementId(n) = $id1
AND r.relValue IN $rv
RETURN count(*)"""),
Map.of("v1", "owner", "id1", owner.getId(), "rv", List.of("vr1", "vr2"))).single().get(0).asLong();
assertThat(count).isEqualTo(2L);
}
}
@Test
void relsWithPropOnUpdate(LogbackCapture logbackCapture, @Autowired Repo3 repo3, @Autowired BookmarkCapture bookmarkCapture, @Autowired Driver driver) {
String ownerId;
try (var session = driver.session(bookmarkCapture.createSessionConfig())) {
ownerId = session.run(adaptQueryTo44IfNecessary("""
CREATE (n:NodeWithGeneratedId3 {value: 'owner'}) -[r:RELATED_NODES] -> (m:NodeWithGeneratedId1 {value: 'owned'})
RETURN elementId(n)""")
).single().get(0).asString();
}
var owner = repo3.findById(ownerId).orElseThrow();
owner.setValue("owner_updated");
var rel = owner.getRelatedNodes().get(0);
rel.setRelValue("whatever");
rel.getTarget().setValue("owned_updated");
repo3.save(owner);
assertThatLogMessageDoNotIndicateIDUsage(logbackCapture);
try (var session = driver.session(bookmarkCapture.createSessionConfig())) {
var count = session.run(adaptQueryTo44IfNecessary("""
MATCH (n:NodeWithGeneratedId3 {value: $v1}) -[r:RELATED_NODES] -> (m:NodeWithGeneratedId1 {value: $v2})
WHERE elementId(n) = $id1
AND r.relValue IN $rv
RETURN count(*)"""),
Map.of("v1", "owner_updated", "v2", "owned_updated", "id1", owner.getId(), "rv", List.of("whatever"))).single().get(0).asLong();
assertThat(count).isEqualTo(1L);
}
}
@Test
void relsWithsCyclesOnCreation(LogbackCapture logbackCapture, @Autowired Repo4 repo4, @Autowired BookmarkCapture bookmarkCapture, @Autowired Driver driver) {
var owner = new NodeWithGeneratedId4("owner");
var intermediate = new NodeWithGeneratedId4.Intermediate();
intermediate.setEnd(new NodeWithGeneratedId4("end"));
owner.setIntermediate(intermediate);
owner = repo4.save(owner);
assertThatLogMessageDoNotIndicateIDUsage(logbackCapture);
var validIdForCurrentNeo4j = validIdForCurrentNeo4j();
assertThat(owner.getId()).matches(validIdForCurrentNeo4j);
assertThat(owner.getIntermediate().getId()).matches(validIdForCurrentNeo4j);
assertThat(owner.getIntermediate().getEnd().getId()).matches(validIdForCurrentNeo4j);
try (var session = driver.session(bookmarkCapture.createSessionConfig())) {
var query = """
MATCH (n:NodeWithGeneratedId4 {value: $v1}) -[r:INTERMEDIATE]-> (i:Intermediate) -[:END]-> (e:NodeWithGeneratedId4 {value: $v2})
WHERE elementId(n) = $id1
AND elementId(i) = $id2
AND elementId(e) = $id3
RETURN count(*)""";
var count = session.run(adaptQueryTo44IfNecessary(query),
Map.of("v1", "owner", "v2", "end", "id1", owner.getId(), "id2", owner.getIntermediate().getId(), "id3", owner.getIntermediate().getEnd().getId()))
.single().get(0).asLong();
assertThat(count).isEqualTo(1L);
}
}
@Test
void relsWithsCyclesOnUpdate(LogbackCapture logbackCapture, @Autowired Repo4 repo4, @Autowired BookmarkCapture bookmarkCapture, @Autowired Driver driver) {
String ownerId;
try (var session = driver.session(bookmarkCapture.createSessionConfig())) {
ownerId = session.run(adaptQueryTo44IfNecessary("""
CREATE (n:NodeWithGeneratedId4 {value: 'a'}) -[r:INTERMEDIATE]-> (i:Intermediate) -[:END]-> (e:NodeWithGeneratedId4 {value: 'b'})
RETURN elementId(n)"""))
.single().get(0).asString();
}
var owner = repo4.findAllById(List.of(ownerId)).get(0);
owner.setValue("owner");
owner.getIntermediate().getEnd().setValue("end");
owner = repo4.save(owner);
assertThatLogMessageDoNotIndicateIDUsage(logbackCapture);
var validIdForCurrentNeo4j = validIdForCurrentNeo4j();
assertThat(owner.getId()).matches(validIdForCurrentNeo4j);
assertThat(owner.getIntermediate().getId()).matches(validIdForCurrentNeo4j);
assertThat(owner.getIntermediate().getEnd().getId()).matches(validIdForCurrentNeo4j);
try (var session = driver.session(bookmarkCapture.createSessionConfig())) {
var count = session.run(adaptQueryTo44IfNecessary("""
MATCH (n:NodeWithGeneratedId4 {value: $v1}) -[r:INTERMEDIATE]-> (i:Intermediate) -[:END]-> (e:NodeWithGeneratedId4 {value: $v2})
WHERE elementId(n) = $id1
AND elementId(i) = $id2
AND elementId(e) = $id3
RETURN count(*)"""),
Map.of("v1", "owner", "v2", "end", "id1", owner.getId(), "id2", owner.getIntermediate().getId(), "id3", owner.getIntermediate().getEnd().getId()))
.single().get(0).asLong();
assertThat(count).isEqualTo(1L);
}
}
private static void assertThatLogMessageDoNotIndicateIDUsage(LogbackCapture logbackCapture) {
assertThat(logbackCapture.getFormattedMessages())
.noneMatch(s -> s.contains("Neo.ClientNotification.Statement.FeatureDeprecationWarning") ||
s.contains("The query used a deprecated function. ('id' is no longer supported)"));
}
@Configuration
@EnableTransactionManagement
@EnableNeo4jRepositories(considerNestedRepositories = true)
static class Config extends Neo4jImperativeTestConfiguration {
@Bean
public BookmarkCapture bookmarkCapture() {
return new BookmarkCapture();
}
@Override
@NonNull
public PlatformTransactionManager transactionManager(@NonNull Driver driver, @NonNull DatabaseSelectionProvider databaseNameProvider) {
BookmarkCapture bookmarkCapture = bookmarkCapture();
return new Neo4jTransactionManager(driver, databaseNameProvider,
Neo4jBookmarkManager.create(bookmarkCapture));
}
@Bean
@NonNull
public Driver driver() {
return neo4jConnectionSupport.getDriver();
}
@Override
public boolean isCypher5Compatible() {
return neo4jConnectionSupport.isCypher5SyntaxCompatible();
}
}
}

View File

@@ -13,15 +13,41 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.neo4j.core.schema;
package org.springframework.data.neo4j.integration.issues.pure_element_id;
import org.springframework.data.neo4j.core.schema.GeneratedValue;
import org.springframework.data.neo4j.core.schema.Id;
import org.springframework.data.neo4j.core.schema.Node;
/**
* Default representation of an element id.
*
* @author Michael J. Simons
* @param value The ids value
* @soundtrack GötterDÄmmerung - Tribut an die beste Band der Welt
* @since 7.0.0
*/
record DefaultElementId(String value) implements ElementId {
@Node
public class NodeWithGeneratedId1 {
@Id
@GeneratedValue
private String id;
private String value;
public NodeWithGeneratedId1(String value) {
this.value = value;
}
public void setId(String id) {
this.id = id;
}
public String getId() {
return id;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}

View File

@@ -0,0 +1,63 @@
/*
* Copyright 2011-2023 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.issues.pure_element_id;
import java.util.List;
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;
/**
* @author Michael J. Simons
*/
@Node
public class NodeWithGeneratedId2 {
@Id
@GeneratedValue
private String id;
private String value;
@Relationship
private List<NodeWithGeneratedId1> relatedNodes;
public NodeWithGeneratedId2(String value) {
this.value = value;
}
public String getId() {
return id;
}
public String getValue() {
return value;
}
public List<NodeWithGeneratedId1> getRelatedNodes() {
return relatedNodes;
}
public void setRelatedNodes(List<NodeWithGeneratedId1> relatedNodes) {
this.relatedNodes = relatedNodes;
}
public void setValue(String value) {
this.value = value;
}
}

View File

@@ -0,0 +1,63 @@
/*
* Copyright 2011-2023 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.issues.pure_element_id;
import java.util.List;
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;
/**
* @author Michael J. Simons
*/
@Node
public class NodeWithGeneratedId3 {
@Id
@GeneratedValue
private String id;
private String value;
@Relationship
private List<RelWithProps> relatedNodes;
public NodeWithGeneratedId3(String value) {
this.value = value;
}
public String getId() {
return id;
}
public String getValue() {
return value;
}
public List<RelWithProps> getRelatedNodes() {
return relatedNodes;
}
public void setRelatedNodes(List<RelWithProps> relatedNodes) {
this.relatedNodes = relatedNodes;
}
public void setValue(String value) {
this.value = value;
}
}

View File

@@ -0,0 +1,88 @@
/*
* Copyright 2011-2023 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.issues.pure_element_id;
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;
/**
* @author Michael J. Simons
*/
@Node
public class NodeWithGeneratedId4 {
@Node
static class Intermediate {
@Id
@GeneratedValue
private String id;
@Relationship
NodeWithGeneratedId4 end;
String getId() {
return id;
}
void setId(String id) {
this.id = id;
}
NodeWithGeneratedId4 getEnd() {
return end;
}
void setEnd(NodeWithGeneratedId4 end) {
this.end = end;
}
}
@Id
@GeneratedValue
private String id;
private String value;
@Relationship
private Intermediate intermediate;
public NodeWithGeneratedId4(String value) {
this.value = value;
}
public String getId() {
return id;
}
public String getValue() {
return value;
}
public Intermediate getIntermediate() {
return intermediate;
}
public void setIntermediate(Intermediate intermediate) {
this.intermediate = intermediate;
}
public void setValue(String value) {
this.value = value;
}
}

View File

@@ -0,0 +1,376 @@
/*
* Copyright 2011-2023 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.issues.pure_element_id;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.neo4j.driver.Driver;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.neo4j.core.ReactiveDatabaseSelectionProvider;
import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager;
import org.springframework.data.neo4j.core.transaction.ReactiveNeo4jTransactionManager;
import org.springframework.data.neo4j.repository.ReactiveNeo4jRepository;
import org.springframework.data.neo4j.repository.config.EnableReactiveNeo4jRepositories;
import org.springframework.data.neo4j.test.BookmarkCapture;
import org.springframework.data.neo4j.test.LogbackCapture;
import org.springframework.data.neo4j.test.LogbackCapturingExtension;
import org.springframework.data.neo4j.test.Neo4jIntegrationTest;
import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration;
import org.springframework.lang.NonNull;
import org.springframework.transaction.ReactiveTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
/**
* Assertions that no {@code id()} calls are generated when no deprecated id types are present.
* This test deliberately uses blocking calls into reactor because it's really not about the reactive flows but about
* catching all the code paths that might interact with ids on the reactive side of things. Yes, Reactors testing tools
* are known.
*
* @author Michael J. Simons
*/
@Neo4jIntegrationTest
@ExtendWith(LogbackCapturingExtension.class)
public class ReactiveElementIdIT extends AbstractElementIdTestBase {
interface Repo1 extends ReactiveNeo4jRepository<NodeWithGeneratedId1, String> {
}
interface Repo2 extends ReactiveNeo4jRepository<NodeWithGeneratedId2, String> {
}
interface Repo3 extends ReactiveNeo4jRepository<NodeWithGeneratedId3, String> {
}
interface Repo4 extends ReactiveNeo4jRepository<NodeWithGeneratedId4, String> {
}
@Test
void simpleNodeCreationShouldFillIdAndNotUseIdFunction(LogbackCapture logbackCapture, @Autowired Repo1 repo1) {
var node = repo1.save(new NodeWithGeneratedId1("from-sdn-repo")).block();
assertThat(node).isNotNull();
assertThat(node.getId())
.matches(validIdForCurrentNeo4j());
assertThatLogMessageDoNotIndicateIDUsage(logbackCapture);
}
@Test
void simpleNodeAllCreationShouldFillIdAndNotUseIdFunction(LogbackCapture logbackCapture, @Autowired Repo1 repo1) {
var nodes = repo1.saveAll(List.of(new NodeWithGeneratedId1("from-sdn-repo"))).collectList().block();
assertThat(nodes).isNotEmpty()
.extracting(NodeWithGeneratedId1::getId)
.allMatch(validIdForCurrentNeo4j());
assertThatLogMessageDoNotIndicateIDUsage(logbackCapture);
}
@Test
void findByIdMustNotCallIdFunction(LogbackCapture logbackCapture, @Autowired Repo1 repo1, @Autowired BookmarkCapture bookmarkCapture, @Autowired Driver driver) {
String id;
try (var session = driver.session(bookmarkCapture.createSessionConfig())) {
id = session.run("CREATE (n:NodeWithGeneratedId1 {value: 'whatever'}) RETURN n").single().get("n").asNode().elementId();
}
var optionalNode = Optional.ofNullable(repo1.findById(id).block());
assertThat(optionalNode).map(NodeWithGeneratedId1::getValue)
.hasValue("whatever");
assertThatLogMessageDoNotIndicateIDUsage(logbackCapture);
}
@Test
void findAllMustNotCallIdFunction(LogbackCapture logbackCapture, @Autowired Repo1 repo1, @Autowired BookmarkCapture bookmarkCapture, @Autowired Driver driver) {
try (var session = driver.session(bookmarkCapture.createSessionConfig())) {
session.run("CREATE (n:NodeWithGeneratedId1 {value: 'whatever'}) RETURN n").single().get("n").asNode().elementId();
}
var nodes = repo1.findAll().collectList().block();
assertThat(nodes).isNotEmpty()
.extracting(NodeWithGeneratedId1::getId)
.allMatch(validIdForCurrentNeo4j());
assertThatLogMessageDoNotIndicateIDUsage(logbackCapture);
}
@Test
void updateMustNotCallIdFunction(LogbackCapture logbackCapture, @Autowired Repo1 repo1, @Autowired BookmarkCapture bookmarkCapture, @Autowired Driver driver) {
NodeWithGeneratedId1 node;
try (var session = driver.session(bookmarkCapture.createSessionConfig())) {
var dbNode = session.run("CREATE (n:NodeWithGeneratedId1 {value: 'whatever'}) RETURN n").single().get("n").asNode();
node = new NodeWithGeneratedId1(dbNode.get("value").asString() + "_edited");
node.setId(dbNode.elementId());
}
node = repo1.save(node).block();
assertThat(node).extracting(NodeWithGeneratedId1::getValue)
.isEqualTo("whatever_edited");
assertThatLogMessageDoNotIndicateIDUsage(logbackCapture);
}
@Test
void updateAllMustNotCallIdFunction(LogbackCapture logbackCapture, @Autowired Repo1 repo1, @Autowired BookmarkCapture bookmarkCapture, @Autowired Driver driver) {
NodeWithGeneratedId1 node;
try (var session = driver.session(bookmarkCapture.createSessionConfig())) {
var dbNode = session.run("CREATE (n:NodeWithGeneratedId1 {value: 'whatever'}) RETURN n").single().get("n").asNode();
node = new NodeWithGeneratedId1(dbNode.get("value").asString() + "_edited");
node.setId(dbNode.elementId());
}
var nodes = repo1.saveAll(List.of(node)).collectList().block();
assertThat(nodes).isNotEmpty()
.extracting(NodeWithGeneratedId1::getId)
.allMatch(validIdForCurrentNeo4j());
assertThatLogMessageDoNotIndicateIDUsage(logbackCapture);
}
@Test
void nodeAndRelationshipsWithoutPropsAndIdsMustNotUseIdFunctionWhileCreating(LogbackCapture logbackCapture, @Autowired Repo2 repo2, @Autowired BookmarkCapture bookmarkCapture, @Autowired Driver driver) {
var owner = new NodeWithGeneratedId2("owner");
owner.setRelatedNodes(List.of(new NodeWithGeneratedId1("child1"), new NodeWithGeneratedId1("child2")));
owner = repo2.save(owner).block();
assertThat(owner).isNotNull();
assertThat(owner.getId()).isNotNull();
assertThat(owner.getRelatedNodes())
.allSatisfy(owned -> assertThat(owned.getId()).matches(validIdForCurrentNeo4j()));
assertThatLogMessageDoNotIndicateIDUsage(logbackCapture);
}
@Test
void nodeAndRelationshipsWithoutPropsAndIdsMustNotUseIdFunctionWhileUpdating(LogbackCapture logbackCapture, @Autowired Repo2 repo2, @Autowired BookmarkCapture bookmarkCapture, @Autowired Driver driver) {
String ownerId;
String ownedId;
try (var session = driver.session(bookmarkCapture.createSessionConfig())) {
var row = session.run("CREATE (n:NodeWithGeneratedId2 {value: 'owner'}) -[r:RELATED_NODES] -> (m:NodeWithGeneratedId1 {value:'owned'}) RETURN *").single();
ownerId = row.get("n").asNode().elementId();
ownedId = row.get("m").asNode().elementId();
}
var owner = repo2.findById(ownerId).block();
assertThat(owner).isNotNull();
assertThat(owner.getRelatedNodes())
.hasSize(1)
.first()
.extracting(NodeWithGeneratedId1::getId)
.isEqualTo(ownedId);
owner.getRelatedNodes().get(0).setValue("owned_changed");
owner.setValue("owner_changed");
repo2.save(owner).block();
assertThatLogMessageDoNotIndicateIDUsage(logbackCapture);
try (var session = driver.session(bookmarkCapture.createSessionConfig())) {
var count = session.run(adaptQueryTo44IfNecessary("""
MATCH (n:NodeWithGeneratedId2 {value: $v1}) -[r:RELATED_NODES] -> (m:NodeWithGeneratedId1 {value: $v2})
WHERE elementId(n) = $id1 AND elementId(m) = $id2
RETURN count(*)"""),
Map.of("v1", "owner_changed", "v2", "owned_changed", "id1", ownerId, "id2", ownedId)).single().get(0).asLong();
assertThat(count).isOne();
}
}
@Test
void relsWithPropOnCreation(LogbackCapture logbackCapture, @Autowired Repo3 repo3, @Autowired BookmarkCapture bookmarkCapture, @Autowired Driver driver) {
var owner = new NodeWithGeneratedId3("owner");
var target1 = new NodeWithGeneratedId1("target1");
var target2 = new NodeWithGeneratedId1("target2");
owner.setRelatedNodes(
List.of(
new RelWithProps(target1, "vr1"),
new RelWithProps(target2, "vr2"))
);
owner = repo3.save(owner).block();
assertThat(owner).isNotNull();
assertThat(owner.getId()).matches(validIdForCurrentNeo4j());
assertThat(owner.getRelatedNodes())
.hasSize(2)
.allSatisfy(r -> assertThat(r.getTarget().getId()).isNotNull())
.extracting(RelWithProps::getRelValue)
.containsExactlyInAnyOrder("vr1", "vr2");
assertThatLogMessageDoNotIndicateIDUsage(logbackCapture);
try (var session = driver.session(bookmarkCapture.createSessionConfig())) {
var count = session.run(adaptQueryTo44IfNecessary("""
MATCH (n:NodeWithGeneratedId3 {value: $v1}) -[r:RELATED_NODES] -> (m:NodeWithGeneratedId1)
WHERE elementId(n) = $id1
AND r.relValue IN $rv
RETURN count(*)"""),
Map.of("v1", "owner", "id1", owner.getId(), "rv", List.of("vr1", "vr2"))).single().get(0).asLong();
assertThat(count).isEqualTo(2L);
}
}
@Test
void relsWithPropOnUpdate(LogbackCapture logbackCapture, @Autowired Repo3 repo3, @Autowired BookmarkCapture bookmarkCapture, @Autowired Driver driver) {
String ownerId;
try (var session = driver.session(bookmarkCapture.createSessionConfig())) {
ownerId = session.run(adaptQueryTo44IfNecessary("""
CREATE (n:NodeWithGeneratedId3 {value: 'owner'}) -[r:RELATED_NODES] -> (m:NodeWithGeneratedId1 {value: 'owned'})
RETURN elementId(n)""")
).single().get(0).asString();
}
var owner = repo3.findById(ownerId).block();
assertThat(owner).isNotNull();
owner.setValue("owner_updated");
var rel = owner.getRelatedNodes().get(0);
rel.setRelValue("whatever");
rel.getTarget().setValue("owned_updated");
repo3.save(owner).block();
assertThatLogMessageDoNotIndicateIDUsage(logbackCapture);
try (var session = driver.session(bookmarkCapture.createSessionConfig())) {
var count = session.run(adaptQueryTo44IfNecessary("""
MATCH (n:NodeWithGeneratedId3 {value: $v1}) -[r:RELATED_NODES] -> (m:NodeWithGeneratedId1 {value: $v2})
WHERE elementId(n) = $id1
AND r.relValue IN $rv
RETURN count(*)"""),
Map.of("v1", "owner_updated", "v2", "owned_updated", "id1", owner.getId(), "rv", List.of("whatever"))).single().get(0).asLong();
assertThat(count).isEqualTo(1L);
}
}
@Test
void relsWithsCyclesOnCreation(LogbackCapture logbackCapture, @Autowired Repo4 repo4, @Autowired BookmarkCapture bookmarkCapture, @Autowired Driver driver) {
var owner = new NodeWithGeneratedId4("owner");
var intermediate = new NodeWithGeneratedId4.Intermediate();
intermediate.setEnd(new NodeWithGeneratedId4("end"));
owner.setIntermediate(intermediate);
owner = repo4.save(owner).block();
assertThatLogMessageDoNotIndicateIDUsage(logbackCapture);
var validIdForCurrentNeo4j = validIdForCurrentNeo4j();
assertThat(owner).isNotNull();
assertThat(owner.getId()).matches(validIdForCurrentNeo4j);
assertThat(owner.getIntermediate().getId()).matches(validIdForCurrentNeo4j);
assertThat(owner.getIntermediate().getEnd().getId()).matches(validIdForCurrentNeo4j);
try (var session = driver.session(bookmarkCapture.createSessionConfig())) {
var query = """
MATCH (n:NodeWithGeneratedId4 {value: $v1}) -[r:INTERMEDIATE]-> (i:Intermediate) -[:END]-> (e:NodeWithGeneratedId4 {value: $v2})
WHERE elementId(n) = $id1
AND elementId(i) = $id2
AND elementId(e) = $id3
RETURN count(*)""";
var count = session.run(adaptQueryTo44IfNecessary(query),
Map.of("v1", "owner", "v2", "end", "id1", owner.getId(), "id2", owner.getIntermediate().getId(), "id3", owner.getIntermediate().getEnd().getId()))
.single().get(0).asLong();
assertThat(count).isEqualTo(1L);
}
}
@Test
void relsWithsCyclesOnUpdate(LogbackCapture logbackCapture, @Autowired Repo4 repo4, @Autowired BookmarkCapture bookmarkCapture, @Autowired Driver driver) {
String ownerId;
try (var session = driver.session(bookmarkCapture.createSessionConfig())) {
ownerId = session.run(adaptQueryTo44IfNecessary("""
CREATE (n:NodeWithGeneratedId4 {value: 'a'}) -[r:INTERMEDIATE]-> (i:Intermediate) -[:END]-> (e:NodeWithGeneratedId4 {value: 'b'})
RETURN elementId(n)"""))
.single().get(0).asString();
}
var owner = repo4.findAllById(List.of(ownerId)).blockFirst();
assertThat(owner).isNotNull();
owner.setValue("owner");
owner.getIntermediate().getEnd().setValue("end");
owner = repo4.save(owner).block();
assertThatLogMessageDoNotIndicateIDUsage(logbackCapture);
var validIdForCurrentNeo4j = validIdForCurrentNeo4j();
assertThat(owner).isNotNull();
assertThat(owner.getId()).matches(validIdForCurrentNeo4j);
assertThat(owner.getIntermediate().getId()).matches(validIdForCurrentNeo4j);
assertThat(owner.getIntermediate().getEnd().getId()).matches(validIdForCurrentNeo4j);
try (var session = driver.session(bookmarkCapture.createSessionConfig())) {
var count = session.run(adaptQueryTo44IfNecessary("""
MATCH (n:NodeWithGeneratedId4 {value: $v1}) -[r:INTERMEDIATE]-> (i:Intermediate) -[:END]-> (e:NodeWithGeneratedId4 {value: $v2})
WHERE elementId(n) = $id1
AND elementId(i) = $id2
AND elementId(e) = $id3
RETURN count(*)"""),
Map.of("v1", "owner", "v2", "end", "id1", owner.getId(), "id2", owner.getIntermediate().getId(), "id3", owner.getIntermediate().getEnd().getId()))
.single().get(0).asLong();
assertThat(count).isEqualTo(1L);
}
}
private static void assertThatLogMessageDoNotIndicateIDUsage(LogbackCapture logbackCapture) {
assertThat(logbackCapture.getFormattedMessages())
.noneMatch(s -> s.contains("Neo.ClientNotification.Statement.FeatureDeprecationWarning") ||
s.contains("The query used a deprecated function. ('id' is no longer supported)"));
}
@Configuration
@EnableTransactionManagement
@EnableReactiveNeo4jRepositories(considerNestedRepositories = true)
static class Config extends Neo4jReactiveTestConfiguration {
@Bean
public BookmarkCapture bookmarkCapture() {
return new BookmarkCapture();
}
@Override
@NonNull
public ReactiveTransactionManager reactiveTransactionManager(@NonNull Driver driver, @NonNull ReactiveDatabaseSelectionProvider databaseSelectionProvider) {
BookmarkCapture bookmarkCapture = bookmarkCapture();
return new ReactiveNeo4jTransactionManager(driver, databaseSelectionProvider,
Neo4jBookmarkManager.create(bookmarkCapture));
}
@Bean
@NonNull
public Driver driver() {
return neo4jConnectionSupport.getDriver();
}
@Override
public boolean isCypher5Compatible() {
return neo4jConnectionSupport.isCypher5SyntaxCompatible();
}
}
}

View File

@@ -0,0 +1,64 @@
/*
* Copyright 2011-2023 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.issues.pure_element_id;
import org.springframework.data.neo4j.core.schema.RelationshipId;
import org.springframework.data.neo4j.core.schema.RelationshipProperties;
import org.springframework.data.neo4j.core.schema.TargetNode;
/**
* @author Michael J. Simons
*/
@RelationshipProperties
public class RelWithProps {
@RelationshipId
private String id;
@TargetNode
private NodeWithGeneratedId1 target;
private String relValue;
public RelWithProps(NodeWithGeneratedId1 target, String relValue) {
this.target = target;
this.relValue = relValue;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getRelValue() {
return relValue;
}
public void setRelValue(String relValue) {
this.relValue = relValue;
}
public NodeWithGeneratedId1 getTarget() {
return target;
}
public void setTarget(NodeWithGeneratedId1 target) {
this.target = target;
}
}

View File

@@ -17,23 +17,6 @@ package org.springframework.data.neo4j.integration.reactive;
import static org.assertj.core.api.Assertions.assertThat;
import org.neo4j.cypherdsl.core.Statement;
import org.neo4j.cypherdsl.core.executables.ExecutableResultStatement;
import org.neo4j.driver.Query;
import org.neo4j.driver.Record;
import org.neo4j.driver.SimpleQueryRunner;
import org.neo4j.driver.async.AsyncQueryRunner;
import org.neo4j.driver.reactivestreams.ReactiveQueryRunner;
import org.neo4j.driver.reactivestreams.ReactiveResult;
import org.neo4j.driver.summary.ResultSummary;
import org.reactivestreams.Publisher;
import org.springframework.data.neo4j.core.mapping.IdentitySupport;
import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
@@ -48,9 +31,19 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.neo4j.cypherdsl.core.Cypher;
import org.neo4j.cypherdsl.core.Node;
import org.neo4j.cypherdsl.core.Statement;
import org.neo4j.cypherdsl.core.executables.ExecutableResultStatement;
import org.neo4j.driver.Driver;
import org.neo4j.driver.Query;
import org.neo4j.driver.Record;
import org.neo4j.driver.Session;
import org.neo4j.driver.SimpleQueryRunner;
import org.neo4j.driver.Transaction;
import org.neo4j.driver.async.AsyncQueryRunner;
import org.neo4j.driver.reactivestreams.ReactiveQueryRunner;
import org.neo4j.driver.reactivestreams.ReactiveResult;
import org.neo4j.driver.summary.ResultSummary;
import org.reactivestreams.Publisher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -62,10 +55,16 @@ import org.springframework.data.neo4j.integration.shared.common.PersonWithAllCon
import org.springframework.data.neo4j.test.BookmarkCapture;
import org.springframework.data.neo4j.test.Neo4jExtension.Neo4jConnectionSupport;
import org.springframework.data.neo4j.test.Neo4jIntegrationTest;
import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration;
import org.springframework.data.neo4j.test.TestIdentitySupport;
import org.springframework.transaction.ReactiveTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.reactive.TransactionalOperator;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
/**
* @author Michael J. Simons
*/
@@ -99,7 +98,7 @@ class ReactiveNeo4jClientIT {
transactionalOperator.execute(transaction -> {
Flux<Long> inner = client.getQueryRunner()
.flatMapMany(statement::fetchWith)
.doOnNext(r -> vanishedId.set(IdentitySupport.getInternalId(r.get("n").asNode())))
.doOnNext(r -> vanishedId.set(TestIdentitySupport.getInternalId(r.get("n").asNode())))
.map(record -> record.get("n").get("value").asLong());
transaction.setRollbackOnly();

View File

@@ -19,16 +19,6 @@ 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.neo4j.driver.TransactionContext;
import org.neo4j.driver.reactivestreams.ReactiveResult;
import org.neo4j.driver.reactivestreams.ReactiveSession;
import org.springframework.data.neo4j.core.mapping.IdentitySupport;
import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Arrays;
@@ -54,8 +44,11 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.neo4j.driver.Driver;
import org.neo4j.driver.Record;
import org.neo4j.driver.Session;
import org.neo4j.driver.TransactionContext;
import org.neo4j.driver.Value;
import org.neo4j.driver.Values;
import org.neo4j.driver.reactivestreams.ReactiveResult;
import org.neo4j.driver.reactivestreams.ReactiveSession;
import org.neo4j.driver.types.Node;
import org.neo4j.driver.types.Point;
import org.neo4j.driver.types.Relationship;
@@ -111,6 +104,8 @@ import org.springframework.data.neo4j.repository.config.EnableReactiveNeo4jRepos
import org.springframework.data.neo4j.repository.query.Query;
import org.springframework.data.neo4j.test.BookmarkCapture;
import org.springframework.data.neo4j.test.Neo4jExtension;
import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration;
import org.springframework.data.neo4j.test.TestIdentitySupport;
import org.springframework.data.neo4j.types.CartesianPoint2d;
import org.springframework.data.neo4j.types.GeographicPoint2d;
import org.springframework.data.repository.query.FluentQuery;
@@ -122,6 +117,10 @@ import org.springframework.transaction.ReactiveTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.reactive.TransactionalOperator;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
/**
* @author Gerrit Meier
* @author Michael J. Simons
@@ -440,10 +439,10 @@ class ReactiveRepositoryIT {
Node petNode1 = record.get("p1").asNode();
Node petNode2 = record.get("p2").asNode();
long personId = IdentitySupport.getInternalId(personNode);
long hobbyNodeId = IdentitySupport.getInternalId(hobbyNode1);
long petNode1Id = IdentitySupport.getInternalId(petNode1);
long petNode2Id = IdentitySupport.getInternalId(petNode2);
long personId = TestIdentitySupport.getInternalId(personNode);
long hobbyNodeId = TestIdentitySupport.getInternalId(hobbyNode1);
long petNode1Id = TestIdentitySupport.getInternalId(petNode1);
long petNode2Id = TestIdentitySupport.getInternalId(petNode2);
PersonWithRelationship probe = new PersonWithRelationship();
probe.setName("Freddie");
@@ -481,10 +480,10 @@ class ReactiveRepositoryIT {
Node petNode1 = record.get("p1").asNode();
Node petNode2 = record.get("p2").asNode();
long personId = IdentitySupport.getInternalId(personNode);
long hobbyNodeId = IdentitySupport.getInternalId(hobbyNode1);
long petNode1Id = IdentitySupport.getInternalId(petNode1);
long petNode2Id = IdentitySupport.getInternalId(petNode2);
long personId = TestIdentitySupport.getInternalId(personNode);
long hobbyNodeId = TestIdentitySupport.getInternalId(hobbyNode1);
long petNode1Id = TestIdentitySupport.getInternalId(petNode1);
long petNode2Id = TestIdentitySupport.getInternalId(petNode2);
PersonWithRelationship probe = new PersonWithRelationship();
probe.setName("Freddie");
@@ -522,10 +521,10 @@ class ReactiveRepositoryIT {
Node petNode1 = record.get("p1").asNode();
Node petNode2 = record.get("p2").asNode();
long personId = IdentitySupport.getInternalId(personNode);
long hobbyNodeId = IdentitySupport.getInternalId(hobbyNode1);
long petNode1Id = IdentitySupport.getInternalId(petNode1);
long petNode2Id = IdentitySupport.getInternalId(petNode2);
long personId = TestIdentitySupport.getInternalId(personNode);
long hobbyNodeId = TestIdentitySupport.getInternalId(hobbyNode1);
long petNode1Id = TestIdentitySupport.getInternalId(petNode1);
long petNode2Id = TestIdentitySupport.getInternalId(petNode2);
PersonWithRelationship probe = new PersonWithRelationship();
probe.setName("Freddie");
@@ -844,12 +843,12 @@ class ReactiveRepositoryIT {
Node petNode1 = record.get("p1").asNode();
Node petNode2 = record.get("p2").asNode();
long personId = IdentitySupport.getInternalId(personNode);
long clubId = IdentitySupport.getInternalId(clubNode);
long hobbyNode1Id = IdentitySupport.getInternalId(hobbyNode1);
long hobbyNode2Id = IdentitySupport.getInternalId(hobbyNode2);
long petNode1Id = IdentitySupport.getInternalId(petNode1);
long petNode2Id = IdentitySupport.getInternalId(petNode2);
long personId = TestIdentitySupport.getInternalId(personNode);
long clubId = TestIdentitySupport.getInternalId(clubNode);
long hobbyNode1Id = TestIdentitySupport.getInternalId(hobbyNode1);
long hobbyNode2Id = TestIdentitySupport.getInternalId(hobbyNode2);
long petNode1Id = TestIdentitySupport.getInternalId(petNode1);
long petNode2Id = TestIdentitySupport.getInternalId(petNode2);
StepVerifier.create(repository.findById(personId)).assertNext(loadedPerson -> {
@@ -880,7 +879,7 @@ class ReactiveRepositoryIT {
}
@Test
void loadEntityWithRelationshipToTheSameNode(@Autowired ReactiveRelationshipRepository repository) {
void findEntityWithRelationshipToTheSameNode(@Autowired ReactiveRelationshipRepository repository) {
Record record = doWithSession(session -> session
.run("""
@@ -895,9 +894,9 @@ class ReactiveRepositoryIT {
Node hobbyNode1 = record.get("h1").asNode();
Node petNode1 = record.get("p1").asNode();
long personId = IdentitySupport.getInternalId(personNode);
long hobbyNode1Id = IdentitySupport.getInternalId(hobbyNode1);
long petNode1Id = IdentitySupport.getInternalId(petNode1);
long personId = TestIdentitySupport.getInternalId(personNode);
long hobbyNode1Id = TestIdentitySupport.getInternalId(hobbyNode1);
long petNode1Id = TestIdentitySupport.getInternalId(petNode1);
StepVerifier.create(repository.findById(personId)).assertNext(loadedPerson -> {
@@ -938,7 +937,7 @@ class ReactiveRepositoryIT {
RETURN t1
""").single();
return IdentitySupport.getInternalId(record.get("t1").asNode());
return TestIdentitySupport.getInternalId(record.get("t1").asNode());
});
StepVerifier.create(loopingRelationshipRepository.findById(type1Id)).assertNext(type1 -> {
@@ -969,18 +968,17 @@ class ReactiveRepositoryIT {
@Test
void loadEntityWithBidirectionalRelationship(@Autowired BidirectionalStartRepository repository) {
long startId = doWithSession(session -> {
Node startNode = doWithSession(session -> {
Record record = session.run("CREATE (n:BidirectionalStart{name:'Ernie'})-[:CONNECTED]->(e:BidirectionalEnd{name:'Bert'}) RETURN n").single();
Node startNode = record.get("n").asNode();
return IdentitySupport.getInternalId(startNode);
return record.get("n").asNode();
});
StepVerifier.create(repository.findById(startId))
StepVerifier.create(repository.findById(TestIdentitySupport.getInternalId(startNode)))
.verifyErrorMatches(error -> {
Throwable cause = error.getCause();
return cause instanceof MappingException && cause.getMessage().equals(
"The node with id " + startId + " has a logical cyclic mapping dependency; " +
"The node with id " + startNode.elementId() + " has a logical cyclic mapping dependency; " +
"its creation caused the creation of another node that has a reference to this");
});
@@ -993,7 +991,7 @@ class ReactiveRepositoryIT {
Record record = session.run("CREATE (n:BidirectionalStart{name:'Ernie'})-[:CONNECTED]->(e:BidirectionalEnd{name:'Bert'}) RETURN e").single();
Node endNode = record.get("e").asNode();
return IdentitySupport.getInternalId(endNode);
return TestIdentitySupport.getInternalId(endNode);
});
StepVerifier.create(repository.findById(endId)).assertNext(entity -> {
@@ -1008,8 +1006,8 @@ class ReactiveRepositoryIT {
.run("CREATE (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(h:Hobby{name:'Music'}), (n)-[:Has]->(p:Pet{name: 'Jerry'}) RETURN n, h, p")
.single());
long hobbyNode1Id = IdentitySupport.getInternalId(record.get("h").asNode());
long petNode1Id = IdentitySupport.getInternalId(record.get("p").asNode());
long hobbyNode1Id = TestIdentitySupport.getInternalId(record.get("h").asNode());
long petNode1Id = TestIdentitySupport.getInternalId(record.get("p").asNode());
record = doWithSession(session -> session.run("""
CREATE
@@ -1018,8 +1016,8 @@ class ReactiveRepositoryIT {
RETURN n, h, p
""").single());
long hobbyNode2Id = IdentitySupport.getInternalId(record.get("h").asNode());
long petNode2Id = IdentitySupport.getInternalId(record.get("p").asNode());
long hobbyNode2Id = TestIdentitySupport.getInternalId(record.get("h").asNode());
long petNode2Id = TestIdentitySupport.getInternalId(record.get("p").asNode());
StepVerifier.create(repository.findAll()).recordWith(ArrayList::new).expectNextCount(2)
.consumeRecordedWith(loadedPersons -> {
@@ -1058,10 +1056,10 @@ class ReactiveRepositoryIT {
Node petNode1 = record.get("p1").asNode();
Node petNode2 = record.get("p2").asNode();
long personId = IdentitySupport.getInternalId(personNode);
long hobbyNodeId = IdentitySupport.getInternalId(hobbyNode1);
long petNode1Id = IdentitySupport.getInternalId(petNode1);
long petNode2Id = IdentitySupport.getInternalId(petNode2);
long personId = TestIdentitySupport.getInternalId(personNode);
long hobbyNodeId = TestIdentitySupport.getInternalId(hobbyNode1);
long petNode1Id = TestIdentitySupport.getInternalId(petNode1);
long petNode2Id = TestIdentitySupport.getInternalId(petNode2);
StepVerifier.create(repository.getPersonWithRelationshipsViaQuery()).assertNext(loadedPerson -> {
assertThat(loadedPerson.getName()).isEqualTo("Freddie");
@@ -1085,7 +1083,7 @@ class ReactiveRepositoryIT {
Record record = session.run("CREATE (p:Pet{name:'Jerry'})-[:Has]->(t:Thing{theId:'t1', name:'Thing1'}) RETURN p, t").single();
Node petNode = record.get("p").asNode();
return IdentitySupport.getInternalId(petNode);
return TestIdentitySupport.getInternalId(petNode);
});
StepVerifier.create(repository.findById(petNodeId)).assertNext(pet -> {
@@ -1182,9 +1180,9 @@ class ReactiveRepositoryIT {
Node hobbyNode1 = record.get("h1").asNode();
Node hobbyNode2 = record.get("h2").asNode();
long personId = IdentitySupport.getInternalId(personNode);
long hobbyNode1Id = IdentitySupport.getInternalId(hobbyNode1);
long hobbyNode2Id = IdentitySupport.getInternalId(hobbyNode2);
long personId = TestIdentitySupport.getInternalId(personNode);
long hobbyNode1Id = TestIdentitySupport.getInternalId(hobbyNode1);
long hobbyNode2Id = TestIdentitySupport.getInternalId(hobbyNode2);
StepVerifier.create(repository.findById(personId)).assertNext(person -> {
assertThat(person.getName()).isEqualTo("Freddie");
@@ -1324,9 +1322,9 @@ class ReactiveRepositoryIT {
Node hobbyNode1 = record.get("h1").asNode();
Node hobbyNode2 = record.get("h2").asNode();
long personId = IdentitySupport.getInternalId(personNode);
long hobbyNode1Id = IdentitySupport.getInternalId(hobbyNode1);
long hobbyNode2Id = IdentitySupport.getInternalId(hobbyNode2);
long personId = TestIdentitySupport.getInternalId(personNode);
long hobbyNode1Id = TestIdentitySupport.getInternalId(hobbyNode1);
long hobbyNode2Id = TestIdentitySupport.getInternalId(hobbyNode2);
StepVerifier.create(repository.loadFromCustomQuery(personId)).assertNext(person -> {
assertThat(person.getName()).isEqualTo("Freddie");
@@ -1365,7 +1363,7 @@ class ReactiveRepositoryIT {
long personId = doWithSession(session -> {
Record record = session.run("CREATE (n:AltPerson{name:'Freddie'}), (n)-[l1:LIKES {rating: 5}]->(h1:AltHobby{name:'Music'}) RETURN n, h1").single();
return IdentitySupport.getInternalId(record.get("n").asNode());
return TestIdentitySupport.getInternalId(record.get("n").asNode());
});
StepVerifier.create(repository.loadFromCustomQuery(personId)).assertNext(hobby -> {
@@ -1386,7 +1384,7 @@ class ReactiveRepositoryIT {
" (n)-[l1:LIKES {rating: 5}]->(h1:AltHobby{name:'Music'})," +
" (n)-[l2:LIKES {rating: 1}]->(h1)" +
" RETURN n, h1").single();
return IdentitySupport.getInternalId(record.get("n").asNode());
return TestIdentitySupport.getInternalId(record.get("n").asNode());
});
StepVerifier.create(repository.loadFromCustomQuery(personId)).assertNext(hobby -> {
@@ -1907,7 +1905,7 @@ class ReactiveRepositoryIT {
assertThat(record.containsKey("n")).isTrue();
Node rootNode = record.get("n").asNode();
assertThat(ids.get(0)).isEqualTo(IdentitySupport.getInternalId(rootNode));
assertThat(ids.get(0)).isEqualTo(TestIdentitySupport.getInternalId(rootNode));
assertThat(rootNode.get("name").asString()).isEqualTo("Freddie");
List<List<Object>> petsWithHobbies = record.get("petsWithHobbies").asList(Value::asList);
@@ -1977,7 +1975,7 @@ class ReactiveRepositoryIT {
assertThat(record.containsKey("n")).isTrue();
Node rootNode = record.get("n").asNode();
assertThat(ids.get(0)).isEqualTo(IdentitySupport.getInternalId(rootNode));
assertThat(ids.get(0)).isEqualTo(TestIdentitySupport.getInternalId(rootNode));
assertThat(rootNode.get("name").asString()).isEqualTo("Freddie");
List<List<Object>> petsWithHobbies = record.get("petsWithHobbies").asList(Value::asList);
@@ -2038,7 +2036,7 @@ class ReactiveRepositoryIT {
assertThat(record.containsKey("n")).isTrue();
Node rootNode = record.get("n").asNode();
assertThat(ids.get(0)).isEqualTo(IdentitySupport.getInternalId(rootNode));
assertThat(ids.get(0)).isEqualTo(TestIdentitySupport.getInternalId(rootNode));
assertThat(rootNode.get("name").asString()).isEqualTo("Freddie");
assertThat(record.get("hobbies").asList(entry -> entry.asNode().get("name").asString()))
@@ -2504,9 +2502,9 @@ class ReactiveRepositoryIT {
long n3Id;
Record record = doWithSession(session -> session.run("CREATE (n1:A:B:C), (n2:B:C), (n3:A) return n1, n2, n3").single());
n1Id = IdentitySupport.getInternalId(record.get("n1").asNode());
n2Id = IdentitySupport.getInternalId(record.get("n2").asNode());
n3Id = IdentitySupport.getInternalId(record.get("n3").asNode());
n1Id = TestIdentitySupport.getInternalId(record.get("n1").asNode());
n2Id = TestIdentitySupport.getInternalId(record.get("n2").asNode());
n3Id = TestIdentitySupport.getInternalId(record.get("n3").asNode());
StepVerifier.create(repository.findById(n1Id)).expectNextCount(1).verifyComplete();
StepVerifier.create(repository.findById(n2Id)).verifyComplete();
@@ -2521,9 +2519,9 @@ class ReactiveRepositoryIT {
long n3Id;
Record record = doWithSession(session -> session.run("CREATE (n1:A:B:C), (n2:B:C), (n3:A) return n1, n2, n3").single());
n1Id = IdentitySupport.getInternalId(record.get("n1").asNode());
n2Id = IdentitySupport.getInternalId(record.get("n2").asNode());
n3Id = IdentitySupport.getInternalId(record.get("n3").asNode());
n1Id = TestIdentitySupport.getInternalId(record.get("n1").asNode());
n2Id = TestIdentitySupport.getInternalId(record.get("n2").asNode());
n3Id = TestIdentitySupport.getInternalId(record.get("n3").asNode());
repository.deleteById(n1Id).block();
repository.deleteById(n2Id).block();

View File

@@ -67,4 +67,12 @@ public class AltLikedByPersonRelationship {
public int hashCode() {
return Objects.hash(rating, altPerson);
}
@Override
public String toString() {
return "AltLikedByPersonRelationship{" +
"rating=" + rating +
", altPerson=" + altPerson.getName() +
'}';
}
}

View File

@@ -13,33 +13,24 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.neo4j.core.schema;
package org.springframework.data.neo4j.test;
import org.neo4j.driver.types.Entity;
/**
* A Neo4j element id.
*
* @author Michael J. Simons
* @soundtrack GötterDÄmmerung - Tribut an die beste Band der Welt
* @since 7.0.0
*/
public interface ElementId {
static ElementId of(String value) {
return new DefaultElementId(value);
}
public final class TestIdentitySupport {
/**
* @param entity The entity container as received from the server.
* @return The element id
* @return The internal id
*/
static ElementId of(Entity entity) {
return of(entity.elementId());
@SuppressWarnings("deprecation")
public static Long getInternalId(Entity entity) {
return entity.id();
}
/**
* @return A unique textual representation.
*/
String value();
private TestIdentitySupport() {
}
}