diff --git a/src/main/asciidoc/object-mapping/sdn-extensions.adoc b/src/main/asciidoc/object-mapping/sdn-extensions.adoc index f36da52b7c..1cc3d60f59 100644 --- a/src/main/asciidoc/object-mapping/sdn-extensions.adoc +++ b/src/main/asciidoc/object-mapping/sdn-extensions.adoc @@ -19,6 +19,8 @@ Additional mixins provided are: * `QuerydslPredicateExecutor` * `CypherdslConditionExecutor` * `CypherdslStatementExecutor` +* `ReactiveQuerydslPredicateExecutor` +* `ReactiveCypherdslConditionExecutor` * `ReactiveCypherdslStatementExecutor` [[sdn-mixins.dynamic-conditions]] diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/CypherdslConditionExecutorImpl.java b/src/main/java/org/springframework/data/neo4j/repository/query/CypherdslConditionExecutorImpl.java index 04bf4715ce..bd659f36b2 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/CypherdslConditionExecutorImpl.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/CypherdslConditionExecutorImpl.java @@ -131,8 +131,6 @@ public long count(Condition condition) { @Override public boolean exists(Condition condition) { - Statement statement = CypherGenerator.INSTANCE.prepareMatchOf(this.metaData, condition) - .returning(Functions.count(asterisk())).build(); - return this.neo4jOperations.count(statement, statement.getParameters()) > 0; + return count(condition) > 0; } } diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveCypherdslConditionExecutorImpl.java b/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveCypherdslConditionExecutorImpl.java new file mode 100644 index 0000000000..82b99cfb0f --- /dev/null +++ b/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveCypherdslConditionExecutorImpl.java @@ -0,0 +1,123 @@ +/* + * Copyright 2011-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.neo4j.repository.query; + +import static org.neo4j.cypherdsl.core.Cypher.asterisk; + +import java.util.Arrays; + +import org.apiguardian.api.API; +import org.neo4j.cypherdsl.core.Condition; +import org.neo4j.cypherdsl.core.Conditions; +import org.neo4j.cypherdsl.core.Functions; +import org.neo4j.cypherdsl.core.SortItem; +import org.neo4j.cypherdsl.core.Statement; +import org.springframework.data.domain.Sort; +import org.springframework.data.neo4j.core.ReactiveNeo4jOperations; +import org.springframework.data.neo4j.core.mapping.CypherGenerator; +import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity; +import org.springframework.data.neo4j.repository.support.ReactiveCypherdslConditionExecutor; +import org.springframework.data.neo4j.repository.support.Neo4jEntityInformation; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * @author Niklas Krieger + * @author Michael J. Simons + * @param The returned domain type. + * @since 6.3.3 + */ +@API(status = API.Status.INTERNAL, since = "6.3.3") +public final class ReactiveCypherdslConditionExecutorImpl implements ReactiveCypherdslConditionExecutor { + + private final Neo4jEntityInformation entityInformation; + + private final ReactiveNeo4jOperations neo4jOperations; + + private final Neo4jPersistentEntity metaData; + + public ReactiveCypherdslConditionExecutorImpl(Neo4jEntityInformation entityInformation, + ReactiveNeo4jOperations neo4jOperations) { + + this.entityInformation = entityInformation; + this.neo4jOperations = neo4jOperations; + this.metaData = this.entityInformation.getEntityMetaData(); + } + + @Override + public Mono findOne(Condition condition) { + + return this.neo4jOperations.toExecutableQuery( + this.metaData.getType(), + QueryFragmentsAndParameters.forCondition(this.metaData, condition, null, null) + ).flatMap(ReactiveNeo4jOperations.ExecutableQuery::getSingleResult); + } + + @Override + public Flux findAll(Condition condition) { + + return this.neo4jOperations.toExecutableQuery( + this.metaData.getType(), + QueryFragmentsAndParameters.forCondition(this.metaData, condition, null, null) + ).flatMapMany(ReactiveNeo4jOperations.ExecutableQuery::getResults); + } + + @Override + public Flux findAll(Condition condition, Sort sort) { + + return this.neo4jOperations.toExecutableQuery( + metaData.getType(), + QueryFragmentsAndParameters.forCondition( + this.metaData, condition, null, CypherAdapterUtils.toSortItems(this.metaData, sort) + ) + ).flatMapMany(ReactiveNeo4jOperations.ExecutableQuery::getResults); + } + + @Override + public Flux findAll(Condition condition, SortItem... sortItems) { + + return this.neo4jOperations.toExecutableQuery( + this.metaData.getType(), + QueryFragmentsAndParameters.forCondition( + this.metaData, condition, null, Arrays.asList(sortItems) + ) + ).flatMapMany(ReactiveNeo4jOperations.ExecutableQuery::getResults); + } + + @Override + public Flux findAll(SortItem... sortItems) { + + return this.neo4jOperations.toExecutableQuery( + this.metaData.getType(), + QueryFragmentsAndParameters.forCondition(this.metaData, Conditions.noCondition(), null, + Arrays.asList(sortItems)) + ).flatMapMany(ReactiveNeo4jOperations.ExecutableQuery::getResults); + } + + @Override + public Mono count(Condition condition) { + + Statement statement = CypherGenerator.INSTANCE.prepareMatchOf(this.metaData, condition) + .returning(Functions.count(asterisk())).build(); + return this.neo4jOperations.count(statement, statement.getParameters()); + } + + @Override + public Mono exists(Condition condition) { + return count(condition).map(count -> count > 0); + } +} diff --git a/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveCypherdslConditionExecutor.java b/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveCypherdslConditionExecutor.java new file mode 100644 index 0000000000..2bf85f61f3 --- /dev/null +++ b/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveCypherdslConditionExecutor.java @@ -0,0 +1,52 @@ +/* + * Copyright 2011-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.neo4j.repository.support; + +import org.apiguardian.api.API; +import org.neo4j.cypherdsl.core.Condition; +import org.neo4j.cypherdsl.core.SortItem; +import org.springframework.data.domain.Sort; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * An interface that can be added to any repository so that queries can be enriched by {@link Condition conditions} of the + * Cypher-DSL. This interface behaves the same as the {@link org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor}. + * + * @author Niklas Krieger + * @author Michael J. Simons + * @param Type of the domain + * @since 6.3.3 + */ +@API(status = API.Status.STABLE, since = "6.3.3") +public interface ReactiveCypherdslConditionExecutor { + + Mono findOne(Condition condition); + + Flux findAll(Condition condition); + + Flux findAll(Condition condition, Sort sort); + + Flux findAll(Condition condition, SortItem... sortItems); + + Flux findAll(SortItem... sortItems); + + Mono count(Condition condition); + + Mono exists(Condition condition); +} + diff --git a/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactory.java b/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactory.java index 7573865809..1c17cc9d11 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactory.java +++ b/src/main/java/org/springframework/data/neo4j/repository/support/ReactiveNeo4jRepositoryFactory.java @@ -27,6 +27,7 @@ import org.springframework.data.neo4j.repository.query.ReactiveNeo4jQueryLookupStrategy; import org.springframework.data.neo4j.repository.query.ReactiveQuerydslNeo4jPredicateExecutor; import org.springframework.data.neo4j.repository.query.SimpleReactiveQueryByExampleExecutor; +import org.springframework.data.neo4j.repository.query.ReactiveCypherdslConditionExecutorImpl; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.querydsl.QuerydslUtils; @@ -45,6 +46,7 @@ * * @author Gerrit Meier * @author Michael J. Simons + * @author Niklas Krieger * @since 6.0 */ final class ReactiveNeo4jRepositoryFactory extends ReactiveRepositoryFactorySupport { @@ -93,6 +95,11 @@ protected RepositoryFragments getRepositoryFragments(RepositoryMetadata metadata fragments = fragments.append(createDSLExecutorFragment(metadata, ReactiveQuerydslNeo4jPredicateExecutor.class)); } + if (ReactiveCypherdslConditionExecutor.class.isAssignableFrom(metadata.getRepositoryInterface())) { + + fragments = fragments.append(createDSLExecutorFragment(metadata, ReactiveCypherdslConditionExecutorImpl.class)); + } + return fragments; } diff --git a/src/test/java/org/springframework/data/neo4j/integration/imperative/CypherdslConditionExecutorIT.java b/src/test/java/org/springframework/data/neo4j/integration/imperative/CypherdslConditionExecutorIT.java index 73a4769c09..1045bbf22c 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/imperative/CypherdslConditionExecutorIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/imperative/CypherdslConditionExecutorIT.java @@ -58,17 +58,11 @@ class CypherdslConditionExecutorIT { protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; - private final Driver driver; - private final BookmarkCapture bookmarkCapture; - private final Node person; private final Property firstName; private final Property lastName; @Autowired - CypherdslConditionExecutorIT(Driver driver, BookmarkCapture bookmarkCapture) { - - this.driver = driver; - this.bookmarkCapture = bookmarkCapture; + CypherdslConditionExecutorIT() { //CHECKSTYLE:OFF // tag::sdn-mixins.dynamic-conditions.usage[] @@ -78,7 +72,6 @@ class CypherdslConditionExecutorIT { // end::sdn-mixins.dynamic-conditions.usage[] //CHECKSTYLE:ON - this.person = person; this.firstName = firstName; this.lastName = lastName; } diff --git a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveCypherdslConditionExecutorIT.java b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveCypherdslConditionExecutorIT.java new file mode 100644 index 0000000000..cc619c5440 --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveCypherdslConditionExecutorIT.java @@ -0,0 +1,192 @@ +/* + * Copyright 2011-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.neo4j.integration.reactive; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.neo4j.cypherdsl.core.Cypher; +import org.neo4j.cypherdsl.core.Node; +import org.neo4j.cypherdsl.core.Property; +import org.neo4j.driver.Driver; +import org.neo4j.driver.Session; +import org.neo4j.driver.Transaction; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.domain.Sort; +import org.springframework.data.neo4j.core.ReactiveDatabaseSelectionProvider; +import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager; +import org.springframework.data.neo4j.core.transaction.ReactiveNeo4jTransactionManager; +import org.springframework.data.neo4j.integration.shared.common.Person; +import org.springframework.data.neo4j.repository.ReactiveNeo4jRepository; +import org.springframework.data.neo4j.repository.config.EnableReactiveNeo4jRepositories; +import org.springframework.data.neo4j.repository.support.ReactiveCypherdslConditionExecutor; +import org.springframework.data.neo4j.test.BookmarkCapture; +import org.springframework.data.neo4j.test.Neo4jExtension; +import org.springframework.data.neo4j.test.Neo4jIntegrationTest; +import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration; +import org.springframework.transaction.ReactiveTransactionManager; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import reactor.test.StepVerifier; + +/** + * @author Niklas Krieger + * @author Michael J. Simons + */ +@Tag(Neo4jExtension.NEEDS_REACTIVE_SUPPORT) +@Neo4jIntegrationTest +class ReactiveCypherdslConditionExecutorIT { + + protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; + + private final Node person = Cypher.node("Person").named("person"); + private final Property firstName = person.property("firstName"); + private final Property lastName = person.property("lastName"); + + @BeforeAll + protected static void setupData(@Autowired BookmarkCapture bookmarkCapture) { + try (Session session = neo4jConnectionSupport.getDriver().session(bookmarkCapture.createSessionConfig()); + Transaction transaction = session.beginTransaction()) { + transaction.run("MATCH (n) detach delete n"); + transaction.run("CREATE (p:Person{firstName: 'A', lastName: 'LA'})"); + transaction.run("CREATE (p:Person{firstName: 'B', lastName: 'LB'})"); + transaction + .run("CREATE (p:Person{firstName: 'Helge', lastName: 'Schneider'}) -[:LIVES_AT]-> (a:Address {city: 'Mülheim an der Ruhr'})"); + transaction.run("CREATE (p:Person{firstName: 'Bela', lastName: 'B.'})"); + transaction.commit(); + bookmarkCapture.seedWith(session.lastBookmark()); + } + } + + @Test + void findOneShouldWork(@Autowired PersonRepository repository) { + + repository.findOne(firstName.eq(Cypher.literalOf("Helge"))) + .as(StepVerifier::create) + .expectNextMatches(p -> p.getLastName().equals("Schneider")) + .verifyComplete(); + } + + @Test + void findAllShouldWork(@Autowired PersonRepository repository) { + + repository.findAll(firstName.eq(Cypher.literalOf("Helge")).or(lastName.eq(Cypher.literalOf("B.")))) + .map(Person::getFirstName) + .sort() + .as(StepVerifier::create) + .expectNext("Bela", "Helge") + .verifyComplete(); + } + + @Test + void sortedFindAllShouldWork(@Autowired PersonRepository repository) { + + repository.findAll(firstName.eq(Cypher.literalOf("Helge")).or(lastName.eq(Cypher.literalOf("B."))), + Sort.by("lastName").descending() + ) + .map(Person::getFirstName) + .as(StepVerifier::create) + .expectNext("Helge", "Bela") + .verifyComplete(); + } + + @Test + void sortedFindAllShouldWorkWithParameter(@Autowired PersonRepository repository) { + + repository.findAll( + firstName.eq(Cypher.anonParameter("Helge")) + .or(lastName.eq(Cypher.parameter("someName", "B."))), // <.> + lastName.descending() // <.> + ) + .map(Person::getFirstName) + .as(StepVerifier::create) + .expectNext("Helge", "Bela") + .verifyComplete(); + } + + @Test + void orderedFindAllShouldWork(@Autowired PersonRepository repository) { + + repository.findAll(firstName.eq(Cypher.literalOf("Helge")).or(lastName.eq(Cypher.literalOf("B."))), + Sort.by("lastName").descending() + ) + .map(Person::getFirstName) + .as(StepVerifier::create) + .expectNext("Helge", "Bela") + .verifyComplete(); + } + + @Test + void orderedFindAllWithoutPredicateShouldWork(@Autowired PersonRepository repository) { + + repository.findAll(lastName.descending()) + .map(Person::getFirstName) + .as(StepVerifier::create) + .expectNext("Helge", "B", "A", "Bela") + .verifyComplete(); + } + + @Test + void countShouldWork(@Autowired PersonRepository repository) { + + repository.count(firstName.eq(Cypher.literalOf("Helge")).or(lastName.eq(Cypher.literalOf("B.")))) + .as(StepVerifier::create) + .expectNext(2L) + .verifyComplete(); + } + + @Test + void existsShouldWork(@Autowired PersonRepository repository) { + + repository.exists(firstName.eq(Cypher.literalOf("A"))) + .as(StepVerifier::create) + .expectNext(true) + .verifyComplete(); + } + + interface PersonRepository extends ReactiveNeo4jRepository, ReactiveCypherdslConditionExecutor { + } + + @Configuration + @EnableTransactionManagement + @EnableReactiveNeo4jRepositories(considerNestedRepositories = true) + static class Config extends Neo4jReactiveTestConfiguration { + + @Bean + public Driver driver() { + + return neo4jConnectionSupport.getDriver(); + } + + @Bean + public BookmarkCapture bookmarkCapture() { + return new BookmarkCapture(); + } + + @Override + public ReactiveTransactionManager reactiveTransactionManager(Driver driver, ReactiveDatabaseSelectionProvider databaseSelectionProvider) { + + BookmarkCapture bookmarkCapture = bookmarkCapture(); + return new ReactiveNeo4jTransactionManager(driver, databaseSelectionProvider, Neo4jBookmarkManager.create(bookmarkCapture)); + } + + @Override + public boolean isCypher5Compatible() { + return neo4jConnectionSupport.isCypher5SyntaxCompatible(); + } + } +}