From 16051106c04b170f0967ab0095047596264c599f Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 11 Jan 2019 15:10:57 +0100 Subject: [PATCH] DATAMONGO-2182 - Add Querydsl support for reactive repositories. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We now support execution of Querydsl Predicates using reactive MongoDB repositories. Reactive repositories are only required to add ReactiveQuerydslPredicateExecutor to their declaration to enable Querydsl. public interface PersonRepository extends ReactiveMongoRepository, ReactiveQuerydslPredicateExecutor { // additional query methods go here } PersonRepository repository = …; QPerson person = new QPerson("person"); Flux result = repository.findAll(person.address.zipCode.eq("C0123")); Original Pull Request: #635 --- .../ReactiveMongoRepositoryFactory.java | 35 ++- ...eactiveQuerydslMongoPredicateExecutor.java | 232 ++++++++++++++ .../ReactiveSpringDataMongodbQuery.java | 282 ++++++++++++++++++ .../ReactiveMongoRepositoryTests.java | 16 +- ...ongoPredicateExecutorIntegrationTests.java | 278 +++++++++++++++++ src/main/asciidoc/new-features.adoc | 1 + .../reactive-mongo-repositories.adoc | 59 ++++ 7 files changed, 899 insertions(+), 4 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveQuerydslMongoPredicateExecutor.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveSpringDataMongodbQuery.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/ReactiveQuerydslMongoPredicateExecutorIntegrationTests.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java index 5436a0a81..afef1a6e1 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java @@ -15,6 +15,8 @@ */ package org.springframework.data.mongodb.repository.support; +import static org.springframework.data.querydsl.QuerydslUtils.*; + import lombok.AccessLevel; import lombok.RequiredArgsConstructor; @@ -32,10 +34,13 @@ import org.springframework.data.mongodb.repository.query.ReactiveMongoQueryMetho import org.springframework.data.mongodb.repository.query.ReactivePartTreeMongoQuery; import org.springframework.data.mongodb.repository.query.ReactiveStringBasedMongoQuery; import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor; import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.ReactiveRepositoryFactorySupport; +import org.springframework.data.repository.core.support.RepositoryComposition.RepositoryFragments; +import org.springframework.data.repository.core.support.RepositoryFragment; import org.springframework.data.repository.query.QueryLookupStrategy; import org.springframework.data.repository.query.QueryLookupStrategy.Key; import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; @@ -81,6 +86,30 @@ public class ReactiveMongoRepositoryFactory extends ReactiveRepositoryFactorySup return SimpleReactiveMongoRepository.class; } + /* + * (non-Javadoc) + * @see org.springframework.data.repository.core.support.RepositoryFactorySupport#getRepositoryFragments(org.springframework.data.repository.core.RepositoryMetadata) + */ + @Override + protected RepositoryFragments getRepositoryFragments(RepositoryMetadata metadata) { + + RepositoryFragments fragments = RepositoryFragments.empty(); + + boolean isQueryDslRepository = QUERY_DSL_PRESENT + && ReactiveQuerydslPredicateExecutor.class.isAssignableFrom(metadata.getRepositoryInterface()); + + if (isQueryDslRepository) { + + MongoEntityInformation entityInformation = getEntityInformation(metadata.getDomainType(), + metadata); + + fragments = fragments.append(RepositoryFragment.implemented(getTargetRepositoryViaReflection( + ReactiveQuerydslMongoPredicateExecutor.class, entityInformation, operations))); + } + + return fragments; + } + /* * (non-Javadoc) * @see org.springframework.data.repository.core.support.RepositoryFactorySupport#getTargetRepository(org.springframework.data.repository.core.RepositoryInformation) @@ -113,12 +142,12 @@ public class ReactiveMongoRepositoryFactory extends ReactiveRepositoryFactorySup @SuppressWarnings("unchecked") private MongoEntityInformation getEntityInformation(Class domainClass, - @Nullable RepositoryInformation information) { + @Nullable RepositoryMetadata metadata) { MongoPersistentEntity entity = mappingContext.getRequiredPersistentEntity(domainClass); - return new MappingMongoEntityInformation((MongoPersistentEntity) entity, - information != null ? (Class) information.getIdType() : null); + return new MappingMongoEntityInformation<>((MongoPersistentEntity) entity, + metadata != null ? (Class) metadata.getIdType() : null); } /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveQuerydslMongoPredicateExecutor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveQuerydslMongoPredicateExecutor.java new file mode 100644 index 000000000..807696c05 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveQuerydslMongoPredicateExecutor.java @@ -0,0 +1,232 @@ +/* + * Copyright 2019 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 + * + * http://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.mongodb.repository.support; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.List; + +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Order; +import org.springframework.data.mongodb.core.ReactiveMongoOperations; +import org.springframework.data.mongodb.repository.query.MongoEntityInformation; +import org.springframework.data.querydsl.EntityPathResolver; +import org.springframework.data.querydsl.QSort; +import org.springframework.data.querydsl.QuerydslPredicateExecutor; +import org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor; +import org.springframework.data.querydsl.SimpleEntityPathResolver; +import org.springframework.data.repository.core.EntityInformation; +import org.springframework.util.Assert; + +import com.querydsl.core.types.EntityPath; +import com.querydsl.core.types.Expression; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.Predicate; +import com.querydsl.core.types.dsl.PathBuilder; + +/** + * MongoDB-specific {@link QuerydslPredicateExecutor} that allows execution {@link Predicate}s in various forms. + * + * @author Mark Paluch + * @since 2.2 + */ +public class ReactiveQuerydslMongoPredicateExecutor implements ReactiveQuerydslPredicateExecutor { + + private final PathBuilder builder; + private final EntityInformation entityInformation; + private final ReactiveMongoOperations mongoOperations; + + /** + * Creates a new {@link ReactiveQuerydslMongoPredicateExecutor} for the given {@link MongoEntityInformation} and + * {@link ReactiveMongoOperations}. Uses the {@link SimpleEntityPathResolver} to create an {@link EntityPath} for the + * given domain class. + * + * @param entityInformation must not be {@literal null}. + * @param mongoOperations must not be {@literal null}. + */ + public ReactiveQuerydslMongoPredicateExecutor(MongoEntityInformation entityInformation, + ReactiveMongoOperations mongoOperations) { + this(entityInformation, mongoOperations, SimpleEntityPathResolver.INSTANCE); + } + + /** + * Creates a new {@link ReactiveQuerydslMongoPredicateExecutor} for the given {@link MongoEntityInformation}, + * {@link ReactiveMongoOperations} and {@link EntityPathResolver}. + * + * @param entityInformation must not be {@literal null}. + * @param mongoOperations must not be {@literal null}. + * @param resolver must not be {@literal null}. + */ + public ReactiveQuerydslMongoPredicateExecutor(MongoEntityInformation entityInformation, + ReactiveMongoOperations mongoOperations, EntityPathResolver resolver) { + + Assert.notNull(resolver, "EntityPathResolver must not be null!"); + + EntityPath path = resolver.createPath(entityInformation.getJavaType()); + + this.builder = new PathBuilder(path.getType(), path.getMetadata()); + this.entityInformation = entityInformation; + this.mongoOperations = mongoOperations; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor#findOne(com.querydsl.core.types.Predicate) + */ + @Override + public Mono findOne(Predicate predicate) { + + Assert.notNull(predicate, "Predicate must not be null!"); + + return createQueryFor(predicate).fetchOne(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor#findAll(com.querydsl.core.types.Predicate) + */ + @Override + public Flux findAll(Predicate predicate) { + + Assert.notNull(predicate, "Predicate must not be null!"); + + return createQueryFor(predicate).fetch(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor#findAll(com.querydsl.core.types.Predicate, com.querydsl.core.types.OrderSpecifier[]) + */ + @Override + public Flux findAll(Predicate predicate, OrderSpecifier... orders) { + + Assert.notNull(predicate, "Predicate must not be null!"); + Assert.notNull(orders, "Order specifiers must not be null!"); + + return createQueryFor(predicate).orderBy(orders).fetch(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor#findAll(com.querydsl.core.types.Predicate, org.springframework.data.domain.Sort) + */ + @Override + public Flux findAll(Predicate predicate, Sort sort) { + + Assert.notNull(predicate, "Predicate must not be null!"); + Assert.notNull(sort, "Sort must not be null!"); + + return applySorting(createQueryFor(predicate), sort).fetch(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor#findAll(com.querydsl.core.types.OrderSpecifier[]) + */ + @Override + public Flux findAll(OrderSpecifier... orders) { + + Assert.notNull(orders, "Order specifiers must not be null!"); + + return createQuery().orderBy(orders).fetch(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor#count(com.querydsl.core.types.Predicate) + */ + @Override + public Mono count(Predicate predicate) { + + Assert.notNull(predicate, "Predicate must not be null!"); + + return createQueryFor(predicate).fetchCount(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor#exists(com.querydsl.core.types.Predicate) + */ + @Override + public Mono exists(Predicate predicate) { + + Assert.notNull(predicate, "Predicate must not be null!"); + + return createQueryFor(predicate).fetchCount().map(it -> it != 0); + } + + /** + * Creates a {@link ReactiveSpringDataMongodbQuery} for the given {@link Predicate}. + * + * @param predicate + * @return + */ + private ReactiveSpringDataMongodbQuery createQueryFor(Predicate predicate) { + return createQuery().where(predicate); + } + + /** + * Creates a {@link ReactiveSpringDataMongodbQuery}. + * + * @return + */ + private ReactiveSpringDataMongodbQuery createQuery() { + SpringDataMongodbSerializer serializer = new SpringDataMongodbSerializer(mongoOperations.getConverter()); + + Class javaType = entityInformation.getJavaType(); + return new ReactiveSpringDataMongodbQuery<>(serializer, mongoOperations, javaType, + mongoOperations.getCollectionName(javaType)); + } + + /** + * Applies the given {@link Sort} to the given {@link ReactiveSpringDataMongodbQuery}. + * + * @param query + * @param sort + * @return + */ + private ReactiveSpringDataMongodbQuery applySorting(ReactiveSpringDataMongodbQuery query, Sort sort) { + + // TODO: find better solution than instanceof check + if (sort instanceof QSort) { + + List> orderSpecifiers = ((QSort) sort).getOrderSpecifiers(); + query.orderBy(orderSpecifiers.toArray(new OrderSpecifier[orderSpecifiers.size()])); + + return query; + } + + sort.stream().map(this::toOrder).forEach(query::orderBy); + + return query; + } + + /** + * Transforms a plain {@link Order} into a Querydsl specific {@link OrderSpecifier}. + * + * @param order + * @return + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + private OrderSpecifier toOrder(Order order) { + + Expression property = builder.get(order.getProperty()); + + return new OrderSpecifier( + order.isAscending() ? com.querydsl.core.types.Order.ASC : com.querydsl.core.types.Order.DESC, property); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveSpringDataMongodbQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveSpringDataMongodbQuery.java new file mode 100644 index 000000000..575139330 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveSpringDataMongodbQuery.java @@ -0,0 +1,282 @@ +/* + * Copyright 2019 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 + * + * http://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.mongodb.repository.support; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import org.springframework.data.mongodb.core.ReactiveFindOperation.FindWithProjection; +import org.springframework.data.mongodb.core.ReactiveMongoOperations; +import org.springframework.data.mongodb.core.query.BasicQuery; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.lang.Nullable; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +import com.querydsl.core.JoinExpression; +import com.querydsl.core.QueryMetadata; +import com.querydsl.core.QueryModifiers; +import com.querydsl.core.types.Expression; +import com.querydsl.core.types.ExpressionUtils; +import com.querydsl.core.types.Operation; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.Predicate; +import com.querydsl.core.types.dsl.CollectionPathBase; + +/** + * MongoDB query with utilizing {@link ReactiveMongoOperations} for command execution. + * + * @param result type + * @param concrete subtype + * @author Mark Paluch + * @since 2.2 + */ +class ReactiveSpringDataMongodbQuery extends QuerydslAbstractMongodbQuery> { + + private final Class entityClass; + private final ReactiveMongoOperations mongoOperations; + private final FindWithProjection find; + + ReactiveSpringDataMongodbQuery(ReactiveMongoOperations mongoOperations, Class entityClass) { + + super(new SpringDataMongodbSerializer(mongoOperations.getConverter())); + + this.entityClass = (Class) entityClass; + this.mongoOperations = mongoOperations; + this.find = mongoOperations.query(this.entityClass); + } + + ReactiveSpringDataMongodbQuery(MongodbDocumentSerializer serializer, ReactiveMongoOperations mongoOperations, + Class entityClass, String collection) { + + super(serializer); + + this.entityClass = (Class) entityClass; + this.mongoOperations = mongoOperations; + this.find = mongoOperations.query(this.entityClass).inCollection(collection); + } + + /** + * Fetch all matching query results. + * + * @return {@link Flux} emitting all query results or {@link Flux#empty()} if there are none. + */ + public Flux fetch() { + return createQuery().flatMapMany(it -> find.matching(it).all()); + } + + /** + * Fetch the first matching query result. + * + * @return {@link Mono} emitting the first query result or {@link Mono#empty()} if there are none. + * @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one match found. + */ + public Mono fetchOne() { + return createQuery().flatMap(it -> find.matching(it).one()); + } + + /** + * Fetch the count of matching query results. + * + * @return {@link Mono} emitting the first query result count. Emits always a count even item. + */ + public Mono fetchCount() { + return createQuery().flatMap(it -> find.matching(it).count()); + } + + /** + * Define a join. + * + * @param ref reference + * @param target join target + * @return new instance of {@link QuerydslJoinBuilder}. + */ + public QuerydslJoinBuilder, K, T> join(Path ref, Path target) { + return new QuerydslJoinBuilder<>(getQueryMixin(), ref, target); + } + + /** + * Define a join. + * + * @param ref reference + * @param target join target + * @return new instance of {@link QuerydslJoinBuilder}. + */ + public QuerydslJoinBuilder, K, T> join(CollectionPathBase ref, + Path target) { + return new QuerydslJoinBuilder<>(getQueryMixin(), ref, target); + } + + /** + * Define a constraint for an embedded object. + * + * @param collection collection must not be {@literal null}. + * @param target target must not be {@literal null}. + * @return new instance of {@link QuerydslAnyEmbeddedBuilder}. + */ + public QuerydslAnyEmbeddedBuilder, K> anyEmbedded( + Path> collection, Path target) { + return new QuerydslAnyEmbeddedBuilder<>(getQueryMixin(), collection); + } + + protected Mono createQuery() { + + QueryMetadata metadata = getQueryMixin().getMetadata(); + + return createQuery(createFilter(metadata), metadata.getProjection(), metadata.getModifiers(), + metadata.getOrderBy()); + } + + /** + * Creates a MongoDB query that is emitted through a {@link Mono} given {@link Mono} of {@link Predicate}. + * + * @param filter must not be {@literal null}. + * @param projection can be {@literal null} if no projection is given. Query requests all fields in such case. + * @param modifiers must not be {@literal null}. + * @param orderBy must not be {@literal null}. + * @return {@link Mono} emitting the {@link Query}. + */ + protected Mono createQuery(Mono filter, @Nullable Expression projection, + QueryModifiers modifiers, List> orderBy) { + + return filter.map(this::createQuery) // + .defaultIfEmpty(createQuery(null)) // + .map(it -> { + + BasicQuery basicQuery = new BasicQuery(it, createProjection(projection)); + + Integer limit = modifiers.getLimitAsInteger(); + Integer offset = modifiers.getOffsetAsInteger(); + + if (limit != null) { + basicQuery.limit(limit); + } + if (offset != null) { + basicQuery.skip(offset); + } + if (orderBy.size() > 0) { + basicQuery.setSortObject(createSort(orderBy)); + } + + return basicQuery; + }); + } + + protected Mono createFilter(QueryMetadata metadata) { + + if (!metadata.getJoins().isEmpty()) { + + return createJoinFilter(metadata).map(it -> ExpressionUtils.allOf(metadata.getWhere(), it)) + .switchIfEmpty(Mono.justOrEmpty(metadata.getWhere())); + } + + return Mono.justOrEmpty(metadata.getWhere()); + } + + /** + * Creates a Join filter by querying {@link com.mongodb.DBRef references}. + * + * @param metadata + * @return + */ + @SuppressWarnings("unchecked") + protected Mono createJoinFilter(QueryMetadata metadata) { + + MultiValueMap, Mono> predicates = new LinkedMultiValueMap<>(); + List joins = metadata.getJoins(); + + for (int i = joins.size() - 1; i >= 0; i--) { + + JoinExpression join = joins.get(i); + Path source = (Path) ((Operation) join.getTarget()).getArg(0); + Path target = (Path) ((Operation) join.getTarget()).getArg(1); + Collection> extraFilters = predicates.get(target.getRoot()); + + Mono filter = allOf(extraFilters).map(it -> ExpressionUtils.allOf(join.getCondition(), it)) + .switchIfEmpty(Mono.justOrEmpty(join.getCondition())); + + Mono predicate = getIds(target.getType(), filter) // + .collectList() // + .handle((it, sink) -> { + + if (it.isEmpty()) { + sink.error(new NoMatchException(source)); + return; + } + + Path path = ExpressionUtils.path(String.class, source, "$id"); + sink.next(ExpressionUtils.in((Path) path, it)); + }); + + predicates.add(source.getRoot(), predicate); + } + + Path source = (Path) ((Operation) joins.get(0).getTarget()).getArg(0); + return allOf(predicates.get(source.getRoot())).onErrorResume(NoMatchException.class, + e -> Mono.just(ExpressionUtils.predicate(QuerydslMongoOps.NO_MATCH, e.source))); + } + + private Mono allOf(@Nullable Collection> predicates) { + return predicates != null ? Flux.concat(predicates).collectList().map(ExpressionUtils::allOf) : Mono.empty(); + } + + /** + * Fetch the list of ids matching a given condition. + * + * @param targetType must not be {@literal null}. + * @param condition must not be {@literal null}. + * @return empty {@link List} if none found. + */ + protected Flux getIds(Class targetType, Mono condition) { + + return condition.flatMapMany(it -> getIds(targetType, it)) + .switchIfEmpty(Flux.defer(() -> getIds(targetType, (Predicate) null))); + } + + /** + * Fetch the list of ids matching a given condition. + * + * @param targetType must not be {@literal null}. + * @param condition must not be {@literal null}. + * @return empty {@link List} if none found. + */ + protected Flux getIds(Class targetType, @Nullable Predicate condition) { + return createQuery(Mono.justOrEmpty(condition), null, QueryModifiers.EMPTY, Collections.emptyList()) + .flatMapMany(query -> mongoOperations.findDistinct(query, "_id", targetType, Object.class)); + } + + /** + * Marker exception to indicate no matches for a query using reference Id's. + */ + static class NoMatchException extends RuntimeException { + + final Path source; + + public NoMatchException(Path source) { + this.source = source; + } + + @Override + public synchronized Throwable fillInStackTrace() { + return null; + } + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/ReactiveMongoRepositoryTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/ReactiveMongoRepositoryTests.java index d1b711a90..bdcc0dafe 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/ReactiveMongoRepositoryTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/ReactiveMongoRepositoryTests.java @@ -56,6 +56,7 @@ import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.repository.Person.Sex; import org.springframework.data.mongodb.repository.support.ReactiveMongoRepositoryFactory; import org.springframework.data.mongodb.repository.support.SimpleReactiveMongoRepository; +import org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor; import org.springframework.data.repository.Repository; import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.test.context.ContextConfiguration; @@ -82,6 +83,7 @@ public class ReactiveMongoRepositoryTests implements BeanClassLoaderAware, BeanF ReactiveCappedCollectionRepository cappedRepository; Person dave, oliver, carter, boyd, stefan, leroi, alicia; + QPerson person = new QPerson("person"); @Override public void setBeanClassLoader(ClassLoader classLoader) { @@ -376,7 +378,19 @@ public class ReactiveMongoRepositoryTests implements BeanClassLoaderAware, BeanF .verifyComplete(); } - interface ReactivePersonRepository extends ReactiveMongoRepository { + @Test // DATAMONGO-2182 + public void shouldFindPersonsWhenUsingQueryDslPerdicatedOnIdProperty() { + + repository.findAll(person.id.in(Arrays.asList(dave.id, carter.id))) // + .collectList() // + .as(StepVerifier::create) // + .assertNext(actual -> { + assertThat(actual).containsExactlyInAnyOrder(dave, carter); + }).verifyComplete(); + } + + interface ReactivePersonRepository + extends ReactiveMongoRepository, ReactiveQuerydslPredicateExecutor { Flux findByLastname(String lastname); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/ReactiveQuerydslMongoPredicateExecutorIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/ReactiveQuerydslMongoPredicateExecutorIntegrationTests.java new file mode 100644 index 000000000..2fd94afe0 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/ReactiveQuerydslMongoPredicateExecutorIntegrationTests.java @@ -0,0 +1,278 @@ +/* + * Copyright 2019 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 + * + * http://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.mongodb.repository.support; + +import reactor.core.publisher.Flux; +import reactor.test.StepVerifier; + +import java.util.Arrays; +import java.util.LinkedHashSet; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.IncorrectResultSizeDataAccessException; +import org.springframework.dao.PermissionDeniedDataAccessException; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Direction; +import org.springframework.data.mongodb.ReactiveMongoDatabaseFactory; +import org.springframework.data.mongodb.core.ReactiveMongoOperations; +import org.springframework.data.mongodb.core.ReactiveMongoTemplate; +import org.springframework.data.mongodb.repository.Address; +import org.springframework.data.mongodb.repository.Person; +import org.springframework.data.mongodb.repository.QAddress; +import org.springframework.data.mongodb.repository.QPerson; +import org.springframework.data.mongodb.repository.QUser; +import org.springframework.data.mongodb.repository.User; +import org.springframework.data.mongodb.repository.query.MongoEntityInformation; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringRunner; + +import com.mongodb.MongoException; +import com.mongodb.reactivestreams.client.MongoDatabase; + +/** + * Tests for {@link ReactiveQuerydslMongoPredicateExecutor}. + * + * @author Mark Paluch + */ +@RunWith(SpringRunner.class) +@ContextConfiguration("classpath:reactive-infrastructure.xml") +public class ReactiveQuerydslMongoPredicateExecutorIntegrationTests { + + @Autowired ReactiveMongoOperations operations; + @Autowired ReactiveMongoDatabaseFactory dbFactory; + + ReactiveQuerydslMongoPredicateExecutor repository; + + Person dave, oliver, carter; + QPerson person; + + @Before + public void setup() { + + ReactiveMongoRepositoryFactory factory = new ReactiveMongoRepositoryFactory(operations); + MongoEntityInformation entityInformation = factory.getEntityInformation(Person.class); + repository = new ReactiveQuerydslMongoPredicateExecutor<>(entityInformation, operations); + + operations.dropCollection(Person.class) // + .as(StepVerifier::create) // + .verifyComplete(); + + dave = new Person("Dave", "Matthews", 42); + oliver = new Person("Oliver August", "Matthews", 4); + carter = new Person("Carter", "Beauford", 49); + + person = new QPerson("person"); + + operations.insertAll(Arrays.asList(oliver, dave, carter)).as(StepVerifier::create) // + .expectNextCount(3) // + .verifyComplete(); + } + + @Test // DATAMONGO-2182 + public void shouldSupportExistsWithPredicate() { + + repository.exists(person.firstname.eq("Dave")) // + .as(StepVerifier::create) // + .expectNext(true) // + .verifyComplete(); + + repository.exists(person.firstname.eq("Unknown")) // + .as(StepVerifier::create) // + .expectNext(false) // + .verifyComplete(); + } + + @Test // DATAMONGO-2182 + public void shouldSupportFindAllWithPredicateAndSort() { + + repository.findAll(person.lastname.isNotNull(), Sort.by(Direction.ASC, "firstname")) // + .as(StepVerifier::create) // + .expectNext(carter, dave, oliver) // + .verifyComplete(); + } + + @Test // DATAMONGO-2182 + public void findOneWithPredicateReturnsResultCorrectly() { + + repository.findOne(person.firstname.eq(dave.getFirstname())) // + .as(StepVerifier::create) // + .expectNext(dave) // + .verifyComplete(); + } + + @Test // DATAMONGO-2182 + public void findOneWithPredicateReturnsEmptyWhenNoDataFound() { + + repository.findOne(person.firstname.eq("batman")) // + .as(StepVerifier::create) // + .verifyComplete(); + } + + @Test // DATAMONGO-2182 + public void findOneWithPredicateThrowsExceptionForNonUniqueResults() { + + repository.findOne(person.firstname.contains("e")) // + .as(StepVerifier::create) // + .expectError(IncorrectResultSizeDataAccessException.class) // + .verify(); + } + + @Test // DATAMONGO-2182 + public void findUsingAndShouldWork() { + + repository + .findAll(person.lastname.startsWith(oliver.getLastname()).and(person.firstname.startsWith(dave.getFirstname()))) // + .as(StepVerifier::create) // + .expectNext(dave) // + .verifyComplete(); + } + + @Test // DATAMONGO-2182 + public void queryShouldTerminateWithUnsupportedOperationWithJoinOnDBref() { + + User user1 = new User(); + user1.setUsername("user-1"); + + User user2 = new User(); + user2.setUsername("user-2"); + + User user3 = new User(); + user3.setUsername("user-3"); + + operations.insertAll(Arrays.asList(user1, user2, user3)) // + .as(StepVerifier::create) // + .expectNextCount(3) // + .verifyComplete(); + + Person person1 = new Person("Max", "The Mighty"); + person1.setCoworker(user1); + + Person person2 = new Person("Jack", "The Ripper"); + person2.setCoworker(user2); + + Person person3 = new Person("Bob", "The Builder"); + person3.setCoworker(user3); + + operations.save(person1) // + .as(StepVerifier::create) // + .expectNextCount(1) // + .verifyComplete(); + operations.save(person2)// + .as(StepVerifier::create) // + .expectNextCount(1) // + .verifyComplete(); + operations.save(person3) // + .as(StepVerifier::create) // + .expectNextCount(1) // + .verifyComplete(); + + Flux result = new ReactiveSpringDataMongodbQuery<>(operations, Person.class).where() + .join(person.coworker, QUser.user).on(QUser.user.username.eq("user-2")).fetch(); + + result.as(StepVerifier::create) // + .expectError(UnsupportedOperationException.class) // + .verify(); + } + + @Test // DATAMONGO-2182 + public void queryShouldTerminateWithUnsupportedOperationOnJoinWithNoResults() { + + User user1 = new User(); + user1.setUsername("user-1"); + + User user2 = new User(); + user2.setUsername("user-2"); + + operations.insertAll(Arrays.asList(user1, user2)) // + .as(StepVerifier::create) // + .expectNextCount(2) // + .verifyComplete(); + + Person person1 = new Person("Max", "The Mighty"); + person1.setCoworker(user1); + + Person person2 = new Person("Jack", "The Ripper"); + person2.setCoworker(user2); + + operations.save(person1) // + .as(StepVerifier::create) // + .expectNextCount(1) // + .verifyComplete(); + ; + operations.save(person2) // + .as(StepVerifier::create) // + .expectNextCount(1) // + .verifyComplete(); + ; + + Flux result = new ReactiveSpringDataMongodbQuery<>(operations, Person.class).where() + .join(person.coworker, QUser.user).on(QUser.user.username.eq("does-not-exist")).fetch(); + + result.as(StepVerifier::create) // + .expectError(UnsupportedOperationException.class) // + .verify(); + } + + @Test // DATAMONGO-2182 + public void springDataMongodbQueryShouldAllowElemMatchOnArrays() { + + Address adr1 = new Address("Hauptplatz", "4020", "Linz"); + Address adr2 = new Address("Stephansplatz", "1010", "Wien"); + Address adr3 = new Address("Tower of London", "EC3N 4AB", "London"); + + Person person1 = new Person("Max", "The Mighty"); + person1.setShippingAddresses(new LinkedHashSet<>(Arrays.asList(adr1, adr2))); + + Person person2 = new Person("Jack", "The Ripper"); + person2.setShippingAddresses(new LinkedHashSet<>(Arrays.asList(adr2, adr3))); + + operations.insertAll(Arrays.asList(person1, person2)) // + .as(StepVerifier::create) // + .expectNextCount(2) // + .verifyComplete(); + + Flux result = new ReactiveSpringDataMongodbQuery<>(operations, Person.class).where() + .anyEmbedded(person.shippingAddresses, QAddress.address).on(QAddress.address.city.eq("London")).fetch(); + + result.as(StepVerifier::create) // + .expectNext(person2) // + .verifyComplete(); + } + + @Test // DATAMONGO-2182 + public void translatesExceptionsCorrectly() { + + ReactiveMongoOperations ops = new ReactiveMongoTemplate(dbFactory) { + + @Override + protected MongoDatabase doGetDatabase() { + throw new MongoException(18, "Authentication Failed"); + } + }; + + ReactiveMongoRepositoryFactory factory = new ReactiveMongoRepositoryFactory(ops); + MongoEntityInformation entityInformation = factory.getEntityInformation(Person.class); + repository = new ReactiveQuerydslMongoPredicateExecutor<>(entityInformation, ops); + + repository.findOne(person.firstname.contains("batman")) // + .as(StepVerifier::create) // + .expectError(PermissionDeniedDataAccessException.class) // + .verify(); + } +} diff --git a/src/main/asciidoc/new-features.adoc b/src/main/asciidoc/new-features.adoc index 2439883e4..8135ad698 100644 --- a/src/main/asciidoc/new-features.adoc +++ b/src/main/asciidoc/new-features.adoc @@ -4,6 +4,7 @@ [[new-features.2-2-0]] == What's New in Spring Data MongoDB 2.2 * <> +* <> via `ReactiveQuerydslPredicateExecutor`. [[new-features.2-1-0]] == What's New in Spring Data MongoDB 2.1 diff --git a/src/main/asciidoc/reference/reactive-mongo-repositories.adoc b/src/main/asciidoc/reference/reactive-mongo-repositories.adoc index 0f7db07d2..faa022e5d 100644 --- a/src/main/asciidoc/reference/reactive-mongo-repositories.adoc +++ b/src/main/asciidoc/reference/reactive-mongo-repositories.adoc @@ -129,6 +129,7 @@ It supports the following features: * <> * <> * <> +* <> * <> WARNING: Reactive Repositories do not support type-safe query methods that use `Querydsl`. @@ -198,3 +199,61 @@ public interface PersonRepository extends ReactiveMongoRepository> findByLocationNear(Point location); } ---- + +[[mongodb.reactive.repositories.queries.type-safe]] +=== Type-safe Query Methods + +Reactive MongoDB repository support integrates with the http://www.querydsl.com/[Querydsl] project, which provides a way to perform type-safe queries. To quote from the project description, "Instead of writing queries as inline strings or externalizing them into XML files they are constructed via a fluent API." It provides the following features: + +* Code completion in the IDE (all properties, methods, and operations can be expanded in your favorite Java IDE). +* Almost no syntactically invalid queries allowed (type-safe on all levels). +* Domain types and properties can be referenced safely -- no strings involved! +* Adapts better to refactoring changes in domain types. +* Incremental query definition is easier. + +See the http://www.querydsl.com/static/querydsl/latest/reference/html/[QueryDSL documentation] for how to bootstrap your environment for APT-based code generation using Maven or Ant. + +QueryDSL lets you write queries such as the following: + +[source,java] +---- +QPerson person = new QPerson("person"); + +Flux result = repository.findAll(person.address.zipCode.eq("C0123")); +---- + +`QPerson` is a class that is generated by the Java annotation post-processing tool. It is a `Predicate` that lets you write type-safe queries. Notice that there are no strings in the query other than the `C0123` value. + +You can use the generated `Predicate` class by using the `ReactiveQuerydslPredicateExecutor` interface, which the following listing shows: + +[source,java] +---- +public interface ReactiveQuerydslPredicateExecutor { + + Mono findOne(Predicate predicate); + + Flux findAll(Predicate predicate); + + Flux findAll(Predicate predicate, Sort sort); + + Flux findAll(Predicate predicate, OrderSpecifier... orders); + + Flux findAll(OrderSpecifier... orders); + + Mono count(Predicate predicate); + + Mono exists(Predicate predicate); +} +---- + +To use this in your repository implementation, add it to the list of repository interfaces from which your interface inherits, as the following example shows: + +[source,java] +---- +public interface PersonRepository extends ReactiveMongoRepository, ReactiveQuerydslPredicateExecutor { + + // additional query methods go here +} +---- + +NOTE: Please note that joins (DBRef's) are not supported with Reactive MongoDB support.