GH-2343 - Add FluentQuery support to QuerydslPredicateExecutor and QueryByExampleExecutor.
This change uses the existing `FluentOperations` to cater for the above usescases. It introduces a new `matching` operation for taking in a `QueryFragmentsAndParameters` on those operations. That operation is of limited external use, but necessary to implement that feature without adding more overhead than necessary. This closes #2343. Original pull request: #2360.
This commit is contained in:
committed by
Mark Paluch
parent
fb38347284
commit
d996d88ccc
@@ -22,6 +22,7 @@ import java.util.Optional;
|
||||
|
||||
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 @@ public interface FluentFindOperation {
|
||||
*/
|
||||
TerminatingFind<T> matching(String query, @Nullable Map<String, Object> 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<T> matching(QueryFragmentsAndParameters queryFragmentsAndParameters);
|
||||
|
||||
/**
|
||||
* Set the filter query to be used.
|
||||
*
|
||||
|
||||
@@ -19,6 +19,7 @@ import java.util.Collections;
|
||||
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 @@ final class FluentOperationSupport implements FluentFindOperation, FluentSaveOpe
|
||||
private final Class<T> returnType;
|
||||
private final String query;
|
||||
private final Map<String, Object> parameters;
|
||||
private final QueryFragmentsAndParameters queryFragmentsAndParameters;
|
||||
|
||||
ExecutableFindSupport(Neo4jTemplate template, Class<?> domainType, Class<T> returnType, String query,
|
||||
Map<String, Object> parameters) {
|
||||
@@ -59,6 +61,16 @@ final class FluentOperationSupport implements FluentFindOperation, FluentSaveOpe
|
||||
this.returnType = returnType;
|
||||
this.query = query;
|
||||
this.parameters = parameters;
|
||||
this.queryFragmentsAndParameters = null;
|
||||
}
|
||||
|
||||
ExecutableFindSupport(Neo4jTemplate template, Class<?> domainType, Class<T> 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 @@ final class FluentOperationSupport implements FluentFindOperation, FluentSaveOpe
|
||||
return new ExecutableFindSupport<>(template, domainType, returnType, query, parameters);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("HiddenField")
|
||||
public TerminatingFind<T> 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 @@ final class FluentOperationSupport implements FluentFindOperation, FluentSaveOpe
|
||||
}
|
||||
|
||||
private List<T> doFind(TemplateSupport.FetchType fetchType) {
|
||||
return template.doFind(query, parameters, domainType, returnType, fetchType);
|
||||
return template.doFind(query, parameters, domainType, returnType, fetchType, queryFragmentsAndParameters);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -248,14 +248,19 @@ public final class Neo4jTemplate implements
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
<T, R> List<R> doFind(@Nullable String cypherQuery, @Nullable Map<String, Object> parameters, Class<T> domainType, Class<R> resultType, TemplateSupport.FetchType fetchType) {
|
||||
<T, R> List<R> doFind(@Nullable String cypherQuery, @Nullable Map<String, Object> parameters, Class<T> domainType, Class<R> resultType, TemplateSupport.FetchType fetchType, @Nullable QueryFragmentsAndParameters queryFragmentsAndParameters) {
|
||||
|
||||
List<T> 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<T> executableQuery = createExecutableQuery(domainType, resultType, cypherQuery,
|
||||
parameters == null ? Collections.emptyMap() : parameters);
|
||||
ExecutableQuery<T> 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();
|
||||
|
||||
@@ -23,6 +23,7 @@ import java.util.Map;
|
||||
|
||||
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 @@ public interface ReactiveFluentFindOperation {
|
||||
*/
|
||||
TerminatingFind<T> matching(String query, @Nullable Map<String, Object> 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<T> matching(QueryFragmentsAndParameters queryFragmentsAndParameters);
|
||||
|
||||
/**
|
||||
* Set the filter query to be used.
|
||||
*
|
||||
|
||||
@@ -21,6 +21,7 @@ import reactor.core.publisher.Mono;
|
||||
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 @@ final class ReactiveFluentOperationSupport implements ReactiveFluentFindOperatio
|
||||
private final Class<T> returnType;
|
||||
private final String query;
|
||||
private final Map<String, Object> parameters;
|
||||
private final QueryFragmentsAndParameters queryFragmentsAndParameters;
|
||||
|
||||
ExecutableFindSupport(ReactiveNeo4jTemplate template, Class<?> domainType, Class<T> returnType, String query,
|
||||
Map<String, Object> parameters) {
|
||||
@@ -62,6 +64,16 @@ final class ReactiveFluentOperationSupport implements ReactiveFluentFindOperatio
|
||||
this.returnType = returnType;
|
||||
this.query = query;
|
||||
this.parameters = parameters;
|
||||
this.queryFragmentsAndParameters = null;
|
||||
}
|
||||
|
||||
ExecutableFindSupport(ReactiveNeo4jTemplate template, Class<?> domainType, Class<T> 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 @@ final class ReactiveFluentOperationSupport implements ReactiveFluentFindOperatio
|
||||
return new ExecutableFindSupport<>(template, domainType, returnType, query, parameters);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("HiddenField")
|
||||
public TerminatingFind<T> matching(QueryFragmentsAndParameters queryFragmentsAndParameters) {
|
||||
|
||||
return new ExecutableFindSupport<>(template, domainType, returnType, queryFragmentsAndParameters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<T> one() {
|
||||
return doFind(TemplateSupport.FetchType.ONE).single();
|
||||
@@ -93,7 +112,7 @@ final class ReactiveFluentOperationSupport implements ReactiveFluentFindOperatio
|
||||
}
|
||||
|
||||
private Flux<T> doFind(TemplateSupport.FetchType fetchType) {
|
||||
return template.doFind(query, parameters, domainType, returnType, fetchType);
|
||||
return template.doFind(query, parameters, domainType, returnType, fetchType, queryFragmentsAndParameters);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -227,14 +227,19 @@ public final class ReactiveNeo4jTemplate implements
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
<T, R> Flux<R> doFind(@Nullable String cypherQuery, @Nullable Map<String, Object> parameters, Class<T> domainType, Class<R> resultType, TemplateSupport.FetchType fetchType) {
|
||||
<T, R> Flux<R> doFind(@Nullable String cypherQuery, @Nullable Map<String, Object> parameters, Class<T> domainType, Class<R> resultType, TemplateSupport.FetchType fetchType, @Nullable QueryFragmentsAndParameters queryFragmentsAndParameters) {
|
||||
|
||||
Flux<T> 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<T>> executableQuery = createExecutableQuery(domainType, resultType, cypherQuery,
|
||||
parameters == null ? Collections.emptyMap() : parameters);
|
||||
Mono<ExecutableQuery<T>> executableQuery;
|
||||
if (queryFragmentsAndParameters == null) {
|
||||
executableQuery = createExecutableQuery(domainType, resultType, cypherQuery,
|
||||
parameters == null ? Collections.emptyMap() : parameters);
|
||||
} else {
|
||||
executableQuery = createExecutableQuery(domainType, resultType, queryFragmentsAndParameters);
|
||||
}
|
||||
|
||||
switch (fetchType) {
|
||||
case ALL:
|
||||
|
||||
@@ -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 <S> Source type
|
||||
* @param <R> Result type
|
||||
* @since 6.2
|
||||
*/
|
||||
@API(status = API.Status.INTERNAL, since = "6.2")
|
||||
final class FetchableFluentQueryByExample<S, R> extends FluentQuerySupport<R> implements FetchableFluentQuery<R> {
|
||||
|
||||
private final Neo4jMappingContext mappingContext;
|
||||
|
||||
private final Example<S> example;
|
||||
|
||||
private final FluentFindOperation findOperation;
|
||||
|
||||
private final Function<Example<S>, Long> countOperation;
|
||||
|
||||
private final Function<Example<S>, Boolean> existsOperation;
|
||||
|
||||
FetchableFluentQueryByExample(
|
||||
Example<S> example,
|
||||
Class<R> resultType,
|
||||
Neo4jMappingContext mappingContext,
|
||||
FluentFindOperation findOperation,
|
||||
Function<Example<S>, Long> countOperation,
|
||||
Function<Example<S>, Boolean> existsOperation
|
||||
) {
|
||||
this(example, resultType, mappingContext, findOperation, countOperation, existsOperation, Sort.unsorted(),
|
||||
null);
|
||||
}
|
||||
|
||||
FetchableFluentQueryByExample(
|
||||
Example<S> example,
|
||||
Class<R> resultType,
|
||||
Neo4jMappingContext mappingContext,
|
||||
FluentFindOperation findOperation,
|
||||
Function<Example<S>, Long> countOperation,
|
||||
Function<Example<S>, Boolean> existsOperation,
|
||||
Sort sort,
|
||||
@Nullable Collection<String> 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<R> 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 <NR> FetchableFluentQuery<NR> as(Class<NR> resultType) {
|
||||
|
||||
return new FetchableFluentQueryByExample<>(this.example, resultType, this.mappingContext, this.findOperation,
|
||||
this.countOperation, this.existsOperation);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("HiddenField")
|
||||
public FetchableFluentQuery<R> project(Collection<String> properties) {
|
||||
|
||||
return new FetchableFluentQueryByExample<>(this.example, this.resultType, this.mappingContext, this.findOperation,
|
||||
this.countOperation, this.existsOperation, this.sort, mergeProperties(properties));
|
||||
}
|
||||
|
||||
@Override
|
||||
public R one() {
|
||||
|
||||
return findOperation.find(example.getProbeType())
|
||||
.as(resultType)
|
||||
.matching(QueryFragmentsAndParameters.forExample(mappingContext, example, sort,
|
||||
createIncludedFieldsPredicate()))
|
||||
.oneValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public R first() {
|
||||
|
||||
List<R> all = all();
|
||||
return all.isEmpty() ? null : all.get(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<R> all() {
|
||||
|
||||
return findOperation.find(example.getProbeType())
|
||||
.as(resultType)
|
||||
.matching(QueryFragmentsAndParameters.forExample(mappingContext, example, sort,
|
||||
createIncludedFieldsPredicate()))
|
||||
.all();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Page<R> page(Pageable pageable) {
|
||||
|
||||
List<R> 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<R> stream() {
|
||||
return all().stream();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long count() {
|
||||
return countOperation.apply(example);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exists() {
|
||||
return existsOperation.apply(example);
|
||||
}
|
||||
}
|
||||
@@ -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 <S> Source type
|
||||
* @param <R> Result type
|
||||
* @since 6.2
|
||||
* @soundtrack Die Ärzte - Geräusch
|
||||
*/
|
||||
@API(status = API.Status.INTERNAL, since = "6.2")
|
||||
final class FetchableFluentQueryByPredicate<S, R> extends FluentQuerySupport<R> implements FetchableFluentQuery<R> {
|
||||
|
||||
private final Predicate predicate;
|
||||
|
||||
private final Neo4jPersistentEntity<S> metaData;
|
||||
|
||||
private final FluentFindOperation findOperation;
|
||||
|
||||
private final Function<Predicate, Long> countOperation;
|
||||
|
||||
private final Function<Predicate, Boolean> existsOperation;
|
||||
|
||||
FetchableFluentQueryByPredicate(
|
||||
Predicate predicate,
|
||||
Neo4jPersistentEntity<S> metaData,
|
||||
Class<R> resultType,
|
||||
FluentFindOperation findOperation,
|
||||
Function<Predicate, Long> countOperation,
|
||||
Function<Predicate, Boolean> existsOperation
|
||||
) {
|
||||
this(predicate, metaData, resultType, findOperation, countOperation, existsOperation, Sort.unsorted(), null);
|
||||
}
|
||||
|
||||
FetchableFluentQueryByPredicate(
|
||||
Predicate predicate,
|
||||
Neo4jPersistentEntity<S> metaData,
|
||||
Class<R> resultType,
|
||||
FluentFindOperation findOperation,
|
||||
Function<Predicate, Long> countOperation,
|
||||
Function<Predicate, Boolean> existsOperation,
|
||||
Sort sort,
|
||||
@Nullable Collection<String> 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<R> 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 <NR> FetchableFluentQuery<NR> as(Class<NR> resultType) {
|
||||
|
||||
return new FetchableFluentQueryByPredicate<>(this.predicate, this.metaData, resultType, this.findOperation,
|
||||
this.countOperation, this.existsOperation);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("HiddenField")
|
||||
public FetchableFluentQuery<R> project(Collection<String> properties) {
|
||||
|
||||
return new FetchableFluentQueryByPredicate<>(this.predicate, this.metaData, this.resultType, this.findOperation,
|
||||
this.countOperation, this.existsOperation, sort, mergeProperties(properties));
|
||||
}
|
||||
|
||||
@Override
|
||||
public R one() {
|
||||
|
||||
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 first() {
|
||||
|
||||
List<R> all = all();
|
||||
return all.isEmpty() ? null : all.get(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<R> 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<R> page(Pageable pageable) {
|
||||
|
||||
List<R> 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<R> stream() {
|
||||
return all().stream();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long count() {
|
||||
return countOperation.apply(predicate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exists() {
|
||||
return existsOperation.apply(predicate);
|
||||
}
|
||||
}
|
||||
@@ -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 <R> The result type
|
||||
* @soundtrack Die Ärzte - Geräusch
|
||||
*/
|
||||
abstract class FluentQuerySupport<R> {
|
||||
|
||||
protected final Class<R> resultType;
|
||||
|
||||
protected final Sort sort;
|
||||
|
||||
@Nullable
|
||||
protected final Set<String> properties;
|
||||
|
||||
FluentQuerySupport(
|
||||
Class<R> resultType,
|
||||
Sort sort,
|
||||
@Nullable Collection<String> properties
|
||||
) {
|
||||
this.resultType = resultType;
|
||||
this.sort = sort;
|
||||
if (properties != null) {
|
||||
this.properties = new HashSet<>(properties);
|
||||
} else {
|
||||
this.properties = null;
|
||||
}
|
||||
}
|
||||
|
||||
final Predicate<PropertyFilter.RelaxedPropertyPath> createIncludedFieldsPredicate() {
|
||||
|
||||
if (this.properties == null) {
|
||||
return path -> true;
|
||||
}
|
||||
return path -> this.properties.contains(path.toDotPath());
|
||||
}
|
||||
|
||||
final Collection<String> mergeProperties(Collection<String> additionalProperties) {
|
||||
Set<String> newProperties = new HashSet<>();
|
||||
if (this.properties != null) {
|
||||
newProperties.addAll(this.properties);
|
||||
}
|
||||
newProperties.addAll(additionalProperties);
|
||||
return Collections.unmodifiableCollection(newProperties);
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,7 @@ import org.springframework.data.neo4j.core.mapping.CypherGenerator;
|
||||
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 final class QueryFragmentsAndParameters {
|
||||
* 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<PropertyFilter.RelaxedPropertyPath>) null);
|
||||
}
|
||||
|
||||
static QueryFragmentsAndParameters forExample(Neo4jMappingContext mappingContext, Example<?> example, @Nullable java.util.function.Predicate<PropertyFilter.RelaxedPropertyPath> 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<PropertyFilter.RelaxedPropertyPath> 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<PropertyFilter.RelaxedPropertyPath> includeField) {
|
||||
return QueryFragmentsAndParameters.forExample(mappingContext, example, pageable, null, includeField);
|
||||
}
|
||||
|
||||
static QueryFragmentsAndParameters forCondition(Neo4jPersistentEntity<?> entityMetaData,
|
||||
Condition condition,
|
||||
@Nullable Pageable pageable,
|
||||
@Nullable Collection<SortItem> sortItems
|
||||
) {
|
||||
return forCondition(entityMetaData, condition, pageable, sortItems, null);
|
||||
}
|
||||
|
||||
static QueryFragmentsAndParameters forCondition(Neo4jPersistentEntity<?> entityMetaData,
|
||||
Condition condition,
|
||||
@Nullable Pageable pageable,
|
||||
@Nullable Collection<SortItem> sortItems,
|
||||
@Nullable java.util.function.Predicate<PropertyFilter.RelaxedPropertyPath> 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 @@ public final class QueryFragmentsAndParameters {
|
||||
}
|
||||
|
||||
static QueryFragmentsAndParameters forExample(Neo4jMappingContext mappingContext, Example<?> example,
|
||||
@Nullable Pageable pageable, @Nullable Sort sort) {
|
||||
@Nullable Pageable pageable, @Nullable Sort sort, java.util.function.Predicate<PropertyFilter.RelaxedPropertyPath> includeField) {
|
||||
|
||||
Predicate predicate = Predicate.create(mappingContext, example);
|
||||
Map<String, Object> 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<String, Object> parameters, @Nullable Condition condition) {
|
||||
@Nullable Map<String, Object> parameters, @Nullable Condition condition, @Nullable
|
||||
java.util.function.Predicate<PropertyFilter.RelaxedPropertyPath> 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);
|
||||
|
||||
@@ -17,6 +17,7 @@ package org.springframework.data.neo4j.repository.query;
|
||||
|
||||
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.neo4j.cypherdsl.core.SortItem;
|
||||
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 <T> The returned domain type.
|
||||
@@ -47,12 +50,27 @@ import com.querydsl.core.types.Predicate;
|
||||
@API(status = API.Status.INTERNAL, since = "6.1")
|
||||
public final class QuerydslNeo4jPredicateExecutor<T> implements QuerydslPredicateExecutor<T> {
|
||||
|
||||
/**
|
||||
* Non-fluent operations are translated directly into Cypherdsl conditions and executed elsewhere.
|
||||
*/
|
||||
private final CypherdslConditionExecutor<T> delegate;
|
||||
|
||||
/**
|
||||
* Needed to support the fluent operations.
|
||||
*/
|
||||
private final Neo4jOperations neo4jOperations;
|
||||
|
||||
/**
|
||||
* Needed to support the fluent operations.
|
||||
*/
|
||||
private final Neo4jPersistentEntity<T> metaData;
|
||||
|
||||
public QuerydslNeo4jPredicateExecutor(Neo4jEntityInformation<T, Object> entityInformation,
|
||||
Neo4jOperations neo4jOperations) {
|
||||
|
||||
this.delegate = new CypherdslConditionExecutorImpl<>(entityInformation, neo4jOperations);
|
||||
this.neo4jOperations = neo4jOperations;
|
||||
this.metaData = entityInformation.getEntityMetaData();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -74,15 +92,15 @@ public final class QuerydslNeo4jPredicateExecutor<T> implements QuerydslPredicat
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<T> findAll(Predicate predicate, OrderSpecifier<?>... orderSpecifiers) {
|
||||
public Iterable<T> 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<T> findAll(OrderSpecifier<?>... orderSpecifiers) {
|
||||
public Iterable<T> findAll(OrderSpecifier<?>... orders) {
|
||||
|
||||
return this.delegate.findAll(toSortItems(orderSpecifiers));
|
||||
return this.delegate.findAll(toSortItems(orders));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -97,7 +115,7 @@ public final class QuerydslNeo4jPredicateExecutor<T> implements QuerydslPredicat
|
||||
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 @@ public final class QuerydslNeo4jPredicateExecutor<T> implements QuerydslPredicat
|
||||
public boolean exists(Predicate predicate) {
|
||||
return findAll(predicate).iterator().hasNext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <S extends T, R> R findBy(Predicate predicate, Function<FetchableFluentQuery<S>, R> queryFunction) {
|
||||
|
||||
if (this.neo4jOperations instanceof FluentFindOperation) {
|
||||
@SuppressWarnings("unchecked") // defaultResultType will be a supertype of S and at this stage, the same.
|
||||
FetchableFluentQuery<S> fluentQuery =
|
||||
(FetchableFluentQuery<S>) 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.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 <S> Source type
|
||||
* @param <R> Result type
|
||||
* @since 6.2
|
||||
*/
|
||||
@API(status = API.Status.INTERNAL, since = "6.2")
|
||||
final class ReactiveFluentQueryByExample<S, R> extends FluentQuerySupport<R> implements ReactiveFluentQuery<R> {
|
||||
|
||||
private final Neo4jMappingContext mappingContext;
|
||||
|
||||
private final Example<S> example;
|
||||
|
||||
private final ReactiveFluentFindOperation findOperation;
|
||||
|
||||
private final Function<Example<S>, Mono<Long>> countOperation;
|
||||
|
||||
private final Function<Example<S>, Mono<Boolean>> existsOperation;
|
||||
|
||||
ReactiveFluentQueryByExample(
|
||||
Example<S> example,
|
||||
Class<R> resultType,
|
||||
Neo4jMappingContext mappingContext,
|
||||
ReactiveFluentFindOperation findOperation,
|
||||
Function<Example<S>, Mono<Long>> countOperation,
|
||||
Function<Example<S>, Mono<Boolean>> existsOperation
|
||||
) {
|
||||
this(example, resultType, mappingContext, findOperation, countOperation, existsOperation, Sort.unsorted(),
|
||||
null);
|
||||
}
|
||||
|
||||
ReactiveFluentQueryByExample(
|
||||
Example<S> example,
|
||||
Class<R> resultType,
|
||||
Neo4jMappingContext mappingContext,
|
||||
ReactiveFluentFindOperation findOperation,
|
||||
Function<Example<S>, Mono<Long>> countOperation,
|
||||
Function<Example<S>, Mono<Boolean>> existsOperation,
|
||||
Sort sort,
|
||||
@Nullable Collection<String> 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<R> 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 <NR> ReactiveFluentQuery<NR> as(Class<NR> resultType) {
|
||||
|
||||
return new ReactiveFluentQueryByExample<>(this.example, resultType, this.mappingContext, this.findOperation,
|
||||
this.countOperation, this.existsOperation);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("HiddenField")
|
||||
public ReactiveFluentQuery<R> project(Collection<String> properties) {
|
||||
|
||||
return new ReactiveFluentQueryByExample<>(this.example, this.resultType, this.mappingContext, this.findOperation,
|
||||
this.countOperation, this.existsOperation, sort, mergeProperties(properties));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<R> one() {
|
||||
|
||||
return findOperation.find(example.getProbeType())
|
||||
.as(resultType)
|
||||
.matching(QueryFragmentsAndParameters.forExample(mappingContext, example, sort,
|
||||
createIncludedFieldsPredicate()))
|
||||
.one();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<R> first() {
|
||||
|
||||
return all().take(1).singleOrEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<R> all() {
|
||||
|
||||
return findOperation.find(example.getProbeType())
|
||||
.as(resultType)
|
||||
.matching(QueryFragmentsAndParameters.forExample(mappingContext, example, sort,
|
||||
createIncludedFieldsPredicate()))
|
||||
.all();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Page<R>> page(Pageable pageable) {
|
||||
|
||||
Flux<R> 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<R> page = PageableExecutionUtils.getPage(tuple.getT1(), pageable, () -> tuple.getT2());
|
||||
return page;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Long> count() {
|
||||
return countOperation.apply(example);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Boolean> exists() {
|
||||
return existsOperation.apply(example);
|
||||
}
|
||||
}
|
||||
@@ -22,14 +22,17 @@ 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.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 final class SimpleQueryByExampleExecutor<T> implements QueryByExampleExec
|
||||
return findAll(example).iterator().hasNext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <S extends T, R> R findBy(Example<S> example, Function<FetchableFluentQuery<S>, R> queryFunction) {
|
||||
|
||||
if (this.neo4jOperations instanceof FluentFindOperation) {
|
||||
FetchableFluentQuery<S> 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.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,17 +18,22 @@ package org.springframework.data.neo4j.repository.query;
|
||||
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 final class SimpleReactiveQueryByExampleExecutor<T> implements ReactiveQu
|
||||
public <S extends T> Mono<Boolean> exists(Example<S> example) {
|
||||
return findAll(example).hasElements();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <S extends T, R, P extends Publisher<R>> P findBy(Example<S> example, Function<ReactiveFluentQuery<S>, P> queryFunction) {
|
||||
if (this.neo4jOperations instanceof ReactiveFluentFindOperation) {
|
||||
ReactiveFluentQuery<S> 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.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,9 @@ package org.springframework.data.neo4j.integration.imperative;
|
||||
|
||||
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.BookmarkCapture;
|
||||
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.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;
|
||||
|
||||
/**
|
||||
@@ -56,14 +61,14 @@ class QuerydslNeo4jPredicateExecutorIT {
|
||||
|
||||
protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport;
|
||||
|
||||
private final Path<Person> person;
|
||||
private final Path<String> firstName;
|
||||
private final Path<String> lastName;
|
||||
private final Path<Person> personPath;
|
||||
private final Path<String> firstNamePath;
|
||||
private final Path<String> 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 @@ class QuerydslNeo4jPredicateExecutorIT {
|
||||
}
|
||||
}
|
||||
|
||||
@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.one());
|
||||
|
||||
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<Person> 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<Person> 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<DtoPersonProjection> 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<Person> 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<DtoPersonProjection> 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")).first());
|
||||
|
||||
assertThat(person).isNotNull();
|
||||
assertThat(person).extracting(Person::getFirstName).isEqualTo("Helge");
|
||||
}
|
||||
|
||||
@Test // GH-2343
|
||||
void fluentFindAllWithSortShouldWork(@Autowired QueryDSLPersonRepository repository) {
|
||||
|
||||
Predicate predicate = Expressions.TRUE.isTrue();
|
||||
List<Person> 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<Person> 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 @@ class QuerydslNeo4jPredicateExecutorIT {
|
||||
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 @@ class QuerydslNeo4jPredicateExecutorIT {
|
||||
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 @@ class QuerydslNeo4jPredicateExecutorIT {
|
||||
@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 @@ class QuerydslNeo4jPredicateExecutorIT {
|
||||
@Test
|
||||
void pagedFindAllShouldWork(@Autowired QueryDSLPersonRepository repository) {
|
||||
|
||||
Page<Person> people = repository.findAll(Expressions.predicate(Ops.EQ, firstName, Expressions.asString("Helge"))
|
||||
.or(Expressions.predicate(Ops.EQ, lastName, Expressions.asString("B."))),
|
||||
Page<Person> 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 @@ class QuerydslNeo4jPredicateExecutorIT {
|
||||
@Test // GH-2194
|
||||
void pagedFindAllShouldWork2(@Autowired QueryDSLPersonRepository repository) {
|
||||
|
||||
Page<Person> people = repository.findAll(Expressions.predicate(Ops.EQ, firstName, Expressions.asString("Helge"))
|
||||
.or(Expressions.predicate(Ops.EQ, lastName, Expressions.asString("B."))),
|
||||
Page<Person> 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 @@ class QuerydslNeo4jPredicateExecutorIT {
|
||||
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 @@ class QuerydslNeo4jPredicateExecutorIT {
|
||||
@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();
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ import java.util.function.Consumer;
|
||||
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;
|
||||
@@ -147,6 +148,7 @@ import org.springframework.data.neo4j.test.BookmarkCapture;
|
||||
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;
|
||||
@@ -2672,6 +2674,17 @@ class RepositoryIT {
|
||||
assertThat(person.get()).isEqualTo(person1);
|
||||
}
|
||||
|
||||
@Test // GH-2343
|
||||
void findOneByExampleFluent(@Autowired PersonRepository repository) {
|
||||
|
||||
Example<PersonWithAllConstructor> example = Example.of(person1,
|
||||
ExampleMatcher.matchingAll().withIgnoreNullValues());
|
||||
PersonWithAllConstructor person = repository.findBy(example, q -> q.one());
|
||||
|
||||
assertThat(person).isNotNull();
|
||||
assertThat(person).isEqualTo(person1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void findAllByExample(@Autowired PersonRepository repository) {
|
||||
|
||||
@@ -2682,6 +2695,76 @@ class RepositoryIT {
|
||||
assertThat(persons).containsExactly(person1);
|
||||
}
|
||||
|
||||
@Test // GH-2343
|
||||
void findAllByExampleFluent(@Autowired PersonRepository repository) {
|
||||
|
||||
Example<PersonWithAllConstructor> example = Example.of(person1,
|
||||
ExampleMatcher.matchingAll().withIgnoreNullValues());
|
||||
List<PersonWithAllConstructor> persons = repository.findBy(example, FluentQuery.FetchableFluentQuery::all);
|
||||
|
||||
assertThat(persons).containsExactly(person1);
|
||||
}
|
||||
|
||||
@Test // GH-2343
|
||||
void findAllByExampleFluentProjecting(@Autowired PersonRepository repository) {
|
||||
|
||||
Example<PersonWithAllConstructor> example = Example.of(person1,
|
||||
ExampleMatcher.matchingAll().withIgnoreNullValues());
|
||||
List<PersonWithAllConstructor> 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<PersonWithAllConstructor> example = Example.of(person1,
|
||||
ExampleMatcher.matchingAll().withIgnoreNullValues());
|
||||
|
||||
List<DtoPersonProjection> 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<PersonWithAllConstructor> example = Example.of(person1,
|
||||
ExampleMatcher.matchingAll().withIgnoreNullValues());
|
||||
Stream<PersonWithAllConstructor> persons = repository.findBy(example, FluentQuery.FetchableFluentQuery::stream);
|
||||
|
||||
assertThat(persons).containsExactly(person1);
|
||||
}
|
||||
|
||||
@Test // GH-2343
|
||||
void findFirstByExample(@Autowired PersonRepository repository) {
|
||||
|
||||
Example<PersonWithAllConstructor> example = Example.of(person1,
|
||||
ExampleMatcher.matchingAll().withIgnoreNullValues());
|
||||
PersonWithAllConstructor person = repository.findBy(example, q -> q.sortBy(Sort.by(Sort.Direction.DESC, "name")).first());
|
||||
|
||||
assertThat(person).isNotNull();
|
||||
assertThat(person).isEqualTo(person1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void findAllByExampleWithDifferentMatchers(@Autowired PersonRepository repository) {
|
||||
|
||||
@@ -2738,6 +2821,16 @@ class RepositoryIT {
|
||||
assertThat(persons).containsExactly(person2, person1);
|
||||
}
|
||||
|
||||
@Test // GH-2343
|
||||
void findAllByExampleWithSortFluent(@Autowired PersonRepository repository) {
|
||||
|
||||
Example<PersonWithAllConstructor> example = Example.of(personExample(TEST_PERSON_SAMEVALUE));
|
||||
List<PersonWithAllConstructor> 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) {
|
||||
|
||||
@@ -2747,6 +2840,15 @@ class RepositoryIT {
|
||||
assertThat(persons).containsExactly(person2);
|
||||
}
|
||||
|
||||
@Test // GH-2343
|
||||
void findAllByExampleWithPaginationFluent(@Autowired PersonRepository repository) {
|
||||
|
||||
Example<PersonWithAllConstructor> example = Example.of(personExample(TEST_PERSON_SAMEVALUE));
|
||||
Iterable<PersonWithAllConstructor> persons = repository.findBy(example, q -> q.page(PageRequest.of(1, 1, Sort.by("name"))));
|
||||
|
||||
assertThat(persons).containsExactly(person2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void existsByExample(@Autowired PersonRepository repository) {
|
||||
|
||||
@@ -2756,6 +2858,15 @@ class RepositoryIT {
|
||||
assertThat(exists).isTrue();
|
||||
}
|
||||
|
||||
@Test // GH-2343
|
||||
void existsByExampleFluent(@Autowired PersonRepository repository) {
|
||||
|
||||
Example<PersonWithAllConstructor> example = Example.of(personExample(TEST_PERSON_SAMEVALUE));
|
||||
boolean exists = repository.findBy(example, q -> q.exists());
|
||||
|
||||
assertThat(exists).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void countByExample(@Autowired PersonRepository repository) {
|
||||
|
||||
@@ -2765,6 +2876,15 @@ class RepositoryIT {
|
||||
assertThat(count).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test // GH-2343
|
||||
void countByExampleFluent(@Autowired PersonRepository repository) {
|
||||
|
||||
Example<PersonWithAllConstructor> example = Example.of(person1);
|
||||
long count = repository.findBy(example, q -> q.count());
|
||||
|
||||
assertThat(count).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void findEntityWithRelationshipByFindOneByExample(@Autowired RelationshipRepository repository) {
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
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.Example;
|
||||
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.BookmarkCapture;
|
||||
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 @@ class ReactiveRepositoryIT {
|
||||
StepVerifier.create(repository.findOne(example)).expectNext(person1).verifyComplete();
|
||||
}
|
||||
|
||||
@Test // GH-2343
|
||||
void findOneByExampleFluent(@Autowired ReactivePersonRepository repository) {
|
||||
|
||||
Example<PersonWithAllConstructor> 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<PersonWithAllConstructor> example = Example.of(person1,
|
||||
@@ -283,6 +296,66 @@ class ReactiveRepositoryIT {
|
||||
StepVerifier.create(repository.findAll(example)).expectNext(person1).verifyComplete();
|
||||
}
|
||||
|
||||
@Test // GH-2343
|
||||
void findAllByExampleFluent(@Autowired ReactivePersonRepository repository) {
|
||||
|
||||
Example<PersonWithAllConstructor> 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<PersonWithAllConstructor> 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<PersonWithAllConstructor> 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<PersonWithAllConstructor> 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 @@ class ReactiveRepositoryIT {
|
||||
.expectNext(person2, person1).verifyComplete();
|
||||
}
|
||||
|
||||
@Test // GH-2343
|
||||
void findAllByExampleWithSortFluent(@Autowired ReactivePersonRepository repository) {
|
||||
|
||||
Example<PersonWithAllConstructor> 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 @@ class ReactiveRepositoryIT {
|
||||
StepVerifier.create(repository.existsById(NOT_EXISTING_NODE_ID)).expectNext(false).verifyComplete();
|
||||
}
|
||||
|
||||
@Test // GH-2343
|
||||
void findAllByExampleWithPaginationFluent(@Autowired ReactivePersonRepository repository) {
|
||||
|
||||
Example<PersonWithAllConstructor> 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<PersonWithAllConstructor> example = Example.of(personExample(TEST_PERSON_SAMEVALUE));
|
||||
StepVerifier.create(repository.exists(example)).expectNext(true).verifyComplete();
|
||||
}
|
||||
|
||||
@Test // GH-2343
|
||||
void existsByExampleFluent(@Autowired ReactivePersonRepository repository) {
|
||||
|
||||
Example<PersonWithAllConstructor> example = Example.of(personExample(TEST_PERSON_SAMEVALUE));
|
||||
repository.findBy(example, q -> q.exists())
|
||||
.as(StepVerifier::create)
|
||||
.expectNext(true)
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -483,6 +593,16 @@ class ReactiveRepositoryIT {
|
||||
StepVerifier.create(repository.count(example)).expectNext(1L).verifyComplete();
|
||||
}
|
||||
|
||||
@Test // GH-2343
|
||||
void countByExampleFluent(@Autowired ReactivePersonRepository repository) {
|
||||
|
||||
Example<PersonWithAllConstructor> 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();
|
||||
|
||||
Reference in New Issue
Block a user