From 40f7fc01a094477d6d67f5ea0abc81ae78c26c52 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Tue, 23 Jun 2020 14:56:25 +0200 Subject: [PATCH] Allow different relationship types to the same entity instance. This allows for multiple relationships of different types between the same two objects. It is implemented via a simple state machine that checks both the type of the relationship and the state of the related objects already processed before terminating the traversal. In addition the change makes sure that only the object returned by Spring Data Commons event support is passed on in the recursive traversal. Last but not least, we switch to Neo4j 4.0 in tests by default to make testing of reactive components active by default. --- .../data/core/Neo4jTemplate.java | 51 ++-- ...edRelationshipProcessingStateMachine.java} | 26 +- .../data/core/ReactiveNeo4jTemplate.java | 95 ++++--- .../imperative/RelationshipsIT.java | 243 +++++++++++++++++ .../data/integration/kotlin/KotlinIT.java | 17 +- .../reactive/ReactiveRelationshipsIT.java | 249 ++++++++++++++++++ .../shared/MultipleRelationshipsThing.java | 82 ++++++ .../shared/RelationshipsITBase.java | 52 ++++ .../data/test/Neo4jExtension.java | 2 +- 9 files changed, 745 insertions(+), 72 deletions(-) rename spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/{NestedRelationshipProcessState.java => NestedRelationshipProcessingStateMachine.java} (79%) create mode 100644 spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/RelationshipsIT.java create mode 100644 spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveRelationshipsIT.java create mode 100644 spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/MultipleRelationshipsThing.java create mode 100644 spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/RelationshipsITBase.java diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/Neo4jTemplate.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/Neo4jTemplate.java index 2699dfff4..f476e0cc6 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/Neo4jTemplate.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/Neo4jTemplate.java @@ -36,11 +36,12 @@ import org.apiguardian.api.API; import org.neo4j.driver.exceptions.NoSuchRecordException; import org.neo4j.driver.summary.ResultSummary; import org.neo4j.driver.summary.SummaryCounters; -import org.neo4j.springframework.data.core.Neo4jClient.RunnableSpecTightToDatabase; import org.neo4j.opencypherdsl.Condition; import org.neo4j.opencypherdsl.Functions; import org.neo4j.opencypherdsl.Statement; import org.neo4j.opencypherdsl.renderer.Renderer; +import org.neo4j.springframework.data.core.Neo4jClient.RunnableSpecTightToDatabase; +import org.neo4j.springframework.data.core.NestedRelationshipProcessingStateMachine.ProcessState; import org.neo4j.springframework.data.core.mapping.Neo4jMappingContext; import org.neo4j.springframework.data.core.mapping.Neo4jPersistentEntity; import org.neo4j.springframework.data.core.mapping.Neo4jPersistentProperty; @@ -399,21 +400,23 @@ public final class Neo4jTemplate implements Neo4jOperations, BeanFactoryAware { return toExecutableQuery(preparedQuery); } - private void processRelations(Neo4jPersistentEntity neo4jPersistentEntity, Object parentObject, @Nullable String inDatabase) { + private void processRelations(Neo4jPersistentEntity neo4jPersistentEntity, Object parentObject, + @Nullable String inDatabase) { - processNestedRelations(neo4jPersistentEntity, parentObject, inDatabase, new NestedRelationshipProcessState()); + processNestedRelations(neo4jPersistentEntity, parentObject, inDatabase, new NestedRelationshipProcessingStateMachine()); } private void processNestedRelations(Neo4jPersistentEntity neo4jPersistentEntity, Object parentObject, - @Nullable String inDatabase, NestedRelationshipProcessState processState) { + @Nullable String inDatabase, NestedRelationshipProcessingStateMachine stateMachine) { PersistentPropertyAccessor propertyAccessor = neo4jPersistentEntity.getPropertyAccessor(parentObject); + Object fromId = propertyAccessor.getProperty(neo4jPersistentEntity.getRequiredIdProperty()); - neo4jPersistentEntity.doWithAssociations((AssociationHandler) handler -> { + neo4jPersistentEntity.doWithAssociations((AssociationHandler) association -> { // create context to bundle parameters NestedRelationshipContext relationshipContext = NestedRelationshipContext - .of(handler, propertyAccessor, neo4jPersistentEntity); + .of(association, propertyAccessor, neo4jPersistentEntity); Collection relatedValuesToStore = Relationships .unifyRelationshipValue(relationshipContext.getInverse(), relationshipContext.getValue()); @@ -422,19 +425,20 @@ public final class Neo4jTemplate implements Neo4jOperations, BeanFactoryAware { RelationshipDescription relationshipDescriptionObverse = relationshipDescription.getRelationshipObverse(); // break recursive procession and deletion of previously created relationships - if (processState.hasProcessedEither(relationshipDescriptionObverse, relatedValuesToStore)) { + ProcessState processState = stateMachine + .getStateOf(relationshipDescriptionObverse, relatedValuesToStore); + if (processState == ProcessState.PROCESSED_BOTH) { return; } - Neo4jPersistentEntity relationshipsToRemoveDescription = neo4jMappingContext - .getPersistentEntity(relationshipContext.getAssociationTargetType()); - - Object fromId = propertyAccessor.getProperty(neo4jPersistentEntity.getRequiredIdProperty()); // remove all relationships before creating all new if the entity is not new // this avoids the usage of cache but might have significant impact on overall performance if (!neo4jPersistentEntity.isNew(parentObject)) { + Neo4jPersistentEntity previouslyRelatedPersistentEntity = neo4jMappingContext + .getPersistentEntity(relationshipContext.getAssociationTargetType()); + Statement relationshipRemoveQuery = cypherGenerator.createRelationshipRemoveQuery(neo4jPersistentEntity, - relationshipDescription, relationshipsToRemoveDescription); + relationshipDescription, previouslyRelatedPersistentEntity); neo4jClient.query(renderer.render(relationshipRemoveQuery)) .in(inDatabase) @@ -446,22 +450,25 @@ public final class Neo4jTemplate implements Neo4jOperations, BeanFactoryAware { return; } - processState.markAsProcessed(relationshipDescription, relatedValuesToStore); + stateMachine.markAsProcessed(relationshipDescription, relatedValuesToStore); for (Object relatedValueToStore : relatedValuesToStore) { // here map entry is not always anymore a dynamic association - Object valueToBeSaved = relationshipContext.identifyAndExtractRelationshipValue(relatedValueToStore); + Object valueToBeSavedPreEvt = relationshipContext + .identifyAndExtractRelationshipValue(relatedValueToStore); + valueToBeSavedPreEvt = eventSupport.maybeCallBeforeBind(valueToBeSavedPreEvt); - Neo4jPersistentEntity targetNodeDescription = neo4jMappingContext.getPersistentEntity(valueToBeSaved.getClass()); + Neo4jPersistentEntity targetNodeDescription = neo4jMappingContext + .getPersistentEntity(valueToBeSavedPreEvt.getClass()); - valueToBeSaved = eventSupport.maybeCallBeforeBind(valueToBeSaved); - - Long relatedInternalId = saveRelatedNode(valueToBeSaved, relationshipContext.getAssociationTargetType(), + Long relatedInternalId = saveRelatedNode(valueToBeSavedPreEvt, + relationshipContext.getAssociationTargetType(), targetNodeDescription, inDatabase); RelationshipStatementHolder statementHolder = RelationshipStatementHolder.createStatement( - neo4jMappingContext, neo4jPersistentEntity, relationshipContext, relatedInternalId, relatedValueToStore); + neo4jMappingContext, neo4jPersistentEntity, relationshipContext, relatedInternalId, + relatedValueToStore); neo4jClient.query(renderer.render(statementHolder.getRelationshipCreationQuery())) .in(inDatabase) @@ -472,11 +479,13 @@ public final class Neo4jTemplate implements Neo4jOperations, BeanFactoryAware { // if an internal id is used this must get set to link this entity in the next iteration if (targetNodeDescription.isUsingInternalIds()) { PersistentPropertyAccessor targetPropertyAccessor = targetNodeDescription - .getPropertyAccessor(valueToBeSaved); + .getPropertyAccessor(valueToBeSavedPreEvt); targetPropertyAccessor .setProperty(targetNodeDescription.getRequiredIdProperty(), relatedInternalId); } - processNestedRelations(targetNodeDescription, valueToBeSaved, inDatabase, processState); + if (processState != ProcessState.PROCESSED_ALL_VALUES) { + processNestedRelations(targetNodeDescription, valueToBeSavedPreEvt, inDatabase, stateMachine); + } } }); } diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/NestedRelationshipProcessState.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/NestedRelationshipProcessingStateMachine.java similarity index 79% rename from spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/NestedRelationshipProcessState.java rename to spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/NestedRelationshipProcessingStateMachine.java index c58737ceb..bf42356b1 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/NestedRelationshipProcessState.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/NestedRelationshipProcessingStateMachine.java @@ -34,7 +34,14 @@ import org.springframework.lang.Nullable; * @author Michael J. Simons * @soundtrack Helge Schneider - Heart Attack No. 1 */ -final class NestedRelationshipProcessState { +final class NestedRelationshipProcessingStateMachine { + + enum ProcessState { + PROCESSED_NONE, + PROCESSED_BOTH, + PROCESSED_ONLY_RELATIONSHIP, + PROCESSED_ALL_VALUES + } private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); private final Lock read = lock.readLock(); @@ -53,13 +60,24 @@ final class NestedRelationshipProcessState { /** * @param relationshipDescription Check whether this relationship description has been processed * @param valuesToStore Check whether all the values in the collection have been processed - * @return True, if either the relationship has been already process or all of the values to store. + * @return The state of things processed */ - boolean hasProcessedEither(RelationshipDescription relationshipDescription, @Nullable Collection valuesToStore) { + ProcessState getStateOf(RelationshipDescription relationshipDescription, @Nullable Collection valuesToStore) { try { read.lock(); - return hasProcessed(relationshipDescription) || hasProcessedAllOf(valuesToStore); + boolean hasProcessedRelationship = hasProcessed(relationshipDescription); + boolean hasProcessedAllValues = hasProcessedAllOf(valuesToStore); + if (hasProcessedRelationship && hasProcessedAllValues) { + return ProcessState.PROCESSED_BOTH; + } + if (hasProcessedRelationship) { + return ProcessState.PROCESSED_ONLY_RELATIONSHIP; + } + if (hasProcessedAllValues) { + return ProcessState.PROCESSED_ALL_VALUES; + } + return ProcessState.PROCESSED_NONE; } finally { read.unlock(); } diff --git a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/ReactiveNeo4jTemplate.java b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/ReactiveNeo4jTemplate.java index 19caa0e3d..db51637e4 100644 --- a/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/ReactiveNeo4jTemplate.java +++ b/spring-data-neo4j/src/main/java/org/neo4j/springframework/data/core/ReactiveNeo4jTemplate.java @@ -20,8 +20,8 @@ package org.neo4j.springframework.data.core; import static java.util.Collections.*; import static java.util.stream.Collectors.*; -import static org.neo4j.springframework.data.core.DatabaseSelection.*; import static org.neo4j.opencypherdsl.Cypher.*; +import static org.neo4j.springframework.data.core.DatabaseSelection.*; import static org.neo4j.springframework.data.core.schema.Constants.*; import reactor.core.publisher.Flux; @@ -45,6 +45,7 @@ import org.neo4j.opencypherdsl.Condition; import org.neo4j.opencypherdsl.Functions; import org.neo4j.opencypherdsl.Statement; import org.neo4j.opencypherdsl.renderer.Renderer; +import org.neo4j.springframework.data.core.NestedRelationshipProcessingStateMachine.ProcessState; import org.neo4j.springframework.data.core.mapping.Neo4jMappingContext; import org.neo4j.springframework.data.core.mapping.Neo4jPersistentEntity; import org.neo4j.springframework.data.core.mapping.Neo4jPersistentProperty; @@ -407,41 +408,46 @@ public final class ReactiveNeo4jTemplate implements ReactiveNeo4jOperations, Bea private Mono processRelations(Neo4jPersistentEntity neo4jPersistentEntity, Object parentObject, @Nullable String inDatabase) { - return processNestedRelations(neo4jPersistentEntity, parentObject, inDatabase, new NestedRelationshipProcessState()); + return processNestedRelations(neo4jPersistentEntity, parentObject, inDatabase, new NestedRelationshipProcessingStateMachine()); } private Mono processNestedRelations(Neo4jPersistentEntity neo4jPersistentEntity, Object parentObject, - @Nullable String inDatabase, NestedRelationshipProcessState processState) { + @Nullable String inDatabase, NestedRelationshipProcessingStateMachine stateMachine) { return Mono.defer(() -> { PersistentPropertyAccessor propertyAccessor = neo4jPersistentEntity.getPropertyAccessor(parentObject); Object fromId = propertyAccessor.getProperty(neo4jPersistentEntity.getRequiredIdProperty()); List> relationshipCreationMonos = new ArrayList<>(); - neo4jPersistentEntity.doWithAssociations((AssociationHandler) handler -> { + neo4jPersistentEntity.doWithAssociations((AssociationHandler) association -> { // create context to bundle parameters NestedRelationshipContext relationshipContext = NestedRelationshipContext - .of(handler, propertyAccessor, neo4jPersistentEntity); + .of(association, propertyAccessor, neo4jPersistentEntity); Collection relatedValuesToStore = Relationships .unifyRelationshipValue(relationshipContext.getInverse(), relationshipContext.getValue()); RelationshipDescription relationshipDescription = relationshipContext.getRelationship(); - RelationshipDescription relationshipDescriptionObverse = relationshipDescription.getRelationshipObverse(); + RelationshipDescription relationshipDescriptionObverse = relationshipDescription + .getRelationshipObverse(); // break recursive procession and deletion of previously created relationships - if (processState.hasProcessedEither(relationshipDescriptionObverse, relatedValuesToStore)) { + ProcessState processState = stateMachine + .getStateOf(relationshipDescriptionObverse, relatedValuesToStore); + if (processState == ProcessState.PROCESSED_BOTH) { return; } - Neo4jPersistentEntity targetNodeDescription = (Neo4jPersistentEntity) neo4jMappingContext - .getRequiredNodeDescription(relationshipContext.getAssociationTargetType()); - // remove all relationships before creating all new if the entity is not new // this avoids the usage of cache but might have significant impact on overall performance if (!neo4jPersistentEntity.isNew(parentObject)) { - Statement relationshipRemoveQuery = cypherGenerator.createRelationshipRemoveQuery(neo4jPersistentEntity, relationshipDescription, targetNodeDescription); + Neo4jPersistentEntity previouslyRelatedPersistentEntity = neo4jMappingContext + .getPersistentEntity(relationshipContext.getAssociationTargetType()); + + Statement relationshipRemoveQuery = cypherGenerator + .createRelationshipRemoveQuery(neo4jPersistentEntity, relationshipDescription, + previouslyRelatedPersistentEntity); relationshipCreationMonos.add( neo4jClient.query(renderer.render(relationshipRemoveQuery)) .in(inDatabase) @@ -454,43 +460,54 @@ public final class ReactiveNeo4jTemplate implements ReactiveNeo4jOperations, Bea return; } - processState.markAsProcessed(relationshipDescription, relatedValuesToStore); + stateMachine.markAsProcessed(relationshipDescription, relatedValuesToStore); for (Object relatedValueToStore : relatedValuesToStore) { - Object valueToBeSavedPreEvt = relationshipContext.identifyAndExtractRelationshipValue(relatedValueToStore); - Mono valueToBeSavedMono = eventSupport.maybeCallBeforeBind(valueToBeSavedPreEvt); + Object valueToBeSavedPreEvt = relationshipContext + .identifyAndExtractRelationshipValue(relatedValueToStore); - relationshipCreationMonos.add( - valueToBeSavedMono - .flatMap(valueToBeSaved -> - saveRelatedNode(valueToBeSaved, relationshipContext.getAssociationTargetType(), - targetNodeDescription, inDatabase) - .flatMap(relatedInternalId -> { + Mono createRelationship = eventSupport + .maybeCallBeforeBind(valueToBeSavedPreEvt) + .flatMap(valueToBeSaved -> { + Neo4jPersistentEntity targetNodeDescription = neo4jMappingContext + .getPersistentEntity(valueToBeSavedPreEvt.getClass()); + return saveRelatedNode(valueToBeSaved, relationshipContext.getAssociationTargetType(), + targetNodeDescription, inDatabase) + .flatMap(relatedInternalId -> { - // if an internal id is used this must get set to link this entity in the next iteration - if (targetNodeDescription.isUsingInternalIds()) { - PersistentPropertyAccessor targetPropertyAccessor = targetNodeDescription - .getPropertyAccessor(valueToBeSaved); - targetPropertyAccessor - .setProperty(targetNodeDescription.getRequiredIdProperty(), - relatedInternalId); - } + // if an internal id is used this must get set to link this entity in the next iteration + if (targetNodeDescription.isUsingInternalIds()) { + PersistentPropertyAccessor targetPropertyAccessor = targetNodeDescription + .getPropertyAccessor(valueToBeSaved); + targetPropertyAccessor + .setProperty(targetNodeDescription.getRequiredIdProperty(), + relatedInternalId); + } - RelationshipStatementHolder statementHolder = RelationshipStatementHolder.createStatement( - neo4jMappingContext, neo4jPersistentEntity, relationshipContext, relatedInternalId, relatedValueToStore); + RelationshipStatementHolder statementHolder = RelationshipStatementHolder + .createStatement( + neo4jMappingContext, neo4jPersistentEntity, relationshipContext, + relatedInternalId, relatedValueToStore); - // in case of no properties the bind will just return an empty map - Mono relationshipCreationMonoNested = neo4jClient - .query(renderer.render(statementHolder.getRelationshipCreationQuery())) - .in(inDatabase) - .bind(convertIdValues(fromId)).to(FROM_ID_PARAMETER_NAME) - .bindAll(statementHolder.getProperties()) - .run(); + // in case of no properties the bind will just return an empty map + Mono relationshipCreationMonoNested = neo4jClient + .query(renderer.render(statementHolder.getRelationshipCreationQuery())) + .in(inDatabase) + .bind(convertIdValues(fromId)).to(FROM_ID_PARAMETER_NAME) + .bindAll(statementHolder.getProperties()) + .run(); + if (processState != ProcessState.PROCESSED_ALL_VALUES) { return relationshipCreationMonoNested.checkpoint() - .then(processNestedRelations(targetNodeDescription, valueToBeSaved, inDatabase, processState)); - }).checkpoint())); + .then(processNestedRelations(targetNodeDescription, valueToBeSaved, + inDatabase, stateMachine)); + } else { + return relationshipCreationMonoNested.checkpoint().then(); + } + }).checkpoint(); + }); + relationshipCreationMonos.add(createRelationship); } }); diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/RelationshipsIT.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/RelationshipsIT.java new file mode 100644 index 000000000..cdcc74173 --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/imperative/RelationshipsIT.java @@ -0,0 +1,243 @@ +/* + * Copyright (c) 2019-2020 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.neo4j.springframework.data.integration.imperative; + +import static org.assertj.core.api.Assertions.*; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; + +import org.junit.jupiter.api.Test; +import org.neo4j.driver.Driver; +import org.neo4j.driver.Record; +import org.neo4j.driver.Session; +import org.neo4j.springframework.data.config.AbstractNeo4jConfig; +import org.neo4j.springframework.data.integration.shared.MultipleRelationshipsThing; +import org.neo4j.springframework.data.integration.shared.RelationshipsITBase; +import org.neo4j.springframework.data.repository.config.EnableNeo4jRepositories; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.repository.CrudRepository; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +/** + * Test cases for various relationship scenarios (self references, multiple times to same instance). + * + * @author Michael J. Simons + */ +class RelationshipsIT extends RelationshipsITBase { + + @Autowired RelationshipsIT(Driver driver) { + super(driver); + } + + @Test + void shouldSaveSingleRelationship(@Autowired MultipleRelationshipsThingRepository repository) { + + MultipleRelationshipsThing p = new MultipleRelationshipsThing("p"); + p.setTypeA(new MultipleRelationshipsThing("c")); + + p = repository.save(p); + + Optional loadedThing = repository.findById(p.getId()); + assertThat(loadedThing).isPresent() + .map(MultipleRelationshipsThing::getTypeA) + .map(MultipleRelationshipsThing::getName) + .hasValue("c"); + + try (Session session = driver.session()) { + List names = session.run("MATCH (n:MultipleRelationshipsThing) RETURN n.name AS name") + .list(r -> r.get("name").asString()); + assertThat(names).hasSize(2).containsExactlyInAnyOrder("p", "c"); + } + } + + @Test + void shouldSaveSingleRelationshipInList(@Autowired MultipleRelationshipsThingRepository repository) { + + MultipleRelationshipsThing p = new MultipleRelationshipsThing("p"); + p.setTypeB(Collections.singletonList(new MultipleRelationshipsThing("c"))); + + p = repository.save(p); + + Optional loadedThing = repository.findById(p.getId()); + assertThat(loadedThing).isPresent() + .map(MultipleRelationshipsThing::getTypeB) + .hasValueSatisfying( + l -> assertThat(l).extracting(MultipleRelationshipsThing::getName).containsExactly("c")); + + try (Session session = driver.session()) { + List names = session.run("MATCH (n:MultipleRelationshipsThing) RETURN n.name AS name") + .list(r -> r.get("name").asString()); + assertThat(names).hasSize(2).containsExactlyInAnyOrder("p", "c"); + } + } + + /** + * This stores multiple, different instances. + * + * @param repository The repository to use. + */ + @Test + void shouldSaveMultipleRelationshipsOfSameObjectType(@Autowired MultipleRelationshipsThingRepository repository) { + + MultipleRelationshipsThing p = new MultipleRelationshipsThing("p"); + p.setTypeA(new MultipleRelationshipsThing("c1")); + p.setTypeB(Collections.singletonList(new MultipleRelationshipsThing("c2"))); + p.setTypeC(Collections.singletonList(new MultipleRelationshipsThing("c3"))); + + p = repository.save(p); + + Optional loadedThing = repository.findById(p.getId()); + assertThat(loadedThing).isPresent() + .hasValueSatisfying(t -> { + + MultipleRelationshipsThing typeA = t.getTypeA(); + List typeB = t.getTypeB(); + List typeC = t.getTypeC(); + + assertThat(typeA).isNotNull(); + assertThat(typeA).extracting(MultipleRelationshipsThing::getName).isEqualTo("c1"); + assertThat(typeB).extracting(MultipleRelationshipsThing::getName).containsExactly("c2"); + assertThat(typeC).extracting(MultipleRelationshipsThing::getName).containsExactly("c3"); + }); + + try (Session session = driver.session()) { + + List names = session.run( + "MATCH (n:MultipleRelationshipsThing {name: 'p'}) - [r:TYPE_A|TYPE_B|TYPE_C] -> (o) RETURN r, o") + .list(record -> { + String type = record.get("r").asRelationship().type(); + String name = record.get("o").get("name").asString(); + return type + "_" + name; + }); + assertThat(names).containsExactlyInAnyOrder("TYPE_A_c1", "TYPE_B_c2", "TYPE_C_c3"); + } + } + + /** + * This stores the same instance in different relationships + * + * @param repository The repository to use. + */ + @Test + void shouldSaveMultipleRelationshipsOfSameInstance(@Autowired MultipleRelationshipsThingRepository repository) { + + MultipleRelationshipsThing p = new MultipleRelationshipsThing("p"); + MultipleRelationshipsThing c = new MultipleRelationshipsThing("c1"); + p.setTypeA(c); + p.setTypeB(Collections.singletonList(c)); + p.setTypeC(Collections.singletonList(c)); + + p = repository.save(p); + + Optional loadedThing = repository.findById(p.getId()); + assertThat(loadedThing).isPresent() + .hasValueSatisfying(t -> { + + MultipleRelationshipsThing typeA = t.getTypeA(); + List typeB = t.getTypeB(); + List typeC = t.getTypeC(); + + assertThat(typeA).isNotNull(); + assertThat(typeA).extracting(MultipleRelationshipsThing::getName).isEqualTo("c1"); + assertThat(typeB).extracting(MultipleRelationshipsThing::getName).containsExactly("c1"); + assertThat(typeC).extracting(MultipleRelationshipsThing::getName).containsExactly("c1"); + }); + + try (Session session = driver.session()) { + + List names = session.run( + "MATCH (n:MultipleRelationshipsThing {name: 'p'}) - [r:TYPE_A|TYPE_B|TYPE_C] -> (o) RETURN r, o") + .list(record -> { + String type = record.get("r").asRelationship().type(); + String name = record.get("o").get("name").asString(); + return type + "_" + name; + }); + assertThat(names).containsExactlyInAnyOrder("TYPE_A_c1", "TYPE_B_c1", "TYPE_C_c1"); + } + } + + /** + * This stores the same instance in different relationships + * + * @param repository The repository to use. + */ + @Test + void shouldSaveMultipleRelationshipsOfSameInstanceWithBackReference( + @Autowired MultipleRelationshipsThingRepository repository) { + + MultipleRelationshipsThing p = new MultipleRelationshipsThing("p"); + MultipleRelationshipsThing c = new MultipleRelationshipsThing("c1"); + p.setTypeA(c); + p.setTypeB(Collections.singletonList(c)); + p.setTypeC(Collections.singletonList(c)); + + c.setTypeA(p); + + p = repository.save(p); + + Optional loadedThing = repository.findById(p.getId()); + assertThat(loadedThing).isPresent() + .hasValueSatisfying(t -> { + + MultipleRelationshipsThing typeA = t.getTypeA(); + List typeB = t.getTypeB(); + List typeC = t.getTypeC(); + + assertThat(typeA).isNotNull(); + assertThat(typeA).extracting(MultipleRelationshipsThing::getName).isEqualTo("c1"); + assertThat(typeB).extracting(MultipleRelationshipsThing::getName).containsExactly("c1"); + assertThat(typeC).extracting(MultipleRelationshipsThing::getName).containsExactly("c1"); + }); + + try (Session session = driver.session()) { + + Function withMapper = record -> { + String type = record.get("r").asRelationship().type(); + String name = record.get("o").get("name").asString(); + return type + "_" + name; + }; + + String query = "MATCH (n:MultipleRelationshipsThing {name: $name}) - [r:TYPE_A|TYPE_B|TYPE_C] -> (o) RETURN r, o"; + List names = session.run(query, Collections.singletonMap("name", "p")).list(withMapper); + assertThat(names).containsExactlyInAnyOrder("TYPE_A_c1", "TYPE_B_c1", "TYPE_C_c1"); + + names = session.run(query, Collections.singletonMap("name", "c1")).list(withMapper); + assertThat(names).containsExactlyInAnyOrder("TYPE_A_p"); + } + } + + interface MultipleRelationshipsThingRepository extends CrudRepository { + } + + @Configuration + @EnableTransactionManagement + @EnableNeo4jRepositories(considerNestedRepositories = true) + static class Config extends AbstractNeo4jConfig { + + @Bean + public Driver driver() { + return neo4jConnectionSupport.getDriver(); + } + } +} diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/kotlin/KotlinIT.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/kotlin/KotlinIT.java index f9e85864a..a0d9b18e1 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/kotlin/KotlinIT.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/kotlin/KotlinIT.java @@ -56,13 +56,16 @@ class KotlinIT { @BeforeEach void setup() { - Session session = driver.session(); - Transaction transaction = session.beginTransaction(); - transaction.run("MATCH (n) detach delete n"); - transaction.run("CREATE (n:KotlinPerson) SET n.name = $personName", Values.parameters("personName", PERSON_NAME)); - transaction.commit(); - transaction.close(); - session.close(); + try ( + Session session = driver.session(); + Transaction transaction = session.beginTransaction() + ) { + transaction.run("MATCH (n) detach delete n").consume(); + transaction + .run("CREATE (n:KotlinPerson) SET n.name = $personName", Values.parameters("personName", PERSON_NAME)) + .consume(); + transaction.commit(); + } } @Test diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveRelationshipsIT.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveRelationshipsIT.java new file mode 100644 index 000000000..855153a20 --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/reactive/ReactiveRelationshipsIT.java @@ -0,0 +1,249 @@ +/* + * Copyright (c) 2019-2020 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.neo4j.springframework.data.integration.reactive; + +import static org.assertj.core.api.Assertions.*; + +import reactor.test.StepVerifier; + +import java.util.Collections; +import java.util.List; +import java.util.function.Function; + +import org.junit.jupiter.api.Test; +import org.neo4j.driver.Driver; +import org.neo4j.driver.Record; +import org.neo4j.driver.Session; +import org.neo4j.springframework.data.config.AbstractReactiveNeo4jConfig; +import org.neo4j.springframework.data.integration.shared.MultipleRelationshipsThing; +import org.neo4j.springframework.data.integration.shared.RelationshipsITBase; +import org.neo4j.springframework.data.repository.config.EnableReactiveNeo4jRepositories; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.repository.reactive.ReactiveCrudRepository; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +/** + * Test cases for various relationship scenarios (self references, multiple times to same instance). + * + * @author Michael J. Simons + */ +class ReactiveRelationshipsIT extends RelationshipsITBase { + + @Autowired ReactiveRelationshipsIT(Driver driver) { + super(driver); + } + + @Test + void shouldSaveSingleRelationship(@Autowired MultipleRelationshipsThingRepository repository) { + + MultipleRelationshipsThing p = new MultipleRelationshipsThing("p"); + p.setTypeA(new MultipleRelationshipsThing("c")); + + repository.save(p) + .map(MultipleRelationshipsThing::getId) + .flatMap(repository::findById) + .as(StepVerifier::create) + .assertNext(loadedThing -> assertThat(loadedThing) + .extracting(MultipleRelationshipsThing::getTypeA) + .extracting(MultipleRelationshipsThing::getName) + .isEqualTo("c")) + .verifyComplete(); + + try (Session session = driver.session()) { + List names = session.run("MATCH (n:MultipleRelationshipsThing) RETURN n.name AS name") + .list(r -> r.get("name").asString()); + assertThat(names).hasSize(2).containsExactlyInAnyOrder("p", "c"); + } + } + + @Test + void shouldSaveSingleRelationshipInList(@Autowired MultipleRelationshipsThingRepository repository) { + + MultipleRelationshipsThing p = new MultipleRelationshipsThing("p"); + p.setTypeB(Collections.singletonList(new MultipleRelationshipsThing("c"))); + + repository.save(p) + .map(MultipleRelationshipsThing::getId) + .flatMap(repository::findById) + .as(StepVerifier::create) + .assertNext(loadedThing -> assertThat(loadedThing.getTypeB()) + .extracting(MultipleRelationshipsThing::getName) + .containsExactly("c")) + .verifyComplete(); + + try (Session session = driver.session()) { + List names = session.run("MATCH (n:MultipleRelationshipsThing) RETURN n.name AS name") + .list(r -> r.get("name").asString()); + assertThat(names).hasSize(2).containsExactlyInAnyOrder("p", "c"); + } + } + + /** + * This stores multiple, different instances. + * + * @param repository The repository to use. + */ + @Test + void shouldSaveMultipleRelationshipsOfSameObjectType(@Autowired MultipleRelationshipsThingRepository repository) { + + MultipleRelationshipsThing p = new MultipleRelationshipsThing("p"); + p.setTypeA(new MultipleRelationshipsThing("c1")); + p.setTypeB(Collections.singletonList(new MultipleRelationshipsThing("c2"))); + p.setTypeC(Collections.singletonList(new MultipleRelationshipsThing("c3"))); + + repository.save(p) + .map(MultipleRelationshipsThing::getId) + .flatMap(repository::findById) + .as(StepVerifier::create) + .assertNext(loadedThing -> { + MultipleRelationshipsThing typeA = loadedThing.getTypeA(); + List typeB = loadedThing.getTypeB(); + List typeC = loadedThing.getTypeC(); + + assertThat(typeA).isNotNull(); + assertThat(typeA).extracting(MultipleRelationshipsThing::getName).isEqualTo("c1"); + assertThat(typeB).extracting(MultipleRelationshipsThing::getName).containsExactly("c2"); + assertThat(typeC).extracting(MultipleRelationshipsThing::getName).containsExactly("c3"); + }) + .verifyComplete(); + + try (Session session = driver.session()) { + + List names = session.run( + "MATCH (n:MultipleRelationshipsThing {name: 'p'}) - [r:TYPE_A|TYPE_B|TYPE_C] -> (o) RETURN r, o") + .list(record -> { + String type = record.get("r").asRelationship().type(); + String name = record.get("o").get("name").asString(); + return type + "_" + name; + }); + assertThat(names).containsExactlyInAnyOrder("TYPE_A_c1", "TYPE_B_c2", "TYPE_C_c3"); + } + } + + /** + * This stores the same instance in different relationships + * + * @param repository The repository to use. + */ + @Test + void shouldSaveMultipleRelationshipsOfSameInstance(@Autowired MultipleRelationshipsThingRepository repository) { + + MultipleRelationshipsThing p = new MultipleRelationshipsThing("p"); + MultipleRelationshipsThing c = new MultipleRelationshipsThing("c1"); + p.setTypeA(c); + p.setTypeB(Collections.singletonList(c)); + p.setTypeC(Collections.singletonList(c)); + + repository.save(p) + .map(MultipleRelationshipsThing::getId) + .flatMap(repository::findById) + .as(StepVerifier::create) + .assertNext(loadedThing -> { + + MultipleRelationshipsThing typeA = loadedThing.getTypeA(); + List typeB = loadedThing.getTypeB(); + List typeC = loadedThing.getTypeC(); + + assertThat(typeA).isNotNull(); + assertThat(typeA).extracting(MultipleRelationshipsThing::getName).isEqualTo("c1"); + assertThat(typeB).extracting(MultipleRelationshipsThing::getName).containsExactly("c1"); + assertThat(typeC).extracting(MultipleRelationshipsThing::getName).containsExactly("c1"); + }) + .verifyComplete(); + + try (Session session = driver.session()) { + + List names = session.run( + "MATCH (n:MultipleRelationshipsThing {name: 'p'}) - [r:TYPE_A|TYPE_B|TYPE_C] -> (o) RETURN r, o") + .list(record -> { + String type = record.get("r").asRelationship().type(); + String name = record.get("o").get("name").asString(); + return type + "_" + name; + }); + assertThat(names).containsExactlyInAnyOrder("TYPE_A_c1", "TYPE_B_c1", "TYPE_C_c1"); + } + } + + /** + * This stores the same instance in different relationships + * + * @param repository The repository to use. + */ + @Test + void shouldSaveMultipleRelationshipsOfSameInstanceWithBackReference( + @Autowired MultipleRelationshipsThingRepository repository) { + + MultipleRelationshipsThing p = new MultipleRelationshipsThing("p"); + MultipleRelationshipsThing c = new MultipleRelationshipsThing("c1"); + p.setTypeA(c); + p.setTypeB(Collections.singletonList(c)); + p.setTypeC(Collections.singletonList(c)); + + c.setTypeA(p); + + repository.save(p) + .map(MultipleRelationshipsThing::getId) + .flatMap(repository::findById) + .as(StepVerifier::create) + .assertNext(loadedThing -> { + + MultipleRelationshipsThing typeA = loadedThing.getTypeA(); + List typeB = loadedThing.getTypeB(); + List typeC = loadedThing.getTypeC(); + + assertThat(typeA).isNotNull(); + assertThat(typeA).extracting(MultipleRelationshipsThing::getName).isEqualTo("c1"); + assertThat(typeB).extracting(MultipleRelationshipsThing::getName).containsExactly("c1"); + assertThat(typeC).extracting(MultipleRelationshipsThing::getName).containsExactly("c1"); + }) + .verifyComplete(); + + try (Session session = driver.session()) { + + Function withMapper = record -> { + String type = record.get("r").asRelationship().type(); + String name = record.get("o").get("name").asString(); + return type + "_" + name; + }; + + String query = "MATCH (n:MultipleRelationshipsThing {name: $name}) - [r:TYPE_A|TYPE_B|TYPE_C] -> (o) RETURN r, o"; + List names = session.run(query, Collections.singletonMap("name", "p")).list(withMapper); + assertThat(names).containsExactlyInAnyOrder("TYPE_A_c1", "TYPE_B_c1", "TYPE_C_c1"); + + names = session.run(query, Collections.singletonMap("name", "c1")).list(withMapper); + assertThat(names).containsExactlyInAnyOrder("TYPE_A_p"); + } + } + + interface MultipleRelationshipsThingRepository extends ReactiveCrudRepository { + } + + @Configuration + @EnableTransactionManagement + @EnableReactiveNeo4jRepositories(considerNestedRepositories = true) + static class Config extends AbstractReactiveNeo4jConfig { + + @Bean + public Driver driver() { + return neo4jConnectionSupport.getDriver(); + } + } +} diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/MultipleRelationshipsThing.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/MultipleRelationshipsThing.java new file mode 100644 index 000000000..4271c558d --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/MultipleRelationshipsThing.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2019-2020 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.neo4j.springframework.data.integration.shared; + +import java.util.List; + +import org.neo4j.springframework.data.core.schema.GeneratedValue; +import org.neo4j.springframework.data.core.schema.Id; +import org.neo4j.springframework.data.core.schema.Node; + +/** + * This thing has several relationships to other things of the same kind but with a different type. + * It is used to test whether all types are stored correctly even if those relationships point to + * the same instance of the thing. + * + * @author Michael J. Simons + */ +@Node +public class MultipleRelationshipsThing { + + @Id @GeneratedValue Long id; + + private String name; + + private MultipleRelationshipsThing typeA; + + private List typeB; + + private List typeC; + + public MultipleRelationshipsThing(String name) { + this.name = name; + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public MultipleRelationshipsThing getTypeA() { + return typeA; + } + + public void setTypeA(MultipleRelationshipsThing typeA) { + this.typeA = typeA; + } + + public List getTypeB() { + return typeB; + } + + public void setTypeB(List typeB) { + this.typeB = typeB; + } + + public List getTypeC() { + return typeC; + } + + public void setTypeC(List typeC) { + this.typeC = typeC; + } +} diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/RelationshipsITBase.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/RelationshipsITBase.java new file mode 100644 index 000000000..eab5affef --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/integration/shared/RelationshipsITBase.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2019-2020 "Neo4j," + * Neo4j Sweden AB [https://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.neo4j.springframework.data.integration.shared; + +import org.junit.jupiter.api.BeforeEach; +import org.neo4j.driver.Driver; +import org.neo4j.driver.Session; +import org.neo4j.driver.Transaction; +import org.neo4j.springframework.data.test.Neo4jExtension; +import org.neo4j.springframework.data.test.Neo4jIntegrationTest; + +/** + * @author Michael J. Simons + */ +@Neo4jIntegrationTest +public abstract class RelationshipsITBase { + + protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; + + protected final Driver driver; + + protected RelationshipsITBase(Driver driver) { + this.driver = driver; + } + + @BeforeEach + void setup() { + try ( + Session session = driver.session(); + Transaction transaction = session.beginTransaction() + ) { + transaction.run("MATCH (n) detach delete n").consume(); + transaction.commit(); + } + } +} diff --git a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/test/Neo4jExtension.java b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/test/Neo4jExtension.java index e3fc01566..3075fc3bd 100644 --- a/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/test/Neo4jExtension.java +++ b/spring-data-neo4j/src/test/java/org/neo4j/springframework/data/test/Neo4jExtension.java @@ -258,7 +258,7 @@ public class Neo4jExtension implements BeforeAllCallback, BeforeEachCallback { private final String repository = Optional.ofNullable(System.getenv(SYS_PROPERTY_NEO4J_REPOSITORY)).orElse("neo4j"); - private final String imageVersion = Optional.ofNullable(System.getenv(SYS_PROPERTY_NEO4J_VERSION)).orElse("3.5.12"); + private final String imageVersion = Optional.ofNullable(System.getenv(SYS_PROPERTY_NEO4J_VERSION)).orElse("4.0"); private final Neo4jContainer neo4jContainer = new Neo4jContainer<>(repository + ":" + imageVersion) .withoutAuthentication()