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:
Michael Simons
2021-08-24 15:33:04 +02:00
committed by Mark Paluch
parent fb38347284
commit d996d88ccc
17 changed files with 1178 additions and 51 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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