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.
This commit is contained in:
Michael Simons
2020-06-23 14:56:25 +02:00
committed by GitHub
parent 4a7afb85dd
commit 40f7fc01a0
9 changed files with 745 additions and 72 deletions

View File

@@ -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<Neo4jPersistentProperty>) handler -> {
neo4jPersistentEntity.doWithAssociations((AssociationHandler<Neo4jPersistentProperty>) 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);
}
}
});
}

View File

@@ -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();
}

View File

@@ -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<Void> processRelations(Neo4jPersistentEntity<?> neo4jPersistentEntity, Object parentObject, @Nullable String inDatabase) {
return processNestedRelations(neo4jPersistentEntity, parentObject, inDatabase, new NestedRelationshipProcessState());
return processNestedRelations(neo4jPersistentEntity, parentObject, inDatabase, new NestedRelationshipProcessingStateMachine());
}
private Mono<Void> 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<Mono<Void>> relationshipCreationMonos = new ArrayList<>();
neo4jPersistentEntity.doWithAssociations((AssociationHandler<Neo4jPersistentProperty>) handler -> {
neo4jPersistentEntity.doWithAssociations((AssociationHandler<Neo4jPersistentProperty>) 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<Object> valueToBeSavedMono = eventSupport.maybeCallBeforeBind(valueToBeSavedPreEvt);
Object valueToBeSavedPreEvt = relationshipContext
.identifyAndExtractRelationshipValue(relatedValueToStore);
relationshipCreationMonos.add(
valueToBeSavedMono
.flatMap(valueToBeSaved ->
saveRelatedNode(valueToBeSaved, relationshipContext.getAssociationTargetType(),
targetNodeDescription, inDatabase)
.flatMap(relatedInternalId -> {
Mono<Void> 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<ResultSummary> 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<ResultSummary> 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);
}
});

View File

@@ -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<MultipleRelationshipsThing> loadedThing = repository.findById(p.getId());
assertThat(loadedThing).isPresent()
.map(MultipleRelationshipsThing::getTypeA)
.map(MultipleRelationshipsThing::getName)
.hasValue("c");
try (Session session = driver.session()) {
List<String> 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<MultipleRelationshipsThing> 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<String> 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<MultipleRelationshipsThing> loadedThing = repository.findById(p.getId());
assertThat(loadedThing).isPresent()
.hasValueSatisfying(t -> {
MultipleRelationshipsThing typeA = t.getTypeA();
List<MultipleRelationshipsThing> typeB = t.getTypeB();
List<MultipleRelationshipsThing> 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<String> 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<MultipleRelationshipsThing> loadedThing = repository.findById(p.getId());
assertThat(loadedThing).isPresent()
.hasValueSatisfying(t -> {
MultipleRelationshipsThing typeA = t.getTypeA();
List<MultipleRelationshipsThing> typeB = t.getTypeB();
List<MultipleRelationshipsThing> 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<String> 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<MultipleRelationshipsThing> loadedThing = repository.findById(p.getId());
assertThat(loadedThing).isPresent()
.hasValueSatisfying(t -> {
MultipleRelationshipsThing typeA = t.getTypeA();
List<MultipleRelationshipsThing> typeB = t.getTypeB();
List<MultipleRelationshipsThing> 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<Record, String> 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<String> 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<MultipleRelationshipsThing, Long> {
}
@Configuration
@EnableTransactionManagement
@EnableNeo4jRepositories(considerNestedRepositories = true)
static class Config extends AbstractNeo4jConfig {
@Bean
public Driver driver() {
return neo4jConnectionSupport.getDriver();
}
}
}

View File

@@ -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

View File

@@ -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<String> 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<String> 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<MultipleRelationshipsThing> typeB = loadedThing.getTypeB();
List<MultipleRelationshipsThing> 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<String> 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<MultipleRelationshipsThing> typeB = loadedThing.getTypeB();
List<MultipleRelationshipsThing> 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<String> 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<MultipleRelationshipsThing> typeB = loadedThing.getTypeB();
List<MultipleRelationshipsThing> 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<Record, String> 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<String> 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<MultipleRelationshipsThing, Long> {
}
@Configuration
@EnableTransactionManagement
@EnableReactiveNeo4jRepositories(considerNestedRepositories = true)
static class Config extends AbstractReactiveNeo4jConfig {
@Bean
public Driver driver() {
return neo4jConnectionSupport.getDriver();
}
}
}

View File

@@ -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<MultipleRelationshipsThing> typeB;
private List<MultipleRelationshipsThing> 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<MultipleRelationshipsThing> getTypeB() {
return typeB;
}
public void setTypeB(List<MultipleRelationshipsThing> typeB) {
this.typeB = typeB;
}
public List<MultipleRelationshipsThing> getTypeC() {
return typeC;
}
public void setTypeC(List<MultipleRelationshipsThing> typeC) {
this.typeC = typeC;
}
}

View File

@@ -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();
}
}
}

View File

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