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