From 7cb0a3dd21c3691ff72adfb067692c47e053f608 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 18 Jun 2018 13:27:50 +0200 Subject: [PATCH 1/4] DATAMONGO-1848 - Prepare issue branch. --- pom.xml | 2 +- spring-data-mongodb-benchmarks/pom.xml | 2 +- spring-data-mongodb-cross-store/pom.xml | 4 ++-- spring-data-mongodb-distribution/pom.xml | 2 +- spring-data-mongodb/pom.xml | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index 0ff20c5825..aa17486981 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 2.1.0.BUILD-SNAPSHOT + 2.1.0.DATAMONGO-1848-SNAPSHOT pom Spring Data MongoDB diff --git a/spring-data-mongodb-benchmarks/pom.xml b/spring-data-mongodb-benchmarks/pom.xml index 9baccaa905..d9e456b116 100644 --- a/spring-data-mongodb-benchmarks/pom.xml +++ b/spring-data-mongodb-benchmarks/pom.xml @@ -7,7 +7,7 @@ org.springframework.data spring-data-mongodb-parent - 2.1.0.BUILD-SNAPSHOT + 2.1.0.DATAMONGO-1848-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb-cross-store/pom.xml b/spring-data-mongodb-cross-store/pom.xml index 47a5b7aba7..cb11335699 100644 --- a/spring-data-mongodb-cross-store/pom.xml +++ b/spring-data-mongodb-cross-store/pom.xml @@ -6,7 +6,7 @@ org.springframework.data spring-data-mongodb-parent - 2.1.0.BUILD-SNAPSHOT + 2.1.0.DATAMONGO-1848-SNAPSHOT ../pom.xml @@ -50,7 +50,7 @@ org.springframework.data spring-data-mongodb - 2.1.0.BUILD-SNAPSHOT + 2.1.0.DATAMONGO-1848-SNAPSHOT diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml index e5c865ea08..3ef74f0491 100644 --- a/spring-data-mongodb-distribution/pom.xml +++ b/spring-data-mongodb-distribution/pom.xml @@ -13,7 +13,7 @@ org.springframework.data spring-data-mongodb-parent - 2.1.0.BUILD-SNAPSHOT + 2.1.0.DATAMONGO-1848-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index b86dc2808c..fdd6ad4a86 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -11,7 +11,7 @@ org.springframework.data spring-data-mongodb-parent - 2.1.0.BUILD-SNAPSHOT + 2.1.0.DATAMONGO-1848-SNAPSHOT ../pom.xml From 6d153e3457f3a07673f68cf7b3e5d8930fe585ee Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 18 Jun 2018 14:34:44 +0200 Subject: [PATCH 2/4] DATAMONGO-1848 - Import Document-based Querydsl support. --- .../support/AbstractMongodbQuery.java | 181 +++++++++ .../support/AnyEmbeddedBuilder.java | 47 +++ .../support/FetchableMongodbQuery.java | 260 +++++++++++++ .../repository/support/JoinBuilder.java | 51 +++ .../support/MongodbDocumentSerializer.java | 361 ++++++++++++++++++ .../support/MongodbExpressions.java | 61 +++ 6 files changed, 961 insertions(+) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/AbstractMongodbQuery.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/AnyEmbeddedBuilder.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/FetchableMongodbQuery.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/JoinBuilder.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongodbDocumentSerializer.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongodbExpressions.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/AbstractMongodbQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/AbstractMongodbQuery.java new file mode 100644 index 0000000000..861e43d731 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/AbstractMongodbQuery.java @@ -0,0 +1,181 @@ +/* + * Copyright 2018 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 java.util.Collection; + +import javax.annotation.Nullable; + +import org.bson.Document; + +import com.querydsl.core.DefaultQueryMetadata; +import com.querydsl.core.QueryModifiers; +import com.querydsl.core.SimpleQuery; +import com.querydsl.core.support.QueryMixin; +import com.querydsl.core.types.Expression; +import com.querydsl.core.types.FactoryExpression; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.ParamExpression; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.Predicate; +import com.querydsl.core.types.dsl.CollectionPathBase; + +/** + * {@code AbstractMongodbQuery} provides a base class for general Querydsl query implementation. + * + * @author Mark Paluch + * @param concrete subtype + */ +abstract class AbstractMongodbQuery> implements SimpleQuery { + + @SuppressWarnings("serial") + static class NoResults extends RuntimeException {} + + private final MongodbDocumentSerializer serializer; + private final QueryMixin queryMixin; + + /** + * Create a new MongodbQuery instance + * + * @param serializer serializer + */ + @SuppressWarnings("unchecked") + public AbstractMongodbQuery(MongodbDocumentSerializer serializer) { + @SuppressWarnings("unchecked") // Q is this plus subclass + Q query = (Q) this; + this.queryMixin = new QueryMixin(query, new DefaultQueryMetadata(), false); + this.serializer = serializer; + } + + /** + * Define a join + * + * @param ref reference + * @param target join target + * @return join builder + */ + public JoinBuilder join(Path ref, Path target) { + return new JoinBuilder(queryMixin, ref, target); + } + + /** + * Define a join + * + * @param ref reference + * @param target join target + * @return join builder + */ + public JoinBuilder join(CollectionPathBase ref, Path target) { + return new JoinBuilder(queryMixin, ref, target); + } + + /** + * Define a constraint for an embedded object + * + * @param collection collection + * @param target target + * @return builder + */ + public AnyEmbeddedBuilder anyEmbedded(Path> collection, Path target) { + return new AnyEmbeddedBuilder(queryMixin, collection); + } + + @Override + public Q distinct() { + return queryMixin.distinct(); + } + + public Q where(Predicate e) { + return queryMixin.where(e); + } + + @Override + public Q where(Predicate... e) { + return queryMixin.where(e); + } + + @Override + public Q limit(long limit) { + return queryMixin.limit(limit); + } + + @Override + public Q offset(long offset) { + return queryMixin.offset(offset); + } + + @Override + public Q restrict(QueryModifiers modifiers) { + return queryMixin.restrict(modifiers); + } + + public Q orderBy(OrderSpecifier o) { + return queryMixin.orderBy(o); + } + + @Override + public Q orderBy(OrderSpecifier... o) { + return queryMixin.orderBy(o); + } + + @Override + public Q set(ParamExpression param, T value) { + return queryMixin.set(param, value); + } + + protected Document createProjection(Expression projection) { + if (projection instanceof FactoryExpression) { + Document obj = new Document(); + for (Object expr : ((FactoryExpression) projection).getArgs()) { + if (expr instanceof Expression) { + obj.put((String) serializer.handle((Expression) expr), 1); + } + } + return obj; + } + return null; + } + + protected Document createQuery(@Nullable Predicate predicate) { + if (predicate != null) { + return (Document) serializer.handle(predicate); + } else { + return new Document(); + } + } + + QueryMixin getQueryMixin() { + return queryMixin; + } + + MongodbDocumentSerializer getSerializer() { + return serializer; + } + + /** + * Get the where definition as a Document instance + * + * @return + */ + public Document asDocument() { + return createQuery(queryMixin.getMetadata().getWhere()); + } + + @Override + public String toString() { + return asDocument().toString(); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/AnyEmbeddedBuilder.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/AnyEmbeddedBuilder.java new file mode 100644 index 0000000000..e2260fd272 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/AnyEmbeddedBuilder.java @@ -0,0 +1,47 @@ +/* + * Copyright 2018 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 java.util.Collection; + +import com.querydsl.core.support.QueryMixin; +import com.querydsl.core.types.ExpressionUtils; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.Predicate; +import com.querydsl.mongodb.MongodbOps; + +/** + * {@code AnyEmbeddedBuilder} is a builder for constraints on embedded objects + * + * @param query type + * @author Mark Paluch + */ +class AnyEmbeddedBuilder> { + + private final QueryMixin queryMixin; + + private final Path> collection; + + public AnyEmbeddedBuilder(QueryMixin queryMixin, Path> collection) { + this.queryMixin = queryMixin; + this.collection = collection; + } + + public Q on(Predicate... conditions) { + return queryMixin + .where(ExpressionUtils.predicate(MongodbOps.ELEM_MATCH, collection, ExpressionUtils.allOf(conditions))); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/FetchableMongodbQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/FetchableMongodbQuery.java new file mode 100644 index 0000000000..41f0537210 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/FetchableMongodbQuery.java @@ -0,0 +1,260 @@ +/* + * Copyright 2018 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 java.util.Collection; +import java.util.Collections; +import java.util.List; + +import javax.annotation.Nullable; + +import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.data.mongodb.core.query.BasicQuery; +import org.springframework.data.mongodb.core.query.Query; + +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; +import com.mysema.commons.lang.CloseableIterator; +import com.querydsl.core.Fetchable; +import com.querydsl.core.JoinExpression; +import com.querydsl.core.QueryMetadata; +import com.querydsl.core.QueryModifiers; +import com.querydsl.core.QueryResults; +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; + +/** + * {@link Fetchable} Mongodb query with a pluggable Document to Bean transformation. + * + * @param result type + * @param concrete subtype + * @author Mark Paluch + */ +class FetchableMongodbQuery extends AbstractMongodbQuery> implements Fetchable { + + private final Class entityClass; + private final String collection; + private final MongoOperations mongoOperations; + + public FetchableMongodbQuery(MongodbDocumentSerializer serializer, Class entityClass, + MongoOperations mongoOperations) { + + super(serializer); + + this.entityClass = (Class) entityClass; + this.collection = mongoOperations.getCollectionName(entityClass); + this.mongoOperations = mongoOperations; + } + + public FetchableMongodbQuery(MongodbDocumentSerializer serializer, Class entityClass, String collection, + MongoOperations mongoOperations) { + + super(serializer); + + this.entityClass = (Class) entityClass; + this.collection = collection; + this.mongoOperations = mongoOperations; + } + + /** + * Iterate with the specific fields + * + * @param paths fields to return + * @return iterator + */ + public CloseableIterator iterate(Path... paths) { + getQueryMixin().setProjection(paths); + return iterate(); + } + + @Override + public CloseableIterator iterate() { + + org.springframework.data.util.CloseableIterator stream = mongoOperations.stream(createQuery(), + entityClass, collection); + + return new CloseableIterator() { + @Override + public boolean hasNext() { + return stream.hasNext(); + } + + @Override + public K next() { + return stream.next(); + } + + @Override + public void remove() { + + } + + @Override + public void close() { + stream.close(); + } + }; + } + + /** + * Fetch with the specific fields + * + * @param paths fields to return + * @return results + */ + public List fetch(Path... paths) { + getQueryMixin().setProjection(paths); + return fetch(); + } + + @Override + public List fetch() { + return mongoOperations.query(entityClass).matching(createQuery()).all(); + } + + /** + * Fetch first with the specific fields + * + * @param paths fields to return + * @return first result + */ + public K fetchFirst(Path... paths) { + getQueryMixin().setProjection(paths); + return fetchFirst(); + } + + @Override + public K fetchFirst() { + return mongoOperations.query(entityClass).matching(createQuery()).firstValue(); + } + + /** + * Fetch one with the specific fields + * + * @param paths fields to return + * @return first result + */ + public K fetchOne(Path... paths) { + getQueryMixin().setProjection(paths); + return fetchOne(); + } + + @Override + public K fetchOne() { + return mongoOperations.query(entityClass).matching(createQuery()).oneValue(); + + } + + /** + * Fetch results with the specific fields + * + * @param paths fields to return + * @return results + */ + public QueryResults fetchResults(Path... paths) { + getQueryMixin().setProjection(paths); + return fetchResults(); + } + + @Override + public QueryResults fetchResults() { + long total = fetchCount(); + if (total > 0L) { + return new QueryResults<>(fetch(), getQueryMixin().getMetadata().getModifiers(), total); + } else { + return QueryResults.emptyResults(); + } + } + + @Override + public long fetchCount() { + return mongoOperations.query(entityClass).matching(createQuery()).count(); + } + + protected org.springframework.data.mongodb.core.query.Query createQuery() { + QueryMetadata metadata = getQueryMixin().getMetadata(); + Predicate filter = createFilter(metadata); + return createQuery(filter, metadata.getProjection(), metadata.getModifiers(), metadata.getOrderBy()); + } + + protected org.springframework.data.mongodb.core.query.Query createQuery(@Nullable Predicate where, + Expression projection, QueryModifiers modifiers, List> orderBy) { + + BasicQuery basicQuery = new BasicQuery(createQuery(where), 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(getSerializer().toSort(orderBy)); + } + return basicQuery; + } + + @Nullable + protected Predicate createFilter(QueryMetadata metadata) { + Predicate filter; + if (!metadata.getJoins().isEmpty()) { + filter = ExpressionUtils.allOf(metadata.getWhere(), createJoinFilter(metadata)); + } else { + filter = metadata.getWhere(); + } + return filter; + } + + @SuppressWarnings("unchecked") + @Nullable + protected Predicate createJoinFilter(QueryMetadata metadata) { + Multimap, Predicate> predicates = HashMultimap.create(); + 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()); + Predicate filter = ExpressionUtils.allOf(join.getCondition(), allOf(extraFilters)); + List ids = getIds(target.getType(), filter); + if (ids.isEmpty()) { + throw new NoResults(); + } + Path path = ExpressionUtils.path(String.class, source, "$id"); + predicates.put(source.getRoot(), ExpressionUtils.in((Path) path, ids)); + } + Path source = (Path) ((Operation) joins.get(0).getTarget()).getArg(0); + return allOf(predicates.get(source.getRoot())); + } + + private Predicate allOf(Collection predicates) { + return predicates != null ? ExpressionUtils.allOf(predicates) : null; + } + + protected List getIds(Class targetType, Predicate condition) { + // TODO : fetch only ids + Query query = createQuery(condition, null, QueryModifiers.EMPTY, Collections.emptyList()); + + return mongoOperations.findDistinct(query, "_id", targetType, Object.class); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/JoinBuilder.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/JoinBuilder.java new file mode 100644 index 0000000000..4459ab1edf --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/JoinBuilder.java @@ -0,0 +1,51 @@ +/* + * Copyright 2018 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 com.querydsl.core.JoinType; +import com.querydsl.core.support.QueryMixin; +import com.querydsl.core.types.ExpressionUtils; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.Predicate; + +/** + * {@code JoinBuilder} is a builder for join constraints + * + * @author Mark Paluch + * @param + * @param + */ +class JoinBuilder, T> { + + private final QueryMixin queryMixin; + + private final Path ref; + + private final Path target; + + public JoinBuilder(QueryMixin queryMixin, Path ref, Path target) { + this.queryMixin = queryMixin; + this.ref = ref; + this.target = target; + } + + @SuppressWarnings("unchecked") + public Q on(Predicate... conditions) { + queryMixin.addJoin(JoinType.JOIN, ExpressionUtils.as((Path) ref, target)); + queryMixin.on(conditions); + return queryMixin.getSelf(); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongodbDocumentSerializer.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongodbDocumentSerializer.java new file mode 100644 index 0000000000..eb125d7ddb --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongodbDocumentSerializer.java @@ -0,0 +1,361 @@ +/* + * Copyright 2018 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 java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +import org.bson.Document; +import org.bson.types.ObjectId; + +import com.google.common.collect.Sets; +import com.mongodb.DBRef; +import com.querydsl.core.types.*; +import com.querydsl.mongodb.MongodbOps; + +/** + * Serializes the given Querydsl query to a Document query for MongoDB. + * + * @author Mark Paluch + */ +abstract class MongodbDocumentSerializer implements Visitor { + + public Object handle(Expression expression) { + return expression.accept(this, null); + } + + public Document toSort(List> orderBys) { + Document sort = new Document(); + for (OrderSpecifier orderBy : orderBys) { + Object key = orderBy.getTarget().accept(this, null); + sort.append(key.toString(), orderBy.getOrder() == Order.ASC ? 1 : -1); + } + return sort; + } + + @Override + public Object visit(Constant expr, Void context) { + if (Enum.class.isAssignableFrom(expr.getType())) { + @SuppressWarnings("unchecked") // Guarded by previous check + Constant> expectedExpr = (Constant>) expr; + return expectedExpr.getConstant().name(); + } else { + return expr.getConstant(); + } + } + + @Override + public Object visit(TemplateExpression expr, Void context) { + throw new UnsupportedOperationException(); + } + + @Override + public Object visit(FactoryExpression expr, Void context) { + throw new UnsupportedOperationException(); + } + + protected String asDBKey(Operation expr, int index) { + return (String) asDBValue(expr, index); + } + + protected Object asDBValue(Operation expr, int index) { + return expr.getArg(index).accept(this, null); + } + + private String regexValue(Operation expr, int index) { + return Pattern.quote(expr.getArg(index).accept(this, null).toString()); + } + + protected Document asDocument(String key, Object value) { + return new Document(key, value); + } + + @SuppressWarnings("unchecked") + @Override + public Object visit(Operation expr, Void context) { + Operator op = expr.getOperator(); + if (op == Ops.EQ) { + if (expr.getArg(0) instanceof Operation) { + Operation lhs = (Operation) expr.getArg(0); + if (lhs.getOperator() == Ops.COL_SIZE || lhs.getOperator() == Ops.ARRAY_SIZE) { + return asDocument(asDBKey(lhs, 0), asDocument("$size", asDBValue(expr, 1))); + } else { + throw new UnsupportedOperationException("Illegal operation " + expr); + } + } else if (expr.getArg(0) instanceof Path) { + Path path = (Path) expr.getArg(0); + Constant constant = (Constant) expr.getArg(1); + return asDocument(asDBKey(expr, 0), convert(path, constant)); + } + } else if (op == Ops.STRING_IS_EMPTY) { + return asDocument(asDBKey(expr, 0), ""); + + } else if (op == Ops.AND) { + Map lhs = (Map) handle(expr.getArg(0)); + Map rhs = (Map) handle(expr.getArg(1)); + if (Sets.intersection(lhs.keySet(), rhs.keySet()).isEmpty()) { + lhs.putAll(rhs); + return lhs; + } else { + List list = new ArrayList(2); + list.add(handle(expr.getArg(0))); + list.add(handle(expr.getArg(1))); + return asDocument("$and", list); + } + + } else if (op == Ops.NOT) { + // Handle the not's child + Operation subOperation = (Operation) expr.getArg(0); + Operator subOp = subOperation.getOperator(); + if (subOp == Ops.IN) { + return visit( + ExpressionUtils.operation(Boolean.class, Ops.NOT_IN, subOperation.getArg(0), subOperation.getArg(1)), + context); + } else { + Document arg = (Document) handle(expr.getArg(0)); + return negate(arg); + } + + } else if (op == Ops.OR) { + List list = new ArrayList(2); + list.add(handle(expr.getArg(0))); + list.add(handle(expr.getArg(1))); + return asDocument("$or", list); + + } else if (op == Ops.NE) { + Path path = (Path) expr.getArg(0); + Constant constant = (Constant) expr.getArg(1); + return asDocument(asDBKey(expr, 0), asDocument("$ne", convert(path, constant))); + + } else if (op == Ops.STARTS_WITH) { + return asDocument(asDBKey(expr, 0), Pattern.compile("^" + regexValue(expr, 1))); + + } else if (op == Ops.STARTS_WITH_IC) { + return asDocument(asDBKey(expr, 0), Pattern.compile("^" + regexValue(expr, 1), Pattern.CASE_INSENSITIVE)); + + } else if (op == Ops.ENDS_WITH) { + return asDocument(asDBKey(expr, 0), Pattern.compile(regexValue(expr, 1) + "$")); + + } else if (op == Ops.ENDS_WITH_IC) { + return asDocument(asDBKey(expr, 0), Pattern.compile(regexValue(expr, 1) + "$", Pattern.CASE_INSENSITIVE)); + + } else if (op == Ops.EQ_IGNORE_CASE) { + return asDocument(asDBKey(expr, 0), Pattern.compile("^" + regexValue(expr, 1) + "$", Pattern.CASE_INSENSITIVE)); + + } else if (op == Ops.STRING_CONTAINS) { + return asDocument(asDBKey(expr, 0), Pattern.compile(".*" + regexValue(expr, 1) + ".*")); + + } else if (op == Ops.STRING_CONTAINS_IC) { + return asDocument(asDBKey(expr, 0), Pattern.compile(".*" + regexValue(expr, 1) + ".*", Pattern.CASE_INSENSITIVE)); + + } else if (op == Ops.MATCHES) { + return asDocument(asDBKey(expr, 0), Pattern.compile(asDBValue(expr, 1).toString())); + + } else if (op == Ops.MATCHES_IC) { + return asDocument(asDBKey(expr, 0), Pattern.compile(asDBValue(expr, 1).toString(), Pattern.CASE_INSENSITIVE)); + + } else if (op == Ops.LIKE) { + String regex = ExpressionUtils.likeToRegex((Expression) expr.getArg(1)).toString(); + return asDocument(asDBKey(expr, 0), Pattern.compile(regex)); + + } else if (op == Ops.BETWEEN) { + Document value = new Document("$gte", asDBValue(expr, 1)); + value.append("$lte", asDBValue(expr, 2)); + return asDocument(asDBKey(expr, 0), value); + + } else if (op == Ops.IN) { + int constIndex = 0; + int exprIndex = 1; + if (expr.getArg(1) instanceof Constant) { + constIndex = 1; + exprIndex = 0; + } + if (Collection.class.isAssignableFrom(expr.getArg(constIndex).getType())) { + @SuppressWarnings("unchecked") // guarded by previous check + Collection values = ((Constant>) expr.getArg(constIndex)).getConstant(); + return asDocument(asDBKey(expr, exprIndex), asDocument("$in", values)); + } else { + Path path = (Path) expr.getArg(exprIndex); + Constant constant = (Constant) expr.getArg(constIndex); + return asDocument(asDBKey(expr, exprIndex), convert(path, constant)); + } + + } else if (op == Ops.NOT_IN) { + int constIndex = 0; + int exprIndex = 1; + if (expr.getArg(1) instanceof Constant) { + constIndex = 1; + exprIndex = 0; + } + if (Collection.class.isAssignableFrom(expr.getArg(constIndex).getType())) { + @SuppressWarnings("unchecked") // guarded by previous check + Collection values = ((Constant>) expr.getArg(constIndex)).getConstant(); + return asDocument(asDBKey(expr, exprIndex), asDocument("$nin", values)); + } else { + Path path = (Path) expr.getArg(exprIndex); + Constant constant = (Constant) expr.getArg(constIndex); + return asDocument(asDBKey(expr, exprIndex), asDocument("$ne", convert(path, constant))); + } + + } else if (op == Ops.COL_IS_EMPTY) { + List list = new ArrayList(2); + list.add(asDocument(asDBKey(expr, 0), new ArrayList())); + list.add(asDocument(asDBKey(expr, 0), asDocument("$exists", false))); + return asDocument("$or", list); + + } else if (op == Ops.LT) { + return asDocument(asDBKey(expr, 0), asDocument("$lt", asDBValue(expr, 1))); + + } else if (op == Ops.GT) { + return asDocument(asDBKey(expr, 0), asDocument("$gt", asDBValue(expr, 1))); + + } else if (op == Ops.LOE) { + return asDocument(asDBKey(expr, 0), asDocument("$lte", asDBValue(expr, 1))); + + } else if (op == Ops.GOE) { + return asDocument(asDBKey(expr, 0), asDocument("$gte", asDBValue(expr, 1))); + + } else if (op == Ops.IS_NULL) { + return asDocument(asDBKey(expr, 0), asDocument("$exists", false)); + + } else if (op == Ops.IS_NOT_NULL) { + return asDocument(asDBKey(expr, 0), asDocument("$exists", true)); + + } else if (op == Ops.CONTAINS_KEY) { + Path path = (Path) expr.getArg(0); + Expression key = expr.getArg(1); + return asDocument(visit(path, context) + "." + key.toString(), asDocument("$exists", true)); + + } else if (op == MongodbOps.NEAR) { + return asDocument(asDBKey(expr, 0), asDocument("$near", asDBValue(expr, 1))); + + } else if (op == MongodbOps.NEAR_SPHERE) { + return asDocument(asDBKey(expr, 0), asDocument("$nearSphere", asDBValue(expr, 1))); + + } else if (op == MongodbOps.ELEM_MATCH) { + return asDocument(asDBKey(expr, 0), asDocument("$elemMatch", asDBValue(expr, 1))); + } + + throw new UnsupportedOperationException("Illegal operation " + expr); + } + + private Object negate(Document arg) { + List list = new ArrayList(); + for (Map.Entry entry : arg.entrySet()) { + if (entry.getKey().equals("$or")) { + list.add(asDocument("$nor", entry.getValue())); + + } else if (entry.getKey().equals("$and")) { + List list2 = new ArrayList(); + for (Object o : ((Collection) entry.getValue())) { + list2.add(negate((Document) o)); + } + list.add(asDocument("$or", list2)); + + } else if (entry.getValue() instanceof Pattern) { + list.add(asDocument(entry.getKey(), asDocument("$not", entry.getValue()))); + + } else if (entry.getValue() instanceof Document) { + list.add(negate(entry.getKey(), (Document) entry.getValue())); + + } else { + list.add(asDocument(entry.getKey(), asDocument("$ne", entry.getValue()))); + } + } + return list.size() == 1 ? list.get(0) : asDocument("$or", list); + } + + private Object negate(String key, Document value) { + if (value.size() == 1) { + return asDocument(key, asDocument("$not", value)); + + } else { + List list2 = new ArrayList(); + for (Map.Entry entry2 : value.entrySet()) { + list2.add(asDocument(key, asDocument("$not", asDocument(entry2.getKey(), entry2.getValue())))); + } + return asDocument("$or", list2); + } + } + + protected Object convert(Path property, Constant constant) { + if (isReference(property)) { + return asReference(constant.getConstant()); + } else if (isId(property)) { + if (isReference(property.getMetadata().getParent())) { + return asReferenceKey(property.getMetadata().getParent().getType(), constant.getConstant()); + } else if (constant.getType().equals(String.class) && isImplicitObjectIdConversion()) { + String id = (String) constant.getConstant(); + return ObjectId.isValid(id) ? new ObjectId(id) : id; + } + } + return visit(constant, null); + } + + protected boolean isImplicitObjectIdConversion() { + return true; + } + + protected DBRef asReferenceKey(Class entity, Object id) { + // TODO override in subclass + throw new UnsupportedOperationException(); + } + + protected abstract DBRef asReference(Object constant); + + protected abstract boolean isReference(Path arg); + + protected boolean isId(Path arg) { + // TODO override in subclass + return false; + } + + @Override + public String visit(Path expr, Void context) { + PathMetadata metadata = expr.getMetadata(); + if (metadata.getParent() != null) { + Path parent = metadata.getParent(); + if (parent.getMetadata().getPathType() == PathType.DELEGATE) { + parent = parent.getMetadata().getParent(); + } + if (metadata.getPathType() == PathType.COLLECTION_ANY) { + return visit(parent, context); + } else if (parent.getMetadata().getPathType() != PathType.VARIABLE) { + String rv = getKeyForPath(expr, metadata); + String parentStr = visit(parent, context); + return rv != null ? parentStr + "." + rv : parentStr; + } + } + return getKeyForPath(expr, metadata); + } + + protected String getKeyForPath(Path expr, PathMetadata metadata) { + return metadata.getElement().toString(); + } + + @Override + public Object visit(SubQueryExpression expr, Void context) { + throw new UnsupportedOperationException(); + } + + @Override + public Object visit(ParamExpression expr, Void context) { + throw new UnsupportedOperationException(); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongodbExpressions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongodbExpressions.java new file mode 100644 index 0000000000..79fd892a3b --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongodbExpressions.java @@ -0,0 +1,61 @@ +/* + * Copyright 2018 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 java.util.Arrays; + +import com.querydsl.core.types.ConstantImpl; +import com.querydsl.core.types.Expression; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.mongodb.MongodbOps; + +/** + * Mongodb Document-API-specific operations. + * + * @author tiwe + * @author Mark Paluch + */ +class MongodbExpressions { + + private MongodbExpressions() {} + + /** + * Finds the closest points relative to the given location and orders the results with decreasing proximity + * + * @param expr location + * @param latVal latitude + * @param longVal longitude + * @return predicate + */ + public static BooleanExpression near(Expression expr, double latVal, double longVal) { + return Expressions.booleanOperation(MongodbOps.NEAR, expr, ConstantImpl.create(Arrays.asList(latVal, longVal))); + } + + /** + * Finds the closest points relative to the given location on a sphere and orders the results with decreasing + * proximity + * + * @param expr location + * @param latVal latitude + * @param longVal longitude + * @return predicate + */ + public static BooleanExpression nearSphere(Expression expr, double latVal, double longVal) { + return Expressions.booleanOperation(MongodbOps.NEAR_SPHERE, expr, + ConstantImpl.create(Arrays.asList(latVal, longVal))); + } +} From 6949c2621ed5c025eeb48468bec5ba33c7a916ca Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 18 Jun 2018 15:26:46 +0200 Subject: [PATCH 3/4] DATAMONGO-1848 - Use imported Querydsl support for Document API MongoDB. --- .../data/mongodb/MongoDbFactory.java | 3 + .../support/AbstractMongodbQuery.java | 2 +- .../support/FetchableMongodbQuery.java | 3 +- .../QuerydslMongoPredicateExecutor.java | 13 +- .../support/QuerydslRepositorySupport.java | 5 +- .../support/SimpleFetchableQuery.java | 27 +++ .../support/SpringDataMongodbQuery.java | 163 +++++++++++++++--- .../support/SpringDataMongodbSerializer.java | 42 +---- .../SpringDataMongodbSerializerUnitTests.java | 36 ++-- 9 files changed, 206 insertions(+), 88 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleFetchableQuery.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoDbFactory.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoDbFactory.java index ef6c1423c9..15a9ae691b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoDbFactory.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoDbFactory.java @@ -62,7 +62,10 @@ public interface MongoDbFactory extends CodecRegistryProvider, MongoSessionProvi * Get the legacy database entry point. Please consider {@link #getDb()} instead. * * @return + * @deprecated since 2.1, use {@link #getDb()}. This method will be removed with a future version as it works only + * with the legacy MongoDB driver. */ + @Deprecated DB getLegacyDb(); /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/AbstractMongodbQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/AbstractMongodbQuery.java index 861e43d731..4f425da98a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/AbstractMongodbQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/AbstractMongodbQuery.java @@ -146,7 +146,7 @@ protected Document createProjection(Expression projection) { } return obj; } - return null; + return new Document(); } protected Document createQuery(@Nullable Predicate predicate) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/FetchableMongodbQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/FetchableMongodbQuery.java index 41f0537210..dc1cbe0a3f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/FetchableMongodbQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/FetchableMongodbQuery.java @@ -47,7 +47,8 @@ * @param concrete subtype * @author Mark Paluch */ -class FetchableMongodbQuery extends AbstractMongodbQuery> implements Fetchable { +abstract class FetchableMongodbQuery> extends AbstractMongodbQuery + implements Fetchable { private final Class entityClass; private final String collection; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslMongoPredicateExecutor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslMongoPredicateExecutor.java index 19bf1782b2..dba3c34c55 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslMongoPredicateExecutor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslMongoPredicateExecutor.java @@ -39,7 +39,6 @@ import com.querydsl.core.types.OrderSpecifier; import com.querydsl.core.types.Predicate; import com.querydsl.core.types.dsl.PathBuilder; -import com.querydsl.mongodb.AbstractMongodbQuery; /** * MongoDB-specific {@link QuerydslPredicateExecutor} that allows execution {@link Predicate}s in various forms. @@ -166,7 +165,7 @@ public Page findAll(Predicate predicate, Pageable pageable) { Assert.notNull(predicate, "Predicate must not be null!"); Assert.notNull(pageable, "Pageable must not be null!"); - AbstractMongodbQuery> query = createQueryFor(predicate); + SimpleFetchableQuery query = createQueryFor(predicate); return PageableExecutionUtils.getPage(applyPagination(query, pageable).fetch(), pageable, query::fetchCount); } @@ -201,7 +200,7 @@ public boolean exists(Predicate predicate) { * @param predicate * @return */ - private AbstractMongodbQuery> createQueryFor(Predicate predicate) { + private SimpleFetchableQuery createQueryFor(Predicate predicate) { return createQuery().where(predicate); } @@ -210,7 +209,7 @@ private AbstractMongodbQuery> createQueryFor(Predic * * @return */ - private AbstractMongodbQuery> createQuery() { + private SimpleFetchableQuery createQuery() { return new SpringDataMongodbQuery<>(mongoOperations, entityInformation.getJavaType()); } @@ -221,8 +220,7 @@ private AbstractMongodbQuery> createQuery() { * @param pageable * @return */ - private AbstractMongodbQuery> applyPagination( - AbstractMongodbQuery> query, Pageable pageable) { + private SimpleFetchableQuery applyPagination(SimpleFetchableQuery query, Pageable pageable) { query = query.offset(pageable.getOffset()).limit(pageable.getPageSize()); return applySorting(query, pageable.getSort()); @@ -235,8 +233,7 @@ private AbstractMongodbQuery> applyPagination( * @param sort * @return */ - private AbstractMongodbQuery> applySorting( - AbstractMongodbQuery> query, Sort sort) { + private SimpleFetchableQuery applySorting(SimpleFetchableQuery query, Sort sort) { // TODO: find better solution than instanceof check if (sort instanceof QSort) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslRepositorySupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslRepositorySupport.java index da39c9e73d..30ac168a95 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslRepositorySupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslRepositorySupport.java @@ -21,7 +21,6 @@ import org.springframework.util.Assert; import com.querydsl.core.types.EntityPath; -import com.querydsl.mongodb.AbstractMongodbQuery; /** * Base class to create repository implementations based on Querydsl. @@ -54,7 +53,7 @@ public QuerydslRepositorySupport(MongoOperations operations) { * @param path * @return */ - protected AbstractMongodbQuery> from(final EntityPath path) { + protected SpringDataMongodbQuery from(final EntityPath path) { Assert.notNull(path, "EntityPath must not be null!"); MongoPersistentEntity entity = context.getRequiredPersistentEntity(path.getType()); @@ -68,7 +67,7 @@ protected AbstractMongodbQuery> from(final Enti * @param collection must not be blank or {@literal null} * @return */ - protected AbstractMongodbQuery> from(final EntityPath path, String collection) { + protected SpringDataMongodbQuery from(final EntityPath path, String collection) { Assert.notNull(path, "EntityPath must not be null!"); Assert.hasText(collection, "Collection name must not be null or empty!"); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleFetchableQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleFetchableQuery.java new file mode 100644 index 0000000000..1edb489b27 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleFetchableQuery.java @@ -0,0 +1,27 @@ +/* + * Copyright 2018 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 com.querydsl.core.Fetchable; +import com.querydsl.core.SimpleQuery; + +/** + * Interface that combines {@link Fetchable} and {@link SimpleQuery}. + * + * @author Mark Paluch + * @since 2.1 + */ +public interface SimpleFetchableQuery extends Fetchable, SimpleQuery> {} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbQuery.java index fa7185468d..296d0f5d10 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbQuery.java @@ -15,15 +15,17 @@ */ package org.springframework.data.mongodb.repository.support; -import org.bson.Document; +import java.util.List; + import org.springframework.data.mongodb.core.MongoOperations; -import org.springframework.data.mongodb.core.MongoTemplate; -import org.springframework.lang.Nullable; -import com.google.common.base.Function; -import com.mongodb.BasicDBObject; -import com.mongodb.DBCollection; -import com.mongodb.DBObject; +import com.mysema.commons.lang.CloseableIterator; +import com.querydsl.core.NonUniqueResultException; +import com.querydsl.core.QueryModifiers; +import com.querydsl.core.QueryResults; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.ParamExpression; +import com.querydsl.core.types.Predicate; import com.querydsl.mongodb.AbstractMongodbQuery; /** @@ -32,9 +34,9 @@ * @author Oliver Gierke * @author Mark Paluch */ -public class SpringDataMongodbQuery extends AbstractMongodbQuery> { +public class SpringDataMongodbQuery implements SimpleFetchableQuery { - private final MongoOperations operations; + private final OperationsMongodbQuery query; /** * Creates a new {@link SpringDataMongodbQuery}. @@ -56,25 +58,144 @@ public SpringDataMongodbQuery(final MongoOperations operations, final Class type, String collectionName) { - super(((MongoTemplate) operations).getMongoDbFactory().getLegacyDb().getCollection(collectionName), - new Function() { + query = new OperationsMongodbQuery<>(new SpringDataMongodbSerializer(operations.getConverter()), type, + collectionName, operations); + } - @Override - public T apply(@Nullable DBObject input) { - return operations.getConverter().read(type, new Document((BasicDBObject) input)); - } - }, new SpringDataMongodbSerializer(operations.getConverter())); + /* + * (non-Javadoc) + * @see com.querydsl.core.Fetchable#fetch() + */ + @Override + public List fetch() { + return query.fetch(); + } - this.operations = operations; + /* + * (non-Javadoc) + * @see com.querydsl.core.Fetchable#fetchFirst() + */ + @Override + public T fetchFirst() { + return query.fetchFirst(); + } + + /* + * (non-Javadoc) + * @see com.querydsl.core.Fetchable#fetchOne() + */ + @Override + public T fetchOne() throws NonUniqueResultException { + return query.fetchOne(); + } + + /* + * (non-Javadoc) + * @see com.querydsl.core.Fetchable#iterate() + */ + @Override + public CloseableIterator iterate() { + return query.iterate(); + } + + /* + * (non-Javadoc) + * @see com.querydsl.core.Fetchable#fetchResults() + */ + @Override + public QueryResults fetchResults() { + return query.fetchResults(); + } + + /* + * (non-Javadoc) + * @see com.querydsl.core.Fetchable#fetchCount() + */ + @Override + public long fetchCount() { + return query.fetchCount(); + } + + /* + * (non-Javadoc) + * @see com.querydsl.core.SimpleQuery#limit(long) + */ + @Override + public SpringDataMongodbQuery limit(long limit) { + query.limit(limit); + return this; + } + + /* + * (non-Javadoc) + * @see com.querydsl.core.SimpleQuery#offset(long) + */ + @Override + public SpringDataMongodbQuery offset(long offset) { + query.offset(offset); + return this; + } + + /* + * (non-Javadoc) + * @see com.querydsl.core.SimpleQuery#restrict(com.querydsl.core.QueryModifiers) + */ + @Override + public SpringDataMongodbQuery restrict(QueryModifiers modifiers) { + query.restrict(modifiers); + return this; } /* * (non-Javadoc) - * @see com.querydsl.mongodb.AbstractMongodbQuery#getCollection(java.lang.Class) + * @see com.querydsl.core.SimpleQuery#orderBy(com.querydsl.core.types.OrderSpecifier[]) + */ + @Override + public SpringDataMongodbQuery orderBy(OrderSpecifier... o) { + query.orderBy(o); + return this; + } + + /* + * (non-Javadoc) + * @see com.querydsl.core.SimpleQuery#set(com.querydsl.core.types.ParamExpression, java.lang.Object) */ @Override - protected DBCollection getCollection(Class type) { - return ((MongoTemplate) operations).getMongoDbFactory().getLegacyDb() - .getCollection(operations.getCollectionName(type)); + public SpringDataMongodbQuery set(ParamExpression param, V value) { + query.set(param, value); + return this; + } + + /* + * (non-Javadoc) + * @see com.querydsl.core.SimpleQuery#distinct() + */ + @Override + public SpringDataMongodbQuery distinct() { + query.distinct(); + return this; + } + + /* + * (non-Javadoc) + * @see com.querydsl.core.FilteredClause#where(com.querydsl.core.types.Predicate[]) + */ + @Override + public SpringDataMongodbQuery where(Predicate... o) { + query.where(o); + return this; + } + + /** + * Concrete implementation of {@link FetchableMongodbQuery}. + * + * @param + */ + static class OperationsMongodbQuery extends FetchableMongodbQuery> { + + public OperationsMongodbQuery(MongodbDocumentSerializer serializer, Class entityClass, + String collection, MongoOperations mongoOperations) { + super(serializer, entityClass, collection, mongoOperations); + } } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbSerializer.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbSerializer.java index 7add845d41..f0f2316db4 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbSerializer.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbSerializer.java @@ -17,7 +17,6 @@ import java.util.Collections; import java.util.HashSet; -import java.util.List; import java.util.Optional; import java.util.Set; import java.util.regex.Pattern; @@ -28,16 +27,11 @@ import org.springframework.data.mongodb.core.convert.QueryMapper; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; -import org.springframework.data.mongodb.util.BsonUtils; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; -import com.mongodb.BasicDBList; -import com.mongodb.BasicDBObject; -import com.mongodb.DBObject; import com.mongodb.DBRef; -import com.mongodb.util.JSON; import com.querydsl.core.types.Constant; import com.querydsl.core.types.Expression; import com.querydsl.core.types.Operation; @@ -53,7 +47,7 @@ * @author Christoph Strobl * @author Mark Paluch */ -class SpringDataMongodbSerializer extends MongodbSerializer { +class SpringDataMongodbSerializer extends MongodbDocumentSerializer { private static final String ID_KEY = "_id"; private static final Set PATH_TYPES; @@ -96,7 +90,7 @@ public Object visit(Constant expr, Void context) { return super.visit(expr, context); } - return toQuerydslMongoType(expr.getConstant()); + return converter.convertToMongoType(expr.getConstant()); } /* @@ -119,10 +113,10 @@ protected String getKeyForPath(Path expr, PathMetadata metadata) { /* * (non-Javadoc) - * @see com.querydsl.mongodb.MongodbSerializer#asDBObject(java.lang.String, java.lang.Object) + * @see com.querydsl.mongodb.MongodbSerializer#asDocument(java.lang.String, java.lang.Object) */ @Override - protected DBObject asDBObject(@Nullable String key, @Nullable Object value) { + protected Document asDocument(@Nullable String key, @Nullable Object value) { value = value instanceof Optional ? ((Optional) value).orElse(null) : value; @@ -130,7 +124,7 @@ protected DBObject asDBObject(@Nullable String key, @Nullable Object value) { return convertId(key, value); } - return super.asDBObject(key, value instanceof Pattern ? value : toQuerydslMongoType(value)); + return super.asDocument(key, value instanceof Pattern ? value : converter.convertToMongoType(value)); } /** @@ -141,13 +135,12 @@ protected DBObject asDBObject(@Nullable String key, @Nullable Object value) { * @param idValue the raw {@literal id} value. * @return the {@literal id} representation in the required format. */ - private DBObject convertId(String key, Object idValue) { + private Document convertId(String key, Object idValue) { Object convertedId = mapper.convertId(idValue); - Document mappedIdValue = mapper.getMappedObject((BasicDBObject) super.asDBObject(key, convertedId), + return mapper.getMappedObject(super.asDocument(key, convertedId), Optional.empty()); - return (DBObject) JSON.parse(mappedIdValue.toJson()); } /* @@ -250,25 +243,4 @@ private MongoPersistentProperty getPropertyForPotentialDbRef(Path path) { return property; } - - private Object toQuerydslMongoType(Object source) { - - Object target = converter.convertToMongoType(source); - - if (target instanceof List) { - - List newList = new BasicDBList(); - - for (Object item : (List) target) { - if (item instanceof Document) { - newList.add(new BasicDBObject(BsonUtils.asMap((Document) item))); - } else { - newList.add(item); - } - } - return newList; - } - - return target; - } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbSerializerUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbSerializerUnitTests.java index 55faae109d..0af4338004 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbSerializerUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbSerializerUnitTests.java @@ -19,6 +19,7 @@ import static org.junit.Assert.*; import static org.springframework.data.mongodb.core.DocumentTestUtils.*; +import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -26,6 +27,7 @@ import org.bson.types.ObjectId; import org.hamcrest.collection.IsIterableContainingInOrder; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -42,10 +44,6 @@ import org.springframework.data.mongodb.repository.QAddress; import org.springframework.data.mongodb.repository.QPerson; -import com.mongodb.BasicDBList; -import com.mongodb.BasicDBObject; -import com.mongodb.DBObject; -import com.mongodb.util.JSON; import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.core.types.dsl.BooleanOperation; import com.querydsl.core.types.dsl.PathBuilder; @@ -96,9 +94,7 @@ public void convertsComplexObjectOnSerializing() { address.street = "Foo"; address.zipCode = "01234"; - DBObject result = serializer.asDBObject("foo", address); - assertThat(result, is(instanceOf(BasicDBObject.class))); - BasicDBObject document = (BasicDBObject) result; + Document document = serializer.asDocument("foo", address); Object value = document.get("foo"); assertThat(value, is(notNullValue())); @@ -123,10 +119,10 @@ public void convertsIdPropertyCorrectly() { PathBuilder
builder = new PathBuilder
(Address.class, "address"); StringPath idPath = builder.getString("id"); - DBObject result = (DBObject) serializer.visit((BooleanOperation) idPath.eq(id.toString()), (Void) null); + Document result = (Document) serializer.visit((BooleanOperation) idPath.eq(id.toString()), null); assertThat(result.get("_id"), is(notNullValue())); assertThat(result.get("_id"), is(instanceOf(ObjectId.class))); - assertThat(result.get("_id"), is((Object) id)); + assertThat(result.get("_id"), is(id)); } @Test // DATAMONGO-761 @@ -143,10 +139,10 @@ public void looksUpKeyForNonPropertyPath() { public void shouldConvertObjectIdEvenWhenNestedInOperatorDbObject() { ObjectId value = new ObjectId("53bb9fd14438765b29c2d56e"); - DBObject serialized = serializer.asDBObject("_id", new Document("$ne", value.toString())); + Document serialized = serializer.asDocument("_id", new Document("$ne", value.toString())); - DBObject _id = getTypedValue(new Document(serialized.toMap()), "_id", DBObject.class); - ObjectId $ne = getTypedValue(new Document(_id.toMap()), "$ne", ObjectId.class); + Document _id = getTypedValue(serialized, "_id", Document.class); + ObjectId $ne = getTypedValue(_id, "$ne", ObjectId.class); assertThat($ne, is(value)); } @@ -156,14 +152,14 @@ public void shouldConvertCollectionOfObjectIdEvenWhenNestedInOperatorDocument() ObjectId firstId = new ObjectId("53bb9fd14438765b29c2d56e"); ObjectId secondId = new ObjectId("53bb9fda4438765b29c2d56f"); - BasicDBList objectIds = new BasicDBList(); + List objectIds = new ArrayList<>(); objectIds.add(firstId.toString()); objectIds.add(secondId.toString()); - DBObject serialized = serializer.asDBObject("_id", new Document("$in", objectIds)); + Document serialized = serializer.asDocument("_id", new Document("$in", objectIds)); - DBObject _id = getTypedValue(new Document(serialized.toMap()), "_id", DBObject.class); - List $in = getTypedValue(new Document(_id.toMap()), "$in", List.class); + Document _id = getTypedValue(serialized, "_id", Document.class); + List $in = getTypedValue(_id, "$in", List.class); assertThat($in, IsIterableContainingInOrder. contains(firstId, secondId)); } @@ -182,17 +178,19 @@ public void takesCustomConversionForEnumsIntoAccount() { Object mappedPredicate = this.serializer.handle(QPerson.person.sex.eq(Sex.FEMALE)); - assertThat(mappedPredicate, is(instanceOf(DBObject.class))); - assertThat(((DBObject) mappedPredicate).get("sex"), is((Object) "f")); + assertThat(mappedPredicate, is(instanceOf(Document.class))); + assertThat(((Document) mappedPredicate).get("sex"), is("f")); } @Test // DATAMONGO-1943 + @Ignore("FIXME mp911de") public void shouldRemarshallListsAndDocuments() { BooleanExpression criteria = QPerson.person.firstname.isNotEmpty() .and(QPerson.person.firstname.containsIgnoreCase("foo")).not(); - assertThat(this.serializer.handle(criteria), is(equalTo(JSON.parse("{ \"$or\" : [ { \"firstname\" : { \"$ne\" : { " + assertThat(this.serializer.handle(criteria), + is(equalTo(Document.parse("{ \"$or\" : [ { \"firstname\" : { \"$not\" : { " + "\"$ne\" : \"\"}}} , { \"firstname\" : { \"$not\" : { \"$regex\" : \".*\\\\Qfoo\\\\E.*\" , \"$options\" : \"i\"}}}]}")))); } From df6f7d6b7f725fa882e613e874bd918e836f6463 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Wed, 11 Jul 2018 11:02:29 +0200 Subject: [PATCH 4/4] DATAMONGO-1848 - Polishing. Prefix types with Querydsl and update visibility to allow construction of custom queries using SpringDataMongodbQuery. Reintroduce generics for JoinBuilder usage, fix warnings and nullability issues. Also add BsonValue types to simple types and use native BsonRegularExpression for regex conversion. Add tests for "in" on dbref, exception translation, any embedded, join and lifecycle events related to DATAMONGO-362, DATAMONGO-595, DATAMONGO-700, DATAMONGO-1434, DATAMONGO-1810 and DATAMONGO-2010. --- .../core/mapping/MongoSimpleTypes.java | 22 +- .../support/AbstractMongodbQuery.java | 181 ---------------- .../support/MongodbDocumentSerializer.java | 202 ++++++++++++------ .../support/MongodbExpressions.java | 61 ------ .../support/QuerydslAbstractMongodbQuery.java | 202 ++++++++++++++++++ ...r.java => QuerydslAnyEmbeddedBuilder.java} | 27 ++- ...ava => QuerydslFetchableMongodbQuery.java} | 189 ++++++++-------- ...nBuilder.java => QuerydslJoinBuilder.java} | 28 ++- ...chableQuery.java => QuerydslMongoOps.java} | 26 ++- .../QuerydslMongoPredicateExecutor.java | 18 +- .../support/QuerydslRepositorySupport.java | 4 +- .../support/SpringDataMongodbQuery.java | 159 +------------- .../support/SpringDataMongodbSerializer.java | 14 +- .../event/ApplicationContextEventTests.java | 28 +++ ...ongoPredicateExecutorIntegrationTests.java | 125 ++++++++++- .../QuerydslRepositorySupportTests.java | 75 ++++++- .../SpringDataMongodbSerializerUnitTests.java | 10 +- 17 files changed, 778 insertions(+), 593 deletions(-) delete mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/AbstractMongodbQuery.java delete mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongodbExpressions.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslAbstractMongodbQuery.java rename spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/{AnyEmbeddedBuilder.java => QuerydslAnyEmbeddedBuilder.java} (61%) rename spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/{FetchableMongodbQuery.java => QuerydslFetchableMongodbQuery.java} (57%) rename spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/{JoinBuilder.java => QuerydslJoinBuilder.java} (63%) rename spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/{SimpleFetchableQuery.java => QuerydslMongoOps.java} (61%) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoSimpleTypes.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoSimpleTypes.java index 6b2202db43..73e9ecdb36 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoSimpleTypes.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoSimpleTypes.java @@ -22,15 +22,13 @@ import java.util.UUID; import java.util.regex.Pattern; -import org.bson.BsonObjectId; +import org.bson.*; import org.bson.types.Binary; import org.bson.types.CodeWScope; import org.bson.types.CodeWithScope; import org.bson.types.Decimal128; import org.bson.types.ObjectId; import org.springframework.data.mapping.model.SimpleTypeHolder; -import org.springframework.data.mongodb.util.MongoClientVersion; -import org.springframework.util.ClassUtils; import com.mongodb.DBRef; @@ -62,6 +60,24 @@ public abstract class MongoSimpleTypes { simpleTypes.add(Binary.class); simpleTypes.add(UUID.class); simpleTypes.add(Decimal128.class); + + simpleTypes.add(BsonBinary.class); + simpleTypes.add(BsonBoolean.class); + simpleTypes.add(BsonDateTime.class); + simpleTypes.add(BsonDbPointer.class); + simpleTypes.add(BsonDecimal128.class); + simpleTypes.add(BsonDocument.class); + simpleTypes.add(BsonDocument.class); + simpleTypes.add(BsonDouble.class); + simpleTypes.add(BsonInt32.class); + simpleTypes.add(BsonInt64.class); + simpleTypes.add(BsonJavaScript.class); + simpleTypes.add(BsonJavaScriptWithScope.class); + simpleTypes.add(BsonObjectId.class); + simpleTypes.add(BsonRegularExpression.class); + simpleTypes.add(BsonString.class); + simpleTypes.add(BsonTimestamp.class); + MONGO_SIMPLE_TYPES = Collections.unmodifiableSet(simpleTypes); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/AbstractMongodbQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/AbstractMongodbQuery.java deleted file mode 100644 index 4f425da98a..0000000000 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/AbstractMongodbQuery.java +++ /dev/null @@ -1,181 +0,0 @@ -/* - * Copyright 2018 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 java.util.Collection; - -import javax.annotation.Nullable; - -import org.bson.Document; - -import com.querydsl.core.DefaultQueryMetadata; -import com.querydsl.core.QueryModifiers; -import com.querydsl.core.SimpleQuery; -import com.querydsl.core.support.QueryMixin; -import com.querydsl.core.types.Expression; -import com.querydsl.core.types.FactoryExpression; -import com.querydsl.core.types.OrderSpecifier; -import com.querydsl.core.types.ParamExpression; -import com.querydsl.core.types.Path; -import com.querydsl.core.types.Predicate; -import com.querydsl.core.types.dsl.CollectionPathBase; - -/** - * {@code AbstractMongodbQuery} provides a base class for general Querydsl query implementation. - * - * @author Mark Paluch - * @param concrete subtype - */ -abstract class AbstractMongodbQuery> implements SimpleQuery { - - @SuppressWarnings("serial") - static class NoResults extends RuntimeException {} - - private final MongodbDocumentSerializer serializer; - private final QueryMixin queryMixin; - - /** - * Create a new MongodbQuery instance - * - * @param serializer serializer - */ - @SuppressWarnings("unchecked") - public AbstractMongodbQuery(MongodbDocumentSerializer serializer) { - @SuppressWarnings("unchecked") // Q is this plus subclass - Q query = (Q) this; - this.queryMixin = new QueryMixin(query, new DefaultQueryMetadata(), false); - this.serializer = serializer; - } - - /** - * Define a join - * - * @param ref reference - * @param target join target - * @return join builder - */ - public JoinBuilder join(Path ref, Path target) { - return new JoinBuilder(queryMixin, ref, target); - } - - /** - * Define a join - * - * @param ref reference - * @param target join target - * @return join builder - */ - public JoinBuilder join(CollectionPathBase ref, Path target) { - return new JoinBuilder(queryMixin, ref, target); - } - - /** - * Define a constraint for an embedded object - * - * @param collection collection - * @param target target - * @return builder - */ - public AnyEmbeddedBuilder anyEmbedded(Path> collection, Path target) { - return new AnyEmbeddedBuilder(queryMixin, collection); - } - - @Override - public Q distinct() { - return queryMixin.distinct(); - } - - public Q where(Predicate e) { - return queryMixin.where(e); - } - - @Override - public Q where(Predicate... e) { - return queryMixin.where(e); - } - - @Override - public Q limit(long limit) { - return queryMixin.limit(limit); - } - - @Override - public Q offset(long offset) { - return queryMixin.offset(offset); - } - - @Override - public Q restrict(QueryModifiers modifiers) { - return queryMixin.restrict(modifiers); - } - - public Q orderBy(OrderSpecifier o) { - return queryMixin.orderBy(o); - } - - @Override - public Q orderBy(OrderSpecifier... o) { - return queryMixin.orderBy(o); - } - - @Override - public Q set(ParamExpression param, T value) { - return queryMixin.set(param, value); - } - - protected Document createProjection(Expression projection) { - if (projection instanceof FactoryExpression) { - Document obj = new Document(); - for (Object expr : ((FactoryExpression) projection).getArgs()) { - if (expr instanceof Expression) { - obj.put((String) serializer.handle((Expression) expr), 1); - } - } - return obj; - } - return new Document(); - } - - protected Document createQuery(@Nullable Predicate predicate) { - if (predicate != null) { - return (Document) serializer.handle(predicate); - } else { - return new Document(); - } - } - - QueryMixin getQueryMixin() { - return queryMixin; - } - - MongodbDocumentSerializer getSerializer() { - return serializer; - } - - /** - * Get the where definition as a Document instance - * - * @return - */ - public Document asDocument() { - return createQuery(queryMixin.getMetadata().getWhere()); - } - - @Override - public String toString() { - return asDocument().toString(); - } -} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongodbDocumentSerializer.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongodbDocumentSerializer.java index eb125d7ddb..048233d5ba 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongodbDocumentSerializer.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongodbDocumentSerializer.java @@ -17,80 +17,154 @@ import java.util.ArrayList; import java.util.Collection; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.regex.Pattern; +import org.bson.BsonJavaScript; +import org.bson.BsonRegularExpression; import org.bson.Document; import org.bson.types.ObjectId; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; -import com.google.common.collect.Sets; import com.mongodb.DBRef; import com.querydsl.core.types.*; import com.querydsl.mongodb.MongodbOps; /** + *

* Serializes the given Querydsl query to a Document query for MongoDB. + *

+ *

+ * Original implementation source {@link com.querydsl.mongodb.MongodbSerializer} by {@literal The Querydsl Team} + * (http://www.querydsl.com/team) licensed under the Apache License, Version + * 2.0. + *

+ * Modified to use {@link Document} instead of {@link com.mongodb.DBObject}, updated nullable types and code format. Use + * Bson specific types and add {@link QuerydslMongoOps#NO_MATCH}. * + * @author laimw * @author Mark Paluch + * @author Christoph Strobl + * @since 2.1 */ abstract class MongodbDocumentSerializer implements Visitor { - public Object handle(Expression expression) { + @Nullable + Object handle(Expression expression) { return expression.accept(this, null); } - public Document toSort(List> orderBys) { - Document sort = new Document(); - for (OrderSpecifier orderBy : orderBys) { - Object key = orderBy.getTarget().accept(this, null); - sort.append(key.toString(), orderBy.getOrder() == Order.ASC ? 1 : -1); + /** + * Create the MongoDB specific query document. + * + * @param predicate must not be {@literal null}. + * @return empty {@link Document} by default. + */ + Document toQuery(Predicate predicate) { + + Object value = handle(predicate); + + if (value == null) { + return new Document(); } + + Assert.isInstanceOf(Document.class, value, + () -> String.format("Invalid type. Expected Document but found %s", value.getClass())); + + return (Document) value; + } + + /** + * Create the MongoDB specific sort document. + * + * @param orderBys must not be {@literal null}. + * @return empty {@link Document} by default. + */ + Document toSort(List> orderBys) { + + Document sort = new Document(); + + orderBys.forEach(orderSpecifier -> { + + Object key = orderSpecifier.getTarget().accept(this, null); + + Assert.notNull(key, () -> String.format("Mapped sort key for %s must not be null!", orderSpecifier)); + sort.append(key.toString(), orderSpecifier.getOrder() == Order.ASC ? 1 : -1); + }); + return sort; } + /* + * (non-Javadoc) + * @see com.querydsl.core.types.Visitor#visit(com.querydsl.core.types.Constant, java.lang.Void) + */ @Override public Object visit(Constant expr, Void context) { - if (Enum.class.isAssignableFrom(expr.getType())) { - @SuppressWarnings("unchecked") // Guarded by previous check - Constant> expectedExpr = (Constant>) expr; - return expectedExpr.getConstant().name(); - } else { + + if (!Enum.class.isAssignableFrom(expr.getType())) { return expr.getConstant(); } + + @SuppressWarnings("unchecked") // Guarded by previous check + Constant> expectedExpr = (Constant>) expr; + return expectedExpr.getConstant().name(); } + /* + * (non-Javadoc) + * @see com.querydsl.core.types.Visitor#visit(com.querydsl.core.types.TemplateExpression, java.lang.Void) + */ @Override public Object visit(TemplateExpression expr, Void context) { throw new UnsupportedOperationException(); } + /* + * (non-Javadoc) + * @see com.querydsl.core.types.Visitor#visit(com.querydsl.core.types.FactoryExpression, java.lang.Void) + */ @Override public Object visit(FactoryExpression expr, Void context) { throw new UnsupportedOperationException(); } protected String asDBKey(Operation expr, int index) { - return (String) asDBValue(expr, index); + + String key = (String) asDBValue(expr, index); + + Assert.hasText(key, () -> String.format("Mapped key must not be null nor empty for expression %s.", expr)); + return key; } + @Nullable protected Object asDBValue(Operation expr, int index) { return expr.getArg(index).accept(this, null); } private String regexValue(Operation expr, int index) { - return Pattern.quote(expr.getArg(index).accept(this, null).toString()); + + Object value = expr.getArg(index).accept(this, null); + + Assert.notNull(value, () -> String.format("Regex for %s must not be null.", expr)); + return Pattern.quote(value.toString()); } - protected Document asDocument(String key, Object value) { + protected Document asDocument(String key, @Nullable Object value) { return new Document(key, value); } @SuppressWarnings("unchecked") @Override public Object visit(Operation expr, Void context) { + Operator op = expr.getOperator(); if (op == Ops.EQ) { + if (expr.getArg(0) instanceof Operation) { Operation lhs = (Operation) expr.getArg(0); if (lhs.getOperator() == Ops.COL_SIZE || lhs.getOperator() == Ops.ARRAY_SIZE) { @@ -105,15 +179,19 @@ public Object visit(Operation expr, Void context) { } } else if (op == Ops.STRING_IS_EMPTY) { return asDocument(asDBKey(expr, 0), ""); - } else if (op == Ops.AND) { + Map lhs = (Map) handle(expr.getArg(0)); Map rhs = (Map) handle(expr.getArg(1)); - if (Sets.intersection(lhs.keySet(), rhs.keySet()).isEmpty()) { + + LinkedHashSet> lhs2 = new LinkedHashSet<>(lhs.entrySet()); + lhs2.retainAll(rhs.entrySet()); + + if (lhs2.isEmpty()) { lhs.putAll(rhs); return lhs; } else { - List list = new ArrayList(2); + List list = new ArrayList<>(2); list.add(handle(expr.getArg(0))); list.add(handle(expr.getArg(1))); return asDocument("$and", list); @@ -133,53 +211,47 @@ public Object visit(Operation expr, Void context) { } } else if (op == Ops.OR) { - List list = new ArrayList(2); + + List list = new ArrayList<>(2); list.add(handle(expr.getArg(0))); list.add(handle(expr.getArg(1))); return asDocument("$or", list); } else if (op == Ops.NE) { + Path path = (Path) expr.getArg(0); Constant constant = (Constant) expr.getArg(1); return asDocument(asDBKey(expr, 0), asDocument("$ne", convert(path, constant))); } else if (op == Ops.STARTS_WITH) { - return asDocument(asDBKey(expr, 0), Pattern.compile("^" + regexValue(expr, 1))); - + return asDocument(asDBKey(expr, 0), new BsonRegularExpression("^" + regexValue(expr, 1))); } else if (op == Ops.STARTS_WITH_IC) { - return asDocument(asDBKey(expr, 0), Pattern.compile("^" + regexValue(expr, 1), Pattern.CASE_INSENSITIVE)); - + return asDocument(asDBKey(expr, 0), new BsonRegularExpression("^" + regexValue(expr, 1), "i")); } else if (op == Ops.ENDS_WITH) { - return asDocument(asDBKey(expr, 0), Pattern.compile(regexValue(expr, 1) + "$")); - + return asDocument(asDBKey(expr, 0), new BsonRegularExpression(regexValue(expr, 1) + "$")); } else if (op == Ops.ENDS_WITH_IC) { - return asDocument(asDBKey(expr, 0), Pattern.compile(regexValue(expr, 1) + "$", Pattern.CASE_INSENSITIVE)); - + return asDocument(asDBKey(expr, 0), new BsonRegularExpression(regexValue(expr, 1) + "$", "i")); } else if (op == Ops.EQ_IGNORE_CASE) { - return asDocument(asDBKey(expr, 0), Pattern.compile("^" + regexValue(expr, 1) + "$", Pattern.CASE_INSENSITIVE)); - + return asDocument(asDBKey(expr, 0), new BsonRegularExpression("^" + regexValue(expr, 1) + "$", "i")); } else if (op == Ops.STRING_CONTAINS) { - return asDocument(asDBKey(expr, 0), Pattern.compile(".*" + regexValue(expr, 1) + ".*")); - + return asDocument(asDBKey(expr, 0), new BsonRegularExpression(".*" + regexValue(expr, 1) + ".*")); } else if (op == Ops.STRING_CONTAINS_IC) { - return asDocument(asDBKey(expr, 0), Pattern.compile(".*" + regexValue(expr, 1) + ".*", Pattern.CASE_INSENSITIVE)); - + return asDocument(asDBKey(expr, 0), new BsonRegularExpression(".*" + regexValue(expr, 1) + ".*", "i")); } else if (op == Ops.MATCHES) { - return asDocument(asDBKey(expr, 0), Pattern.compile(asDBValue(expr, 1).toString())); - + return asDocument(asDBKey(expr, 0), new BsonRegularExpression(asDBValue(expr, 1).toString())); } else if (op == Ops.MATCHES_IC) { - return asDocument(asDBKey(expr, 0), Pattern.compile(asDBValue(expr, 1).toString(), Pattern.CASE_INSENSITIVE)); - + return asDocument(asDBKey(expr, 0), new BsonRegularExpression(asDBValue(expr, 1).toString(), "i")); } else if (op == Ops.LIKE) { - String regex = ExpressionUtils.likeToRegex((Expression) expr.getArg(1)).toString(); - return asDocument(asDBKey(expr, 0), Pattern.compile(regex)); + String regex = ExpressionUtils.likeToRegex((Expression) expr.getArg(1)).toString(); + return asDocument(asDBKey(expr, 0), new BsonRegularExpression(regex)); } else if (op == Ops.BETWEEN) { + Document value = new Document("$gte", asDBValue(expr, 1)); value.append("$lte", asDBValue(expr, 2)); return asDocument(asDBKey(expr, 0), value); - } else if (op == Ops.IN) { + int constIndex = 0; int exprIndex = 1; if (expr.getArg(1) instanceof Constant) { @@ -195,85 +267,80 @@ public Object visit(Operation expr, Void context) { Constant constant = (Constant) expr.getArg(constIndex); return asDocument(asDBKey(expr, exprIndex), convert(path, constant)); } - } else if (op == Ops.NOT_IN) { + int constIndex = 0; int exprIndex = 1; if (expr.getArg(1) instanceof Constant) { + constIndex = 1; exprIndex = 0; } if (Collection.class.isAssignableFrom(expr.getArg(constIndex).getType())) { + @SuppressWarnings("unchecked") // guarded by previous check Collection values = ((Constant>) expr.getArg(constIndex)).getConstant(); return asDocument(asDBKey(expr, exprIndex), asDocument("$nin", values)); } else { + Path path = (Path) expr.getArg(exprIndex); Constant constant = (Constant) expr.getArg(constIndex); return asDocument(asDBKey(expr, exprIndex), asDocument("$ne", convert(path, constant))); } - } else if (op == Ops.COL_IS_EMPTY) { - List list = new ArrayList(2); + + List list = new ArrayList<>(2); list.add(asDocument(asDBKey(expr, 0), new ArrayList())); list.add(asDocument(asDBKey(expr, 0), asDocument("$exists", false))); return asDocument("$or", list); - } else if (op == Ops.LT) { return asDocument(asDBKey(expr, 0), asDocument("$lt", asDBValue(expr, 1))); - } else if (op == Ops.GT) { return asDocument(asDBKey(expr, 0), asDocument("$gt", asDBValue(expr, 1))); - } else if (op == Ops.LOE) { return asDocument(asDBKey(expr, 0), asDocument("$lte", asDBValue(expr, 1))); - } else if (op == Ops.GOE) { return asDocument(asDBKey(expr, 0), asDocument("$gte", asDBValue(expr, 1))); - } else if (op == Ops.IS_NULL) { return asDocument(asDBKey(expr, 0), asDocument("$exists", false)); - } else if (op == Ops.IS_NOT_NULL) { return asDocument(asDBKey(expr, 0), asDocument("$exists", true)); - } else if (op == Ops.CONTAINS_KEY) { + Path path = (Path) expr.getArg(0); Expression key = expr.getArg(1); return asDocument(visit(path, context) + "." + key.toString(), asDocument("$exists", true)); - } else if (op == MongodbOps.NEAR) { return asDocument(asDBKey(expr, 0), asDocument("$near", asDBValue(expr, 1))); - } else if (op == MongodbOps.NEAR_SPHERE) { return asDocument(asDBKey(expr, 0), asDocument("$nearSphere", asDBValue(expr, 1))); - } else if (op == MongodbOps.ELEM_MATCH) { return asDocument(asDBKey(expr, 0), asDocument("$elemMatch", asDBValue(expr, 1))); + } else if (op == QuerydslMongoOps.NO_MATCH) { + return new Document("$where", new BsonJavaScript("function() { return false }")); } throw new UnsupportedOperationException("Illegal operation " + expr); } private Object negate(Document arg) { - List list = new ArrayList(); + + List list = new ArrayList<>(); for (Map.Entry entry : arg.entrySet()) { + if (entry.getKey().equals("$or")) { list.add(asDocument("$nor", entry.getValue())); - } else if (entry.getKey().equals("$and")) { - List list2 = new ArrayList(); + + List list2 = new ArrayList<>(); for (Object o : ((Collection) entry.getValue())) { list2.add(negate((Document) o)); } list.add(asDocument("$or", list2)); - - } else if (entry.getValue() instanceof Pattern) { + } else if (entry.getValue() instanceof Pattern || entry.getValue() instanceof BsonRegularExpression) { list.add(asDocument(entry.getKey(), asDocument("$not", entry.getValue()))); - } else if (entry.getValue() instanceof Document) { list.add(negate(entry.getKey(), (Document) entry.getValue())); - } else { list.add(asDocument(entry.getKey(), asDocument("$ne", entry.getValue()))); } @@ -282,25 +349,30 @@ private Object negate(Document arg) { } private Object negate(String key, Document value) { + if (value.size() == 1) { return asDocument(key, asDocument("$not", value)); - } else { - List list2 = new ArrayList(); + + List list2 = new ArrayList<>(); for (Map.Entry entry2 : value.entrySet()) { list2.add(asDocument(key, asDocument("$not", asDocument(entry2.getKey(), entry2.getValue())))); } + return asDocument("$or", list2); } } protected Object convert(Path property, Constant constant) { + if (isReference(property)) { return asReference(constant.getConstant()); } else if (isId(property)) { + if (isReference(property.getMetadata().getParent())) { return asReferenceKey(property.getMetadata().getParent().getType(), constant.getConstant()); } else if (constant.getType().equals(String.class) && isImplicitObjectIdConversion()) { + String id = (String) constant.getConstant(); return ObjectId.isValid(id) ? new ObjectId(id) : id; } @@ -319,7 +391,7 @@ protected DBRef asReferenceKey(Class entity, Object id) { protected abstract DBRef asReference(Object constant); - protected abstract boolean isReference(Path arg); + protected abstract boolean isReference(@Nullable Path arg); protected boolean isId(Path arg) { // TODO override in subclass @@ -328,8 +400,11 @@ protected boolean isId(Path arg) { @Override public String visit(Path expr, Void context) { + PathMetadata metadata = expr.getMetadata(); + if (metadata.getParent() != null) { + Path parent = metadata.getParent(); if (parent.getMetadata().getPathType() == PathType.DELEGATE) { parent = parent.getMetadata().getParent(); @@ -337,6 +412,7 @@ public String visit(Path expr, Void context) { if (metadata.getPathType() == PathType.COLLECTION_ANY) { return visit(parent, context); } else if (parent.getMetadata().getPathType() != PathType.VARIABLE) { + String rv = getKeyForPath(expr, metadata); String parentStr = visit(parent, context); return rv != null ? parentStr + "." + rv : parentStr; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongodbExpressions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongodbExpressions.java deleted file mode 100644 index 79fd892a3b..0000000000 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongodbExpressions.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2018 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 java.util.Arrays; - -import com.querydsl.core.types.ConstantImpl; -import com.querydsl.core.types.Expression; -import com.querydsl.core.types.dsl.BooleanExpression; -import com.querydsl.core.types.dsl.Expressions; -import com.querydsl.mongodb.MongodbOps; - -/** - * Mongodb Document-API-specific operations. - * - * @author tiwe - * @author Mark Paluch - */ -class MongodbExpressions { - - private MongodbExpressions() {} - - /** - * Finds the closest points relative to the given location and orders the results with decreasing proximity - * - * @param expr location - * @param latVal latitude - * @param longVal longitude - * @return predicate - */ - public static BooleanExpression near(Expression expr, double latVal, double longVal) { - return Expressions.booleanOperation(MongodbOps.NEAR, expr, ConstantImpl.create(Arrays.asList(latVal, longVal))); - } - - /** - * Finds the closest points relative to the given location on a sphere and orders the results with decreasing - * proximity - * - * @param expr location - * @param latVal latitude - * @param longVal longitude - * @return predicate - */ - public static BooleanExpression nearSphere(Expression expr, double latVal, double longVal) { - return Expressions.booleanOperation(MongodbOps.NEAR_SPHERE, expr, - ConstantImpl.create(Arrays.asList(latVal, longVal))); - } -} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslAbstractMongodbQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslAbstractMongodbQuery.java new file mode 100644 index 0000000000..74ff1789b9 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslAbstractMongodbQuery.java @@ -0,0 +1,202 @@ +/* + * Copyright 2018 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 java.util.List; + +import org.bson.Document; +import org.springframework.lang.Nullable; + +import com.querydsl.core.DefaultQueryMetadata; +import com.querydsl.core.QueryModifiers; +import com.querydsl.core.SimpleQuery; +import com.querydsl.core.support.QueryMixin; +import com.querydsl.core.types.Expression; +import com.querydsl.core.types.FactoryExpression; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.ParamExpression; +import com.querydsl.core.types.Predicate; + +/** + * {@code QuerydslAbstractMongodbQuery} provides a base class for general Querydsl query implementation. + *

+ * Original implementation source {@link com.querydsl.mongodb.AbstractMongodbQuery} by {@literal The Querydsl Team} + * (http://www.querydsl.com/team) licensed under the Apache License, Version + * 2.0. + *

+ * Modified for usage with {@link MongodbDocumentSerializer}. + * + * @param concrete subtype + * @author laimw + * @author Mark Paluch + * @author Christoph Strobl + * @since 2.1 + */ +public abstract class QuerydslAbstractMongodbQuery> + implements SimpleQuery { + + private final MongodbDocumentSerializer serializer; + private final QueryMixin queryMixin; + + /** + * Create a new MongodbQuery instance + * + * @param serializer serializer + */ + @SuppressWarnings("unchecked") + QuerydslAbstractMongodbQuery(MongodbDocumentSerializer serializer) { + + this.queryMixin = new QueryMixin<>((Q) this, new DefaultQueryMetadata(), false); + this.serializer = serializer; + } + + /* + * (non-Javadoc) + * @see com.querydsl.core.SimpleQuery#distinct() + */ + @Override + public Q distinct() { + return queryMixin.distinct(); + } + + /* + * (non-Javadoc) + * @see com.querydsl.core.FilteredClause#where(com.querydsl.core.types.Predicate[]) + */ + @Override + public Q where(Predicate... e) { + return queryMixin.where(e); + } + + /* + * (non-Javadoc) + * @see com.querydsl.core.SimpleQuery#limit(long) + */ + @Override + public Q limit(long limit) { + return queryMixin.limit(limit); + } + + /* + * (non-Javadoc) + * @see com.querydsl.core.SimpleQuery#offset() + */ + @Override + public Q offset(long offset) { + return queryMixin.offset(offset); + } + + /* + * (non-Javadoc) + * @see com.querydsl.core.SimpleQuery#restrict(com.querydsl.core.QueryModifiers) + */ + @Override + public Q restrict(QueryModifiers modifiers) { + return queryMixin.restrict(modifiers); + } + + /* + * (non-Javadoc) + * @see com.querydsl.core.SimpleQuery#orderBy(com.querydsl.core.types.OrderSpecifier) + */ + @Override + public Q orderBy(OrderSpecifier... o) { + return queryMixin.orderBy(o); + } + + /* + * (non-Javadoc) + * @see com.querydsl.core.SimpleQuery#set(com.querydsl.core.types.ParamExpression, Object) + */ + @Override + public Q set(ParamExpression param, T value) { + return queryMixin.set(param, value); + } + + /** + * Compute the actual projection {@link Document} from a given projectionExpression by serializing the contained + * {@link Expression expressions} individually. + * + * @param projectionExpression the computed projection {@link Document}. + * @return never {@literal null}. An empty {@link Document} by default. + * @see MongodbDocumentSerializer#handle(Expression) + */ + protected Document createProjection(@Nullable Expression projectionExpression) { + + if (!(projectionExpression instanceof FactoryExpression)) { + return new Document(); + } + + Document projection = new Document(); + ((FactoryExpression) projectionExpression).getArgs().stream() // + .filter(Expression.class::isInstance) // + .map(Expression.class::cast) // + .map(serializer::handle) // + .forEach(it -> projection.append(it.toString(), 1)); + + return projection; + } + + /** + * Compute the filer {@link Document} from the given {@link Predicate}. + * + * @param predicate can be {@literal null}. + * @return an empty {@link Document} if predicate is {@literal null}. + * @see MongodbDocumentSerializer#toQuery(Predicate) + */ + protected Document createQuery(@Nullable Predicate predicate) { + + if (predicate == null) { + return new Document(); + } + + return serializer.toQuery(predicate); + } + + /** + * Compute the sort {@link Document} from the given list of {@link OrderSpecifier order specifiers}. + * + * @param orderSpecifiers can be {@literal null}. + * @return an empty {@link Document} if predicate is {@literal null}. + * @see MongodbDocumentSerializer#toSort(List) + */ + protected Document createSort(List> orderSpecifiers) { + return serializer.toSort(orderSpecifiers); + } + + /** + * Get the actual {@link QueryMixin} delegate. + * + * @return + */ + QueryMixin getQueryMixin() { + return queryMixin; + } + + /** + * Get the where definition as a Document instance + * + * @return + */ + Document asDocument() { + return createQuery(queryMixin.getMetadata().getWhere()); + } + + @Override + public String toString() { + return asDocument().toString(); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/AnyEmbeddedBuilder.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslAnyEmbeddedBuilder.java similarity index 61% rename from spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/AnyEmbeddedBuilder.java rename to spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslAnyEmbeddedBuilder.java index e2260fd272..af9380ba2f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/AnyEmbeddedBuilder.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslAnyEmbeddedBuilder.java @@ -24,23 +24,40 @@ import com.querydsl.mongodb.MongodbOps; /** - * {@code AnyEmbeddedBuilder} is a builder for constraints on embedded objects - * + * {@code QuerydslAnyEmbeddedBuilder} is a builder for constraints on embedded objects. + *

+ * Original implementation source {@link com.querydsl.mongodb.AnyEmbeddedBuilder} by {@literal The Querydsl Team} + * (http://www.querydsl.com/team) licensed under the Apache License, Version + * 2.0. + *

+ * Modified for usage with {@link QuerydslAbstractMongodbQuery}. + * * @param query type + * @author tiwe * @author Mark Paluch + * @author Christoph Strobl + * @since 2.1 */ -class AnyEmbeddedBuilder> { +public class QuerydslAnyEmbeddedBuilder, K> { private final QueryMixin queryMixin; - private final Path> collection; - public AnyEmbeddedBuilder(QueryMixin queryMixin, Path> collection) { + QuerydslAnyEmbeddedBuilder(QueryMixin queryMixin, Path> collection) { + this.queryMixin = queryMixin; this.collection = collection; } + /** + * Add the given where conditions. + * + * @param conditions must not be {@literal null}. + * @return the target {@link QueryMixin}. + * @see QueryMixin#where(Predicate) + */ public Q on(Predicate... conditions) { + return queryMixin .where(ExpressionUtils.predicate(MongodbOps.ELEM_MATCH, collection, ExpressionUtils.allOf(conditions))); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/FetchableMongodbQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslFetchableMongodbQuery.java similarity index 57% rename from spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/FetchableMongodbQuery.java rename to spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslFetchableMongodbQuery.java index dc1cbe0a3f..e9f1b09865 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/FetchableMongodbQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslFetchableMongodbQuery.java @@ -19,14 +19,13 @@ import java.util.Collections; import java.util.List; -import javax.annotation.Nullable; - +import org.springframework.data.mongodb.core.ExecutableFindOperation.FindWithProjection; import org.springframework.data.mongodb.core.MongoOperations; 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 com.google.common.collect.HashMultimap; -import com.google.common.collect.Multimap; import com.mysema.commons.lang.CloseableIterator; import com.querydsl.core.Fetchable; import com.querydsl.core.JoinExpression; @@ -39,32 +38,26 @@ 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; /** - * {@link Fetchable} Mongodb query with a pluggable Document to Bean transformation. + * {@link Fetchable} MongoDB query with utilizing {@link MongoOperations} for command execution. * * @param result type * @param concrete subtype * @author Mark Paluch + * @author Christoph Strobl + * @since 2.1 */ -abstract class FetchableMongodbQuery> extends AbstractMongodbQuery - implements Fetchable { +abstract class QuerydslFetchableMongodbQuery> + extends QuerydslAbstractMongodbQuery implements Fetchable { private final Class entityClass; private final String collection; private final MongoOperations mongoOperations; + private final FindWithProjection find; - public FetchableMongodbQuery(MongodbDocumentSerializer serializer, Class entityClass, - MongoOperations mongoOperations) { - - super(serializer); - - this.entityClass = (Class) entityClass; - this.collection = mongoOperations.getCollectionName(entityClass); - this.mongoOperations = mongoOperations; - } - - public FetchableMongodbQuery(MongodbDocumentSerializer serializer, Class entityClass, String collection, + QuerydslFetchableMongodbQuery(MongodbDocumentSerializer serializer, Class entityClass, String collection, MongoOperations mongoOperations) { super(serializer); @@ -72,19 +65,13 @@ public FetchableMongodbQuery(MongodbDocumentSerializer serializer, Class) entityClass; this.collection = collection; this.mongoOperations = mongoOperations; + find = mongoOperations.query(this.entityClass).inCollection(collection); } - /** - * Iterate with the specific fields - * - * @param paths fields to return - * @return iterator + /* + * (non-Javadoc) + * @see com.querydsl.core.Fetchable#iterable() */ - public CloseableIterator iterate(Path... paths) { - getQueryMixin().setProjection(paths); - return iterate(); - } - @Override public CloseableIterator iterate() { @@ -92,6 +79,7 @@ public CloseableIterator iterate() { entityClass, collection); return new CloseableIterator() { + @Override public boolean hasNext() { return stream.hasNext(); @@ -104,7 +92,7 @@ public K next() { @Override public void remove() { - + throw new UnsupportedOperationException("Cannot remove from iterator while streaming data."); } @Override @@ -114,91 +102,99 @@ public void close() { }; } - /** - * Fetch with the specific fields - * - * @param paths fields to return - * @return results + /* + * (non-Javadoc) + * @see com.querydsl.core.Fetchable#fetch() */ - public List fetch(Path... paths) { - getQueryMixin().setProjection(paths); - return fetch(); - } - @Override public List fetch() { - return mongoOperations.query(entityClass).matching(createQuery()).all(); + return find.matching(createQuery()).all(); } - /** - * Fetch first with the specific fields - * - * @param paths fields to return - * @return first result + /* + * (non-Javadoc) + * @see com.querydsl.core.Fetchable#fetchFirst() */ - public K fetchFirst(Path... paths) { - getQueryMixin().setProjection(paths); - return fetchFirst(); - } - @Override public K fetchFirst() { - return mongoOperations.query(entityClass).matching(createQuery()).firstValue(); + return find.matching(createQuery()).firstValue(); } - /** - * Fetch one with the specific fields - * - * @param paths fields to return - * @return first result + /* + * (non-Javadoc) + * @see com.querydsl.core.Fetchable#fetchOne() */ - public K fetchOne(Path... paths) { - getQueryMixin().setProjection(paths); - return fetchOne(); + @Override + public K fetchOne() { + return find.matching(createQuery()).oneValue(); } + /* + * (non-Javadoc) + * @see com.querydsl.core.Fetchable#fetchResults() + */ @Override - public K fetchOne() { - return mongoOperations.query(entityClass).matching(createQuery()).oneValue(); + public QueryResults fetchResults() { + long total = fetchCount(); + return total > 0L ? new QueryResults<>(fetch(), getQueryMixin().getMetadata().getModifiers(), total) + : QueryResults.emptyResults(); + } + + /* + * (non-Javadoc) + * @see com.querydsl.core.Fetchable#fetchCount() + */ + @Override + public long fetchCount() { + return find.matching(createQuery()).count(); } /** - * Fetch results with the specific fields + * Define a join. * - * @param paths fields to return - * @return results + * @param ref reference + * @param target join target + * @return new instance of {@link QuerydslJoinBuilder}. */ - public QueryResults fetchResults(Path... paths) { - getQueryMixin().setProjection(paths); - return fetchResults(); + public QuerydslJoinBuilder join(Path ref, Path target) { + return new QuerydslJoinBuilder<>(getQueryMixin(), ref, target); } - @Override - public QueryResults fetchResults() { - long total = fetchCount(); - if (total > 0L) { - return new QueryResults<>(fetch(), getQueryMixin().getMetadata().getModifiers(), total); - } else { - return QueryResults.emptyResults(); - } + /** + * Define a join. + * + * @param ref reference + * @param target join target + * @return new instance of {@link QuerydslJoinBuilder}. + */ + public QuerydslJoinBuilder join(CollectionPathBase ref, Path target) { + return new QuerydslJoinBuilder<>(getQueryMixin(), ref, target); } - @Override - public long fetchCount() { - return mongoOperations.query(entityClass).matching(createQuery()).count(); + /** + * 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 anyEmbedded(Path> collection, Path target) { + return new QuerydslAnyEmbeddedBuilder<>(getQueryMixin(), collection); } protected org.springframework.data.mongodb.core.query.Query createQuery() { + QueryMetadata metadata = getQueryMixin().getMetadata(); - Predicate filter = createFilter(metadata); - return createQuery(filter, metadata.getProjection(), metadata.getModifiers(), metadata.getOrderBy()); + + return createQuery(createFilter(metadata), metadata.getProjection(), metadata.getModifiers(), + metadata.getOrderBy()); } - protected org.springframework.data.mongodb.core.query.Query createQuery(@Nullable Predicate where, - Expression projection, QueryModifiers modifiers, List> orderBy) { + protected org.springframework.data.mongodb.core.query.Query createQuery(@Nullable Predicate filter, + @Nullable Expression projection, QueryModifiers modifiers, List> orderBy) { - BasicQuery basicQuery = new BasicQuery(createQuery(where), createProjection(projection)); + BasicQuery basicQuery = new BasicQuery(createQuery(filter), createProjection(projection)); Integer limit = modifiers.getLimitAsInteger(); Integer offset = modifiers.getOffsetAsInteger(); @@ -210,13 +206,15 @@ protected org.springframework.data.mongodb.core.query.Query createQuery(@Nullabl basicQuery.skip(offset); } if (orderBy.size() > 0) { - basicQuery.setSortObject(getSerializer().toSort(orderBy)); + basicQuery.setSortObject(createSort(orderBy)); } + return basicQuery; } @Nullable protected Predicate createFilter(QueryMetadata metadata) { + Predicate filter; if (!metadata.getJoins().isEmpty()) { filter = ExpressionUtils.allOf(metadata.getWhere(), createJoinFilter(metadata)); @@ -229,21 +227,28 @@ protected Predicate createFilter(QueryMetadata metadata) { @SuppressWarnings("unchecked") @Nullable protected Predicate createJoinFilter(QueryMetadata metadata) { - Multimap, Predicate> predicates = HashMultimap.create(); + + LinkedMultiValueMap, Predicate> 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()); Predicate filter = ExpressionUtils.allOf(join.getCondition(), allOf(extraFilters)); + List ids = getIds(target.getType(), filter); + if (ids.isEmpty()) { - throw new NoResults(); + return ExpressionUtils.predicate(QuerydslMongoOps.NO_MATCH, source); } + Path path = ExpressionUtils.path(String.class, source, "$id"); - predicates.put(source.getRoot(), ExpressionUtils.in((Path) path, ids)); + predicates.add(source.getRoot(), ExpressionUtils.in((Path) path, ids)); } + Path source = (Path) ((Operation) joins.get(0).getTarget()).getArg(0); return allOf(predicates.get(source.getRoot())); } @@ -252,10 +257,16 @@ private Predicate allOf(Collection predicates) { return predicates != null ? ExpressionUtils.allOf(predicates) : 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 List getIds(Class targetType, Predicate condition) { - // TODO : fetch only ids - Query query = createQuery(condition, null, QueryModifiers.EMPTY, Collections.emptyList()); + Query query = createQuery(condition, null, QueryModifiers.EMPTY, Collections.emptyList()); return mongoOperations.findDistinct(query, "_id", targetType, Object.class); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/JoinBuilder.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslJoinBuilder.java similarity index 63% rename from spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/JoinBuilder.java rename to spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslJoinBuilder.java index 4459ab1edf..ac4b4bf6e4 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/JoinBuilder.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslJoinBuilder.java @@ -22,28 +22,44 @@ import com.querydsl.core.types.Predicate; /** - * {@code JoinBuilder} is a builder for join constraints + * {@code QuerydslJoinBuilder} is a builder for join constraints. + *

+ * Original implementation source {@link com.querydsl.mongodb.JoinBuilder} by {@literal The Querydsl Team} + * (http://www.querydsl.com/team) licensed under the Apache License, Version + * 2.0. + *

+ * Modified for usage with {@link QuerydslAbstractMongodbQuery}. * - * @author Mark Paluch * @param * @param + * @author tiwe + * @author Mark Paluch + * @author Christoph Strobl + * @since 2.1 */ -class JoinBuilder, T> { +public class QuerydslJoinBuilder, K, T> { private final QueryMixin queryMixin; - private final Path ref; - private final Path target; - public JoinBuilder(QueryMixin queryMixin, Path ref, Path target) { + QuerydslJoinBuilder(QueryMixin queryMixin, Path ref, Path target) { + this.queryMixin = queryMixin; this.ref = ref; this.target = target; } + /** + * Add the given join conditions. + * + * @param conditions must not be {@literal null}. + * @return the target {@link QueryMixin}. + * @see QueryMixin#on(Predicate) + */ @SuppressWarnings("unchecked") public Q on(Predicate... conditions) { + queryMixin.addJoin(JoinType.JOIN, ExpressionUtils.as((Path) ref, target)); queryMixin.on(conditions); return queryMixin.getSelf(); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleFetchableQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslMongoOps.java similarity index 61% rename from spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleFetchableQuery.java rename to spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslMongoOps.java index 1edb489b27..cae326188a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleFetchableQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslMongoOps.java @@ -15,13 +15,29 @@ */ package org.springframework.data.mongodb.repository.support; -import com.querydsl.core.Fetchable; -import com.querydsl.core.SimpleQuery; +import com.querydsl.core.types.Operator; /** - * Interface that combines {@link Fetchable} and {@link SimpleQuery}. + * Spring Data specific {@link Operator operators} for usage with Querydsl and MongoDB. * - * @author Mark Paluch + * @author Christoph Strobl * @since 2.1 */ -public interface SimpleFetchableQuery extends Fetchable, SimpleQuery> {} +enum QuerydslMongoOps implements Operator { + + /** + * {@link Operator} always evaluating to {@literal false}. + */ + NO_MATCH(Boolean.class); + + private final Class type; + + QuerydslMongoOps(Class type) { + this.type = type; + } + + @Override + public Class getType() { + return type; + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslMongoPredicateExecutor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslMongoPredicateExecutor.java index dba3c34c55..0ec3328509 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslMongoPredicateExecutor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslMongoPredicateExecutor.java @@ -165,7 +165,7 @@ public Page findAll(Predicate predicate, Pageable pageable) { Assert.notNull(predicate, "Predicate must not be null!"); Assert.notNull(pageable, "Pageable must not be null!"); - SimpleFetchableQuery query = createQueryFor(predicate); + SpringDataMongodbQuery query = createQueryFor(predicate); return PageableExecutionUtils.getPage(applyPagination(query, pageable).fetch(), pageable, query::fetchCount); } @@ -195,45 +195,45 @@ public boolean exists(Predicate predicate) { } /** - * Creates a {@link AbstractMongodbQuery} for the given {@link Predicate}. + * Creates a {@link SpringDataMongodbQuery} for the given {@link Predicate}. * * @param predicate * @return */ - private SimpleFetchableQuery createQueryFor(Predicate predicate) { + private SpringDataMongodbQuery createQueryFor(Predicate predicate) { return createQuery().where(predicate); } /** - * Creates a {@link AbstractMongodbQuery}. + * Creates a {@link SpringDataMongodbQuery}. * * @return */ - private SimpleFetchableQuery createQuery() { + private SpringDataMongodbQuery createQuery() { return new SpringDataMongodbQuery<>(mongoOperations, entityInformation.getJavaType()); } /** - * Applies the given {@link Pageable} to the given {@link MongodbQuery}. + * Applies the given {@link Pageable} to the given {@link SpringDataMongodbQuery}. * * @param query * @param pageable * @return */ - private SimpleFetchableQuery applyPagination(SimpleFetchableQuery query, Pageable pageable) { + private SpringDataMongodbQuery applyPagination(SpringDataMongodbQuery query, Pageable pageable) { query = query.offset(pageable.getOffset()).limit(pageable.getPageSize()); return applySorting(query, pageable.getSort()); } /** - * Applies the given {@link Sort} to the given {@link MongodbQuery}. + * Applies the given {@link Sort} to the given {@link SpringDataMongodbQuery}. * * @param query * @param sort * @return */ - private SimpleFetchableQuery applySorting(SimpleFetchableQuery query, Sort sort) { + private SpringDataMongodbQuery applySorting(SpringDataMongodbQuery query, Sort sort) { // TODO: find better solution than instanceof check if (sort instanceof QSort) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslRepositorySupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslRepositorySupport.java index 30ac168a95..5b89e85a1e 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslRepositorySupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslRepositorySupport.java @@ -47,7 +47,7 @@ public QuerydslRepositorySupport(MongoOperations operations) { } /** - * Returns a {@link MongodbQuery} for the given {@link EntityPath}. The collection being queried is derived from the + * Returns a {@link SpringDataMongodbQuery} for the given {@link EntityPath}. The collection being queried is derived from the * entity metadata. * * @param path @@ -61,7 +61,7 @@ protected SpringDataMongodbQuery from(final EntityPath path) { } /** - * Returns a {@link MongodbQuery} for the given {@link EntityPath} querying the given collection. + * Returns a {@link SpringDataMongodbQuery} for the given {@link EntityPath} querying the given collection. * * @param path must not be {@literal null} * @param collection must not be blank or {@literal null} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbQuery.java index 296d0f5d10..a5662147ad 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbQuery.java @@ -15,28 +15,17 @@ */ package org.springframework.data.mongodb.repository.support; -import java.util.List; - import org.springframework.data.mongodb.core.MongoOperations; -import com.mysema.commons.lang.CloseableIterator; -import com.querydsl.core.NonUniqueResultException; -import com.querydsl.core.QueryModifiers; -import com.querydsl.core.QueryResults; -import com.querydsl.core.types.OrderSpecifier; -import com.querydsl.core.types.ParamExpression; -import com.querydsl.core.types.Predicate; -import com.querydsl.mongodb.AbstractMongodbQuery; - /** - * Spring Data specific {@link AbstractMongodbQuery} implementation. + * Spring Data specific simple {@link com.querydsl.core.Fetchable} {@link com.querydsl.core.SimpleQuery Query} + * implementation. * * @author Oliver Gierke * @author Mark Paluch + * @author Christoph Strobl */ -public class SpringDataMongodbQuery implements SimpleFetchableQuery { - - private final OperationsMongodbQuery query; +public class SpringDataMongodbQuery extends QuerydslFetchableMongodbQuery> { /** * Creates a new {@link SpringDataMongodbQuery}. @@ -58,144 +47,6 @@ public SpringDataMongodbQuery(final MongoOperations operations, final Class type, String collectionName) { - query = new OperationsMongodbQuery<>(new SpringDataMongodbSerializer(operations.getConverter()), type, - collectionName, operations); - } - - /* - * (non-Javadoc) - * @see com.querydsl.core.Fetchable#fetch() - */ - @Override - public List fetch() { - return query.fetch(); - } - - /* - * (non-Javadoc) - * @see com.querydsl.core.Fetchable#fetchFirst() - */ - @Override - public T fetchFirst() { - return query.fetchFirst(); - } - - /* - * (non-Javadoc) - * @see com.querydsl.core.Fetchable#fetchOne() - */ - @Override - public T fetchOne() throws NonUniqueResultException { - return query.fetchOne(); - } - - /* - * (non-Javadoc) - * @see com.querydsl.core.Fetchable#iterate() - */ - @Override - public CloseableIterator iterate() { - return query.iterate(); - } - - /* - * (non-Javadoc) - * @see com.querydsl.core.Fetchable#fetchResults() - */ - @Override - public QueryResults fetchResults() { - return query.fetchResults(); - } - - /* - * (non-Javadoc) - * @see com.querydsl.core.Fetchable#fetchCount() - */ - @Override - public long fetchCount() { - return query.fetchCount(); - } - - /* - * (non-Javadoc) - * @see com.querydsl.core.SimpleQuery#limit(long) - */ - @Override - public SpringDataMongodbQuery limit(long limit) { - query.limit(limit); - return this; - } - - /* - * (non-Javadoc) - * @see com.querydsl.core.SimpleQuery#offset(long) - */ - @Override - public SpringDataMongodbQuery offset(long offset) { - query.offset(offset); - return this; - } - - /* - * (non-Javadoc) - * @see com.querydsl.core.SimpleQuery#restrict(com.querydsl.core.QueryModifiers) - */ - @Override - public SpringDataMongodbQuery restrict(QueryModifiers modifiers) { - query.restrict(modifiers); - return this; - } - - /* - * (non-Javadoc) - * @see com.querydsl.core.SimpleQuery#orderBy(com.querydsl.core.types.OrderSpecifier[]) - */ - @Override - public SpringDataMongodbQuery orderBy(OrderSpecifier... o) { - query.orderBy(o); - return this; - } - - /* - * (non-Javadoc) - * @see com.querydsl.core.SimpleQuery#set(com.querydsl.core.types.ParamExpression, java.lang.Object) - */ - @Override - public SpringDataMongodbQuery set(ParamExpression param, V value) { - query.set(param, value); - return this; - } - - /* - * (non-Javadoc) - * @see com.querydsl.core.SimpleQuery#distinct() - */ - @Override - public SpringDataMongodbQuery distinct() { - query.distinct(); - return this; - } - - /* - * (non-Javadoc) - * @see com.querydsl.core.FilteredClause#where(com.querydsl.core.types.Predicate[]) - */ - @Override - public SpringDataMongodbQuery where(Predicate... o) { - query.where(o); - return this; - } - - /** - * Concrete implementation of {@link FetchableMongodbQuery}. - * - * @param - */ - static class OperationsMongodbQuery extends FetchableMongodbQuery> { - - public OperationsMongodbQuery(MongodbDocumentSerializer serializer, Class entityClass, - String collection, MongoOperations mongoOperations) { - super(serializer, entityClass, collection, mongoOperations); - } + super(new SpringDataMongodbSerializer(operations.getConverter()), type, collectionName, operations); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbSerializer.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbSerializer.java index f0f2316db4..8a2e8f4f4f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbSerializer.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbSerializer.java @@ -54,7 +54,7 @@ class SpringDataMongodbSerializer extends MongodbDocumentSerializer { static { - Set pathTypes = new HashSet(); + Set pathTypes = new HashSet<>(); pathTypes.add(PathType.VARIABLE); pathTypes.add(PathType.PROPERTY); @@ -66,9 +66,9 @@ class SpringDataMongodbSerializer extends MongodbDocumentSerializer { private final QueryMapper mapper; /** - * Creates a new {@link SpringDataMongodbSerializer} for the given {@link MappingContext}. + * Creates a new {@link SpringDataMongodbSerializer} for the given {@link MongoConverter}. * - * @param mappingContext must not be {@literal null}. + * @param converter must not be {@literal null}. */ public SpringDataMongodbSerializer(MongoConverter converter) { @@ -113,7 +113,7 @@ protected String getKeyForPath(Path expr, PathMetadata metadata) { /* * (non-Javadoc) - * @see com.querydsl.mongodb.MongodbSerializer#asDocument(java.lang.String, java.lang.Object) + * @see org.springframework.data.mongodb.repository.support.MongodbSerializer#asDocument(java.lang.String, java.lang.Object) */ @Override protected Document asDocument(@Nullable String key, @Nullable Object value) { @@ -139,8 +139,7 @@ private Document convertId(String key, Object idValue) { Object convertedId = mapper.convertId(idValue); - return mapper.getMappedObject(super.asDocument(key, convertedId), - Optional.empty()); + return mapper.getMappedObject(super.asDocument(key, convertedId), Optional.empty()); } /* @@ -208,6 +207,7 @@ protected Object convert(@Nullable Path path, @Nullable Constant constant) : asReference(constant.getConstant(), path); } + @Nullable private MongoPersistentProperty getPropertyFor(Path path) { Path parent = path.getMetadata().getParent(); @@ -217,7 +217,7 @@ private MongoPersistentProperty getPropertyFor(Path path) { } MongoPersistentEntity entity = mappingContext.getPersistentEntity(parent.getType()); - return entity != null ? entity.getRequiredPersistentProperty(path.getMetadata().getName()) : null; + return entity != null ? entity.getPersistentProperty(path.getMetadata().getName()) : null; } /** diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/ApplicationContextEventTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/ApplicationContextEventTests.java index dcca0927a0..dbd64e76ff 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/ApplicationContextEventTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/ApplicationContextEventTests.java @@ -40,6 +40,11 @@ import org.springframework.data.mongodb.core.aggregation.Aggregation; import org.springframework.data.mongodb.core.mapping.DBRef; import org.springframework.data.mongodb.core.mapping.PersonPojoStringId; +import org.springframework.data.mongodb.repository.Person; +import org.springframework.data.mongodb.repository.QPerson; +import org.springframework.data.mongodb.repository.query.MongoEntityInformation; +import org.springframework.data.mongodb.repository.support.MongoRepositoryFactory; +import org.springframework.data.mongodb.repository.support.QuerydslMongoPredicateExecutor; import com.mongodb.MongoClient; import com.mongodb.WriteConcern; @@ -386,6 +391,29 @@ public void publishesAfterConvertEventForFindQueriesUsingProjections() { assertThat(listener.onAfterConvertEvents.get(0).getCollectionName()).isEqualTo(COLLECTION_NAME); } + @Test // DATAMONGO-700, DATAMONGO-1185, DATAMONGO-1848 + public void publishesEventsForQuerydslFindQueries() { + + template.dropCollection(Person.class); + + template.save(new Person("Boba", "Fett", 40)); + + MongoRepositoryFactory factory = new MongoRepositoryFactory(template); + MongoEntityInformation entityInformation = factory.getEntityInformation(Person.class); + QuerydslMongoPredicateExecutor executor = new QuerydslMongoPredicateExecutor<>(entityInformation, template); + + executor.findOne(QPerson.person.lastname.startsWith("Fe")); + + assertThat(listener.onAfterLoadEvents).hasSize(1); + assertThat(listener.onAfterLoadEvents.get(0).getCollectionName()).isEqualTo("person"); + + assertThat(listener.onBeforeConvertEvents).hasSize(1); + assertThat(listener.onBeforeConvertEvents.get(0).getCollectionName()).isEqualTo("person"); + + assertThat(listener.onAfterConvertEvents).hasSize(1); + assertThat(listener.onAfterConvertEvents.get(0).getCollectionName()).isEqualTo("person"); + } + private void comparePersonAndDocument(PersonPojoStringId p, PersonPojoStringId p2, org.bson.Document document) { assertThat(p2.getId()).isEqualTo(p.getId()); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/QuerydslMongoPredicateExecutorIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/QuerydslMongoPredicateExecutorIntegrationTests.java index 6dbede532e..36dacb0ed2 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/QuerydslMongoPredicateExecutorIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/QuerydslMongoPredicateExecutorIntegrationTests.java @@ -18,6 +18,7 @@ import static org.assertj.core.api.Assertions.*; import java.util.Arrays; +import java.util.LinkedHashSet; import java.util.List; import org.junit.Before; @@ -25,15 +26,25 @@ 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.MongoDbFactory; import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.data.mongodb.core.MongoTemplate; +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.SpringJUnit4ClassRunner; +import com.mongodb.MongoException; +import com.mongodb.client.MongoDatabase; + /** * Integration test for {@link QuerydslMongoPredicateExecutor}. * @@ -47,6 +58,8 @@ public class QuerydslMongoPredicateExecutorIntegrationTests { @Autowired MongoOperations operations; + @Autowired MongoDbFactory dbFactory; + QuerydslMongoPredicateExecutor repository; Person dave, oliver, carter; @@ -57,7 +70,7 @@ public void setup() { MongoRepositoryFactory factory = new MongoRepositoryFactory(operations); MongoEntityInformation entityInformation = factory.getEntityInformation(Person.class); - repository = new QuerydslMongoPredicateExecutor(entityInformation, operations); + repository = new QuerydslMongoPredicateExecutor<>(entityInformation, operations); operations.dropCollection(Person.class); @@ -99,4 +112,114 @@ public void findOneWithPredicateReturnsOptionalEmptyWhenNoDataFound() { public void findOneWithPredicateThrowsExceptionForNonUniqueResults() { repository.findOne(person.firstname.contains("e")); } + + @Test // DATAMONGO-1848 + public void findUsingAndShouldWork() { + + assertThat(repository.findAll( + person.lastname.startsWith(oliver.getLastname()).and(person.firstname.startsWith(dave.getFirstname())))) + .containsExactly(dave); + } + + @Test // DATAMONGO-362, DATAMONGO-1848 + public void springDataMongodbQueryShouldAllowJoinOnDBref() { + + 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.save(user1); + operations.save(user2); + operations.save(user3); + + 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); + operations.save(person2); + operations.save(person3); + + List result = new SpringDataMongodbQuery<>(operations, Person.class).where() + .join(person.coworker, QUser.user).on(QUser.user.username.eq("user-2")).fetch(); + + assertThat(result).containsExactly(person2); + } + + @Test // DATAMONGO-362, DATAMONGO-1848 + public void springDataMongodbQueryShouldReturnEmptyOnJoinWithNoResults() { + + User user1 = new User(); + user1.setUsername("user-1"); + + User user2 = new User(); + user2.setUsername("user-2"); + + operations.save(user1); + operations.save(user2); + + Person person1 = new Person("Max", "The Mighty"); + person1.setCoworker(user1); + + Person person2 = new Person("Jack", "The Ripper"); + person2.setCoworker(user2); + + operations.save(person1); + operations.save(person2); + + List result = new SpringDataMongodbQuery<>(operations, Person.class).where() + .join(person.coworker, QUser.user).on(QUser.user.username.eq("does-not-exist")).fetch(); + + assertThat(result).isEmpty(); + } + + @Test // DATAMONGO-595, DATAMONGO-1848 + 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.save(person1); + operations.save(person2); + + List result = new SpringDataMongodbQuery<>(operations, Person.class).where() + .anyEmbedded(person.shippingAddresses, QAddress.address).on(QAddress.address.city.eq("London")).fetch(); + + assertThat(result).containsExactly(person2); + } + + @Test(expected = PermissionDeniedDataAccessException.class) // DATAMONGO-1434, DATAMONGO-1848 + public void translatesExceptionsCorrectly() { + + MongoOperations ops = new MongoTemplate(dbFactory) { + + @Override + protected MongoDatabase doGetDatabase() { + throw new MongoException(18, "Authentication Failed"); + } + }; + + MongoRepositoryFactory factory = new MongoRepositoryFactory(ops); + MongoEntityInformation entityInformation = factory.getEntityInformation(Person.class); + repository = new QuerydslMongoPredicateExecutor<>(entityInformation, ops); + + repository.findOne(person.firstname.contains("batman")); + } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/QuerydslRepositorySupportTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/QuerydslRepositorySupportTests.java index acaa188454..e8fd4b7c4f 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/QuerydslRepositorySupportTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/QuerydslRepositorySupportTests.java @@ -26,6 +26,7 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.springframework.beans.DirectFieldAccessor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.annotation.Id; import org.springframework.data.mongodb.core.MongoOperations; @@ -139,6 +140,79 @@ public void shouldConvertStringIdThatIsAValidObjectIdIntoTheSuch() { assertThat(query.fetchOne(), equalTo(outer)); } + @Test // DATAMONGO-1810, DATAMONGO-1848 + public void shouldFetchObjectsViaStringWhenUsingInOnDbRef() { + + User bart = new User(); + DirectFieldAccessor dfa = new DirectFieldAccessor(bart); + dfa.setPropertyValue("id", "bart"); + + bart.setUsername("bart@simpson.com"); + operations.save(bart); + + User lisa = new User(); + dfa = new DirectFieldAccessor(lisa); + dfa.setPropertyValue("id", "lisa"); + + lisa.setUsername("lisa@simposon.com"); + operations.save(lisa); + + person.setCoworker(bart); + operations.save(person); + + QPerson p = QPerson.person; + + SpringDataMongodbQuery queryUsingIdFieldWithinInClause = repoSupport.from(p) + .where(p.coworker.id.in(Arrays.asList(bart.getId(), lisa.getId()))); + + SpringDataMongodbQuery queryUsingRefObject = repoSupport.from(p).where(p.coworker.eq(bart)); + + assertThat(queryUsingIdFieldWithinInClause.fetchOne(), equalTo(person)); + assertThat(queryUsingIdFieldWithinInClause.fetchOne(), equalTo(queryUsingRefObject.fetchOne())); + } + + @Test // DATAMONGO-1810, DATAMONGO-1848 + public void shouldFetchObjectsViaStringStoredAsObjectIdWhenUsingInOnDbRef() { + + User bart = new User(); + bart.setUsername("bart@simpson.com"); + operations.save(bart); + + User lisa = new User(); + lisa.setUsername("lisa@simposon.com"); + operations.save(lisa); + + person.setCoworker(bart); + operations.save(person); + + QPerson p = QPerson.person; + + SpringDataMongodbQuery queryUsingIdFieldWithinInClause = repoSupport.from(p) + .where(p.coworker.id.in(Arrays.asList(bart.getId(), lisa.getId()))); + + SpringDataMongodbQuery queryUsingRefObject = repoSupport.from(p).where(p.coworker.eq(bart)); + + assertThat(queryUsingIdFieldWithinInClause.fetchOne(), equalTo(person)); + assertThat(queryUsingIdFieldWithinInClause.fetchOne(), equalTo(queryUsingRefObject.fetchOne())); + } + + @Test // DATAMONGO-1848, DATAMONGO-2010 + public void shouldConvertStringIdThatIsAValidObjectIdWhenUsedInInPredicateIntoTheSuch() { + + Outer outer = new Outer(); + outer.id = new ObjectId().toHexString(); + outer.inner = new Inner(); + outer.inner.id = new ObjectId().toHexString(); + outer.inner.value = "eat sleep workout repeat"; + + operations.save(outer); + + QQuerydslRepositorySupportTests_Outer o = QQuerydslRepositorySupportTests_Outer.outer; + SpringDataMongodbQuery query = repoSupport.from(o).where(o.inner.id.in(outer.inner.id, outer.inner.id)); + + assertThat(query.fetchOne(), equalTo(outer)); + } + @Data @Document public static class Outer { @@ -152,6 +226,5 @@ public static class Inner { @Id String id; String value; - } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbSerializerUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbSerializerUnitTests.java index 0af4338004..14f51c091e 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbSerializerUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbSerializerUnitTests.java @@ -27,7 +27,6 @@ import org.bson.types.ObjectId; import org.hamcrest.collection.IsIterableContainingInOrder; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -182,16 +181,15 @@ public void takesCustomConversionForEnumsIntoAccount() { assertThat(((Document) mappedPredicate).get("sex"), is("f")); } - @Test // DATAMONGO-1943 - @Ignore("FIXME mp911de") + @Test // DATAMONGO-1848, DATAMONGO-1943 public void shouldRemarshallListsAndDocuments() { - BooleanExpression criteria = QPerson.person.firstname.isNotEmpty() + BooleanExpression criteria = QPerson.person.lastname.isNotEmpty() .and(QPerson.person.firstname.containsIgnoreCase("foo")).not(); assertThat(this.serializer.handle(criteria), - is(equalTo(Document.parse("{ \"$or\" : [ { \"firstname\" : { \"$not\" : { " - + "\"$ne\" : \"\"}}} , { \"firstname\" : { \"$not\" : { \"$regex\" : \".*\\\\Qfoo\\\\E.*\" , \"$options\" : \"i\"}}}]}")))); + is(equalTo(Document.parse("{ \"$or\" : [ { \"lastname\" : { \"$not\" : { " + + "\"$ne\" : \"\"}}} , { \"firstname\" : { \"$not\" : { \"$regex\" : \".*\\\\Qfoo\\\\E.*\" , \"$options\" : \"i\"}}}]}")))); } class Address {