/* * 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 java.util.Collections.*; import static java.util.stream.Collectors.*; import static org.assertj.core.api.Assertions.*; import static org.neo4j.driver.Values.*; import static org.neo4j.springframework.data.test.Neo4jExtension.*; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import java.time.LocalDate; import java.util.*; import java.util.stream.IntStream; import org.assertj.core.data.MapEntry; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.neo4j.driver.Driver; import org.neo4j.driver.Record; import org.neo4j.driver.Session; import org.neo4j.driver.SessionConfig; import org.neo4j.driver.Transaction; import org.neo4j.driver.Value; import org.neo4j.driver.Values; import org.neo4j.driver.reactive.RxSession; import org.neo4j.driver.types.Node; import org.neo4j.driver.types.Point; import org.neo4j.driver.types.Relationship; import org.neo4j.springframework.data.config.AbstractReactiveNeo4jConfig; import org.neo4j.springframework.data.core.DatabaseSelection; import org.neo4j.springframework.data.core.ReactiveDatabaseSelectionProvider; import org.neo4j.springframework.data.core.convert.Neo4jConversions; import org.neo4j.springframework.data.integration.reactive.repositories.ReactivePersonRepository; import org.neo4j.springframework.data.integration.reactive.repositories.ReactiveThingRepository; import org.neo4j.springframework.data.integration.shared.*; import org.neo4j.springframework.data.repository.ReactiveNeo4jRepository; import org.neo4j.springframework.data.repository.config.EnableReactiveNeo4jRepositories; import org.neo4j.springframework.data.repository.query.Query; import org.neo4j.springframework.data.test.Neo4jExtension; import org.neo4j.springframework.data.types.CartesianPoint2d; import org.neo4j.springframework.data.types.GeographicPoint2d; import org.reactivestreams.Publisher; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.convert.ConverterNotFoundException; import org.springframework.core.convert.converter.GenericConverter; import org.springframework.data.domain.Example; import org.springframework.data.domain.ExampleMatcher; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; import org.springframework.data.repository.query.Param; import org.springframework.data.repository.reactive.ReactiveCrudRepository; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import org.springframework.transaction.ReactiveTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.reactive.TransactionalOperator; /** * @author Gerrit Meier * @author Michael J. Simons * @author Philipp Tölle */ @ExtendWith(Neo4jExtension.class) @SpringJUnitConfig @DirtiesContext @Tag(NEEDS_REACTIVE_SUPPORT) class ReactiveRepositoryIT { protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; protected static DatabaseSelection databaseSelection = DatabaseSelection.undecided(); private static final String TEST_PERSON1_NAME = "Test"; private static final String TEST_PERSON2_NAME = "Test2"; private static final String TEST_PERSON1_FIRST_NAME = "Ernie"; private static final String TEST_PERSON2_FIRST_NAME = "Bert"; private static final LocalDate TEST_PERSON1_BORN_ON = LocalDate.of(2019, 1, 1); private static final LocalDate TEST_PERSON2_BORN_ON = LocalDate.of(2019, 2, 1); private static final String TEST_PERSON_SAMEVALUE = "SameValue"; private static final Point NEO4J_HQ = Values.point(4326, 12.994823, 55.612191).asPoint(); private static final Point SFO = Values.point(4326, -122.38681, 37.61649).asPoint(); private static final long NOT_EXISTING_NODE_ID = 3123131231L; static PersonWithAllConstructor personExample(String sameValue) { return new PersonWithAllConstructor(null, null, null, sameValue, null, null, null, null, null, null, null); } private long id1; private long id2; private PersonWithAllConstructor person1; private PersonWithAllConstructor person2; ReactiveRepositoryIT() { databaseSelection = DatabaseSelection.undecided(); } @Nested class Find extends ReactiveIntegrationTestBase { @Override void setupData(Transaction transaction) { transaction.run("MATCH (n) detach delete n"); id1 = transaction.run("" + "CREATE (n:PersonWithAllConstructor) " + " SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName, n.cool = $cool, n.personNumber = $personNumber, n.bornOn = $bornOn, n.nullable = 'something', n.things = ['a', 'b'], n.place = $place " + "RETURN id(n)", parameters("name", TEST_PERSON1_NAME, "sameValue", TEST_PERSON_SAMEVALUE, "firstName", TEST_PERSON1_FIRST_NAME, "cool", true, "personNumber", 1, "bornOn", TEST_PERSON1_BORN_ON, "place", NEO4J_HQ)) .next().get(0).asLong(); id2 = transaction.run( "CREATE (n:PersonWithAllConstructor) SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName, n.cool = $cool, n.personNumber = $personNumber, n.bornOn = $bornOn, n.things = [], n.place = $place return id(n)", parameters("name", TEST_PERSON2_NAME, "sameValue", TEST_PERSON_SAMEVALUE, "firstName", TEST_PERSON2_FIRST_NAME, "cool", false, "personNumber", 2, "bornOn", TEST_PERSON2_BORN_ON, "place", SFO)) .next().get(0).asLong(); transaction .run("CREATE (a:Thing {theId: 'anId', name: 'Homer'})-[:Has]->(b:Thing2{theId: 4711, name: 'Bart'})"); IntStream.rangeClosed(1, 20).forEach(i -> transaction.run("CREATE (a:Thing {theId: 'id' + $i, name: 'name' + $i})", parameters("i", String.format("%02d", i)))); person1 = new PersonWithAllConstructor(id1, TEST_PERSON1_NAME, TEST_PERSON1_FIRST_NAME, TEST_PERSON_SAMEVALUE, true, 1L, TEST_PERSON1_BORN_ON, "something", Arrays.asList("a", "b"), NEO4J_HQ, null); person2 = new PersonWithAllConstructor(id2, TEST_PERSON2_NAME, TEST_PERSON2_FIRST_NAME, TEST_PERSON_SAMEVALUE, false, 2L, TEST_PERSON2_BORN_ON, null, Collections.emptyList(), SFO, null); } @Test void findAll(@Autowired ReactivePersonRepository repository) { List<PersonWithAllConstructor> personList = Arrays.asList(person1, person2); StepVerifier.create(repository.findAll()).expectNextMatches(personList::contains) .expectNextMatches(personList::contains).verifyComplete(); } @Test void findById(@Autowired ReactivePersonRepository repository) { StepVerifier.create(repository.findById(id1)).expectNext(person1).verifyComplete(); } @Test void findWithPageable(@Autowired ReactivePersonRepository repository) { Sort sort = Sort.by("name"); int page = 0; int limit = 1; StepVerifier.create(repository.findByNameStartingWith("Test", PageRequest.of(page, limit, sort))) .assertNext(person -> assertThat(person).isEqualTo(person1)) .verifyComplete(); sort = Sort.by("name"); page = 1; limit = 1; StepVerifier.create(repository.findByNameStartingWith("Test", PageRequest.of(page, limit, sort))) .assertNext(person -> assertThat(person).isEqualTo(person2)) .verifyComplete(); } @Test void findAllByIds(@Autowired ReactivePersonRepository repository) { List<PersonWithAllConstructor> personList = Arrays.asList(person1, person2); StepVerifier.create(repository.findAllById(Arrays.asList(id1, id2))).expectNextMatches(personList::contains) .expectNextMatches(personList::contains).verifyComplete(); } @Test void findAllByIdsPublisher(@Autowired ReactivePersonRepository repository) { List<PersonWithAllConstructor> personList = Arrays.asList(person1, person2); StepVerifier.create(repository.findAllById(Flux.just(id1, id2))).expectNextMatches(personList::contains) .expectNextMatches(personList::contains).verifyComplete(); } @Test void findByIdNoMatch(@Autowired ReactivePersonRepository repository) { StepVerifier.create(repository.findById(NOT_EXISTING_NODE_ID)).verifyComplete(); } @Test void findByIdPublisher(@Autowired ReactivePersonRepository repository) { StepVerifier.create(repository.findById(Mono.just(id1))).expectNext(person1).verifyComplete(); } @Test void findByIdPublisherNoMatch(@Autowired ReactivePersonRepository repository) { StepVerifier.create(repository.findById(Mono.just(NOT_EXISTING_NODE_ID))).verifyComplete(); } @Test void findAllWithSortByOrderDefault(@Autowired ReactivePersonRepository repository) { StepVerifier.create(repository.findAll(Sort.by("name"))).expectNext(person1, person2).verifyComplete(); } @Test void findAllWithSortByOrderAsc(@Autowired ReactivePersonRepository repository) { StepVerifier.create(repository.findAll(Sort.by(Sort.Order.asc("name")))).expectNext(person1, person2) .verifyComplete(); } @Test void findAllWithSortByOrderDesc(@Autowired ReactivePersonRepository repository) { StepVerifier.create(repository.findAll(Sort.by(Sort.Order.desc("name")))).expectNext(person2, person1) .verifyComplete(); } @Test void findOneByExample(@Autowired ReactivePersonRepository repository) { Example<PersonWithAllConstructor> example = Example.of(person1, ExampleMatcher.matchingAll().withIgnoreNullValues()); StepVerifier.create(repository.findOne(example)).expectNext(person1).verifyComplete(); } @Test void findAllByExample(@Autowired ReactivePersonRepository repository) { Example<PersonWithAllConstructor> example = Example.of(person1, ExampleMatcher.matchingAll().withIgnoreNullValues()); StepVerifier.create(repository.findAll(example)).expectNext(person1).verifyComplete(); } @Test void findAllByExampleWithDifferentMatchers(@Autowired ReactivePersonRepository repository) { PersonWithAllConstructor person; Example<PersonWithAllConstructor> example; person = new PersonWithAllConstructor(null, TEST_PERSON1_NAME, TEST_PERSON2_FIRST_NAME, null, null, null, null, null, null, null, null); example = Example.of(person, ExampleMatcher.matchingAny()); StepVerifier.create(repository.findAll(example)) .recordWith(ArrayList::new) .expectNextCount(2) .expectRecordedMatches(recordedPersons -> recordedPersons.containsAll(Arrays.asList(person1, person2))) .verifyComplete(); person = new PersonWithAllConstructor(null, TEST_PERSON1_NAME.toUpperCase(), TEST_PERSON2_FIRST_NAME, null, null, null, null, null, null, null, null); example = Example.of(person, ExampleMatcher.matchingAny().withIgnoreCase("name")); StepVerifier.create(repository.findAll(example)) .recordWith(ArrayList::new) .expectNextCount(2) .expectRecordedMatches(recordedPersons -> recordedPersons.containsAll(Arrays.asList(person1, person2))) .verifyComplete(); person = new PersonWithAllConstructor(null, TEST_PERSON2_NAME.substring(TEST_PERSON2_NAME.length() - 2).toUpperCase(), TEST_PERSON2_FIRST_NAME.substring(0, 2), TEST_PERSON_SAMEVALUE.substring(3, 5), null, null, null, null, null, null, null); example = Example.of(person, ExampleMatcher.matchingAll() .withMatcher("name", ExampleMatcher.GenericPropertyMatcher.of(ExampleMatcher.StringMatcher.ENDING, true)) .withMatcher("firstName", ExampleMatcher.GenericPropertyMatcher.of(ExampleMatcher.StringMatcher.STARTING)) .withMatcher("sameValue", ExampleMatcher.GenericPropertyMatcher.of(ExampleMatcher.StringMatcher.CONTAINING))); StepVerifier.create(repository.findAll(example)).expectNext(person2).verifyComplete(); person = new PersonWithAllConstructor(null, null, "(?i)ern.*", null, null, null, null, null, null, null, null); example = Example.of(person, ExampleMatcher.matchingAll().withStringMatcher(ExampleMatcher.StringMatcher.REGEX)); StepVerifier.create(repository.findAll(example)).expectNext(person1).verifyComplete(); example = Example.of(person, ExampleMatcher.matchingAll().withStringMatcher(ExampleMatcher.StringMatcher.REGEX).withIncludeNullValues()); StepVerifier.create(repository.findAll(example)).verifyComplete(); } @Test void findAllByExampleWithSort(@Autowired ReactivePersonRepository repository) { Example<PersonWithAllConstructor> example = Example.of(personExample(TEST_PERSON_SAMEVALUE)); StepVerifier.create(repository.findAll(example, Sort.by(Sort.Direction.DESC, "name"))).expectNext(person2, person1) .verifyComplete(); } @Test void existsById(@Autowired ReactivePersonRepository repository) { StepVerifier.create(repository.existsById(id1)).expectNext(true).verifyComplete(); } @Test void existsByIdNoMatch(@Autowired ReactivePersonRepository repository) { StepVerifier.create(repository.existsById(NOT_EXISTING_NODE_ID)).expectNext(false).verifyComplete(); } @Test void existsByIdPublisher(@Autowired ReactivePersonRepository repository) { StepVerifier.create(repository.existsById(id1)).expectNext(true).verifyComplete(); } @Test void existsByIdPublisherNoMatch(@Autowired ReactivePersonRepository repository) { StepVerifier.create(repository.existsById(NOT_EXISTING_NODE_ID)).expectNext(false).verifyComplete(); } @Test void existsByExample(@Autowired ReactivePersonRepository repository) { Example<PersonWithAllConstructor> example = Example.of(personExample(TEST_PERSON_SAMEVALUE)); StepVerifier.create(repository.exists(example)).expectNext(true).verifyComplete(); } @Test void count(@Autowired ReactivePersonRepository repository) { StepVerifier.create(repository.count()).expectNext(2L).verifyComplete(); } @Test void countByExample(@Autowired ReactivePersonRepository repository) { Example<PersonWithAllConstructor> example = Example.of(person1); StepVerifier.create(repository.count(example)).expectNext(1L).verifyComplete(); } @Test void callCustomCypher(@Autowired ReactivePersonRepository repository) { StepVerifier.create(repository.customQuery()).expectNext(1L).verifyComplete(); } @Test void loadAllPersonsWithAllConstructor(@Autowired ReactivePersonRepository repository) { List<PersonWithAllConstructor> personList = Arrays.asList(person1, person2); StepVerifier.create(repository.getAllPersonsViaQuery()).expectNextMatches(personList::contains) .expectNextMatches(personList::contains).verifyComplete(); } @Test void loadOnePersonWithAllConstructor(@Autowired ReactivePersonRepository repository) { StepVerifier.create(repository.getOnePersonViaQuery()).expectNext(person1).verifyComplete(); } @Test void findBySimplePropertiesAnded(@Autowired ReactivePersonRepository repository) { StepVerifier.create(repository.findOneByNameAndFirstName(TEST_PERSON1_NAME, TEST_PERSON1_FIRST_NAME)) .expectNext(person1).verifyComplete(); StepVerifier.create(repository.findOneByNameAndFirstNameAllIgnoreCase(TEST_PERSON1_NAME.toUpperCase(), TEST_PERSON1_FIRST_NAME.toUpperCase())).expectNext(person1).verifyComplete(); } @Test void findBySimplePropertiesOred(@Autowired ReactivePersonRepository repository) { repository.findAllByNameOrName(TEST_PERSON1_NAME, TEST_PERSON2_NAME) .as(StepVerifier::create) .recordWith(ArrayList::new) .expectNextCount(2) .expectRecordedMatches(recordedPersons -> recordedPersons.containsAll(Arrays.asList(person1, person2))) .verifyComplete(); } @Test // GH-112 void countBySimplePropertiesOred(@Autowired ReactivePersonRepository repository) { repository.countAllByNameOrName(TEST_PERSON1_NAME, TEST_PERSON2_NAME) .as(StepVerifier::create).expectNext(2L).verifyComplete(); } @Test void findBySimpleProperty(@Autowired ReactivePersonRepository repository) { List<PersonWithAllConstructor> personList = Arrays.asList(person1, person2); StepVerifier.create(repository.findAllBySameValue(TEST_PERSON_SAMEVALUE)).expectNextMatches(personList::contains) .expectNextMatches(personList::contains).verifyComplete(); } @Test void findByPropertyThatNeedsConversion(@Autowired ReactivePersonRepository repository) { StepVerifier.create(repository.findAllByPlace(new GeographicPoint2d(NEO4J_HQ.y(), NEO4J_HQ.x()))) .expectNextCount(1) .verifyComplete(); } @Test void findByPropertyFailsIfNoConverterIsAvailable(@Autowired ReactivePersonRepository repository) { assertThatExceptionOfType(ConverterNotFoundException.class) .isThrownBy(() -> repository.findAllByPlace(new ThingWithGeneratedId("hello"))) .withMessageStartingWith("No converter found capable of converting from type"); } @Test void findByAssignedId(@Autowired ReactiveThingRepository repository) { StepVerifier.create(repository.findById("anId")) .assertNext(thing -> { assertThat(thing.getTheId()).isEqualTo("anId"); assertThat(thing.getName()).isEqualTo("Homer"); AnotherThingWithAssignedId anotherThing = new AnotherThingWithAssignedId(4711L); anotherThing.setName("Bart"); assertThat(thing.getThings()).containsExactlyInAnyOrder(anotherThing); }) .verifyComplete(); } @Test void loadWithAssignedIdViaQuery(@Autowired ReactiveThingRepository repository) { StepVerifier.create(repository.getViaQuery()) .assertNext(thing -> { assertThat(thing.getTheId()).isEqualTo("anId"); assertThat(thing.getName()).isEqualTo("Homer"); AnotherThingWithAssignedId anotherThing = new AnotherThingWithAssignedId(4711L); anotherThing.setName("Bart"); assertThat(thing.getThings()).containsExactly(anotherThing); }) .verifyComplete(); } @Test void findByConvertedId(@Autowired EntityWithConvertedIdRepository repository) { try (Session session = createSession()) { session.run("CREATE (:EntityWithConvertedId{identifyingEnum:'A'})"); } StepVerifier.create(repository.findById(EntityWithConvertedId.IdentifyingEnum.A)) .assertNext(entity -> { assertThat(entity).isNotNull(); assertThat(entity.getIdentifyingEnum()).isEqualTo(EntityWithConvertedId.IdentifyingEnum.A); }) .verifyComplete(); } @Test void findAllByConvertedId(@Autowired EntityWithConvertedIdRepository repository) { try (Session session = createSession()) { session.run("CREATE (:EntityWithConvertedId{identifyingEnum:'A'})"); } StepVerifier.create(repository.findAllById(singleton(EntityWithConvertedId.IdentifyingEnum.A))) .assertNext( entity -> assertThat(entity.getIdentifyingEnum()).isEqualTo(EntityWithConvertedId.IdentifyingEnum.A) ) .verifyComplete(); } } @Nested class FindWithRelationships extends ReactiveIntegrationTestBase { @Override void setupData(Transaction transaction) { transaction.run("MATCH (n) detach delete n"); id1 = transaction.run("" + "CREATE (n:PersonWithAllConstructor) " + " SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName, n.cool = $cool, n.personNumber = $personNumber, n.bornOn = $bornOn, n.nullable = 'something', n.things = ['a', 'b'], n.place = $place " + "RETURN id(n)", parameters("name", TEST_PERSON1_NAME, "sameValue", TEST_PERSON_SAMEVALUE, "firstName", TEST_PERSON1_FIRST_NAME, "cool", true, "personNumber", 1, "bornOn", TEST_PERSON1_BORN_ON, "place", NEO4J_HQ)) .next().get(0).asLong(); id2 = transaction.run( "CREATE (n:PersonWithAllConstructor) SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName, n.cool = $cool, n.personNumber = $personNumber, n.bornOn = $bornOn, n.things = [], n.place = $place return id(n)", parameters("name", TEST_PERSON2_NAME, "sameValue", TEST_PERSON_SAMEVALUE, "firstName", TEST_PERSON2_FIRST_NAME, "cool", false, "personNumber", 2, "bornOn", TEST_PERSON2_BORN_ON, "place", SFO)) .next().get(0).asLong(); transaction .run("CREATE (a:Thing {theId: 'anId', name: 'Homer'})-[:Has]->(b:Thing2{theId: 4711, name: 'Bart'})"); IntStream.rangeClosed(1, 20).forEach(i -> transaction.run("CREATE (a:Thing {theId: 'id' + $i, name: 'name' + $i})", parameters("i", String.format("%02d", i)))); person1 = new PersonWithAllConstructor(id1, TEST_PERSON1_NAME, TEST_PERSON1_FIRST_NAME, TEST_PERSON_SAMEVALUE, true, 1L, TEST_PERSON1_BORN_ON, "something", Arrays.asList("a", "b"), NEO4J_HQ, null); person2 = new PersonWithAllConstructor(id2, TEST_PERSON2_NAME, TEST_PERSON2_FIRST_NAME, TEST_PERSON_SAMEVALUE, false, 2L, TEST_PERSON2_BORN_ON, null, Collections.emptyList(), SFO, null); } @Test void loadEntityWithRelationship(@Autowired ReactiveRelationshipRepository repository) { long personId; long clubId; long hobbyNode1Id; long hobbyNode2Id; long petNode1Id; long petNode2Id; try (Session session = createSession()) { Record record = session .run("CREATE (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(h1:Hobby{name:'Music'}), " + "(n)-[:Has]->(p1:Pet{name: 'Jerry'}), (n)-[:Has]->(p2:Pet{name: 'Tom'}), " + "(n)<-[:Has]-(c:Club{name:'ClownsClub'}), " + "(p1)-[:Has]->(h2:Hobby{name:'sleeping'}), " + "(p1)-[:Has]->(p2)" + "RETURN n, h1, h2, p1, p2, c").single(); Node personNode = record.get("n").asNode(); Node clubNode = record.get("c").asNode(); Node hobbyNode1 = record.get("h1").asNode(); Node hobbyNode2 = record.get("h2").asNode(); Node petNode1 = record.get("p1").asNode(); Node petNode2 = record.get("p2").asNode(); personId = personNode.id(); clubId = clubNode.id(); hobbyNode1Id = hobbyNode1.id(); hobbyNode2Id = hobbyNode2.id(); petNode1Id = petNode1.id(); petNode2Id = petNode2.id(); } StepVerifier.create(repository.findById(personId)) .assertNext(loadedPerson -> { assertThat(loadedPerson.getName()).isEqualTo("Freddie"); Hobby hobby = loadedPerson.getHobbies(); assertThat(hobby).isNotNull(); assertThat(hobby.getId()).isEqualTo(hobbyNode1Id); assertThat(hobby.getName()).isEqualTo("Music"); Club club = loadedPerson.getClub(); assertThat(club).isNotNull(); assertThat(club.getId()).isEqualTo(clubId); assertThat(club.getName()).isEqualTo("ClownsClub"); List<Pet> pets = loadedPerson.getPets(); Pet comparisonPet1 = new Pet(petNode1Id, "Jerry"); Pet comparisonPet2 = new Pet(petNode2Id, "Tom"); assertThat(pets).containsExactlyInAnyOrder(comparisonPet1, comparisonPet2); Pet pet1 = pets.get(pets.indexOf(comparisonPet1)); Pet pet2 = pets.get(pets.indexOf(comparisonPet2)); Hobby petHobby = pet1.getHobbies().iterator().next(); assertThat(petHobby.getId()).isEqualTo(hobbyNode2Id); assertThat(petHobby.getName()).isEqualTo("sleeping"); assertThat(pet1.getFriends()).containsExactly(pet2); }) .verifyComplete(); } @Test void loadEntityWithRelationshipToTheSameNode(@Autowired ReactiveRelationshipRepository repository) { long personId; long hobbyNode1Id; long petNode1Id; try (Session session = createSession()) { Record record = session .run("CREATE (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(h1:Hobby{name:'Music'}), " + "(n)-[:Has]->(p1:Pet{name: 'Jerry'}), " + "(p1)-[:Has]->(h1)" + "RETURN n, h1, p1").single(); Node personNode = record.get("n").asNode(); Node hobbyNode1 = record.get("h1").asNode(); Node petNode1 = record.get("p1").asNode(); personId = personNode.id(); hobbyNode1Id = hobbyNode1.id(); petNode1Id = petNode1.id(); } StepVerifier.create(repository.findById(personId)) .assertNext(loadedPerson -> { assertThat(loadedPerson.getName()).isEqualTo("Freddie"); Hobby hobby = loadedPerson.getHobbies(); assertThat(hobby).isNotNull(); assertThat(hobby.getId()).isEqualTo(hobbyNode1Id); assertThat(hobby.getName()).isEqualTo("Music"); List<Pet> pets = loadedPerson.getPets(); Pet comparisonPet1 = new Pet(petNode1Id, "Jerry"); assertThat(pets).containsExactlyInAnyOrder(comparisonPet1); Pet pet1 = pets.get(pets.indexOf(comparisonPet1)); Hobby petHobby = pet1.getHobbies().iterator().next(); assertThat(petHobby.getName()).isEqualTo("Music"); assertThat(petHobby).isSameAs(hobby); }) .verifyComplete(); } @Test void loadDeepRelationships(@Autowired ReactiveDeepRelationshipRepository repository) { long type1Id; try (Session session = createSession()) { Record record = session .run("CREATE " + "(t1:Type1)-[:NEXT_TYPE]->(t2:Type2)-[:NEXT_TYPE]->(:Type3)-[:NEXT_TYPE]->(t4:Type4)-" + "[:NEXT_TYPE]->(:Type5)-[:NEXT_TYPE]->(:Type6)-[:NEXT_TYPE]->(:Type7), " + "(t2)-[:SAME_TYPE]->" + "(:Type2)-[:SAME_TYPE]->(:Type2)-[:SAME_TYPE]->(:Type2)-[:SAME_TYPE]->" + "(:Type2)-[:SAME_TYPE]->(:Type2)-[:SAME_TYPE]->(:Type2)-[:SAME_TYPE]->" + "(:Type2) " + "RETURN t1").single(); type1Id = record.get("t1").asNode().id(); } StepVerifier.create(repository.findById(type1Id)) .assertNext(type1 -> { // ensures that the virtual limit for same relationships does not affect distinct relationships assertThat(type1.nextType.nextType.nextType.nextType.nextType.nextType).isNotNull(); // assert that same type relationships not cause stack overflow DeepRelationships.Type2 type2 = type1.nextType; assertThat(type2.sameType.sameType.sameType).isNotNull(); assertThat(type2.sameType.sameType.sameType.sameType).isNull(); }) .verifyComplete(); } @Test void loadLoopingDeepRelationships(@Autowired ReactiveLoopingRelationshipRepository loopingRelationshipRepository) { long type1Id; try (Session session = createSession()) { Record record = session .run("CREATE " + "(t1:LoopingType1)-[:NEXT_TYPE]->(:LoopingType2)-[:NEXT_TYPE]->(:LoopingType3)-[:NEXT_TYPE]->" + "(:LoopingType1)-[:NEXT_TYPE]->(:LoopingType2)-[:NEXT_TYPE]->(:LoopingType3)-[:NEXT_TYPE]->" + "(:LoopingType1)-[:NEXT_TYPE]->(:LoopingType2)-[:NEXT_TYPE]->(:LoopingType3)-[:NEXT_TYPE]->" + "(:LoopingType1)-[:NEXT_TYPE]->(:LoopingType2)-[:NEXT_TYPE]->(:LoopingType3)-[:NEXT_TYPE]->" + "(:LoopingType1)-[:NEXT_TYPE]->(:LoopingType2)-[:NEXT_TYPE]->(:LoopingType3)-[:NEXT_TYPE]->" + "(:LoopingType1)-[:NEXT_TYPE]->(:LoopingType2)-[:NEXT_TYPE]->(:LoopingType3)-[:NEXT_TYPE]->" + "(:LoopingType1)-[:NEXT_TYPE]->(:LoopingType2)-[:NEXT_TYPE]->(:LoopingType3)-[:NEXT_TYPE]->" + "(:LoopingType1)-[:NEXT_TYPE]->(:LoopingType2)-[:NEXT_TYPE]->(:LoopingType3)-[:NEXT_TYPE]->" + "(:LoopingType1)-[:NEXT_TYPE]->(:LoopingType2)-[:NEXT_TYPE]->(:LoopingType3)-[:NEXT_TYPE]->" + "(:LoopingType1)-[:NEXT_TYPE]->(:LoopingType2)-[:NEXT_TYPE]->(:LoopingType3)-[:NEXT_TYPE]->" + "(:LoopingType1)" + "RETURN t1").single(); type1Id = record.get("t1").asNode().id(); } StepVerifier.create(loopingRelationshipRepository.findById(type1Id)) .assertNext(type1 -> { DeepRelationships.LoopingType1 iteration1 = type1.nextType.nextType.nextType; assertThat(iteration1).isNotNull(); DeepRelationships.LoopingType1 iteration2 = iteration1.nextType.nextType.nextType; assertThat(iteration2).isNotNull(); DeepRelationships.LoopingType1 iteration3 = iteration2.nextType.nextType.nextType; assertThat(iteration3.nextType).isNull(); }) .verifyComplete(); } @Test void loadEntityWithBidirectionalRelationship(@Autowired BidirectionalStartRepository repository) { long startId; try (Session session = createSession()) { Record record = session .run("CREATE (n:BidirectionalStart{name:'Ernie'})-[:CONNECTED]->(e:BidirectionalEnd{name:'Bert'}) " + "RETURN n").single(); Node startNode = record.get("n").asNode(); startId = startNode.id(); } StepVerifier.create(repository.findById(startId)) .assertNext(entity -> { assertThat(entity.getEnds()).hasSize(1); }) .verifyComplete(); } @Test void loadEntityWithBidirectionalRelationshipFromIncomingSide(@Autowired BidirectionalEndRepository repository) { long endId; try (Session session = createSession()) { Record record = session .run("CREATE (n:BidirectionalStart{name:'Ernie'})-[:CONNECTED]->(e:BidirectionalEnd{name:'Bert'}) " + "RETURN e").single(); Node endNode = record.get("e").asNode(); endId = endNode.id(); } StepVerifier.create(repository.findById(endId)) .assertNext(entity -> { assertThat(entity.getStart()).isNotNull(); }) .verifyComplete(); } @Test void loadMultipleEntitiesWithRelationship(@Autowired ReactiveRelationshipRepository repository) { long hobbyNode1Id; long hobbyNode2Id; long petNode1Id; long petNode2Id; try (Session session = createSession()) { Record record = session .run("CREATE (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(h:Hobby{name:'Music'}), " + "(n)-[:Has]->(p:Pet{name: 'Jerry'}) " + "RETURN n, h, p").single(); hobbyNode1Id = record.get("h").asNode().id(); petNode1Id = record.get("p").asNode().id(); record = session .run("CREATE (n:PersonWithRelationship{name:'SomeoneElse'})-[:Has]->(h:Hobby{name:'Music2'}), " + "(n)-[:Has]->(p:Pet{name: 'Jerry2'}) " + "RETURN n, h, p").single(); hobbyNode2Id = record.get("h").asNode().id(); petNode2Id = record.get("p").asNode().id(); } StepVerifier.create(repository.findAll()) .recordWith(ArrayList::new) .expectNextCount(2) .consumeRecordedWith(loadedPersons -> { Hobby hobby1 = new Hobby(); hobby1.setId(hobbyNode1Id); hobby1.setName("Music"); Hobby hobby2 = new Hobby(); hobby2.setId(hobbyNode2Id); hobby2.setName("Music2"); Pet pet1 = new Pet(petNode1Id, "Jerry"); Pet pet2 = new Pet(petNode2Id, "Jerry2"); assertThat(loadedPersons).extracting("name").containsExactlyInAnyOrder("Freddie", "SomeoneElse"); assertThat(loadedPersons).extracting("hobbies").containsExactlyInAnyOrder(hobby1, hobby2); assertThat(loadedPersons).flatExtracting("pets").containsExactlyInAnyOrder(pet1, pet2); }) .verifyComplete(); } @Test void loadEntityWithRelationshipViaQuery(@Autowired ReactiveRelationshipRepository repository) { long personId; long hobbyNodeId; long petNode1Id; long petNode2Id; try (Session session = createSession()) { Record record = session .run("CREATE (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(h1:Hobby{name:'Music'}), " + "(n)-[:Has]->(p1:Pet{name: 'Jerry'}), (n)-[:Has]->(p2:Pet{name: 'Tom'}) " + "RETURN n, h1, p1, p2").single(); Node personNode = record.get("n").asNode(); Node hobbyNode1 = record.get("h1").asNode(); Node petNode1 = record.get("p1").asNode(); Node petNode2 = record.get("p2").asNode(); personId = personNode.id(); hobbyNodeId = hobbyNode1.id(); petNode1Id = petNode1.id(); petNode2Id = petNode2.id(); } StepVerifier.create(repository.getPersonWithRelationshipsViaQuery()) .assertNext(loadedPerson -> { assertThat(loadedPerson.getName()).isEqualTo("Freddie"); assertThat(loadedPerson.getId()).isEqualTo(personId); Hobby hobby = loadedPerson.getHobbies(); assertThat(hobby).isNotNull(); assertThat(hobby.getId()).isEqualTo(hobbyNodeId); assertThat(hobby.getName()).isEqualTo("Music"); List<Pet> pets = loadedPerson.getPets(); Pet comparisonPet1 = new Pet(petNode1Id, "Jerry"); Pet comparisonPet2 = new Pet(petNode2Id, "Tom"); assertThat(pets).containsExactlyInAnyOrder(comparisonPet1, comparisonPet2); }) .verifyComplete(); } @Test void loadEntityWithRelationshipWithAssignedId(@Autowired ReactivePetRepository repository) { long petNodeId; try (Session session = createSession()) { Record record = session .run("CREATE (p:Pet{name:'Jerry'})-[:Has]->(t:Thing{theId:'t1', name:'Thing1'}) " + "RETURN p, t").single(); Node petNode = record.get("p").asNode(); petNodeId = petNode.id(); } StepVerifier.create(repository.findById(petNodeId)) .assertNext(pet -> { ThingWithAssignedId relatedThing = pet.getThings().get(0); assertThat(relatedThing.getTheId()).isEqualTo("t1"); assertThat(relatedThing.getName()).isEqualTo("Thing1"); }) .verifyComplete(); } @Test void findEntityWithSelfReferencesInBothDirections(@Autowired ReactivePetRepository repository) { long petId; try (Session session = createSession()) { petId = session.run("CREATE (luna:Pet{name:'Luna'})-[:Has]->(daphne:Pet{name:'Daphne'})" + "-[:Has]->(luna2:Pet{name:'Luna'})" + "RETURN id(luna) as id").single().get("id").asLong(); } StepVerifier.create(repository.findById(petId)) .assertNext(loadedPet -> { assertThat(loadedPet.getFriends().get(0).getName()).isEqualTo("Daphne"); assertThat(loadedPet.getFriends().get(0).getFriends().get(0).getName()).isEqualTo("Luna"); }) .verifyComplete(); } } @Nested class RelationshipProperties extends ReactiveIntegrationTestBase { @Test void loadEntityWithRelationshipWithProperties(@Autowired ReactivePersonWithRelationshipWithPropertiesRepository repository) { long personId; long hobbyNode1Id; long hobbyNode2Id; try (Session session = createSession()) { Record record = session .run("CREATE (n:PersonWithRelationshipWithProperties{name:'Freddie'})," + " (n)-[l1:LIKES" + "{since: 1995, active: true, localDate: date('1995-02-26'), myEnum: 'SOMETHING', point: point({x: 0, y: 1})}" + "]->(h1:Hobby{name:'Music'})," + " (n)-[l2:LIKES" + "{since: 2000, active: false, localDate: date('2000-06-28'), myEnum: 'SOMETHING_DIFFERENT', point: point({x: 2, y: 3})}" + "]->(h2:Hobby{name:'Something else'})" + "RETURN n, h1, h2").single(); Node personNode = record.get("n").asNode(); Node hobbyNode1 = record.get("h1").asNode(); Node hobbyNode2 = record.get("h2").asNode(); personId = personNode.id(); hobbyNode1Id = hobbyNode1.id(); hobbyNode2Id = hobbyNode2.id(); } StepVerifier.create(repository.findById(personId)) .assertNext(person -> { assertThat(person.getName()).isEqualTo("Freddie"); Hobby hobby1 = new Hobby(); hobby1.setName("Music"); hobby1.setId(hobbyNode1Id); LikesHobbyRelationship rel1 = new LikesHobbyRelationship(1995); rel1.setActive(true); rel1.setLocalDate(LocalDate.of(1995, 2, 26)); rel1.setMyEnum(LikesHobbyRelationship.MyEnum.SOMETHING); rel1.setPoint(new CartesianPoint2d(0d, 1d)); Hobby hobby2 = new Hobby(); hobby2.setName("Something else"); hobby2.setId(hobbyNode2Id); LikesHobbyRelationship rel2 = new LikesHobbyRelationship(2000); rel2.setActive(false); rel2.setLocalDate(LocalDate.of(2000, 6, 28)); rel2.setMyEnum(LikesHobbyRelationship.MyEnum.SOMETHING_DIFFERENT); rel2.setPoint(new CartesianPoint2d(2d, 3d)); assertThat(person.getHobbies()).contains(MapEntry.entry(hobby1, rel1), MapEntry.entry(hobby2, rel2)); }) .verifyComplete(); } @Test void saveEntityWithRelationshipWithProperties(@Autowired ReactivePersonWithRelationshipWithPropertiesRepository repository) { // given Hobby h1 = new Hobby(); h1.setName("Music"); int rel1Since = 1995; boolean rel1Active = true; LocalDate rel1LocalDate = LocalDate.of(1995, 2, 26); LikesHobbyRelationship.MyEnum rel1MyEnum = LikesHobbyRelationship.MyEnum.SOMETHING; CartesianPoint2d rel1Point = new CartesianPoint2d(0.0, 1.0); LikesHobbyRelationship rel1 = new LikesHobbyRelationship(rel1Since); rel1.setActive(rel1Active); rel1.setLocalDate(rel1LocalDate); rel1.setMyEnum(rel1MyEnum); rel1.setPoint(rel1Point); Hobby h2 = new Hobby(); h2.setName("Something else"); int rel2Since = 2000; boolean rel2Active = false; LocalDate rel2LocalDate = LocalDate.of(2000, 6, 28); LikesHobbyRelationship.MyEnum rel2MyEnum = LikesHobbyRelationship.MyEnum.SOMETHING_DIFFERENT; CartesianPoint2d rel2Point = new CartesianPoint2d(2.0, 3.0); LikesHobbyRelationship rel2 = new LikesHobbyRelationship(rel2Since); rel2.setActive(rel2Active); rel2.setLocalDate(rel2LocalDate); rel2.setMyEnum(rel2MyEnum); rel2.setPoint(rel2Point); Map<Hobby, LikesHobbyRelationship> hobbies = new HashMap<>(); hobbies.put(h1, rel1); hobbies.put(h2, rel2); PersonWithRelationshipWithProperties clonePerson = new PersonWithRelationshipWithProperties("Freddie clone"); clonePerson.setHobbies(hobbies); // when Mono<PersonWithRelationshipWithProperties> operationUnderTest = repository .save(clonePerson); // then List<PersonWithRelationshipWithProperties> shouldBeDifferentPersons = new ArrayList<>(); TransactionalOperator transactionalOperator = TransactionalOperator.create(getTransactionManager()); transactionalOperator.execute(t -> operationUnderTest) .as(StepVerifier::create) .recordWith(() -> shouldBeDifferentPersons) .expectNextCount(1L) .verifyComplete(); assertThat(shouldBeDifferentPersons).size().isEqualTo(1); PersonWithRelationshipWithProperties shouldBeDifferentPerson = shouldBeDifferentPersons.get(0); assertThat(shouldBeDifferentPerson) .isNotNull() .isEqualToComparingOnlyGivenFields(clonePerson, "hobbies"); assertThat(shouldBeDifferentPerson.getName()).isEqualToIgnoringCase("Freddie clone"); // check content of db String matchQuery = "MATCH (n:PersonWithRelationshipWithProperties {name:'Freddie clone'}) " + "RETURN n, " + "[(n) -[:LIKES]->(h:Hobby) |h] as Hobbies, " + "[(n) -[r:LIKES]->(:Hobby) |r] as rels"; Flux.usingWhen( Mono.fromSupplier(() -> createRxSession()), s -> s.run(matchQuery).records(), RxSession::close ).as(StepVerifier::create) .assertNext(record -> { assertThat(record.containsKey("n")).isTrue(); assertThat(record.containsKey("Hobbies")).isTrue(); assertThat(record.containsKey("rels")).isTrue(); assertThat(record.values()).hasSize(3); assertThat(record.get("Hobbies").values()).hasSize(2); assertThat(record.get("rels").values()).hasSize(2); assertThat(record.get("rels").values(Value::asRelationship)). extracting( Relationship::type, rel -> rel.get("active"), rel -> rel.get("localDate"), rel -> rel.get("point"), rel -> rel.get("myEnum"), rel -> rel.get("since") ) .containsExactlyInAnyOrder( tuple( "LIKES", Values.value(rel1Active), Values.value(rel1LocalDate), Values.point(rel1Point.getSrid(), rel1Point.getX(), rel1Point.getY()), Values.value(rel1MyEnum.name()), Values.value(rel1Since) ), tuple( "LIKES", Values.value(rel2Active), Values.value(rel2LocalDate), Values.point(rel2Point.getSrid(), rel2Point.getX(), rel2Point.getY()), Values.value(rel2MyEnum.name()), Values.value(rel2Since) ) ); }) .verifyComplete(); } @Test void loadEntityWithRelationshipWithPropertiesFromCustomQuery(@Autowired ReactivePersonWithRelationshipWithPropertiesRepository repository) { long personId; long hobbyNode1Id; long hobbyNode2Id; try (Session session = createSession()) { Record record = session .run("CREATE (n:PersonWithRelationshipWithProperties{name:'Freddie'})," + " (n)-[l1:LIKES" + "{since: 1995, active: true, localDate: date('1995-02-26'), myEnum: 'SOMETHING', point: point({x: 0, y: 1})}" + "]->(h1:Hobby{name:'Music'})," + " (n)-[l2:LIKES" + "{since: 2000, active: false, localDate: date('2000-06-28'), myEnum: 'SOMETHING_DIFFERENT', point: point({x: 2, y: 3})}" + "]->(h2:Hobby{name:'Something else'})" + "RETURN n, h1, h2").single(); Node personNode = record.get("n").asNode(); Node hobbyNode1 = record.get("h1").asNode(); Node hobbyNode2 = record.get("h2").asNode(); personId = personNode.id(); hobbyNode1Id = hobbyNode1.id(); hobbyNode2Id = hobbyNode2.id(); } StepVerifier.create(repository.loadFromCustomQuery(personId)) .assertNext(person -> { assertThat(person.getName()).isEqualTo("Freddie"); Hobby hobby1 = new Hobby(); hobby1.setName("Music"); hobby1.setId(hobbyNode1Id); LikesHobbyRelationship rel1 = new LikesHobbyRelationship(1995); rel1.setActive(true); rel1.setLocalDate(LocalDate.of(1995, 2, 26)); rel1.setMyEnum(LikesHobbyRelationship.MyEnum.SOMETHING); rel1.setPoint(new CartesianPoint2d(0d, 1d)); Hobby hobby2 = new Hobby(); hobby2.setName("Something else"); hobby2.setId(hobbyNode2Id); LikesHobbyRelationship rel2 = new LikesHobbyRelationship(2000); rel2.setActive(false); rel2.setLocalDate(LocalDate.of(2000, 6, 28)); rel2.setMyEnum(LikesHobbyRelationship.MyEnum.SOMETHING_DIFFERENT); rel2.setPoint(new CartesianPoint2d(2d, 3d)); assertThat(person.getHobbies()).contains(MapEntry.entry(hobby1, rel1), MapEntry.entry(hobby2, rel2)); }) .verifyComplete(); } } @Nested class RelatedEntityQuery extends ReactiveIntegrationTestBase { @Test void findByPropertyOnRelatedEntity(@Autowired ReactiveRelationshipRepository repository) { try (Session session = createSession()) { session.run("CREATE (:PersonWithRelationship{name:'Freddie'})-[:Has]->(:Pet{name: 'Jerry'})"); } StepVerifier.create(repository.findByPetsName("Jerry")) .assertNext(person -> assertThat(person.getName()).isEqualTo("Freddie")) .verifyComplete(); } @Test void findByPropertyOnRelatedEntitiesOr(@Autowired ReactiveRelationshipRepository repository) { try (Session session = createSession()) { session.run("CREATE (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(:Pet{name: 'Tom'})," + "(n)-[:Has]->(:Hobby{name: 'Music'})"); } StepVerifier.create(repository.findByHobbiesNameOrPetsName("Music", "Jerry")) .assertNext(person -> assertThat(person.getName()).isEqualTo("Freddie")) .verifyComplete(); StepVerifier.create(repository.findByHobbiesNameOrPetsName("Sports", "Tom")) .assertNext(person -> assertThat(person.getName()).isEqualTo("Freddie")) .verifyComplete(); StepVerifier.create(repository.findByHobbiesNameOrPetsName("Sports", "Jerry")) .verifyComplete(); } @Test void findByPropertyOnRelatedEntitiesAnd(@Autowired ReactiveRelationshipRepository repository) { try (Session session = createSession()) { session.run("CREATE (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(:Pet{name: 'Tom'})," + "(n)-[:Has]->(:Hobby{name: 'Music'})"); } StepVerifier.create(repository.findByHobbiesNameAndPetsName("Music", "Tom")) .assertNext(person -> assertThat(person.getName()).isEqualTo("Freddie")) .verifyComplete(); StepVerifier.create(repository.findByHobbiesNameAndPetsName("Sports", "Jerry")) .verifyComplete(); } @Test void findByPropertyOnRelatedEntityOfRelatedEntity(@Autowired ReactiveRelationshipRepository repository) { try (Session session = createSession()) { session.run("CREATE (:PersonWithRelationship{name:'Freddie'})-[:Has]->(:Pet{name: 'Jerry'})" + "-[:Has]->(:Hobby{name: 'Sleeping'})"); } StepVerifier.create(repository.findByPetsHobbiesName("Sleeping")) .assertNext(person -> assertThat(person.getName()).isEqualTo("Freddie")) .verifyComplete(); StepVerifier.create(repository.findByPetsHobbiesName("Sports")) .verifyComplete(); } @Test void findByPropertyOnRelatedEntityOfRelatedSameEntity(@Autowired ReactiveRelationshipRepository repository) { try (Session session = createSession()) { session.run("CREATE (:PersonWithRelationship{name:'Freddie'})-[:Has]->(:Pet{name: 'Jerry'})" + "-[:Has]->(:Pet{name: 'Tom'})"); } StepVerifier.create(repository.findByPetsFriendsName("Tom")) .assertNext(person -> assertThat(person.getName()).isEqualTo("Freddie")) .verifyComplete(); StepVerifier.create(repository.findByPetsFriendsName("Jerry")) .verifyComplete(); } @Test void findByPropertyOnRelationshipWithProperties(@Autowired ReactivePersonWithRelationshipWithPropertiesRepository repository) { try (Session session = createSession()) { session.run("CREATE (:PersonWithRelationshipWithProperties{name:'Freddie'})-[:LIKES{since: 2020}]->(:Hobby{name: 'Bowling'})"); } StepVerifier.create(repository.findByHobbiesSince(2020)) .assertNext(person -> assertThat(person.getName()).isEqualTo("Freddie")) .verifyComplete(); } @Test void findByPropertyOnRelationshipWithPropertiesOr(@Autowired ReactivePersonWithRelationshipWithPropertiesRepository repository) { try (Session session = createSession()) { session.run("CREATE (:PersonWithRelationshipWithProperties{name:'Freddie'})-[:LIKES{since: 2020, active: true}]->(:Hobby{name: 'Bowling'})"); } StepVerifier.create(repository.findByHobbiesSinceOrHobbiesActive(2020, false)) .assertNext(person -> assertThat(person.getName()).isEqualTo("Freddie")) .verifyComplete(); StepVerifier.create(repository.findByHobbiesSinceOrHobbiesActive(2019, true)) .assertNext(person -> assertThat(person.getName()).isEqualTo("Freddie")) .verifyComplete(); StepVerifier.create(repository.findByHobbiesSinceOrHobbiesActive(2019, false)) .verifyComplete(); } @Test void findByPropertyOnRelationshipWithPropertiesAnd(@Autowired ReactivePersonWithRelationshipWithPropertiesRepository repository) { try (Session session = createSession()) { session.run("CREATE (:PersonWithRelationshipWithProperties{name:'Freddie'})-[:LIKES{since: 2020, active: true}]->(:Hobby{name: 'Bowling'})"); } StepVerifier.create(repository.findByHobbiesSinceAndHobbiesActive(2020, true)) .assertNext(person -> assertThat(person.getName()).isEqualTo("Freddie")) .verifyComplete(); StepVerifier.create(repository.findByHobbiesSinceAndHobbiesActive(2019, true)) .verifyComplete(); StepVerifier.create(repository.findByHobbiesSinceAndHobbiesActive(2020, false)) .verifyComplete(); } } @Nested class Save extends ReactiveIntegrationTestBase { @Override void setupData(Transaction transaction) { transaction.run("MATCH (n) detach delete n"); id1 = transaction.run("" + "CREATE (n:PersonWithAllConstructor) " + " SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName, n.cool = $cool, n.personNumber = $personNumber, n.bornOn = $bornOn, n.nullable = 'something', n.things = ['a', 'b'], n.place = $place " + "RETURN id(n)", parameters("name", TEST_PERSON1_NAME, "sameValue", TEST_PERSON_SAMEVALUE, "firstName", TEST_PERSON1_FIRST_NAME, "cool", true, "personNumber", 1, "bornOn", TEST_PERSON1_BORN_ON, "place", NEO4J_HQ)) .next().get(0).asLong(); id2 = transaction.run( "CREATE (n:PersonWithAllConstructor) SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName, n.cool = $cool, n.personNumber = $personNumber, n.bornOn = $bornOn, n.things = [], n.place = $place return id(n)", parameters("name", TEST_PERSON2_NAME, "sameValue", TEST_PERSON_SAMEVALUE, "firstName", TEST_PERSON2_FIRST_NAME, "cool", false, "personNumber", 2, "bornOn", TEST_PERSON2_BORN_ON, "place", SFO)) .next().get(0).asLong(); transaction .run("CREATE (a:Thing {theId: 'anId', name: 'Homer'})-[:Has]->(b:Thing2{theId: 4711, name: 'Bart'})"); IntStream.rangeClosed(1, 20).forEach(i -> transaction.run("CREATE (a:Thing {theId: 'id' + $i, name: 'name' + $i})", parameters("i", String.format("%02d", i)))); person1 = new PersonWithAllConstructor(id1, TEST_PERSON1_NAME, TEST_PERSON1_FIRST_NAME, TEST_PERSON_SAMEVALUE, true, 1L, TEST_PERSON1_BORN_ON, "something", Arrays.asList("a", "b"), NEO4J_HQ, null); person2 = new PersonWithAllConstructor(id2, TEST_PERSON2_NAME, TEST_PERSON2_FIRST_NAME, TEST_PERSON_SAMEVALUE, false, 2L, TEST_PERSON2_BORN_ON, null, Collections.emptyList(), SFO, null); } @Test void saveSingleEntity(@Autowired ReactivePersonRepository repository) { PersonWithAllConstructor person = new PersonWithAllConstructor(null, "Mercury", "Freddie", "Queen", true, 1509L, LocalDate.of(1946, 9, 15), null, Collections.emptyList(), null, null); Mono<Long> operationUnderTest = repository .save(person) .map(PersonWithAllConstructor::getId); List<Long> ids = new ArrayList<>(); TransactionalOperator transactionalOperator = TransactionalOperator.create(getTransactionManager()); transactionalOperator .execute(t -> operationUnderTest) .as(StepVerifier::create) .recordWith(() -> ids) .expectNextCount(1L) .verifyComplete(); Flux.usingWhen( Mono.fromSupplier(() -> createRxSession()), s -> s.run("MATCH (n:PersonWithAllConstructor) WHERE id(n) in $ids RETURN n", parameters("ids", ids)) .records(), RxSession::close ).map(r -> r.get("n").asNode().get("first_name").asString()) .as(StepVerifier::create) .expectNext("Freddie") .verifyComplete(); } @Test void saveAll(@Autowired ReactivePersonRepository repository) { Flux<PersonWithAllConstructor> persons = repository .findById(id1) .map(existingPerson -> { existingPerson.setFirstName("Updated first name"); existingPerson.setNullable("Updated nullable field"); return existingPerson; }) .concatWith( Mono.fromSupplier(() -> { PersonWithAllConstructor newPerson = new PersonWithAllConstructor( null, "Mercury", "Freddie", "Queen", true, 1509L, LocalDate.of(1946, 9, 15), null, Collections.emptyList(), null, null); return newPerson; })); Flux<Long> operationUnderTest = repository .saveAll(persons) .map(PersonWithAllConstructor::getId); List<Long> ids = new ArrayList<>(); TransactionalOperator transactionalOperator = TransactionalOperator.create(getTransactionManager()); transactionalOperator .execute(t -> operationUnderTest) .as(StepVerifier::create) .recordWith(() -> ids) .expectNextCount(2L) .verifyComplete(); Flux .usingWhen( Mono.fromSupplier(() -> createRxSession()), s -> s.run("MATCH (n:PersonWithAllConstructor) WHERE id(n) in $ids RETURN n ORDER BY n.name ASC", parameters("ids", ids)) .records(), RxSession::close ).map(r -> r.get("n").asNode().get("name").asString()) .as(StepVerifier::create) .expectNext("Mercury") .expectNext(TEST_PERSON1_NAME) .verifyComplete(); } @Test void saveAllIterable(@Autowired ReactivePersonRepository repository) { PersonWithAllConstructor newPerson = new PersonWithAllConstructor( null, "Mercury", "Freddie", "Queen", true, 1509L, LocalDate.of(1946, 9, 15), null, Collections.emptyList(), null, null); Flux<Long> operationUnderTest = repository .saveAll(Arrays.asList(newPerson)) .map(PersonWithAllConstructor::getId); List<Long> ids = new ArrayList<>(); TransactionalOperator transactionalOperator = TransactionalOperator.create(getTransactionManager()); transactionalOperator .execute(t -> operationUnderTest) .as(StepVerifier::create) .recordWith(() -> ids) .expectNextCount(1L) .verifyComplete(); Flux .usingWhen( Mono.fromSupplier(() -> createRxSession()), s -> s.run("MATCH (n:PersonWithAllConstructor) WHERE id(n) in $ids RETURN n ORDER BY n.name ASC", parameters("ids", ids)) .records(), RxSession::close ).map(r -> r.get("n").asNode().get("name").asString()) .as(StepVerifier::create) .expectNext("Mercury") .verifyComplete(); } @Test void updateSingleEntity(@Autowired ReactivePersonRepository repository) { Mono<PersonWithAllConstructor> operationUnderTest = repository.findById(id1) .map(originalPerson -> { originalPerson.setFirstName("Updated first name"); originalPerson.setNullable("Updated nullable field"); return originalPerson; }) .flatMap(repository::save); TransactionalOperator transactionalOperator = TransactionalOperator.create(getTransactionManager()); transactionalOperator .execute(t -> operationUnderTest) .as(StepVerifier::create) .expectNextCount(1L) .verifyComplete(); Flux .usingWhen( Mono.fromSupplier(() -> createRxSession()), s -> { Value parameters = parameters("id", id1); return s.run("MATCH (n:PersonWithAllConstructor) WHERE id(n) = $id RETURN n", parameters).records(); }, RxSession::close ) .map(r -> r.get("n").asNode()) .as(StepVerifier::create) .expectNextMatches(node -> node.get("first_name").asString().equals("Updated first name") && node.get("nullable").asString().equals("Updated nullable field")) .verifyComplete(); } @Test void saveWithAssignedId(@Autowired ReactiveThingRepository repository) { Mono<ThingWithAssignedId> operationUnderTest = Mono.fromSupplier(() -> { ThingWithAssignedId thing = new ThingWithAssignedId("aaBB"); thing.setName("That's the thing."); return thing; }).flatMap(repository::save); TransactionalOperator transactionalOperator = TransactionalOperator.create(getTransactionManager()); transactionalOperator .execute(t -> operationUnderTest) .as(StepVerifier::create) .expectNextCount(1L) .verifyComplete(); Flux .usingWhen( Mono.fromSupplier(() -> createRxSession()), s -> s.run("MATCH (n:Thing) WHERE n.theId = $id RETURN n", parameters("id", "aaBB")).records(), RxSession::close ) .map(r -> r.get("n").asNode().get("name").asString()) .as(StepVerifier::create) .expectNext("That's the thing.") .verifyComplete(); repository.count().as(StepVerifier::create).expectNext(22L).verifyComplete(); } @Test void saveAllWithAssignedId(@Autowired ReactiveThingRepository repository) { Flux<ThingWithAssignedId> things = repository .findById("anId") .map(existingThing -> { existingThing.setName("Updated name."); return existingThing; }) .concatWith( Mono.fromSupplier(() -> { ThingWithAssignedId newThing = new ThingWithAssignedId("aaBB"); newThing.setName("That's the thing."); return newThing; }) ); Flux<ThingWithAssignedId> operationUnderTest = repository .saveAll(things); TransactionalOperator transactionalOperator = TransactionalOperator.create(getTransactionManager()); transactionalOperator .execute(t -> operationUnderTest) .as(StepVerifier::create) .expectNextCount(2L) .verifyComplete(); Flux .usingWhen( Mono.fromSupplier(() -> createRxSession()), s -> { Value parameters = parameters("ids", Arrays.asList("anId", "aaBB")); return s.run("MATCH (n:Thing) WHERE n.theId IN ($ids) RETURN n.name as name ORDER BY n.name ASC", parameters) .records(); }, RxSession::close ) .map(r -> r.get("name").asString()) .as(StepVerifier::create) .expectNext("That's the thing.") .expectNext("Updated name.") .verifyComplete(); // Make sure we triggered on insert, one update repository.count().as(StepVerifier::create).expectNext(22L).verifyComplete(); } @Test void saveAllIterableWithAssignedId(@Autowired ReactiveThingRepository repository) { ThingWithAssignedId existingThing = new ThingWithAssignedId("anId"); existingThing.setName("Updated name."); ThingWithAssignedId newThing = new ThingWithAssignedId("aaBB"); newThing.setName("That's the thing."); List<ThingWithAssignedId> things = Arrays.asList(existingThing, newThing); Flux<ThingWithAssignedId> operationUnderTest = repository.saveAll(things); TransactionalOperator transactionalOperator = TransactionalOperator.create(getTransactionManager()); transactionalOperator .execute(t -> operationUnderTest) .as(StepVerifier::create) .expectNextCount(2L) .verifyComplete(); Flux .usingWhen( Mono.fromSupplier(() -> createRxSession()), s -> { Value parameters = parameters("ids", Arrays.asList("anId", "aaBB")); return s.run("MATCH (n:Thing) WHERE n.theId IN ($ids) RETURN n.name as name ORDER BY n.name ASC", parameters) .records(); }, RxSession::close ) .map(r -> r.get("name").asString()) .as(StepVerifier::create) .expectNext("That's the thing.") .expectNext("Updated name.") .verifyComplete(); // Make sure we triggered on insert, one update repository.count().as(StepVerifier::create).expectNext(22L).verifyComplete(); } @Test void updateWithAssignedId(@Autowired ReactiveThingRepository repository) { Flux<ThingWithAssignedId> operationUnderTest = Flux.concat( // Without prior selection Mono.fromSupplier(() -> { ThingWithAssignedId thing = new ThingWithAssignedId("id07"); thing.setName("An updated thing"); return thing; }).flatMap(repository::save), // With prior selection repository.findById("id15") .flatMap(thing -> { thing.setName("Another updated thing"); return repository.save(thing); }) ); TransactionalOperator transactionalOperator = TransactionalOperator.create(getTransactionManager()); transactionalOperator .execute(t -> operationUnderTest) .as(StepVerifier::create) .expectNextCount(2L) .verifyComplete(); Flux .usingWhen( Mono.fromSupplier(() -> createRxSession()), s -> { Value parameters = parameters("ids", Arrays.asList("id07", "id15")); return s.run("MATCH (n:Thing) WHERE n.theId IN ($ids) RETURN n.name as name ORDER BY n.name ASC", parameters).records(); }, RxSession::close ) .map(r -> r.get("name").asString()) .as(StepVerifier::create) .expectNext("An updated thing", "Another updated thing") .verifyComplete(); repository.count().as(StepVerifier::create).expectNext(21L).verifyComplete(); } @Test void saveWithConvertedId(@Autowired EntityWithConvertedIdRepository repository) { EntityWithConvertedId entity = new EntityWithConvertedId(); entity.setIdentifyingEnum(EntityWithConvertedId.IdentifyingEnum.A); repository.save(entity).block(); try (Session session = createSession()) { Record node = session.run("MATCH (e:EntityWithConvertedId) return e").next(); assertThat(node.get("e").get("identifyingEnum").asString()).isEqualTo("A"); } } @Test void saveAllWithConvertedId(@Autowired EntityWithConvertedIdRepository repository) { EntityWithConvertedId entity = new EntityWithConvertedId(); entity.setIdentifyingEnum(EntityWithConvertedId.IdentifyingEnum.A); repository.saveAll(Collections.singleton(entity)).collectList().block(); try (Session session = createSession()) { Record node = session.run("MATCH (e:EntityWithConvertedId) return e").next(); assertThat(node.get("e").get("identifyingEnum").asString()).isEqualTo("A"); } } } @Nested class SaveWithRelationships extends ReactiveIntegrationTestBase { @Test void saveSingleEntityWithRelationships(@Autowired ReactiveRelationshipRepository repository) { PersonWithRelationship person = new PersonWithRelationship(); person.setName("Freddie"); Hobby hobby = new Hobby(); hobby.setName("Music"); person.setHobbies(hobby); Club club = new Club(); club.setName("ClownsClub"); person.setClub(club); Pet pet1 = new Pet("Jerry"); Pet pet2 = new Pet("Tom"); Hobby petHobby = new Hobby(); petHobby.setName("sleeping"); pet1.setHobbies(singleton(petHobby)); person.setPets(Arrays.asList(pet1, pet2)); List<Long> ids = new ArrayList<>(); TransactionalOperator transactionalOperator = TransactionalOperator.create(getTransactionManager()); transactionalOperator .execute(t -> repository.save(person).map(PersonWithRelationship::getId)) .as(StepVerifier::create) .recordWith(() -> ids) .expectNextCount(1L) .verifyComplete(); try (Session session = createSession()) { Record record = session.run("MATCH (n:PersonWithRelationship)" + " RETURN n," + " [(n)-[:Has]->(p:Pet) | [ p , [ (p)-[:Has]-(h:Hobby) | h ] ] ] as petsWithHobbies," + " [(n)-[:Has]->(h:Hobby) | h] as hobbies, " + " [(n)<-[:Has]-(c:Club) | c] as clubs", Values.parameters("name", "Freddie")).single(); assertThat(record.containsKey("n")).isTrue(); Node rootNode = record.get("n").asNode(); assertThat(ids.get(0)).isEqualTo(rootNode.id()); assertThat(rootNode.get("name").asString()).isEqualTo("Freddie"); List<List<Object>> petsWithHobbies = record.get("petsWithHobbies").asList(Value::asList); Map<Object, List<Node>> pets = new HashMap<>(); for (List<Object> petWithHobbies : petsWithHobbies) { pets.put(petWithHobbies.get(0), ((List<Node>) petWithHobbies.get(1))); } assertThat(pets.keySet().stream().map(pet -> ((Node) pet).get("name").asString()).collect(toList())) .containsExactlyInAnyOrder("Jerry", "Tom"); assertThat(pets.values().stream() .flatMap(petHobbies -> petHobbies.stream().map(node -> node.get("name").asString())).collect(toList())) .containsExactlyInAnyOrder("sleeping"); assertThat(record.get("hobbies").asList(entry -> entry.asNode().get("name").asString())) .containsExactlyInAnyOrder("Music"); assertThat(record.get("clubs").asList(entry -> entry.asNode().get("name").asString())) .containsExactlyInAnyOrder("ClownsClub"); } } @Test void saveSingleEntityWithRelationshipsTwiceDoesNotCreateMoreRelationships(@Autowired ReactiveRelationshipRepository repository) { PersonWithRelationship person = new PersonWithRelationship(); person.setName("Freddie"); Hobby hobby = new Hobby(); hobby.setName("Music"); person.setHobbies(hobby); Pet pet1 = new Pet("Jerry"); Pet pet2 = new Pet("Tom"); Hobby petHobby = new Hobby(); petHobby.setName("sleeping"); pet1.setHobbies(singleton(petHobby)); person.setPets(Arrays.asList(pet1, pet2)); List<Long> ids = new ArrayList<>(); TransactionalOperator transactionalOperator = TransactionalOperator.create(getTransactionManager()); transactionalOperator .execute(t -> repository.save(person).map(PersonWithRelationship::getId)) .as(StepVerifier::create) .recordWith(() -> ids) .expectNextCount(1L) .verifyComplete(); transactionalOperator .execute(t -> repository.save(person)) .as(StepVerifier::create) .expectNextCount(1L) .verifyComplete(); try (Session session = createSession()) { List<Record> recordList = session.run("MATCH (n:PersonWithRelationship)" + " RETURN n," + " [(n)-[:Has]->(p:Pet) | [ p , [ (p)-[:Has]-(h:Hobby) | h ] ] ] as petsWithHobbies," + " [(n)-[:Has]->(h:Hobby) | h] as hobbies", Values.parameters("name", "Freddie")).list(); // assert that there is only one record in the returned list assertThat(recordList).hasSize(1); Record record = recordList.get(0); assertThat(record.containsKey("n")).isTrue(); Node rootNode = record.get("n").asNode(); assertThat(ids.get(0)).isEqualTo(rootNode.id()); assertThat(rootNode.get("name").asString()).isEqualTo("Freddie"); List<List<Object>> petsWithHobbies = record.get("petsWithHobbies").asList(Value::asList); Map<Object, List<Node>> pets = new HashMap<>(); for (List<Object> petWithHobbies : petsWithHobbies) { pets.put(petWithHobbies.get(0), ((List<Node>) petWithHobbies.get(1))); } assertThat(pets.keySet().stream().map(pet -> ((Node) pet).get("name").asString()).collect(toList())) .containsExactlyInAnyOrder("Jerry", "Tom"); assertThat(pets.values().stream() .flatMap(petHobbies -> petHobbies.stream().map(node -> node.get("name").asString())).collect(toList())) .containsExactlyInAnyOrder("sleeping"); assertThat(record.get("hobbies").asList(entry -> entry.asNode().get("name").asString())) .containsExactlyInAnyOrder("Music"); // assert that only two hobbies is stored recordList = session.run("MATCH (h:Hobby) RETURN h").list(); assertThat(recordList).hasSize(2); // assert that only two pets is stored recordList = session.run("MATCH (p:Pet) RETURN p").list(); assertThat(recordList).hasSize(2); } } @Test void saveEntityWithAlreadyExistingTargetNode(@Autowired ReactiveRelationshipRepository repository) { Long hobbyId; try (Session session = createSession()) { hobbyId = session.run("CREATE (h:Hobby{name: 'Music'}) return id(h) as hId").single().get("hId").asLong(); } PersonWithRelationship person = new PersonWithRelationship(); person.setName("Freddie"); Hobby hobby = new Hobby(); hobby.setId(hobbyId); hobby.setName("Music"); person.setHobbies(hobby); List<Long> ids = new ArrayList<>(); TransactionalOperator transactionalOperator = TransactionalOperator.create(getTransactionManager()); transactionalOperator .execute(t -> repository.save(person).map(PersonWithRelationship::getId)) .as(StepVerifier::create) .recordWith(() -> ids) .expectNextCount(1L) .verifyComplete(); try (Session session = createSession()) { List<Record> recordList = session.run("MATCH (n:PersonWithRelationship)" + " RETURN n," + " [(n)-[:Has]->(h:Hobby) | h] as hobbies", Values.parameters("name", "Freddie")).list(); Record record = recordList.get(0); assertThat(record.containsKey("n")).isTrue(); Node rootNode = record.get("n").asNode(); assertThat(ids.get(0)).isEqualTo(rootNode.id()); assertThat(rootNode.get("name").asString()).isEqualTo("Freddie"); assertThat(record.get("hobbies").asList(entry -> entry.asNode().get("name").asString())) .containsExactlyInAnyOrder("Music"); // assert that only one hobby is stored recordList = session.run("MATCH (h:Hobby) RETURN h").list(); assertThat(recordList).hasSize(1); } } @Test void saveEntityWithDeepSelfReferences(@Autowired ReactivePetRepository repository) { Pet rootPet = new Pet("Luna"); Pet petOfRootPet = new Pet("Daphne"); Pet petOfChildPet = new Pet("Mucki"); Pet petOfGrandChildPet = new Pet("Blacky"); rootPet.setFriends(singletonList(petOfRootPet)); petOfRootPet.setFriends(singletonList(petOfChildPet)); petOfChildPet.setFriends(singletonList(petOfGrandChildPet)); StepVerifier.create(repository.save(rootPet)) .expectNextCount(1) .verifyComplete(); try (Session session = createSession()) { Record record = session.run("MATCH (rootPet:Pet)-[:Has]->(petOfRootPet:Pet)-[:Has]->(petOfChildPet:Pet)" + "-[:Has]->(petOfGrandChildPet:Pet) " + "RETURN rootPet, petOfRootPet, petOfChildPet, petOfGrandChildPet", emptyMap()).single(); assertThat(record.get("rootPet").asNode().get("name").asString()).isEqualTo("Luna"); assertThat(record.get("petOfRootPet").asNode().get("name").asString()).isEqualTo("Daphne"); assertThat(record.get("petOfChildPet").asNode().get("name").asString()).isEqualTo("Mucki"); assertThat(record.get("petOfGrandChildPet").asNode().get("name").asString()).isEqualTo("Blacky"); } } @Test void saveEntityGraphWithSelfInverseRelationshipDefined(@Autowired ReactiveSimilarThingRepository repository) { SimilarThing originalThing = new SimilarThing().withName("Original"); SimilarThing similarThing = new SimilarThing().withName("Similar"); originalThing.setSimilar(similarThing); similarThing.setSimilarOf(originalThing); StepVerifier.create(repository.save(originalThing)) .expectNextCount(1) .verifyComplete(); try (Session session = createSession()) { Record record = session.run( "MATCH (ot:SimilarThing{name:'Original'})-[r:SimilarTo]->(st:SimilarThing {name:'Similar'})" + " RETURN r").single(); assertThat(record.keys()).isNotEmpty(); assertThat(record.containsKey("r")).isTrue(); assertThat(record.get("r").asRelationship().type()).isEqualToIgnoringCase("SimilarTo"); } } @Test void createComplexSameClassRelationshipsBeforeRootObject( @Autowired ImmutablePersonRepository repository) { ImmutablePerson p1 = new ImmutablePerson("Person1", Collections.emptyList()); ImmutablePerson p2 = new ImmutablePerson("Person2", Arrays.asList(p1)); ImmutablePerson p3 = new ImmutablePerson("Person3", Arrays.asList(p2)); ImmutablePerson p4 = new ImmutablePerson("Person4", Arrays.asList(p1, p3)); ImmutablePerson savedImmutablePerson = repository.save(p4).block(); StepVerifier.create(repository.findAll()) .expectNextCount(4) .verifyComplete(); } @Test void saveEntityWithSelfReferencesInBothDirections(@Autowired ReactivePetRepository repository) { Pet luna = new Pet("Luna"); Pet daphne = new Pet("Daphne"); luna.setFriends(singletonList(daphne)); daphne.setFriends(singletonList(luna)); StepVerifier.create(repository.save(luna)) .expectNextCount(1) .verifyComplete(); try (Session session = createSession()) { Record record = session.run("MATCH (luna:Pet{name:'Luna'})-[:Has]->(daphne:Pet{name:'Daphne'})" + "-[:Has]->(luna2:Pet{name:'Luna'})" + "RETURN luna, daphne, luna2").single(); assertThat(record.get("luna").asNode().get("name").asString()).isEqualTo("Luna"); assertThat(record.get("daphne").asNode().get("name").asString()).isEqualTo("Daphne"); assertThat(record.get("luna2").asNode().get("name").asString()).isEqualTo("Luna"); } } } @Nested class Delete extends ReactiveIntegrationTestBase { @Override void setupData(Transaction transaction) { transaction.run("MATCH (n) detach delete n"); id1 = transaction.run("" + "CREATE (n:PersonWithAllConstructor) " + " SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName, n.cool = $cool, n.personNumber = $personNumber, n.bornOn = $bornOn, n.nullable = 'something', n.things = ['a', 'b'], n.place = $place " + "RETURN id(n)", parameters("name", TEST_PERSON1_NAME, "sameValue", TEST_PERSON_SAMEVALUE, "firstName", TEST_PERSON1_FIRST_NAME, "cool", true, "personNumber", 1, "bornOn", TEST_PERSON1_BORN_ON, "place", NEO4J_HQ)) .next().get(0).asLong(); id2 = transaction.run( "CREATE (n:PersonWithAllConstructor) SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName, n.cool = $cool, n.personNumber = $personNumber, n.bornOn = $bornOn, n.things = [], n.place = $place return id(n)", parameters("name", TEST_PERSON2_NAME, "sameValue", TEST_PERSON_SAMEVALUE, "firstName", TEST_PERSON2_FIRST_NAME, "cool", false, "personNumber", 2, "bornOn", TEST_PERSON2_BORN_ON, "place", SFO)) .next().get(0).asLong(); person1 = new PersonWithAllConstructor(id1, TEST_PERSON1_NAME, TEST_PERSON1_FIRST_NAME, TEST_PERSON_SAMEVALUE, true, 1L, TEST_PERSON1_BORN_ON, "something", Arrays.asList("a", "b"), NEO4J_HQ, null); person2 = new PersonWithAllConstructor(id2, TEST_PERSON2_NAME, TEST_PERSON2_FIRST_NAME, TEST_PERSON_SAMEVALUE, false, 2L, TEST_PERSON2_BORN_ON, null, Collections.emptyList(), SFO, null); } @Test void deleteAll(@Autowired ReactivePersonRepository repository) { repository.deleteAll() .then(repository.count()) .as(StepVerifier::create) .expectNext(0L) .verifyComplete(); } @Test void deleteById(@Autowired ReactivePersonRepository repository) { repository.deleteById(id1) .then(repository.existsById(id1)) .concatWith(repository.existsById(id2)) .as(StepVerifier::create) .expectNext(false, true) .verifyComplete(); } @Test void deleteByIdPublisher(@Autowired ReactivePersonRepository repository) { repository.deleteById(Mono.just(id1)) .then(repository.existsById(id1)) .concatWith(repository.existsById(id2)) .as(StepVerifier::create) .expectNext(false, true) .verifyComplete(); } @Test void delete(@Autowired ReactivePersonRepository repository) { repository.delete(person1) .then(repository.existsById(id1)) .concatWith(repository.existsById(id2)) .as(StepVerifier::create) .expectNext(false, true) .verifyComplete(); } @Test void deleteAllEntities(@Autowired ReactivePersonRepository repository) { repository.deleteAll(Arrays.asList(person1, person2)) .then(repository.existsById(id1)) .concatWith(repository.existsById(id2)) .as(StepVerifier::create) .expectNext(false, false) .verifyComplete(); } @Test void deleteAllEntitiesPublisher(@Autowired ReactivePersonRepository repository) { repository.deleteAll(Flux.just(person1, person2)) .then(repository.existsById(id1)) .concatWith(repository.existsById(id2)) .as(StepVerifier::create) .expectNext(false, false) .verifyComplete(); } @Test void deleteSimpleRelationship(@Autowired ReactiveRelationshipRepository repository) { try (Session session = createSession()) { session.run("CREATE (n:PersonWithRelationship{name:'Freddie'})-[:Has]->(h1:Hobby{name:'Music'})"); } Publisher<PersonWithRelationship> personLoad = repository.getPersonWithRelationshipsViaQuery() .map(person -> { person.setHobbies(null); return person; }); Flux<PersonWithRelationship> personSave = repository.saveAll(personLoad); StepVerifier.create(personSave.then(repository.getPersonWithRelationshipsViaQuery())) .assertNext(person -> { assertThat(person.getHobbies()).isNull(); }) .verifyComplete(); } @Test void deleteCollectionRelationship(@Autowired ReactiveRelationshipRepository repository) { try (Session session = createSession()) { session.run("CREATE (n:PersonWithRelationship{name:'Freddie'}), " + "(n)-[:Has]->(p1:Pet{name: 'Jerry'}), (n)-[:Has]->(p2:Pet{name: 'Tom'})"); } Publisher<PersonWithRelationship> personLoad = repository.getPersonWithRelationshipsViaQuery() .map(person -> { person.getPets().remove(0); return person; }); Flux<PersonWithRelationship> personSave = repository.saveAll(personLoad); StepVerifier.create(personSave.then(repository.getPersonWithRelationshipsViaQuery())) .assertNext(person -> { assertThat(person.getPets()).hasSize(1); }) .verifyComplete(); } } @Nested class Projection extends ReactiveIntegrationTestBase { @Override void setupData(Transaction transaction) { transaction.run("MATCH (n) detach delete n"); transaction.run("" + "CREATE (n:PersonWithAllConstructor) " + " SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName, n.cool = $cool, n.personNumber = $personNumber, n.bornOn = $bornOn, n.nullable = 'something', n.things = ['a', 'b'], n.place = $place " + "RETURN id(n)", parameters("name", TEST_PERSON1_NAME, "sameValue", TEST_PERSON_SAMEVALUE, "firstName", TEST_PERSON1_FIRST_NAME, "cool", true, "personNumber", 1, "bornOn", TEST_PERSON1_BORN_ON, "place", NEO4J_HQ)) .next().get(0).asLong(); transaction.run( "CREATE (n:PersonWithAllConstructor) SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName, n.cool = $cool, n.personNumber = $personNumber, n.bornOn = $bornOn, n.things = [], n.place = $place return id(n)", parameters("name", TEST_PERSON2_NAME, "sameValue", TEST_PERSON_SAMEVALUE, "firstName", TEST_PERSON2_FIRST_NAME, "cool", false, "personNumber", 2, "bornOn", TEST_PERSON2_BORN_ON, "place", SFO)) .next().get(0).asLong(); } @Test void mapsInterfaceProjectionWithDerivedFinderMethod(@Autowired ReactivePersonRepository repository) { StepVerifier.create(repository.findByName(TEST_PERSON1_NAME)) .assertNext(personProjection -> assertThat(personProjection.getName()).isEqualTo(TEST_PERSON1_NAME)) .verifyComplete(); } @Test void mapsDtoProjectionWithDerivedFinderMethod(@Autowired ReactivePersonRepository repository) { StepVerifier.create(repository.findByFirstName(TEST_PERSON1_FIRST_NAME)) .expectNextCount(1) .verifyComplete(); } @Test void mapsInterfaceProjectionWithDerivedFinderMethodWithMultipleResults(@Autowired ReactivePersonRepository repository) { StepVerifier.create(repository.findBySameValue(TEST_PERSON_SAMEVALUE)) .expectNextCount(2) .verifyComplete(); } @Test void mapsInterfaceProjectionWithCustomQueryAndMapProjection(@Autowired ReactivePersonRepository repository) { StepVerifier.create(repository.findByNameWithCustomQueryAndMapProjection(TEST_PERSON1_NAME)) .assertNext(personProjection -> assertThat(personProjection.getName()).isEqualTo(TEST_PERSON1_NAME)) .verifyComplete(); } @Test void mapsInterfaceProjectionWithCustomQueryAndMapProjectionWithMultipleResults(@Autowired ReactivePersonRepository repository) { StepVerifier.create(repository.loadAllProjectionsWithMapProjection()) .expectNextCount(2) .verifyComplete(); } @Test void mapsInterfaceProjectionWithCustomQueryAndNodeReturn(@Autowired ReactivePersonRepository repository) { StepVerifier.create(repository.findByNameWithCustomQueryAndNodeReturn(TEST_PERSON1_NAME)) .assertNext(personProjection -> assertThat(personProjection.getName()).isEqualTo(TEST_PERSON1_NAME)) .verifyComplete(); } @Test void mapsInterfaceProjectionWithCustomQueryAndNodeReturnWithMultipleResults(@Autowired ReactivePersonRepository repository) { StepVerifier.create(repository.loadAllProjectionsWithNodeReturn()) .expectNextCount(2) .verifyComplete(); } } @Nested class MultipleLabel extends ReactiveIntegrationTestBase { @Test void createNodeWithMultipleLabels(@Autowired ReactiveMultipleLabelRepository repository) { repository.save(new MultipleLabels.MultipleLabelsEntity()).block(); try (Session session = createSession()) { Node node = session.run("MATCH (n:A) return n").single().get("n").asNode(); assertThat(node.labels()).containsExactlyInAnyOrder("A", "B", "C"); } } @Test void createAllNodesWithMultipleLabels(@Autowired ReactiveMultipleLabelRepository repository) { repository.saveAll(singletonList(new MultipleLabels.MultipleLabelsEntity())).collectList().block(); try (Session session = createSession()) { Node node = session.run("MATCH (n:A) return n").single().get("n").asNode(); assertThat(node.labels()).containsExactlyInAnyOrder("A", "B", "C"); } } @Test void createNodeAndRelationshipWithMultipleLabels(@Autowired ReactiveMultipleLabelRepository labelRepository) { MultipleLabels.MultipleLabelsEntity entity = new MultipleLabels.MultipleLabelsEntity(); entity.otherMultipleLabelEntity = new MultipleLabels.MultipleLabelsEntity(); labelRepository.save(entity).block(); try (Session session = createSession()) { Record record = session.run("MATCH (n:A)-[:HAS]->(c:A) return n, c").single(); Node parentNode = record.get("n").asNode(); Node childNode = record.get("c").asNode(); assertThat(parentNode.labels()).containsExactlyInAnyOrder("A", "B", "C"); assertThat(childNode.labels()).containsExactlyInAnyOrder("A", "B", "C"); } } @Test void findNodeWithMultipleLabels(@Autowired ReactiveMultipleLabelRepository repository) { long n1Id; long n2Id; long n3Id; try (Session session = createSession()) { Record record = session.run("CREATE (n1:A:B:C), (n2:B:C), (n3:A) return n1, n2, n3").single(); n1Id = record.get("n1").asNode().id(); n2Id = record.get("n2").asNode().id(); n3Id = record.get("n3").asNode().id(); } StepVerifier.create(repository.findById(n1Id)) .expectNextCount(1) .verifyComplete(); StepVerifier.create(repository.findById(n2Id)) .verifyComplete(); StepVerifier.create(repository.findById(n3Id)) .verifyComplete(); } @Test void deleteNodeWithMultipleLabels(@Autowired ReactiveMultipleLabelRepository repository) { long n1Id; long n2Id; long n3Id; try (Session session = createSession()) { Record record = session.run("CREATE (n1:A:B:C), (n2:B:C), (n3:A) return n1, n2, n3").single(); n1Id = record.get("n1").asNode().id(); n2Id = record.get("n2").asNode().id(); n3Id = record.get("n3").asNode().id(); } repository.deleteById(n1Id).block(); repository.deleteById(n2Id).block(); repository.deleteById(n3Id).block(); try (Session session = createSession()) { assertThat(session.run("MATCH (n:A:B:C) return n").list()).hasSize(0); assertThat(session.run("MATCH (n:B:C) return n").list()).hasSize(1); assertThat(session.run("MATCH (n:A) return n").list()).hasSize(1); } } @Test void createNodeWithMultipleLabelsAndAssignedId(@Autowired ReactiveMultipleLabelWithAssignedIdRepository repository) { repository.save(new MultipleLabels.MultipleLabelsEntityWithAssignedId(4711L)).block(); try (Session session = createSession()) { Node node = session.run("MATCH (n:X) return n").single().get("n").asNode(); assertThat(node.labels()).containsExactlyInAnyOrder("X", "Y", "Z"); } } @Test void createAllNodesWithMultipleLabels(@Autowired ReactiveMultipleLabelWithAssignedIdRepository repository) { repository.saveAll(singletonList(new MultipleLabels.MultipleLabelsEntityWithAssignedId(4711L))) .collectList().block(); try (Session session = createSession()) { Node node = session.run("MATCH (n:X) return n").single().get("n").asNode(); assertThat(node.labels()).containsExactlyInAnyOrder("X", "Y", "Z"); } } @Test void createNodeAndRelationshipWithMultipleLabels(@Autowired ReactiveMultipleLabelWithAssignedIdRepository repository) { MultipleLabels.MultipleLabelsEntityWithAssignedId entity = new MultipleLabels.MultipleLabelsEntityWithAssignedId(4711L); entity.otherMultipleLabelEntity = new MultipleLabels.MultipleLabelsEntityWithAssignedId(42L); repository.save(entity).block(); try (Session session = createSession()) { Record record = session.run("MATCH (n:X)-[:HAS]->(c:X) return n, c").single(); Node parentNode = record.get("n").asNode(); Node childNode = record.get("c").asNode(); assertThat(parentNode.labels()).containsExactlyInAnyOrder("X", "Y", "Z"); assertThat(childNode.labels()).containsExactlyInAnyOrder("X", "Y", "Z"); } } @Test void findNodeWithMultipleLabels(@Autowired ReactiveMultipleLabelWithAssignedIdRepository repository) { long n1Id; long n2Id; long n3Id; try (Session session = createSession()) { Record record = session.run("CREATE (n1:X:Y:Z{id:4711}), (n2:Y:Z{id:42}), (n3:X{id:23}) return n1, n2, n3").single(); n1Id = record.get("n1").asNode().get("id").asLong(); n2Id = record.get("n2").asNode().get("id").asLong(); n3Id = record.get("n3").asNode().get("id").asLong(); } StepVerifier.create(repository.findById(n1Id)) .expectNextCount(1) .verifyComplete(); StepVerifier.create(repository.findById(n2Id)) .verifyComplete(); StepVerifier.create(repository.findById(n3Id)) .verifyComplete(); } @Test void deleteNodeWithMultipleLabels(@Autowired ReactiveMultipleLabelWithAssignedIdRepository repository) { long n1Id; long n2Id; long n3Id; try (Session session = createSession()) { Record record = session.run("CREATE (n1:X:Y:Z{id:4711}), (n2:Y:Z{id:42}), (n3:X{id:23}) return n1, n2, n3").single(); n1Id = record.get("n1").asNode().get("id").asLong(); n2Id = record.get("n2").asNode().get("id").asLong(); n3Id = record.get("n3").asNode().get("id").asLong(); } repository.deleteById(n1Id).block(); repository.deleteById(n2Id).block(); repository.deleteById(n3Id).block(); try (Session session = createSession()) { assertThat(session.run("MATCH (n:X:Y:Z) return n").list()).hasSize(0); assertThat(session.run("MATCH (n:Y:Z) return n").list()).hasSize(1); assertThat(session.run("MATCH (n:X) return n").list()).hasSize(1); } } } @Nested class Converter extends ReactiveIntegrationTestBase { @Test void findByConvertedCustomType(@Autowired EntityWithCustomTypePropertyRepository repository) { try (Session session = createSession()) { session.run("CREATE (:CustomTypes{customType:'XYZ'})"); } StepVerifier.create(repository.findByCustomType(ThingWithCustomTypes.CustomType.of("XYZ"))) .expectNextCount(1) .verifyComplete(); } @Test void findByConvertedCustomTypeWithCustomQuery(@Autowired EntityWithCustomTypePropertyRepository repository) { try (Session session = createSession()) { session.run("CREATE (:CustomTypes{customType:'XYZ'})"); } StepVerifier.create(repository.findByCustomTypeCustomQuery(ThingWithCustomTypes.CustomType.of("XYZ"))) .expectNextCount(1) .verifyComplete(); } @Test void findByConvertedCustomTypeWithSpELPropertyAccessQuery(@Autowired EntityWithCustomTypePropertyRepository repository) { try (Session session = createSession()) { session.run("CREATE (:CustomTypes{customType:'XYZ'})"); } StepVerifier.create(repository.findByCustomTypeCustomSpELPropertyAccessQuery(ThingWithCustomTypes.CustomType.of("XYZ"))) .expectNextCount(1) .verifyComplete(); } @Test void findByConvertedCustomTypeWithSpELObjectQuery(@Autowired EntityWithCustomTypePropertyRepository repository) { try (Session session = createSession()) { session.run("CREATE (:CustomTypes{customType:'XYZ'})"); } StepVerifier.create(repository.findByCustomTypeSpELObjectQuery(ThingWithCustomTypes.CustomType.of("XYZ"))) .expectNextCount(1) .verifyComplete(); } } interface BidirectionalStartRepository extends ReactiveNeo4jRepository<BidirectionalStart, Long> { } interface BidirectionalEndRepository extends ReactiveNeo4jRepository<BidirectionalEnd, Long> { } interface ImmutablePersonRepository extends ReactiveNeo4jRepository<ImmutablePerson, String> { } interface ReactiveDeepRelationshipRepository extends ReactiveNeo4jRepository<DeepRelationships.Type1, Long> { } interface ReactiveLoopingRelationshipRepository extends ReactiveNeo4jRepository<DeepRelationships.LoopingType1, Long> { } interface ReactiveMultipleLabelRepository extends ReactiveNeo4jRepository<MultipleLabels.MultipleLabelsEntity, Long> { } interface ReactiveMultipleLabelWithAssignedIdRepository extends ReactiveNeo4jRepository<MultipleLabels.MultipleLabelsEntityWithAssignedId, Long> { } interface ReactivePersonWithRelationshipWithPropertiesRepository extends ReactiveNeo4jRepository<PersonWithRelationshipWithProperties, Long> { @Query("MATCH (p:PersonWithRelationshipWithProperties)-[l:LIKES]->(h:Hobby) return p, collect(l), collect(h)") Mono<PersonWithRelationshipWithProperties> loadFromCustomQuery(@Param("id") Long id); Mono<PersonWithRelationshipWithProperties> findByHobbiesSince(int since); Mono<PersonWithRelationshipWithProperties> findByHobbiesSinceOrHobbiesActive(int since1, boolean active); Mono<PersonWithRelationshipWithProperties> findByHobbiesSinceAndHobbiesActive(int since1, boolean active); } interface ReactivePetRepository extends ReactiveNeo4jRepository<Pet, Long> { } interface ReactiveRelationshipRepository extends ReactiveNeo4jRepository<PersonWithRelationship, Long> { @Query("MATCH (n:PersonWithRelationship{name:'Freddie'}) " + "OPTIONAL MATCH (n)-[r1:Has]->(p:Pet) WITH n, collect(r1) as petRels, collect(p) as pets " + "OPTIONAL MATCH (n)-[r2:Has]->(h:Hobby) " + "return n, petRels, pets, collect(r2) as hobbyRels, collect(h) as hobbies") Mono<PersonWithRelationship> getPersonWithRelationshipsViaQuery(); Mono<PersonWithRelationship> findByPetsName(String petName); Mono<PersonWithRelationship> findByHobbiesNameOrPetsName(String hobbyName, String petName); Mono<PersonWithRelationship> findByHobbiesNameAndPetsName(String hobbyName, String petName); Mono<PersonWithRelationship> findByPetsHobbiesName(String hobbyName); Mono<PersonWithRelationship> findByPetsFriendsName(String petName); } interface ReactiveSimilarThingRepository extends ReactiveCrudRepository<SimilarThing, Long> { } interface EntityWithConvertedIdRepository extends ReactiveNeo4jRepository<EntityWithConvertedId, EntityWithConvertedId.IdentifyingEnum> { } interface EntityWithCustomTypePropertyRepository extends ReactiveNeo4jRepository<ThingWithCustomTypes, Long> { Mono<ThingWithCustomTypes> findByCustomType(ThingWithCustomTypes.CustomType customType); @Query("MATCH (c:CustomTypes) WHERE c.customType = $customType return c") Mono<ThingWithCustomTypes> findByCustomTypeCustomQuery(@Param("customType") ThingWithCustomTypes.CustomType customType); @Query("MATCH (c:CustomTypes) WHERE c.customType = :#{#customType.value} return c") Mono<ThingWithCustomTypes> findByCustomTypeCustomSpELPropertyAccessQuery(@Param("customType") ThingWithCustomTypes.CustomType customType); @Query("MATCH (c:CustomTypes) WHERE c.customType = :#{#customType} return c") Mono<ThingWithCustomTypes> findByCustomTypeSpELObjectQuery(@Param("customType") ThingWithCustomTypes.CustomType customType); } @SpringJUnitConfig(ReactiveRepositoryIT.Config.class) static abstract class ReactiveIntegrationTestBase { @Autowired private Driver driver; @Autowired private ReactiveTransactionManager transactionManager; void setupData(Transaction transaction) { } @BeforeEach void before() { Session session = createSession(); session.writeTransaction(tx -> { tx.run("MATCH (n) detach delete n").consume(); setupData(tx); return null; }); session.close(); } Session createSession() { return driver.session(Optional.ofNullable(databaseSelection.getValue()) .map(SessionConfig::forDatabase).orElseGet(SessionConfig::defaultConfig)); } RxSession createRxSession() { return driver.rxSession(Optional.ofNullable(databaseSelection.getValue()) .map(SessionConfig::forDatabase).orElseGet(SessionConfig::defaultConfig)); } ReactiveTransactionManager getTransactionManager() { return transactionManager; } } @Configuration @EnableReactiveNeo4jRepositories(considerNestedRepositories = true) @EnableTransactionManagement static class Config extends AbstractReactiveNeo4jConfig { @Bean public Driver driver() { return neo4jConnectionSupport.getDriver(); } @Override public Neo4jConversions neo4jConversions() { Set<GenericConverter> additionalConverters = new HashSet<>(); additionalConverters.add(new ThingWithCustomTypes.CustomTypeConverter()); return new Neo4jConversions(additionalConverters); } @Override protected Collection<String> getMappingBasePackages() { return singletonList(PersonWithAllConstructor.class.getPackage().getName()); } @Override @Bean protected ReactiveDatabaseSelectionProvider reactiveNeo4jDatabaseNameProvider() { return Optional.ofNullable(databaseSelection.getValue()) .map(ReactiveDatabaseSelectionProvider::createStaticDatabaseSelectionProvider) .orElse(ReactiveDatabaseSelectionProvider.getDefaultSelectionProvider()); } } }