diff --git a/pom.xml b/pom.xml index 2453bf9af0..e275a5706c 100644 --- a/pom.xml +++ b/pom.xml @@ -24,7 +24,7 @@ org.springframework.data spring-data-neo4j - 6.2.0-SNAPSHOT + 6.2.0-2343-SNAPSHOT Spring Data Neo4j Next generation Object-Graph-Mapping for Spring Data. @@ -117,7 +117,7 @@ ${skipTests} ${skipTests} - 2.6.0-SNAPSHOT + 2.6.0-2228-SNAPSHOT ../../../../spring-data-commons/src/main/asciidoc diff --git a/src/main/java/org/springframework/data/neo4j/core/FluentFindOperation.java b/src/main/java/org/springframework/data/neo4j/core/FluentFindOperation.java index 25ddc676fb..2ef5dacb38 100644 --- a/src/main/java/org/springframework/data/neo4j/core/FluentFindOperation.java +++ b/src/main/java/org/springframework/data/neo4j/core/FluentFindOperation.java @@ -22,6 +22,7 @@ import org.apiguardian.api.API; import org.neo4j.cypherdsl.core.Statement; +import org.springframework.data.neo4j.repository.query.QueryFragmentsAndParameters; import org.springframework.lang.Nullable; /** @@ -106,6 +107,16 @@ interface FindWithQuery extends TerminatingFindWithoutQuery { */ TerminatingFind matching(String query, @Nullable Map parameter); + /** + * Creates an executable query based on fragments and parameters. Hardly useful outside framework-code + * and we actively discourage using this method. + * + * @param queryFragmentsAndParameters Encapsulated query fragements and parameters as created by the repository abstraction. + * @return new instance of {@link TerminatingFind}. + * @throws IllegalArgumentException if queryFragmentsAndParameters is {@literal null}. + */ + TerminatingFind matching(QueryFragmentsAndParameters queryFragmentsAndParameters); + /** * Set the filter query to be used. * diff --git a/src/main/java/org/springframework/data/neo4j/core/FluentOperationSupport.java b/src/main/java/org/springframework/data/neo4j/core/FluentOperationSupport.java index c3af0fd1aa..fd212ca9b9 100644 --- a/src/main/java/org/springframework/data/neo4j/core/FluentOperationSupport.java +++ b/src/main/java/org/springframework/data/neo4j/core/FluentOperationSupport.java @@ -19,6 +19,7 @@ import java.util.List; import java.util.Map; +import org.springframework.data.neo4j.repository.query.QueryFragmentsAndParameters; import org.springframework.util.Assert; /** @@ -51,6 +52,7 @@ private static class ExecutableFindSupport private final Class returnType; private final String query; private final Map parameters; + private final QueryFragmentsAndParameters queryFragmentsAndParameters; ExecutableFindSupport(Neo4jTemplate template, Class domainType, Class returnType, String query, Map parameters) { @@ -59,6 +61,16 @@ private static class ExecutableFindSupport this.returnType = returnType; this.query = query; this.parameters = parameters; + this.queryFragmentsAndParameters = null; + } + + ExecutableFindSupport(Neo4jTemplate template, Class domainType, Class returnType, QueryFragmentsAndParameters queryFragmentsAndParameters) { + this.template = template; + this.domainType = domainType; + this.returnType = returnType; + this.query = null; + this.parameters = null; + this.queryFragmentsAndParameters = queryFragmentsAndParameters; } @Override @@ -79,6 +91,15 @@ public TerminatingFind matching(String query, Map parameters) return new ExecutableFindSupport<>(template, domainType, returnType, query, parameters); } + @Override + @SuppressWarnings("HiddenField") + public TerminatingFind matching(QueryFragmentsAndParameters queryFragmentsAndParameters) { + + Assert.notNull(queryFragmentsAndParameters, "Query fragments must not be null!"); + + return new ExecutableFindSupport<>(template, domainType, returnType, queryFragmentsAndParameters); + } + @Override public T oneValue() { @@ -95,7 +116,7 @@ public List all() { } private List doFind(TemplateSupport.FetchType fetchType) { - return template.doFind(query, parameters, domainType, returnType, fetchType); + return template.doFind(query, parameters, domainType, returnType, fetchType, queryFragmentsAndParameters); } } diff --git a/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java b/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java index 5d5d7e56d3..bd49abc3db 100644 --- a/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java +++ b/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java @@ -245,14 +245,19 @@ public ExecutableFind find(Class domainType) { } @SuppressWarnings("unchecked") - List doFind(@Nullable String cypherQuery, @Nullable Map parameters, Class domainType, Class resultType, TemplateSupport.FetchType fetchType) { + List doFind(@Nullable String cypherQuery, @Nullable Map parameters, Class domainType, Class resultType, TemplateSupport.FetchType fetchType, @Nullable QueryFragmentsAndParameters queryFragmentsAndParameters) { List intermediaResults = Collections.emptyList(); - if (cypherQuery == null && fetchType == TemplateSupport.FetchType.ALL) { + if (cypherQuery == null && queryFragmentsAndParameters == null && fetchType == TemplateSupport.FetchType.ALL) { intermediaResults = doFindAll(domainType, resultType); } else { - ExecutableQuery executableQuery = createExecutableQuery(domainType, resultType, cypherQuery, - parameters == null ? Collections.emptyMap() : parameters); + ExecutableQuery executableQuery; + if (queryFragmentsAndParameters == null) { + executableQuery = createExecutableQuery(domainType, resultType, cypherQuery, + parameters == null ? Collections.emptyMap() : parameters); + } else { + executableQuery = createExecutableQuery(domainType, resultType, queryFragmentsAndParameters); + } switch (fetchType) { case ALL: intermediaResults = executableQuery.getResults(); diff --git a/src/main/java/org/springframework/data/neo4j/core/ReactiveFluentFindOperation.java b/src/main/java/org/springframework/data/neo4j/core/ReactiveFluentFindOperation.java index af2ee66c2e..16c22f6264 100644 --- a/src/main/java/org/springframework/data/neo4j/core/ReactiveFluentFindOperation.java +++ b/src/main/java/org/springframework/data/neo4j/core/ReactiveFluentFindOperation.java @@ -23,6 +23,7 @@ import org.apiguardian.api.API; import org.neo4j.cypherdsl.core.Statement; +import org.springframework.data.neo4j.repository.query.QueryFragmentsAndParameters; import org.springframework.lang.Nullable; /** @@ -96,6 +97,16 @@ interface FindWithQuery extends TerminatingFindWithoutQuery { */ TerminatingFind matching(String query, @Nullable Map parameter); + /** + * Creates an executable query based on fragments and parameters. Hardly useful outside framework-code + * and we actively discourage using this method. + * + * @param queryFragmentsAndParameters Encapsulated query fragements and parameters as created by the repository abstraction. + * @return new instance of {@link FluentFindOperation.TerminatingFind}. + * @throws IllegalArgumentException if queryFragmentsAndParameters is {@literal null}. + */ + TerminatingFind matching(QueryFragmentsAndParameters queryFragmentsAndParameters); + /** * Set the filter query to be used. * diff --git a/src/main/java/org/springframework/data/neo4j/core/ReactiveFluentOperationSupport.java b/src/main/java/org/springframework/data/neo4j/core/ReactiveFluentOperationSupport.java index 9295aeefcc..658bfa7f5d 100644 --- a/src/main/java/org/springframework/data/neo4j/core/ReactiveFluentOperationSupport.java +++ b/src/main/java/org/springframework/data/neo4j/core/ReactiveFluentOperationSupport.java @@ -21,6 +21,7 @@ import java.util.Collections; import java.util.Map; +import org.springframework.data.neo4j.repository.query.QueryFragmentsAndParameters; import org.springframework.util.Assert; /** @@ -54,6 +55,7 @@ private static class ExecutableFindSupport private final Class returnType; private final String query; private final Map parameters; + private final QueryFragmentsAndParameters queryFragmentsAndParameters; ExecutableFindSupport(ReactiveNeo4jTemplate template, Class domainType, Class returnType, String query, Map parameters) { @@ -62,6 +64,16 @@ private static class ExecutableFindSupport this.returnType = returnType; this.query = query; this.parameters = parameters; + this.queryFragmentsAndParameters = null; + } + + ExecutableFindSupport(ReactiveNeo4jTemplate template, Class domainType, Class returnType, QueryFragmentsAndParameters queryFragmentsAndParameters) { + this.template = template; + this.domainType = domainType; + this.returnType = returnType; + this.query = null; + this.parameters = null; + this.queryFragmentsAndParameters = queryFragmentsAndParameters; } @Override @@ -82,6 +94,13 @@ public TerminatingFind matching(String query, Map parameters) return new ExecutableFindSupport<>(template, domainType, returnType, query, parameters); } + @Override + @SuppressWarnings("HiddenField") + public TerminatingFind matching(QueryFragmentsAndParameters queryFragmentsAndParameters) { + + return new ExecutableFindSupport<>(template, domainType, returnType, queryFragmentsAndParameters); + } + @Override public Mono one() { return doFind(TemplateSupport.FetchType.ONE).single(); @@ -93,7 +112,7 @@ public Flux all() { } private Flux doFind(TemplateSupport.FetchType fetchType) { - return template.doFind(query, parameters, domainType, returnType, fetchType); + return template.doFind(query, parameters, domainType, returnType, fetchType, queryFragmentsAndParameters); } } diff --git a/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jTemplate.java b/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jTemplate.java index d9a9b2bf3b..6ea5dec5d6 100644 --- a/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jTemplate.java +++ b/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jTemplate.java @@ -225,14 +225,19 @@ public ExecutableFind find(Class domainType) { } @SuppressWarnings("unchecked") - Flux doFind(@Nullable String cypherQuery, @Nullable Map parameters, Class domainType, Class resultType, TemplateSupport.FetchType fetchType) { + Flux doFind(@Nullable String cypherQuery, @Nullable Map parameters, Class domainType, Class resultType, TemplateSupport.FetchType fetchType, @Nullable QueryFragmentsAndParameters queryFragmentsAndParameters) { Flux intermediaResults = null; - if (cypherQuery == null && fetchType == TemplateSupport.FetchType.ALL) { + if (cypherQuery == null && queryFragmentsAndParameters == null && fetchType == TemplateSupport.FetchType.ALL) { intermediaResults = doFindAll(domainType, resultType); } else { - Mono> executableQuery = createExecutableQuery(domainType, resultType, cypherQuery, - parameters == null ? Collections.emptyMap() : parameters); + Mono> executableQuery; + if (queryFragmentsAndParameters == null) { + executableQuery = createExecutableQuery(domainType, resultType, cypherQuery, + parameters == null ? Collections.emptyMap() : parameters); + } else { + executableQuery = createExecutableQuery(domainType, resultType, queryFragmentsAndParameters); + } switch (fetchType) { case ALL: diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/FetchableFluentQueryByExample.java b/src/main/java/org/springframework/data/neo4j/repository/query/FetchableFluentQueryByExample.java new file mode 100644 index 0000000000..c87dc8bd5d --- /dev/null +++ b/src/main/java/org/springframework/data/neo4j/repository/query/FetchableFluentQueryByExample.java @@ -0,0 +1,166 @@ +/* + * Copyright 2011-2021 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 java.util.Collection; +import java.util.List; +import java.util.function.Function; +import java.util.function.LongSupplier; +import java.util.stream.Stream; + +import org.apiguardian.api.API; +import org.springframework.data.domain.Example; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.neo4j.core.FluentFindOperation; +import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; +import org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery; +import org.springframework.data.support.PageableExecutionUtils; +import org.springframework.lang.Nullable; + +/** + * Immutable implementation of a {@link FetchableFluentQuery}. All + * methods that return a {@link FetchableFluentQuery} return a new instance, the original instance won't be + * modified. + * + * @author Michael J. Simons + * @param Source type + * @param Result type + * @since 6.2 + */ +@API(status = API.Status.INTERNAL, since = "6.2") +final class FetchableFluentQueryByExample extends FluentQuerySupport implements FetchableFluentQuery { + + private final Neo4jMappingContext mappingContext; + + private final Example example; + + private final FluentFindOperation findOperation; + + private final Function, Long> countOperation; + + private final Function, Boolean> existsOperation; + + FetchableFluentQueryByExample( + Example example, + Class resultType, + Neo4jMappingContext mappingContext, + FluentFindOperation findOperation, + Function, Long> countOperation, + Function, Boolean> existsOperation + ) { + this(example, resultType, mappingContext, findOperation, countOperation, existsOperation, Sort.unsorted(), + null); + } + + FetchableFluentQueryByExample( + Example example, + Class resultType, + Neo4jMappingContext mappingContext, + FluentFindOperation findOperation, + Function, Long> countOperation, + Function, Boolean> existsOperation, + Sort sort, + @Nullable Collection properties + ) { + super(resultType, sort, properties); + this.mappingContext = mappingContext; + this.example = example; + this.findOperation = findOperation; + this.countOperation = countOperation; + this.existsOperation = existsOperation; + } + + @Override + @SuppressWarnings("HiddenField") + public FetchableFluentQuery sortBy(Sort sort) { + + return new FetchableFluentQueryByExample<>(this.example, this.resultType, this.mappingContext, this.findOperation, + this.countOperation, this.existsOperation, this.sort.and(sort), this.properties); + } + + @Override + @SuppressWarnings("HiddenField") + public FetchableFluentQuery as(Class resultType) { + + return new FetchableFluentQueryByExample<>(this.example, resultType, this.mappingContext, this.findOperation, + this.countOperation, this.existsOperation); + } + + @Override + @SuppressWarnings("HiddenField") + public FetchableFluentQuery project(Collection properties) { + + return new FetchableFluentQueryByExample<>(this.example, this.resultType, this.mappingContext, this.findOperation, + this.countOperation, this.existsOperation, this.sort, mergeProperties(properties)); + } + + @Override + public R oneValue() { + + return findOperation.find(example.getProbeType()) + .as(resultType) + .matching(QueryFragmentsAndParameters.forExample(mappingContext, example, sort, + createIncludedFieldsPredicate())) + .oneValue(); + } + + @Override + public R firstValue() { + + List all = all(); + return all.isEmpty() ? null : all.get(0); + } + + @Override + public List all() { + + return findOperation.find(example.getProbeType()) + .as(resultType) + .matching(QueryFragmentsAndParameters.forExample(mappingContext, example, sort, + createIncludedFieldsPredicate())) + .all(); + } + + @Override + public Page page(Pageable pageable) { + + List page = findOperation.find(example.getProbeType()) + .as(resultType) + .matching(QueryFragmentsAndParameters.forExample(mappingContext, example, pageable, + createIncludedFieldsPredicate())) + .all(); + + LongSupplier totalCountSupplier = this::count; + return PageableExecutionUtils.getPage(page, pageable, totalCountSupplier); + } + + @Override + public Stream stream() { + return all().stream(); + } + + @Override + public long count() { + return countOperation.apply(example); + } + + @Override + public boolean exists() { + return existsOperation.apply(example); + } +} diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/FetchableFluentQueryByPredicate.java b/src/main/java/org/springframework/data/neo4j/repository/query/FetchableFluentQueryByPredicate.java new file mode 100644 index 0000000000..d880d712c6 --- /dev/null +++ b/src/main/java/org/springframework/data/neo4j/repository/query/FetchableFluentQueryByPredicate.java @@ -0,0 +1,179 @@ +/* + * Copyright 2011-2021 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 java.util.Collection; +import java.util.List; +import java.util.function.Function; +import java.util.function.LongSupplier; +import java.util.stream.Stream; + +import org.apiguardian.api.API; +import org.neo4j.cypherdsl.core.Cypher; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.neo4j.core.FluentFindOperation; +import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity; +import org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery; +import org.springframework.data.support.PageableExecutionUtils; +import org.springframework.lang.Nullable; + +import com.querydsl.core.types.Predicate; + +/** + * Immutable implementation of a {@link FetchableFluentQuery}. All + * methods that return a {@link FetchableFluentQuery} return a new instance, the original instance won't be + * modified. + * + * @author Michael J. Simons + * @param Source type + * @param Result type + * @since 6.2 + * @soundtrack Die Ärzte - Geräusch + */ +@API(status = API.Status.INTERNAL, since = "6.2") +final class FetchableFluentQueryByPredicate extends FluentQuerySupport implements FetchableFluentQuery { + + private final Predicate predicate; + + private final Neo4jPersistentEntity metaData; + + private final FluentFindOperation findOperation; + + private final Function countOperation; + + private final Function existsOperation; + + FetchableFluentQueryByPredicate( + Predicate predicate, + Neo4jPersistentEntity metaData, + Class resultType, + FluentFindOperation findOperation, + Function countOperation, + Function existsOperation + ) { + this(predicate, metaData, resultType, findOperation, countOperation, existsOperation, Sort.unsorted(), null); + } + + FetchableFluentQueryByPredicate( + Predicate predicate, + Neo4jPersistentEntity metaData, + Class resultType, + FluentFindOperation findOperation, + Function countOperation, + Function existsOperation, + Sort sort, + @Nullable Collection properties + ) { + super(resultType, sort, properties); + this.predicate = predicate; + this.metaData = metaData; + this.findOperation = findOperation; + this.countOperation = countOperation; + this.existsOperation = existsOperation; + } + + @Override + @SuppressWarnings("HiddenField") + public FetchableFluentQuery sortBy(Sort sort) { + + return new FetchableFluentQueryByPredicate<>(this.predicate, this.metaData, this.resultType, this.findOperation, + this.countOperation, this.existsOperation, this.sort.and(sort), this.properties); + } + + @Override + @SuppressWarnings("HiddenField") + public FetchableFluentQuery as(Class resultType) { + + return new FetchableFluentQueryByPredicate<>(this.predicate, this.metaData, resultType, this.findOperation, + this.countOperation, this.existsOperation); + } + + @Override + @SuppressWarnings("HiddenField") + public FetchableFluentQuery project(Collection properties) { + + return new FetchableFluentQueryByPredicate<>(this.predicate, this.metaData, this.resultType, this.findOperation, + this.countOperation, this.existsOperation, sort, mergeProperties(properties)); + } + + @Override + public R oneValue() { + + return findOperation.find(metaData.getType()) + .as(resultType) + .matching( + QueryFragmentsAndParameters.forCondition(metaData, + Cypher.adapt(predicate).asCondition(), + null, + CypherAdapterUtils.toSortItems(this.metaData, sort), + createIncludedFieldsPredicate())) + .oneValue(); + } + + @Override + public R firstValue() { + + List all = all(); + return all.isEmpty() ? null : all.get(0); + } + + @Override + public List all() { + + return findOperation.find(metaData.getType()) + .as(resultType) + .matching( + QueryFragmentsAndParameters.forCondition(metaData, + Cypher.adapt(predicate).asCondition(), + null, + CypherAdapterUtils.toSortItems(this.metaData, sort), + createIncludedFieldsPredicate())) + .all(); + } + + @Override + public Page page(Pageable pageable) { + + List page = findOperation.find(metaData.getType()) + .as(resultType) + .matching( + QueryFragmentsAndParameters.forCondition(metaData, + Cypher.adapt(predicate).asCondition(), + pageable, null, + createIncludedFieldsPredicate())) + .all(); + + LongSupplier totalCountSupplier = this::count; + return PageableExecutionUtils.getPage(page, pageable, totalCountSupplier); + } + + @Override + public Stream stream() { + return all().stream(); + } + + @Override + public long count() { + return countOperation.apply(predicate); + } + + @Override + public boolean exists() { + return existsOperation.apply(predicate); + } +} diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/FluentQuerySupport.java b/src/main/java/org/springframework/data/neo4j/repository/query/FluentQuerySupport.java new file mode 100644 index 0000000000..f5bf64efeb --- /dev/null +++ b/src/main/java/org/springframework/data/neo4j/repository/query/FluentQuerySupport.java @@ -0,0 +1,74 @@ +/* + * Copyright 2011-2021 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 java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Predicate; + +import org.springframework.data.domain.Sort; +import org.springframework.data.neo4j.core.mapping.PropertyFilter; +import org.springframework.lang.Nullable; + +/** + * Supporting class containing some state and convenience methods for building fluent queries (both imperative and reactive). + * + * @author Michael J. Simons + * @param The result type + * @soundtrack Die Ärzte - Geräusch + */ +abstract class FluentQuerySupport { + + protected final Class resultType; + + protected final Sort sort; + + @Nullable + protected final Set properties; + + FluentQuerySupport( + Class resultType, + Sort sort, + @Nullable Collection properties + ) { + this.resultType = resultType; + this.sort = sort; + if (properties != null) { + this.properties = new HashSet<>(properties); + } else { + this.properties = null; + } + } + + final Predicate createIncludedFieldsPredicate() { + + if (this.properties == null) { + return path -> true; + } + return path -> this.properties.contains(path.toDotPath()); + } + + final Collection mergeProperties(Collection additionalProperties) { + Set newProperties = new HashSet<>(); + if (this.properties != null) { + newProperties.addAll(this.properties); + } + newProperties.addAll(additionalProperties); + return Collections.unmodifiableCollection(newProperties); + } +} diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/QueryFragmentsAndParameters.java b/src/main/java/org/springframework/data/neo4j/repository/query/QueryFragmentsAndParameters.java index 08de8bbb28..2cb7fed47f 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/QueryFragmentsAndParameters.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/QueryFragmentsAndParameters.java @@ -33,6 +33,7 @@ import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity; import org.springframework.data.neo4j.core.mapping.NodeDescription; +import org.springframework.data.neo4j.core.mapping.PropertyFilter; import org.springframework.lang.Nullable; /** @@ -124,27 +125,54 @@ public static QueryFragmentsAndParameters forFindAll(Neo4jPersistentEntity en * Following methods are used by the Simple(Reactive)QueryByExampleExecutor */ static QueryFragmentsAndParameters forExample(Neo4jMappingContext mappingContext, Example example) { - return QueryFragmentsAndParameters.forExample(mappingContext, example, null, null); + return QueryFragmentsAndParameters.forExample(mappingContext, example, + (java.util.function.Predicate) null); + } + + static QueryFragmentsAndParameters forExample(Neo4jMappingContext mappingContext, Example example, @Nullable java.util.function.Predicate includeField) { + return QueryFragmentsAndParameters.forExample(mappingContext, example, null, null, includeField); } static QueryFragmentsAndParameters forExample(Neo4jMappingContext mappingContext, Example example, Sort sort) { - return QueryFragmentsAndParameters.forExample(mappingContext, example, null, sort); + return QueryFragmentsAndParameters.forExample(mappingContext, example, sort, null); + } + + static QueryFragmentsAndParameters forExample(Neo4jMappingContext mappingContext, Example example, Sort sort, @Nullable java.util.function.Predicate includeField) { + return QueryFragmentsAndParameters.forExample(mappingContext, example, null, sort, includeField); } static QueryFragmentsAndParameters forExample(Neo4jMappingContext mappingContext, Example example, Pageable pageable) { return QueryFragmentsAndParameters.forExample(mappingContext, example, pageable, null); } + static QueryFragmentsAndParameters forExample(Neo4jMappingContext mappingContext, Example example, Pageable pageable, @Nullable java.util.function.Predicate includeField) { + return QueryFragmentsAndParameters.forExample(mappingContext, example, pageable, null, includeField); + } + static QueryFragmentsAndParameters forCondition(Neo4jPersistentEntity entityMetaData, Condition condition, @Nullable Pageable pageable, @Nullable Collection sortItems ) { + return forCondition(entityMetaData, condition, pageable, sortItems, null); + } + + static QueryFragmentsAndParameters forCondition(Neo4jPersistentEntity entityMetaData, + Condition condition, + @Nullable Pageable pageable, + @Nullable Collection sortItems, + @Nullable java.util.function.Predicate includeField + ) { QueryFragments queryFragments = new QueryFragments(); queryFragments.addMatchOn(cypherGenerator.createRootNode(entityMetaData)); queryFragments.setCondition(condition); - queryFragments.setReturnExpressions(cypherGenerator.createReturnStatementForMatch(entityMetaData)); + if (includeField == null) { + queryFragments.setReturnExpressions(cypherGenerator.createReturnStatementForMatch(entityMetaData)); + } else { + queryFragments.setReturnExpressions( + cypherGenerator.createReturnStatementForMatch(entityMetaData, includeField)); + } queryFragments.setRenderConstantsAsParameters(true); if (pageable != null) { @@ -168,30 +196,36 @@ private static void adaptPageable( } static QueryFragmentsAndParameters forExample(Neo4jMappingContext mappingContext, Example example, - @Nullable Pageable pageable, @Nullable Sort sort) { + @Nullable Pageable pageable, @Nullable Sort sort, java.util.function.Predicate includeField) { Predicate predicate = Predicate.create(mappingContext, example); Map parameters = predicate.getParameters(); Condition condition = predicate.getCondition(); return getQueryFragmentsAndParameters(mappingContext.getPersistentEntity(example.getProbeType()), pageable, - sort, parameters, condition); + sort, parameters, condition, includeField); } public static QueryFragmentsAndParameters forPageableAndSort(Neo4jPersistentEntity neo4jPersistentEntity, @Nullable Pageable pageable, @Nullable Sort sort) { - return getQueryFragmentsAndParameters(neo4jPersistentEntity, pageable, sort, Collections.emptyMap(), null); + return getQueryFragmentsAndParameters(neo4jPersistentEntity, pageable, sort, Collections.emptyMap(), null, null); } private static QueryFragmentsAndParameters getQueryFragmentsAndParameters( Neo4jPersistentEntity entityMetaData, @Nullable Pageable pageable, @Nullable Sort sort, - @Nullable Map parameters, @Nullable Condition condition) { + @Nullable Map parameters, @Nullable Condition condition, @Nullable + java.util.function.Predicate includeField) { QueryFragments queryFragments = new QueryFragments(); queryFragments.addMatchOn(cypherGenerator.createRootNode(entityMetaData)); queryFragments.setCondition(condition); - queryFragments.setReturnExpressions(cypherGenerator.createReturnStatementForMatch(entityMetaData)); + if (includeField == null) { + queryFragments.setReturnExpressions(cypherGenerator.createReturnStatementForMatch(entityMetaData)); + } else { + queryFragments.setReturnExpressions( + cypherGenerator.createReturnStatementForMatch(entityMetaData, includeField)); + } if (pageable != null) { adaptPageable(entityMetaData, pageable, queryFragments); diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/QuerydslNeo4jPredicateExecutor.java b/src/main/java/org/springframework/data/neo4j/repository/query/QuerydslNeo4jPredicateExecutor.java index 3a3d16bbf6..9cd5a48cca 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/QuerydslNeo4jPredicateExecutor.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/QuerydslNeo4jPredicateExecutor.java @@ -17,6 +17,7 @@ import java.util.Arrays; import java.util.Optional; +import java.util.function.Function; import org.apiguardian.api.API; import org.neo4j.cypherdsl.core.Cypher; @@ -24,20 +25,22 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; +import org.springframework.data.neo4j.core.FluentFindOperation; import org.springframework.data.neo4j.core.Neo4jOperations; +import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity; import org.springframework.data.neo4j.repository.support.CypherdslConditionExecutor; import org.springframework.data.neo4j.repository.support.Neo4jEntityInformation; -import org.springframework.data.neo4j.repository.support.SimpleNeo4jRepository; import org.springframework.data.querydsl.QuerydslPredicateExecutor; +import org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery; import com.querydsl.core.types.OrderSpecifier; import com.querydsl.core.types.Predicate; /** - * Querydsl specific fragment for extending {@link SimpleNeo4jRepository} with an implementation of {@link QuerydslPredicateExecutor}. - * Provides the necessary infrastructure for translating Query-DSL predicates into conditions that are passed along - * to the Cypher-DSL and eventually to the template infrastructure. This fragment will be loaded by the repository - * infrastructure when + * Querydsl specific fragment for extending {@link org.springframework.data.neo4j.repository.support.SimpleNeo4jRepository} + * with an implementation of {@link QuerydslPredicateExecutor}. Provides the necessary infrastructure for translating + * Query-DSL predicates into conditions that are passed along to the Cypher-DSL and eventually to the template infrastructure. + * This fragment will be loaded by the repository infrastructure when a repository is declared extending the above interface. * * @author Michael J. Simons * @param The returned domain type. @@ -47,12 +50,27 @@ @API(status = API.Status.INTERNAL, since = "6.1") public final class QuerydslNeo4jPredicateExecutor implements QuerydslPredicateExecutor { + /** + * Non-fluent operations are translated directly into Cypherdsl conditions and executed elsewhere. + */ private final CypherdslConditionExecutor delegate; + /** + * Needed to support the fluent operations. + */ + private final Neo4jOperations neo4jOperations; + + /** + * Needed to support the fluent operations. + */ + private final Neo4jPersistentEntity metaData; + public QuerydslNeo4jPredicateExecutor(Neo4jEntityInformation entityInformation, Neo4jOperations neo4jOperations) { this.delegate = new CypherdslConditionExecutorImpl<>(entityInformation, neo4jOperations); + this.neo4jOperations = neo4jOperations; + this.metaData = entityInformation.getEntityMetaData(); } @Override @@ -74,15 +92,15 @@ public Iterable findAll(Predicate predicate, Sort sort) { } @Override - public Iterable findAll(Predicate predicate, OrderSpecifier... orderSpecifiers) { + public Iterable findAll(Predicate predicate, OrderSpecifier... orders) { - return this.delegate.findAll(Cypher.adapt(predicate).asCondition(), toSortItems(orderSpecifiers)); + return this.delegate.findAll(Cypher.adapt(predicate).asCondition(), toSortItems(orders)); } @Override - public Iterable findAll(OrderSpecifier... orderSpecifiers) { + public Iterable findAll(OrderSpecifier... orders) { - return this.delegate.findAll(toSortItems(orderSpecifiers)); + return this.delegate.findAll(toSortItems(orders)); } @Override @@ -97,7 +115,7 @@ public long count(Predicate predicate) { return this.delegate.count(Cypher.adapt(predicate).asCondition()); } - private SortItem[] toSortItems(OrderSpecifier... orderSpecifiers) { + static SortItem[] toSortItems(OrderSpecifier... orderSpecifiers) { return Arrays.stream(orderSpecifiers) .map(os -> Cypher.sort(Cypher.adapt(os.getTarget()).asExpression(), @@ -109,4 +127,18 @@ private SortItem[] toSortItems(OrderSpecifier... orderSpecifiers) { public boolean exists(Predicate predicate) { return findAll(predicate).iterator().hasNext(); } + + @Override + public R findBy(Predicate predicate, Function, R> queryFunction) { + + if (this.neo4jOperations instanceof FluentFindOperation) { + @SuppressWarnings("unchecked") // defaultResultType will be a supertype of S and at this stage, the same. + FetchableFluentQuery fluentQuery = + (FetchableFluentQuery) new FetchableFluentQueryByPredicate<>(predicate, metaData, metaData.getType(), + (FluentFindOperation) this.neo4jOperations, this::count, this::exists); + return queryFunction.apply(fluentQuery); + } + throw new UnsupportedOperationException( + "Fluent find by predicate not supported with standard Neo4jOperations. Must support fluent queries too."); + } } diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveFluentQueryByExample.java b/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveFluentQueryByExample.java new file mode 100644 index 0000000000..fa29f07be2 --- /dev/null +++ b/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveFluentQueryByExample.java @@ -0,0 +1,161 @@ +/* + * Copyright 2011-2021 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 reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.Collection; +import java.util.function.Function; + +import org.apiguardian.api.API; +import org.springframework.data.domain.Example; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.neo4j.core.ReactiveFluentFindOperation; +import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; +import org.springframework.data.repository.query.FluentQuery.ReactiveFluentQuery; +import org.springframework.data.support.PageableExecutionUtils; +import org.springframework.lang.Nullable; + +/** + * Immutable implementation of a {@link ReactiveFluentQuery}. All + * methods that return a {@link ReactiveFluentQuery} return a new instance, the original instance won't be + * modified. + * + * @author Michael J. Simons + * @param Source type + * @param Result type + * @since 6.2 + */ +@API(status = API.Status.INTERNAL, since = "6.2") +final class ReactiveFluentQueryByExample extends FluentQuerySupport implements ReactiveFluentQuery { + + private final Neo4jMappingContext mappingContext; + + private final Example example; + + private final ReactiveFluentFindOperation findOperation; + + private final Function, Mono> countOperation; + + private final Function, Mono> existsOperation; + + ReactiveFluentQueryByExample( + Example example, + Class resultType, + Neo4jMappingContext mappingContext, + ReactiveFluentFindOperation findOperation, + Function, Mono> countOperation, + Function, Mono> existsOperation + ) { + this(example, resultType, mappingContext, findOperation, countOperation, existsOperation, Sort.unsorted(), + null); + } + + ReactiveFluentQueryByExample( + Example example, + Class resultType, + Neo4jMappingContext mappingContext, + ReactiveFluentFindOperation findOperation, + Function, Mono> countOperation, + Function, Mono> existsOperation, + Sort sort, + @Nullable Collection properties + ) { + super(resultType, sort, properties); + this.mappingContext = mappingContext; + this.example = example; + this.findOperation = findOperation; + this.countOperation = countOperation; + this.existsOperation = existsOperation; + } + + @Override + @SuppressWarnings("HiddenField") + public ReactiveFluentQuery sortBy(Sort sort) { + + return new ReactiveFluentQueryByExample<>(this.example, this.resultType, this.mappingContext, this.findOperation, + this.countOperation, this.existsOperation, this.sort.and(sort), this.properties); + } + + @Override + @SuppressWarnings("HiddenField") + public ReactiveFluentQuery as(Class resultType) { + + return new ReactiveFluentQueryByExample<>(this.example, resultType, this.mappingContext, this.findOperation, + this.countOperation, this.existsOperation); + } + + @Override + @SuppressWarnings("HiddenField") + public ReactiveFluentQuery project(Collection properties) { + + return new ReactiveFluentQueryByExample<>(this.example, this.resultType, this.mappingContext, this.findOperation, + this.countOperation, this.existsOperation, sort, mergeProperties(properties)); + } + + @Override + public Mono one() { + + return findOperation.find(example.getProbeType()) + .as(resultType) + .matching(QueryFragmentsAndParameters.forExample(mappingContext, example, sort, + createIncludedFieldsPredicate())) + .one(); + } + + @Override + public Mono first() { + + return all().take(1).singleOrEmpty(); + } + + @Override + public Flux all() { + + return findOperation.find(example.getProbeType()) + .as(resultType) + .matching(QueryFragmentsAndParameters.forExample(mappingContext, example, sort, + createIncludedFieldsPredicate())) + .all(); + } + + @Override + public Mono> page(Pageable pageable) { + + Flux results = findOperation.find(example.getProbeType()) + .as(resultType) + .matching(QueryFragmentsAndParameters.forExample(mappingContext, example, pageable, + createIncludedFieldsPredicate())) + .all(); + return results.collectList().zipWith(countOperation.apply(example)).map(tuple -> { + Page page = PageableExecutionUtils.getPage(tuple.getT1(), pageable, () -> tuple.getT2()); + return page; + }); + } + + @Override + public Mono count() { + return countOperation.apply(example); + } + + @Override + public Mono exists() { + return existsOperation.apply(example); + } +} diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveFluentQueryByPredicate.java b/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveFluentQueryByPredicate.java new file mode 100644 index 0000000000..dfa03d3684 --- /dev/null +++ b/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveFluentQueryByPredicate.java @@ -0,0 +1,174 @@ +/* + * Copyright 2011-2021 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 reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.Collection; +import java.util.function.Function; + +import org.apiguardian.api.API; +import org.neo4j.cypherdsl.core.Cypher; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.neo4j.core.ReactiveFluentFindOperation; +import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity; +import org.springframework.data.repository.query.FluentQuery.ReactiveFluentQuery; +import org.springframework.data.support.PageableExecutionUtils; +import org.springframework.lang.Nullable; + +import com.querydsl.core.types.Predicate; + +/** + * Immutable implementation of a {@link ReactiveFluentQuery}. All + * methods that return a {@link ReactiveFluentQuery} return a new instance, the original instance won't be + * modified. + * + * @author Michael J. Simons + * @param Source type + * @param Result type + * @since 6.2 + */ +@API(status = API.Status.INTERNAL, since = "6.2") final class ReactiveFluentQueryByPredicate + extends FluentQuerySupport implements ReactiveFluentQuery { + + private final Predicate predicate; + + private final Neo4jPersistentEntity metaData; + + private final ReactiveFluentFindOperation findOperation; + + private final Function> countOperation; + + private final Function> existsOperation; + + ReactiveFluentQueryByPredicate( + Predicate predicate, + Neo4jPersistentEntity metaData, + Class resultType, + ReactiveFluentFindOperation findOperation, + Function> countOperation, + Function> existsOperation + ) { + this(predicate, metaData, resultType, findOperation, countOperation, existsOperation, Sort.unsorted(), null); + } + + ReactiveFluentQueryByPredicate( + Predicate predicate, + Neo4jPersistentEntity metaData, + Class resultType, + ReactiveFluentFindOperation findOperation, + Function> countOperation, + Function> existsOperation, + Sort sort, + @Nullable Collection properties + ) { + super(resultType, sort, properties); + this.predicate = predicate; + this.metaData = metaData; + this.findOperation = findOperation; + this.countOperation = countOperation; + this.existsOperation = existsOperation; + } + + @Override + @SuppressWarnings("HiddenField") + public ReactiveFluentQuery sortBy(Sort sort) { + + return new ReactiveFluentQueryByPredicate<>(this.predicate, this.metaData, this.resultType, this.findOperation, + this.countOperation, this.existsOperation, this.sort.and(sort), this.properties); + } + + @Override + @SuppressWarnings("HiddenField") + public ReactiveFluentQuery as(Class resultType) { + + return new ReactiveFluentQueryByPredicate<>(this.predicate, this.metaData, resultType, this.findOperation, + this.countOperation, this.existsOperation); + } + + @Override + @SuppressWarnings("HiddenField") + public ReactiveFluentQuery project(Collection properties) { + + return new ReactiveFluentQueryByPredicate<>(this.predicate, this.metaData, resultType, this.findOperation, + this.countOperation, this.existsOperation, sort, mergeProperties(properties)); + } + + @Override + public Mono one() { + + return findOperation.find(metaData.getType()) + .as(resultType) + .matching( + QueryFragmentsAndParameters.forCondition(metaData, + Cypher.adapt(predicate).asCondition(), + null, + CypherAdapterUtils.toSortItems(this.metaData, sort), + createIncludedFieldsPredicate())) + .one(); + } + + @Override + public Mono first() { + + return all().take(1).singleOrEmpty(); + } + + @Override + public Flux all() { + + return findOperation.find(metaData.getType()) + .as(resultType) + .matching( + QueryFragmentsAndParameters.forCondition(metaData, + Cypher.adapt(predicate).asCondition(), + null, + CypherAdapterUtils.toSortItems(this.metaData, sort), + createIncludedFieldsPredicate())) + .all(); + } + + @Override + public Mono> page(Pageable pageable) { + + Flux results = findOperation.find(metaData.getType()) + .as(resultType) + .matching( + QueryFragmentsAndParameters.forCondition(metaData, + Cypher.adapt(predicate).asCondition(), + pageable, null, + createIncludedFieldsPredicate())) + .all(); + + return results.collectList().zipWith(countOperation.apply(predicate)).map(tuple -> { + Page page = PageableExecutionUtils.getPage(tuple.getT1(), pageable, () -> tuple.getT2()); + return page; + }); + } + + @Override + public Mono count() { + return countOperation.apply(predicate); + } + + @Override + public Mono exists() { + return existsOperation.apply(predicate); + } +} diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveQuerydslNeo4jPredicateExecutor.java b/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveQuerydslNeo4jPredicateExecutor.java new file mode 100644 index 0000000000..aa1fbef240 --- /dev/null +++ b/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveQuerydslNeo4jPredicateExecutor.java @@ -0,0 +1,142 @@ +/* + * Copyright 2011-2021 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 reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.Arrays; +import java.util.Collection; +import java.util.function.Function; + +import org.apiguardian.api.API; +import org.neo4j.cypherdsl.core.Condition; +import org.neo4j.cypherdsl.core.Conditions; +import org.neo4j.cypherdsl.core.Cypher; +import org.neo4j.cypherdsl.core.Functions; +import org.neo4j.cypherdsl.core.SortItem; +import org.neo4j.cypherdsl.core.Statement; +import org.reactivestreams.Publisher; +import org.springframework.data.domain.Sort; +import org.springframework.data.neo4j.core.ReactiveFluentFindOperation; +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.Neo4jEntityInformation; +import org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor; +import org.springframework.data.repository.query.FluentQuery.ReactiveFluentQuery; + +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.Predicate; + +/** + * Querydsl specific fragment for extending {@link org.springframework.data.neo4j.repository.support.SimpleReactiveNeo4jRepository} + * with an implementation of {@link ReactiveQuerydslPredicateExecutor}. Provides the necessary infrastructure for translating + * Query-DSL predicates into conditions that are passed along to the Cypher-DSL and eventually to the template infrastructure. + * This fragment will be loaded by the repository infrastructure when a repository is declared extending the above interface. + * + * @author Michael J. Simons + * @param The returned domain type. + * @since 6.2 + */ +@API(status = API.Status.INTERNAL, since = "6.2") +public final class ReactiveQuerydslNeo4jPredicateExecutor implements ReactiveQuerydslPredicateExecutor { + + private final Neo4jEntityInformation entityInformation; + + private final ReactiveNeo4jOperations neo4jOperations; + + private final Neo4jPersistentEntity metaData; + + public ReactiveQuerydslNeo4jPredicateExecutor(Neo4jEntityInformation entityInformation, + ReactiveNeo4jOperations neo4jOperations) { + + this.entityInformation = entityInformation; + this.neo4jOperations = neo4jOperations; + this.metaData = this.entityInformation.getEntityMetaData(); + } + + @Override + public Mono findOne(Predicate predicate) { + + return this.neo4jOperations.toExecutableQuery( + this.metaData.getType(), + QueryFragmentsAndParameters.forCondition(this.metaData, Cypher.adapt(predicate).asCondition(), null, + null) + ).flatMap(ReactiveNeo4jOperations.ExecutableQuery::getSingleResult); + } + + @Override + public Flux findAll(Predicate predicate) { + + return doFindAll(Cypher.adapt(predicate).asCondition(), null); + } + + @Override + public Flux findAll(Predicate predicate, Sort sort) { + + return doFindAll(Cypher.adapt(predicate).asCondition(), CypherAdapterUtils.toSortItems(this.metaData, sort)); + } + + @Override + public Flux findAll(Predicate predicate, OrderSpecifier... orders) { + return doFindAll(Cypher.adapt(predicate).asCondition(), Arrays.asList(QuerydslNeo4jPredicateExecutor.toSortItems(orders))); + + } + + @Override + public Flux findAll(OrderSpecifier... orders) { + + return doFindAll(Conditions.noCondition(), Arrays.asList(QuerydslNeo4jPredicateExecutor.toSortItems(orders))); + } + + private Flux doFindAll(Condition condition, Collection sortItems) { + return this.neo4jOperations.toExecutableQuery( + this.metaData.getType(), + QueryFragmentsAndParameters.forCondition(this.metaData, condition, null, + sortItems) + ).flatMapMany(ReactiveNeo4jOperations.ExecutableQuery::getResults); + } + + @Override + public Mono count(Predicate predicate) { + + Statement statement = CypherGenerator.INSTANCE.prepareMatchOf(this.metaData, + Cypher.adapt(predicate).asCondition()) + .returning(Functions.count(asterisk())).build(); + return this.neo4jOperations.count(statement, statement.getParameters()); + } + + @Override + public Mono exists(Predicate predicate) { + return findAll(predicate).hasElements(); + } + + @Override + public > P findBy(Predicate predicate, Function, P> queryFunction) { + + if (this.neo4jOperations instanceof ReactiveFluentFindOperation) { + @SuppressWarnings("unchecked") // defaultResultType will be a supertype of S and at this stage, the same. + ReactiveFluentQuery fluentQuery = (ReactiveFluentQuery) new ReactiveFluentQueryByPredicate<>(predicate, metaData, metaData.getType(), + (ReactiveFluentFindOperation) this.neo4jOperations, this::count, this::exists); + return queryFunction.apply(fluentQuery); + } + throw new UnsupportedOperationException( + "Fluent find by example not supported with standard Neo4jOperations. Must support fluent queries too."); + } +} diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/SimpleQueryByExampleExecutor.java b/src/main/java/org/springframework/data/neo4j/repository/query/SimpleQueryByExampleExecutor.java index c9e6924a76..68e6ce49d9 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/SimpleQueryByExampleExecutor.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/SimpleQueryByExampleExecutor.java @@ -22,14 +22,17 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; +import org.springframework.data.neo4j.core.FluentFindOperation; import org.springframework.data.neo4j.core.Neo4jOperations; import org.springframework.data.neo4j.core.mapping.CypherGenerator; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; +import org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery; import org.springframework.data.repository.query.QueryByExampleExecutor; import org.springframework.data.support.PageableExecutionUtils; import java.util.List; import java.util.Optional; +import java.util.function.Function; import java.util.function.LongSupplier; import static org.neo4j.cypherdsl.core.Cypher.asterisk; @@ -101,4 +104,15 @@ public boolean exists(Example example) { return findAll(example).iterator().hasNext(); } + @Override + public R findBy(Example example, Function, R> queryFunction) { + + if (this.neo4jOperations instanceof FluentFindOperation) { + FetchableFluentQuery fluentQuery = new FetchableFluentQueryByExample<>(example, example.getProbeType(), + mappingContext, (FluentFindOperation) this.neo4jOperations, this::count, this::exists); + return queryFunction.apply(fluentQuery); + } + throw new UnsupportedOperationException( + "Fluent find by example not supported with standard Neo4jOperations. Must support fluent queries too."); + } } diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/SimpleReactiveQueryByExampleExecutor.java b/src/main/java/org/springframework/data/neo4j/repository/query/SimpleReactiveQueryByExampleExecutor.java index 2bc08ec6ad..1aac18cf05 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/SimpleReactiveQueryByExampleExecutor.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/SimpleReactiveQueryByExampleExecutor.java @@ -18,17 +18,22 @@ import org.apiguardian.api.API; import org.neo4j.cypherdsl.core.Functions; import org.neo4j.cypherdsl.core.Statement; +import org.reactivestreams.Publisher; import org.springframework.data.domain.Example; import org.springframework.data.domain.Sort; +import org.springframework.data.neo4j.core.ReactiveFluentFindOperation; import org.springframework.data.neo4j.core.ReactiveNeo4jOperations; import org.springframework.data.neo4j.core.mapping.CypherGenerator; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; +import org.springframework.data.repository.query.FluentQuery.ReactiveFluentQuery; import org.springframework.data.repository.query.ReactiveQueryByExampleExecutor; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import static org.neo4j.cypherdsl.core.Cypher.asterisk; +import java.util.function.Function; + /** * A fragment for repositories providing "Query by example" functionality in a reactive way. * @@ -88,4 +93,15 @@ public Mono count(Example example) { public Mono exists(Example example) { return findAll(example).hasElements(); } + + @Override + public > P findBy(Example example, Function, P> queryFunction) { + if (this.neo4jOperations instanceof ReactiveFluentFindOperation) { + ReactiveFluentQuery fluentQuery = new ReactiveFluentQueryByExample<>(example, example.getProbeType(), + mappingContext, (ReactiveFluentFindOperation) this.neo4jOperations, this::count, this::exists); + return queryFunction.apply(fluentQuery); + } + throw new UnsupportedOperationException( + "Fluent find by example not supported with standard Neo4jOperations. Must support fluent queries too."); + } } diff --git a/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java b/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java index 0cb9ad38a8..2e284e633e 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java +++ b/src/main/java/org/springframework/data/neo4j/repository/support/Neo4jRepositoryFactory.java @@ -17,7 +17,6 @@ import java.util.Optional; -import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.neo4j.core.Neo4jOperations; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity; @@ -102,11 +101,6 @@ protected RepositoryFragments getRepositoryFragments(RepositoryMetadata metadata private RepositoryFragment createDSLExecutorFragment(RepositoryMetadata metadata, Class implementor) { - if (metadata.isReactiveRepository()) { - throw new InvalidDataAccessApiUsageException( - "Cannot combine DSL executor and reactive repository support in a single interface"); - } - Neo4jEntityInformation entityInformation = getEntityInformation(metadata.getDomainType()); Object querydslFragment = instantiateClass(implementor, entityInformation, neo4jOperations); 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 4cf97a222e..b5f286ab26 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 @@ -25,7 +25,10 @@ import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity; import org.springframework.data.neo4j.repository.ReactiveNeo4jRepository; 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.querydsl.QuerydslUtils; +import org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.ReactiveRepositoryFactorySupport; @@ -80,9 +83,25 @@ protected RepositoryFragments getRepositoryFragments(RepositoryMetadata metadata fragments = fragments.append(RepositoryFragment.implemented(byExampleExecutor)); + boolean isQueryDslRepository = QuerydslUtils.QUERY_DSL_PRESENT + && ReactiveQuerydslPredicateExecutor.class.isAssignableFrom(metadata.getRepositoryInterface()); + + if (isQueryDslRepository) { + + fragments = fragments.append(createDSLExecutorFragment(metadata, ReactiveQuerydslNeo4jPredicateExecutor.class)); + } + return fragments; } + private RepositoryFragment createDSLExecutorFragment(RepositoryMetadata metadata, Class implementor) { + + Neo4jEntityInformation entityInformation = getEntityInformation(metadata.getDomainType()); + Object querydslFragment = instantiateClass(implementor, entityInformation, neo4jOperations); + + return RepositoryFragment.implemented(querydslFragment); + } + @Override protected Class getRepositoryBaseClass(RepositoryMetadata metadata) { return SimpleReactiveNeo4jRepository.class; diff --git a/src/test/java/org/springframework/data/neo4j/integration/imperative/QuerydslNeo4jPredicateExecutorIT.java b/src/test/java/org/springframework/data/neo4j/integration/imperative/QuerydslNeo4jPredicateExecutorIT.java index 30876625c8..f1f42125d8 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/imperative/QuerydslNeo4jPredicateExecutorIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/imperative/QuerydslNeo4jPredicateExecutorIT.java @@ -17,6 +17,9 @@ import static org.assertj.core.api.Assertions.assertThat; +import java.util.List; +import java.util.stream.Stream; + import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.neo4j.driver.Driver; @@ -39,6 +42,7 @@ import org.springframework.data.neo4j.test.Neo4jExtension; import org.springframework.data.neo4j.test.Neo4jIntegrationTest; import org.springframework.data.querydsl.QuerydslPredicateExecutor; +import org.springframework.data.repository.query.FluentQuery; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; @@ -46,6 +50,7 @@ import com.querydsl.core.types.Order; import com.querydsl.core.types.OrderSpecifier; import com.querydsl.core.types.Path; +import com.querydsl.core.types.Predicate; import com.querydsl.core.types.dsl.Expressions; /** @@ -56,14 +61,14 @@ class QuerydslNeo4jPredicateExecutorIT { protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; - private final Path person; - private final Path firstName; - private final Path lastName; + private final Path personPath; + private final Path firstNamePath; + private final Path lastNamePath; QuerydslNeo4jPredicateExecutorIT() { - this.person = Expressions.path(Person.class, "person"); - this.firstName = Expressions.path(String.class, person, "firstName"); - this.lastName = Expressions.path(String.class, person, "lastName"); + this.personPath = Expressions.path(Person.class, "person"); + this.firstNamePath = Expressions.path(String.class, personPath, "firstName"); + this.lastNamePath = Expressions.path(String.class, personPath, "lastName"); } @BeforeAll @@ -83,18 +88,152 @@ protected static void setupData(@Autowired BookmarkCapture bookmarkCapture) { } } + @Test // GH-2343 + void fluentFindOneShouldWork(@Autowired QueryDSLPersonRepository repository) { + + Predicate predicate = Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")); + Person person = repository.findBy(predicate, q -> q.oneValue()); + + assertThat(person).isNotNull(); + assertThat(person).extracting(Person::getLastName).isEqualTo("Schneider"); + } + + @Test // GH-2343 + void fluentFindAllShouldWork(@Autowired QueryDSLPersonRepository repository) { + + Predicate predicate = Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")) + .or(Expressions.predicate(Ops.EQ, lastNamePath, Expressions.asString("B."))); + List people = repository.findBy(predicate, q -> q.all()); + + assertThat(people).extracting(Person::getFirstName) + .containsExactlyInAnyOrder("Bela", "Helge"); + } + + @Test // GH-2343 + void fluentFindAllProjectingShouldWork(@Autowired QueryDSLPersonRepository repository) { + + Predicate predicate = Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")); + List people = repository.findBy(predicate, q -> q.project("firstName").all()); + + assertThat(people) + .hasSize(1) + .first().satisfies(p -> { + assertThat(p.getFirstName()).isEqualTo("Helge"); + assertThat(p.getId()).isNotNull(); + + assertThat(p.getLastName()).isNull(); + assertThat(p.getAddress()).isNull(); + }); + } + + static class DtoPersonProjection { + + private final String firstName; + + DtoPersonProjection(String firstName) { + this.firstName = firstName; + } + + public String getFirstName() { + return firstName; + } + } + + @Test // GH-2343 + void fluentfindAllAsShouldWork(@Autowired QueryDSLPersonRepository repository) { + + Predicate predicate = Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")); + + List people = repository.findBy(predicate, q -> q.as(DtoPersonProjection.class).all()); + assertThat(people) + .hasSize(1) + .extracting(DtoPersonProjection::getFirstName) + .first().isEqualTo("Helge"); + } + + @Test // GH-2343 + void fluentStreamShouldWork(@Autowired QueryDSLPersonRepository repository) { + + Predicate predicate = Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")); + Stream people = repository.findBy(predicate, FluentQuery.FetchableFluentQuery::stream); + + assertThat(people.map(Person::getFirstName)).containsExactly("Helge"); + } + + @Test // GH-2343 + void fluentStreamProjectingShouldWork(@Autowired QueryDSLPersonRepository repository) { + + Predicate predicate = Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")); + Stream people = repository.findBy(predicate, + q -> q.as(DtoPersonProjection.class).stream()); + + assertThat(people.map(DtoPersonProjection::getFirstName)).containsExactly("Helge"); + } + + @Test // GH-2343 + void fluentFindFirstShouldWork(@Autowired QueryDSLPersonRepository repository) { + + Predicate predicate = Expressions.TRUE.isTrue(); + Person person = repository.findBy(predicate, q -> q.sortBy(Sort.by(Sort.Direction.DESC, "lastName")).firstValue()); + + assertThat(person).isNotNull(); + assertThat(person).extracting(Person::getFirstName).isEqualTo("Helge"); + } + + @Test // GH-2343 + void fluentFindAllWithSortShouldWork(@Autowired QueryDSLPersonRepository repository) { + + Predicate predicate = Expressions.TRUE.isTrue(); + List people = repository.findBy(predicate, + q -> q.sortBy(Sort.by(Sort.Direction.DESC, "lastName")).all()); + + assertThat(people).extracting(Person::getLastName).containsExactly("Schneider", "LB", "LA", "B."); + } + + @Test // GH-2343 + void fluentFindAllWithPaginationShouldWork(@Autowired QueryDSLPersonRepository repository) { + + Predicate predicate = Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")) + .or(Expressions.predicate(Ops.EQ, lastNamePath, Expressions.asString("B."))); + Page people = repository.findBy(predicate, + q -> q.page(PageRequest.of(1, 1, Sort.by("lastName").ascending()))); + + assertThat(people).extracting(Person::getFirstName).containsExactly("Helge"); + assertThat(people.hasPrevious()).isTrue(); + assertThat(people.hasNext()).isFalse(); + } + + @Test // GH-2343 + void fluentExistsShouldWork(@Autowired QueryDSLPersonRepository repository) { + + Predicate predicate = Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")); + boolean exists = repository.findBy(predicate, q -> q.exists()); + + assertThat(exists).isTrue(); + } + + @Test // GH-2343 + void fluentCountShouldWork(@Autowired QueryDSLPersonRepository repository) { + + Predicate predicate = Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")) + .or(Expressions.predicate(Ops.EQ, lastNamePath, Expressions.asString("B."))); + long count = repository.findBy(predicate, q -> q.count()); + + assertThat(count).isEqualTo(2); + } + @Test void findOneShouldWork(@Autowired QueryDSLPersonRepository repository) { - assertThat(repository.findOne(Expressions.predicate(Ops.EQ, firstName, Expressions.asString("Helge")))) + assertThat(repository.findOne(Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")))) .hasValueSatisfying(p -> assertThat(p).extracting(Person::getLastName).isEqualTo("Schneider")); } @Test void findAllShouldWork(@Autowired QueryDSLPersonRepository repository) { - assertThat(repository.findAll(Expressions.predicate(Ops.EQ, firstName, Expressions.asString("Helge")) - .or(Expressions.predicate(Ops.EQ, lastName, Expressions.asString("B."))))) + assertThat(repository.findAll(Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")) + .or(Expressions.predicate(Ops.EQ, lastNamePath, Expressions.asString("B."))))) .extracting(Person::getFirstName) .containsExactlyInAnyOrder("Bela", "Helge"); } @@ -103,9 +242,9 @@ void findAllShouldWork(@Autowired QueryDSLPersonRepository repository) { void sortedFindAllShouldWork(@Autowired QueryDSLPersonRepository repository) { assertThat( - repository.findAll(Expressions.predicate(Ops.EQ, firstName, Expressions.asString("Helge")) - .or(Expressions.predicate(Ops.EQ, lastName, Expressions.asString("B."))), - new OrderSpecifier(Order.DESC, lastName) + repository.findAll(Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")) + .or(Expressions.predicate(Ops.EQ, lastNamePath, Expressions.asString("B."))), + new OrderSpecifier(Order.DESC, lastNamePath) )) .extracting(Person::getFirstName) .containsExactly("Helge", "Bela"); @@ -115,8 +254,8 @@ void sortedFindAllShouldWork(@Autowired QueryDSLPersonRepository repository) { void orderedFindAllShouldWork(@Autowired QueryDSLPersonRepository repository) { assertThat( - repository.findAll(Expressions.predicate(Ops.EQ, firstName, Expressions.asString("Helge")) - .or(Expressions.predicate(Ops.EQ, lastName, Expressions.asString("B."))), + repository.findAll(Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")) + .or(Expressions.predicate(Ops.EQ, lastNamePath, Expressions.asString("B."))), Sort.by("lastName").descending() )) .extracting(Person::getFirstName) @@ -126,7 +265,7 @@ void orderedFindAllShouldWork(@Autowired QueryDSLPersonRepository repository) { @Test void orderedFindAllWithoutPredicateShouldWork(@Autowired QueryDSLPersonRepository repository) { - assertThat(repository.findAll(new OrderSpecifier(Order.DESC, lastName))) + assertThat(repository.findAll(new OrderSpecifier(Order.DESC, lastNamePath))) .extracting(Person::getFirstName) .containsExactly("Helge", "B", "A", "Bela"); } @@ -134,8 +273,8 @@ void orderedFindAllWithoutPredicateShouldWork(@Autowired QueryDSLPersonRepositor @Test void pagedFindAllShouldWork(@Autowired QueryDSLPersonRepository repository) { - Page people = repository.findAll(Expressions.predicate(Ops.EQ, firstName, Expressions.asString("Helge")) - .or(Expressions.predicate(Ops.EQ, lastName, Expressions.asString("B."))), + Page people = repository.findAll(Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")) + .or(Expressions.predicate(Ops.EQ, lastNamePath, Expressions.asString("B."))), PageRequest.of(1, 1, Sort.by("lastName").descending()) ); @@ -150,8 +289,8 @@ void pagedFindAllShouldWork(@Autowired QueryDSLPersonRepository repository) { @Test // GH-2194 void pagedFindAllShouldWork2(@Autowired QueryDSLPersonRepository repository) { - Page people = repository.findAll(Expressions.predicate(Ops.EQ, firstName, Expressions.asString("Helge")) - .or(Expressions.predicate(Ops.EQ, lastName, Expressions.asString("B."))), + Page people = repository.findAll(Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")) + .or(Expressions.predicate(Ops.EQ, lastNamePath, Expressions.asString("B."))), PageRequest.of(0, 20, Sort.by("lastName").descending()) ); @@ -167,8 +306,8 @@ void pagedFindAllShouldWork2(@Autowired QueryDSLPersonRepository repository) { void countShouldWork(@Autowired QueryDSLPersonRepository repository) { assertThat( - repository.count(Expressions.predicate(Ops.EQ, firstName, Expressions.asString("Helge")) - .or(Expressions.predicate(Ops.EQ, lastName, Expressions.asString("B."))) + repository.count(Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")) + .or(Expressions.predicate(Ops.EQ, lastNamePath, Expressions.asString("B."))) )) .isEqualTo(2L); } @@ -176,7 +315,7 @@ void countShouldWork(@Autowired QueryDSLPersonRepository repository) { @Test void existsShouldWork(@Autowired QueryDSLPersonRepository repository) { - assertThat(repository.exists(Expressions.predicate(Ops.EQ, firstName, Expressions.asString("A")))) + assertThat(repository.exists(Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("A")))) .isTrue(); } diff --git a/src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryIT.java b/src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryIT.java index cef305797e..6f354e6ac5 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryIT.java @@ -41,6 +41,7 @@ import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.IntStream; +import java.util.stream.Stream; import java.util.stream.StreamSupport; import org.assertj.core.api.Assertions; @@ -146,6 +147,7 @@ import org.springframework.data.neo4j.test.Neo4jExtension; import org.springframework.data.neo4j.types.CartesianPoint2d; import org.springframework.data.neo4j.types.GeographicPoint2d; +import org.springframework.data.repository.query.FluentQuery; import org.springframework.data.repository.query.Param; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; @@ -2650,6 +2652,17 @@ void findOneByExample(@Autowired PersonRepository repository) { assertThat(person.get()).isEqualTo(person1); } + @Test // GH-2343 + void findOneByExampleFluent(@Autowired PersonRepository repository) { + + Example example = Example.of(person1, + ExampleMatcher.matchingAll().withIgnoreNullValues()); + PersonWithAllConstructor person = repository.findBy(example, q -> q.oneValue()); + + assertThat(person).isNotNull(); + assertThat(person).isEqualTo(person1); + } + @Test void findAllByExample(@Autowired PersonRepository repository) { @@ -2660,6 +2673,76 @@ void findAllByExample(@Autowired PersonRepository repository) { assertThat(persons).containsExactly(person1); } + @Test // GH-2343 + void findAllByExampleFluent(@Autowired PersonRepository repository) { + + Example example = Example.of(person1, + ExampleMatcher.matchingAll().withIgnoreNullValues()); + List persons = repository.findBy(example, FluentQuery.FetchableFluentQuery::all); + + assertThat(persons).containsExactly(person1); + } + + @Test // GH-2343 + void findAllByExampleFluentProjecting(@Autowired PersonRepository repository) { + + Example example = Example.of(person1, + ExampleMatcher.matchingAll().withIgnoreNullValues()); + List persons = repository.findBy(example, + q -> q.project("name", "firstName").all()); + + assertThat(persons) + .hasSize(1) + .first().satisfies(p -> { + assertThat(p.getName()).isEqualTo(person1.getName()); + assertThat(p.getFirstName()).isEqualTo(person1.getFirstName()); + assertThat(p.getId()).isNotNull(); + + assertThat(p.getBornOn()).isNull(); + assertThat(p.getCool()).isNull(); + assertThat(p.getCreatedAt()).isNull(); + assertThat(p.getNullable()).isNull(); + assertThat(p.getPersonNumber()).isNull(); + assertThat(p.getPlace()).isNull(); + assertThat(p.getSameValue()).isNull(); + assertThat(p.getThings()).isNull(); + }); + } + + @Test // GH-2343 + void findAllByExampleFluentAs(@Autowired PersonRepository repository) { + + Example example = Example.of(person1, + ExampleMatcher.matchingAll().withIgnoreNullValues()); + + List people = repository.findBy(example, q -> q.as(DtoPersonProjection.class).all()); + assertThat(people) + .hasSize(1) + .extracting(DtoPersonProjection::getFirstName) + .first().isEqualTo(TEST_PERSON1_FIRST_NAME); + } + + @Test // GH-2343 + void streamByExample(@Autowired PersonRepository repository) { + + Example example = Example.of(person1, + ExampleMatcher.matchingAll().withIgnoreNullValues()); + Stream persons = repository.findBy(example, FluentQuery.FetchableFluentQuery::stream); + + assertThat(persons).containsExactly(person1); + } + + @Test // GH-2343 + void findFirstByExample(@Autowired PersonRepository repository) { + + Example example = Example.of(person1, + ExampleMatcher.matchingAll().withIgnoreNullValues()); + PersonWithAllConstructor person = repository.findBy(example, q -> q.sortBy(Sort.by(Sort.Direction.DESC, "name")).firstValue()); + + assertThat(person).isNotNull(); + assertThat(person).isEqualTo(person1); + } + @Test void findAllByExampleWithDifferentMatchers(@Autowired PersonRepository repository) { @@ -2716,6 +2799,16 @@ void findAllByExampleWithSort(@Autowired PersonRepository repository) { assertThat(persons).containsExactly(person2, person1); } + @Test // GH-2343 + void findAllByExampleWithSortFluent(@Autowired PersonRepository repository) { + + Example example = Example.of(personExample(TEST_PERSON_SAMEVALUE)); + List persons = repository + .findBy(example, q -> q.sortBy(Sort.by(Sort.Direction.DESC, "name")).all()); + + assertThat(persons).containsExactly(person2, person1); + } + @Test void findAllByExampleWithPagination(@Autowired PersonRepository repository) { @@ -2725,6 +2818,15 @@ void findAllByExampleWithPagination(@Autowired PersonRepository repository) { assertThat(persons).containsExactly(person2); } + @Test // GH-2343 + void findAllByExampleWithPaginationFluent(@Autowired PersonRepository repository) { + + Example example = Example.of(personExample(TEST_PERSON_SAMEVALUE)); + Iterable persons = repository.findBy(example, q -> q.page(PageRequest.of(1, 1, Sort.by("name")))); + + assertThat(persons).containsExactly(person2); + } + @Test void existsByExample(@Autowired PersonRepository repository) { @@ -2734,6 +2836,15 @@ void existsByExample(@Autowired PersonRepository repository) { assertThat(exists).isTrue(); } + @Test // GH-2343 + void existsByExampleFluent(@Autowired PersonRepository repository) { + + Example example = Example.of(personExample(TEST_PERSON_SAMEVALUE)); + boolean exists = repository.findBy(example, q -> q.exists()); + + assertThat(exists).isTrue(); + } + @Test void countByExample(@Autowired PersonRepository repository) { @@ -2743,6 +2854,15 @@ void countByExample(@Autowired PersonRepository repository) { assertThat(count).isEqualTo(1); } + @Test // GH-2343 + void countByExampleFluent(@Autowired PersonRepository repository) { + + Example example = Example.of(person1); + long count = repository.findBy(example, q -> q.count()); + + assertThat(count).isEqualTo(1); + } + @Test void findEntityWithRelationshipByFindOneByExample(@Autowired RelationshipRepository repository) { diff --git a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveQuerydslNeo4jPredicateExecutorIT.java b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveQuerydslNeo4jPredicateExecutorIT.java new file mode 100644 index 0000000000..7a6594d147 --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveQuerydslNeo4jPredicateExecutorIT.java @@ -0,0 +1,309 @@ +/* + * Copyright 2011-2021 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 static org.assertj.core.api.Assertions.assertThat; + +import reactor.test.StepVerifier; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +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.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.data.neo4j.config.AbstractReactiveNeo4jConfig; +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.test.BookmarkCapture; +import org.springframework.data.neo4j.test.Neo4jExtension; +import org.springframework.data.neo4j.test.Neo4jIntegrationTest; +import org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor; +import org.springframework.transaction.ReactiveTransactionManager; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +import com.querydsl.core.types.Ops; +import com.querydsl.core.types.Order; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.Predicate; +import com.querydsl.core.types.dsl.Expressions; + +/** + * @author Michael J. Simons + */ +@Neo4jIntegrationTest +class ReactiveQuerydslNeo4jPredicateExecutorIT { + + protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; + + private final Path personPath; + private final Path firstNamePath; + private final Path lastNamePath; + + ReactiveQuerydslNeo4jPredicateExecutorIT() { + this.personPath = Expressions.path(Person.class, "person"); + this.firstNamePath = Expressions.path(String.class, personPath, "firstName"); + this.lastNamePath = Expressions.path(String.class, personPath, "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 // GH-2361 + void fluentFindOneShouldWork(@Autowired QueryDSLPersonRepository repository) { + + Predicate predicate = Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")); + repository.findBy(predicate, q -> q.one()) + .map(Person::getLastName) + .as(StepVerifier::create) + .expectNext("Schneider") + .verifyComplete(); + } + + @Test // GH-2361 + void fluentFindAllShouldWork(@Autowired QueryDSLPersonRepository repository) { + + Predicate predicate = Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")) + .or(Expressions.predicate(Ops.EQ, lastNamePath, Expressions.asString("B."))); + repository.findBy(predicate, q -> q.all()) + .map(Person::getFirstName) + .sort() // Due to not having something like containsExactlyInAnyOrder + .as(StepVerifier::create) + .expectNext("Bela", "Helge") + .verifyComplete(); + } + + @Test // GH-2361 + void fluentFindAllProjectingShouldWork(@Autowired QueryDSLPersonRepository repository) { + + Predicate predicate = Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")); + repository.findBy(predicate, q -> q.project("firstName").all()) + .as(StepVerifier::create) + .expectNextMatches(p -> { + assertThat(p.getFirstName()).isEqualTo("Helge"); + assertThat(p.getId()).isNotNull(); + + assertThat(p.getLastName()).isNull(); + assertThat(p.getAddress()).isNull(); + return true; + }) + .verifyComplete(); + } + + static class DtoPersonProjection { + + private final String firstName; + + DtoPersonProjection(String firstName) { + this.firstName = firstName; + } + + public String getFirstName() { + return firstName; + } + } + + @Test // GH-2361 + void fluentfindAllAsShouldWork(@Autowired QueryDSLPersonRepository repository) { + + Predicate predicate = Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")); + repository.findBy(predicate, q -> q.as(DtoPersonProjection.class).all()) + .map(DtoPersonProjection::getFirstName) + .as(StepVerifier::create) + .expectNext("Helge") + .verifyComplete(); + } + + @Test // GH-2361 + void fluentFindFirstShouldWork(@Autowired QueryDSLPersonRepository repository) { + + Predicate predicate = Expressions.TRUE.isTrue(); + repository.findBy(predicate, q -> q.sortBy(Sort.by(Sort.Direction.DESC, "lastName")).first()) + .map(Person::getFirstName) + .as(StepVerifier::create) + .expectNext("Helge") + .verifyComplete(); + } + + @Test // GH-2361 + void fluentFindAllWithSortShouldWork(@Autowired QueryDSLPersonRepository repository) { + + Predicate predicate = Expressions.TRUE.isTrue(); + repository.findBy(predicate, q -> q.sortBy(Sort.by(Sort.Direction.DESC, "lastName")).all()) + .map(Person::getLastName) + .as(StepVerifier::create) + .expectNext("Schneider", "LB", "LA", "B.") + .verifyComplete(); + } + + @Test // GH-2361 + void fluentFindAllWithPaginationShouldWork(@Autowired QueryDSLPersonRepository repository) { + + Predicate predicate = Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")) + .or(Expressions.predicate(Ops.EQ, lastNamePath, Expressions.asString("B."))); + repository.findBy(predicate, q -> q.page(PageRequest.of(1, 1, Sort.by("lastName").ascending()))) + .as(StepVerifier::create) + .expectNextMatches(people -> { + + assertThat(people).extracting(Person::getFirstName).containsExactly("Helge"); + assertThat(people.hasPrevious()).isTrue(); + assertThat(people.hasNext()).isFalse(); + return true; + }).verifyComplete(); + } + + @Test // GH-2361 + void fluentExistsShouldWork(@Autowired QueryDSLPersonRepository repository) { + + Predicate predicate = Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")); + repository.findBy(predicate, q -> q.exists()).as(StepVerifier::create).expectNext(true).verifyComplete(); + } + + @Test // GH-2361 + void fluentCountShouldWork(@Autowired QueryDSLPersonRepository repository) { + + Predicate predicate = Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")) + .or(Expressions.predicate(Ops.EQ, lastNamePath, Expressions.asString("B."))); + repository.findBy(predicate, q -> q.count()).as(StepVerifier::create).expectNext(2L).verifyComplete(); + } + + @Test // GH-2361 + void findOneShouldWork(@Autowired QueryDSLPersonRepository repository) { + + repository.findOne(Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge"))) + .map(Person::getLastName) + .as(StepVerifier::create) + .expectNext("Schneider") + .verifyComplete(); + } + + @Test // GH-2361 + void findAllShouldWork(@Autowired QueryDSLPersonRepository repository) { + + repository.findAll(Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")) + .or(Expressions.predicate(Ops.EQ, lastNamePath, Expressions.asString("B.")))) + .map(Person::getFirstName) + .sort() // Due to not having something like containsExactlyInAnyOrder + .as(StepVerifier::create) + .expectNext("Bela", "Helge") + .verifyComplete(); + } + + @Test // GH-2361 + void sortedFindAllShouldWork(@Autowired QueryDSLPersonRepository repository) { + + repository.findAll(Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")) + .or(Expressions.predicate(Ops.EQ, lastNamePath, Expressions.asString("B."))), + new OrderSpecifier(Order.DESC, lastNamePath) + ) + .map(Person::getFirstName) + .as(StepVerifier::create) + .expectNext("Helge", "Bela") + .verifyComplete(); + } + + @Test // GH-2361 + void orderedFindAllShouldWork(@Autowired QueryDSLPersonRepository repository) { + + repository.findAll(Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")) + .or(Expressions.predicate(Ops.EQ, lastNamePath, Expressions.asString("B."))), + Sort.by("lastName").descending() + ) + .map(Person::getFirstName) + .as(StepVerifier::create) + .expectNext("Helge", "Bela") + .verifyComplete(); + } + + @Test // GH-2361 + void orderedFindAllWithoutPredicateShouldWork(@Autowired QueryDSLPersonRepository repository) { + + repository.findAll(new OrderSpecifier(Order.DESC, lastNamePath)) + .map(Person::getFirstName) + .as(StepVerifier::create) + .expectNext("Helge", "B", "A", "Bela") + .verifyComplete(); + } + + @Test // GH-2361 + void countShouldWork(@Autowired QueryDSLPersonRepository repository) { + + repository.count(Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("Helge")) + .or(Expressions.predicate(Ops.EQ, lastNamePath, Expressions.asString("B."))) + ).as(StepVerifier::create) + .expectNext(2L) + .verifyComplete(); + } + + @Test // GH-2361 + void existsShouldWork(@Autowired QueryDSLPersonRepository repository) { + + repository.exists(Expressions.predicate(Ops.EQ, firstNamePath, Expressions.asString("A"))) + .as(StepVerifier::create) + .expectNext(true) + .verifyComplete(); + } + + interface QueryDSLPersonRepository extends ReactiveNeo4jRepository, ReactiveQuerydslPredicateExecutor { + } + + @Configuration + @EnableTransactionManagement + @EnableReactiveNeo4jRepositories(considerNestedRepositories = true) + static class Config extends AbstractReactiveNeo4jConfig { + + @Bean + public Driver driver() { + + return neo4jConnectionSupport.getDriver(); + } + + @Bean + public BookmarkCapture bookmarkCapture() { + return new BookmarkCapture(); + } + + @Override + public ReactiveTransactionManager reactiveTransactionManager(Driver driver, ReactiveDatabaseSelectionProvider databaseNameProvider) { + + BookmarkCapture bookmarkCapture = bookmarkCapture(); + return new ReactiveNeo4jTransactionManager(driver, databaseNameProvider, Neo4jBookmarkManager.create(bookmarkCapture)); + } + } +} diff --git a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveRepositoryIT.java b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveRepositoryIT.java index f1f01d75f8..0c89a1f6ab 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveRepositoryIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveRepositoryIT.java @@ -19,7 +19,6 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.tuple; -import org.springframework.data.mapping.MappingException; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -65,6 +64,7 @@ import org.springframework.data.domain.ExampleMatcher; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; +import org.springframework.data.mapping.MappingException; import org.springframework.data.neo4j.config.AbstractReactiveNeo4jConfig; import org.springframework.data.neo4j.core.DatabaseSelection; import org.springframework.data.neo4j.core.ReactiveDatabaseSelectionProvider; @@ -107,6 +107,7 @@ import org.springframework.data.neo4j.test.Neo4jExtension; import org.springframework.data.neo4j.types.CartesianPoint2d; import org.springframework.data.neo4j.types.GeographicPoint2d; +import org.springframework.data.repository.query.FluentQuery; import org.springframework.data.repository.query.Param; import org.springframework.data.repository.reactive.ReactiveCrudRepository; import org.springframework.test.annotation.DirtiesContext; @@ -276,6 +277,18 @@ void findOneByExample(@Autowired ReactivePersonRepository repository) { StepVerifier.create(repository.findOne(example)).expectNext(person1).verifyComplete(); } + @Test // GH-2343 + void findOneByExampleFluent(@Autowired ReactivePersonRepository repository) { + + Example example = Example.of(person1, + ExampleMatcher.matchingAll().withIgnoreNullValues()); + + repository.findBy(example, q -> q.one()) + .as(StepVerifier::create) + .expectNext(person1) + .verifyComplete(); + } + @Test void findAllByExample(@Autowired ReactivePersonRepository repository) { Example example = Example.of(person1, @@ -283,6 +296,66 @@ void findAllByExample(@Autowired ReactivePersonRepository repository) { StepVerifier.create(repository.findAll(example)).expectNext(person1).verifyComplete(); } + @Test // GH-2343 + void findAllByExampleFluent(@Autowired ReactivePersonRepository repository) { + + Example example = Example.of(person1, + ExampleMatcher.matchingAll().withIgnoreNullValues()); + repository.findBy(example, FluentQuery.ReactiveFluentQuery::all) + .as(StepVerifier::create) + .expectNext(person1) + .verifyComplete(); + } + + @Test // GH-2343 + void findAllByExampleFluentProjecting(@Autowired ReactivePersonRepository repository) { + + Example example = Example.of(person1, + ExampleMatcher.matchingAll().withIgnoreNullValues()); + + repository.findBy(example, q -> q.project("name", "firstName").all()) + .as(StepVerifier::create) + .expectNextMatches(p -> { + assertThat(p.getName()).isEqualTo(person1.getName()); + assertThat(p.getFirstName()).isEqualTo(person1.getFirstName()); + assertThat(p.getId()).isNotNull(); + + assertThat(p.getBornOn()).isNull(); + assertThat(p.getCool()).isNull(); + assertThat(p.getCreatedAt()).isNull(); + assertThat(p.getNullable()).isNull(); + assertThat(p.getPersonNumber()).isNull(); + assertThat(p.getPlace()).isNull(); + assertThat(p.getSameValue()).isNull(); + assertThat(p.getThings()).isNull(); + return true; + }).verifyComplete(); + } + + @Test // GH-2343 + void findAllByExampleFluentAs(@Autowired ReactivePersonRepository repository) { + + Example example = Example.of(person1, + ExampleMatcher.matchingAll().withIgnoreNullValues()); + + repository.findBy(example, q -> q.as(DtoPersonProjection.class).all()) + .map(DtoPersonProjection::getFirstName) + .as(StepVerifier::create) + .expectNext(TEST_PERSON1_FIRST_NAME) + .verifyComplete(); + } + + @Test // GH-2343 + void findFirstByExample(@Autowired ReactivePersonRepository repository) { + + Example example = Example.of(person1, + ExampleMatcher.matchingAll().withIgnoreNullValues()); + repository.findBy(example, q -> q.sortBy(Sort.by(Sort.Direction.DESC, "name")).first()) + .as(StepVerifier::create) + .expectNext(person1) + .verifyComplete(); + } + @Test void findAllByExampleWithDifferentMatchers(@Autowired ReactivePersonRepository repository) { PersonWithAllConstructor person; @@ -334,6 +407,17 @@ void findAllByExampleWithSort(@Autowired ReactivePersonRepository repository) { .expectNext(person2, person1).verifyComplete(); } + @Test // GH-2343 + void findAllByExampleWithSortFluent(@Autowired ReactivePersonRepository repository) { + + Example example = Example.of(personExample(TEST_PERSON_SAMEVALUE)); + repository + .findBy(example, q -> q.sortBy(Sort.by(Sort.Direction.DESC, "name")).all()) + .as(StepVerifier::create) + .expectNext(person2, person1) + .verifyComplete(); + } + @Test void findEntityWithRelationshipByFindOneByExample(@Autowired ReactiveRelationshipRepository repository) { @@ -465,11 +549,37 @@ void existsByIdPublisherNoMatch(@Autowired ReactivePersonRepository repository) StepVerifier.create(repository.existsById(NOT_EXISTING_NODE_ID)).expectNext(false).verifyComplete(); } + @Test // GH-2343 + void findAllByExampleWithPaginationFluent(@Autowired ReactivePersonRepository repository) { + + Example example = Example.of(personExample(TEST_PERSON_SAMEVALUE)); + repository.findBy(example, q -> q.page(PageRequest.of(1, 1, Sort.by("name")))) + .as(StepVerifier::create) + .expectNextMatches(page -> { + assertThat(page).containsExactly(person2); + assertThat(page.getTotalPages()).isEqualTo(2L); + assertThat(page.getTotalElements()).isEqualTo(2L); + assertThat(page.hasPrevious()).isTrue(); + assertThat(page.hasNext()).isFalse(); + return true; + }) + .verifyComplete(); + } + @Test void existsByExample(@Autowired ReactivePersonRepository repository) { Example example = Example.of(personExample(TEST_PERSON_SAMEVALUE)); StepVerifier.create(repository.exists(example)).expectNext(true).verifyComplete(); + } + @Test // GH-2343 + void existsByExampleFluent(@Autowired ReactivePersonRepository repository) { + + Example example = Example.of(personExample(TEST_PERSON_SAMEVALUE)); + repository.findBy(example, q -> q.exists()) + .as(StepVerifier::create) + .expectNext(true) + .verifyComplete(); } @Test @@ -483,6 +593,16 @@ void countByExample(@Autowired ReactivePersonRepository repository) { StepVerifier.create(repository.count(example)).expectNext(1L).verifyComplete(); } + @Test // GH-2343 + void countByExampleFluent(@Autowired ReactivePersonRepository repository) { + + Example example = Example.of(person1); + repository.findBy(example, q -> q.count()) + .as(StepVerifier::create) + .expectNext(1L) + .verifyComplete(); + } + @Test void callCustomCypher(@Autowired ReactivePersonRepository repository) { StepVerifier.create(repository.customQuery()).expectNext(1L).verifyComplete(); diff --git a/src/test/java/org/springframework/data/neo4j/repository/config/StartupLoggerTest.java b/src/test/java/org/springframework/data/neo4j/repository/config/StartupLoggerTest.java index 398b302991..e8c988958a 100644 --- a/src/test/java/org/springframework/data/neo4j/repository/config/StartupLoggerTest.java +++ b/src/test/java/org/springframework/data/neo4j/repository/config/StartupLoggerTest.java @@ -30,6 +30,6 @@ void startingMessageShouldFit() { String message = new StartupLogger(StartupLogger.Mode.IMPERATIVE).getStartingMessage(); assertThat(message).matches( - "Bootstrapping imperative Neo4j repositories based on an unknown version of SDN with Spring Data Commons v2\\.\\d+\\.\\d+.(RELEASE|(?:DATACMNS-\\d+-)?SNAPSHOT) and Neo4j Driver v4\\.\\d+\\.\\d+(?:-.*)\\."); + "Bootstrapping imperative Neo4j repositories based on an unknown version of SDN with Spring Data Commons v2\\.\\d+\\.\\d+.(RELEASE|(?:(?:DATACMNS-)?\\d+-)?SNAPSHOT) and Neo4j Driver v4\\.\\d+\\.\\d+-.*\\."); } }