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.

Original Pull Request: #579
This commit is contained in:
Christoph Strobl
2018-07-11 11:02:29 +02:00
parent 7d06f2b040
commit 1e49c95e41
17 changed files with 779 additions and 594 deletions

View File

@@ -22,15 +22,13 @@ import java.util.Set;
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);
}

View File

@@ -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 <Q> concrete subtype
*/
abstract class AbstractMongodbQuery<Q extends AbstractMongodbQuery<Q>> implements SimpleQuery<Q> {
@SuppressWarnings("serial")
static class NoResults extends RuntimeException {}
private final MongodbDocumentSerializer serializer;
private final QueryMixin<Q> 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<Q>(query, new DefaultQueryMetadata(), false);
this.serializer = serializer;
}
/**
* Define a join
*
* @param ref reference
* @param target join target
* @return join builder
*/
public <T> JoinBuilder<Q, T> join(Path<T> ref, Path<T> target) {
return new JoinBuilder<Q, T>(queryMixin, ref, target);
}
/**
* Define a join
*
* @param ref reference
* @param target join target
* @return join builder
*/
public <T> JoinBuilder<Q, T> join(CollectionPathBase<?, T, ?> ref, Path<T> target) {
return new JoinBuilder<Q, T>(queryMixin, ref, target);
}
/**
* Define a constraint for an embedded object
*
* @param collection collection
* @param target target
* @return builder
*/
public <T> AnyEmbeddedBuilder<Q> anyEmbedded(Path<? extends Collection<T>> collection, Path<T> target) {
return new AnyEmbeddedBuilder<Q>(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 <T> Q set(ParamExpression<T> 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<Q> 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();
}
}

View File

@@ -17,80 +17,154 @@ package org.springframework.data.mongodb.repository.support;
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;
/**
* <p>
* Serializes the given Querydsl query to a Document query for MongoDB.
* </p>
* <p>
* Original implementation source {@link com.querydsl.mongodb.MongodbSerializer} by {@literal The Querydsl Team}
* (<a href="http://www.querydsl.com/team">http://www.querydsl.com/team</a>) licensed under the Apache License, Version
* 2.0.
* </p>
* 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<Object, Void> {
public Object handle(Expression<?> expression) {
@Nullable
Object handle(Expression<?> expression) {
return expression.accept(this, null);
}
public Document toSort(List<OrderSpecifier<?>> 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<OrderSpecifier<?>> 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<? extends Enum<?>> expectedExpr = (Constant<? extends Enum<?>>) expr;
return expectedExpr.getConstant().name();
} else {
if (!Enum.class.isAssignableFrom(expr.getType())) {
return expr.getConstant();
}
@SuppressWarnings("unchecked") // Guarded by previous check
Constant<? extends Enum<?>> expectedExpr = (Constant<? extends Enum<?>>) 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 @@ abstract class MongodbDocumentSerializer implements Visitor<Object, Void> {
}
} else if (op == Ops.STRING_IS_EMPTY) {
return asDocument(asDBKey(expr, 0), "");
} else if (op == Ops.AND) {
Map<Object, Object> lhs = (Map<Object, Object>) handle(expr.getArg(0));
Map<Object, Object> rhs = (Map<Object, Object>) handle(expr.getArg(1));
if (Sets.intersection(lhs.keySet(), rhs.keySet()).isEmpty()) {
LinkedHashSet<Entry<Object, Object>> lhs2 = new LinkedHashSet<>(lhs.entrySet());
lhs2.retainAll(rhs.entrySet());
if (lhs2.isEmpty()) {
lhs.putAll(rhs);
return lhs;
} else {
List<Object> list = new ArrayList<Object>(2);
List<Object> list = new ArrayList<>(2);
list.add(handle(expr.getArg(0)));
list.add(handle(expr.getArg(1)));
return asDocument("$and", list);
@@ -133,53 +211,47 @@ abstract class MongodbDocumentSerializer implements Visitor<Object, Void> {
}
} else if (op == Ops.OR) {
List<Object> list = new ArrayList<Object>(2);
List<Object> 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 @@ abstract class MongodbDocumentSerializer implements Visitor<Object, Void> {
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<? extends Collection<?>>) 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<Object> list = new ArrayList<Object>(2);
List<Object> list = new ArrayList<>(2);
list.add(asDocument(asDBKey(expr, 0), new ArrayList<Object>()));
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<Object> list = new ArrayList<Object>();
List<Object> list = new ArrayList<>();
for (Map.Entry<String, Object> entry : arg.entrySet()) {
if (entry.getKey().equals("$or")) {
list.add(asDocument("$nor", entry.getValue()));
} else if (entry.getKey().equals("$and")) {
List<Object> list2 = new ArrayList<Object>();
List<Object> 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 @@ abstract class MongodbDocumentSerializer implements Visitor<Object, Void> {
}
private Object negate(String key, Document value) {
if (value.size() == 1) {
return asDocument(key, asDocument("$not", value));
} else {
List<Object> list2 = new ArrayList<Object>();
List<Object> list2 = new ArrayList<>();
for (Map.Entry<String, Object> 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 @@ abstract class MongodbDocumentSerializer implements Visitor<Object, Void> {
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 @@ abstract class MongodbDocumentSerializer implements Visitor<Object, Void> {
@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 @@ abstract class MongodbDocumentSerializer implements Visitor<Object, Void> {
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;

View File

@@ -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<Double[]> 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<Double[]> expr, double latVal, double longVal) {
return Expressions.booleanOperation(MongodbOps.NEAR_SPHERE, expr,
ConstantImpl.create(Arrays.asList(latVal, longVal)));
}
}

View File

@@ -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.
* <p>
* Original implementation source {@link com.querydsl.mongodb.AbstractMongodbQuery} by {@literal The Querydsl Team}
* (<a href="http://www.querydsl.com/team">http://www.querydsl.com/team</a>) licensed under the Apache License, Version
* 2.0.
* </p>
* Modified for usage with {@link MongodbDocumentSerializer}.
*
* @param <Q> concrete subtype
* @author laimw
* @author Mark Paluch
* @author Christoph Strobl
* @since 2.1
*/
public abstract class QuerydslAbstractMongodbQuery<K, Q extends QuerydslAbstractMongodbQuery<K, Q>>
implements SimpleQuery<Q> {
private final MongodbDocumentSerializer serializer;
private final QueryMixin<Q> 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 <T> Q set(ParamExpression<T> 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<OrderSpecifier<?>> orderSpecifiers) {
return serializer.toSort(orderSpecifiers);
}
/**
* Get the actual {@link QueryMixin} delegate.
*
* @return
*/
QueryMixin<Q> 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();
}
}

View File

@@ -24,23 +24,40 @@ import com.querydsl.core.types.Predicate;
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.
* <p>
* Original implementation source {@link com.querydsl.mongodb.AnyEmbeddedBuilder} by {@literal The Querydsl Team}
* (<a href="http://www.querydsl.com/team">http://www.querydsl.com/team</a>) licensed under the Apache License, Version
* 2.0.
* </p>
* Modified for usage with {@link QuerydslAbstractMongodbQuery}.
*
* @param <Q> query type
* @author tiwe
* @author Mark Paluch
* @author Christoph Strobl
* @since 2.1
*/
class AnyEmbeddedBuilder<Q extends AbstractMongodbQuery<Q>> {
public class QuerydslAnyEmbeddedBuilder<Q extends QuerydslAbstractMongodbQuery<K, Q>, K> {
private final QueryMixin<Q> queryMixin;
private final Path<? extends Collection<?>> collection;
public AnyEmbeddedBuilder(QueryMixin<Q> queryMixin, Path<? extends Collection<?>> collection) {
QuerydslAnyEmbeddedBuilder(QueryMixin<Q> queryMixin, Path<? extends Collection<?>> 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)));
}

View File

@@ -19,14 +19,13 @@ import java.util.Collection;
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.Operation;
import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.core.types.Path;
import com.querydsl.core.types.Predicate;
import com.querydsl.core.types.dsl.CollectionPathBase;
/**
* {@link Fetchable} Mongodb query with a pluggable Document to Bean transformation.
* {@link Fetchable} MongoDB query with utilizing {@link MongoOperations} for command execution.
*
* @param <K> result type
* @param <Q> concrete subtype
* @author Mark Paluch
* @author Christoph Strobl
* @since 2.1
*/
abstract class FetchableMongodbQuery<K, Q extends FetchableMongodbQuery<K, Q>> extends AbstractMongodbQuery<Q>
implements Fetchable<K> {
abstract class QuerydslFetchableMongodbQuery<K, Q extends QuerydslFetchableMongodbQuery<K, Q>>
extends QuerydslAbstractMongodbQuery<K, Q> implements Fetchable<K> {
private final Class<K> entityClass;
private final String collection;
private final MongoOperations mongoOperations;
private final FindWithProjection<K> find;
public FetchableMongodbQuery(MongodbDocumentSerializer serializer, Class<? extends K> entityClass,
MongoOperations mongoOperations) {
super(serializer);
this.entityClass = (Class<K>) entityClass;
this.collection = mongoOperations.getCollectionName(entityClass);
this.mongoOperations = mongoOperations;
}
public FetchableMongodbQuery(MongodbDocumentSerializer serializer, Class<? extends K> entityClass, String collection,
QuerydslFetchableMongodbQuery(MongodbDocumentSerializer serializer, Class<? extends K> entityClass, String collection,
MongoOperations mongoOperations) {
super(serializer);
@@ -72,19 +65,13 @@ abstract class FetchableMongodbQuery<K, Q extends FetchableMongodbQuery<K, Q>> e
this.entityClass = (Class<K>) 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<K> iterate(Path<?>... paths) {
getQueryMixin().setProjection(paths);
return iterate();
}
@Override
public CloseableIterator<K> iterate() {
@@ -92,6 +79,7 @@ abstract class FetchableMongodbQuery<K, Q extends FetchableMongodbQuery<K, Q>> e
entityClass, collection);
return new CloseableIterator<K>() {
@Override
public boolean hasNext() {
return stream.hasNext();
@@ -104,7 +92,7 @@ abstract class FetchableMongodbQuery<K, Q extends FetchableMongodbQuery<K, Q>> e
@Override
public void remove() {
throw new UnsupportedOperationException("Cannot remove from iterator while streaming data.");
}
@Override
@@ -114,91 +102,99 @@ abstract class FetchableMongodbQuery<K, Q extends FetchableMongodbQuery<K, Q>> e
};
}
/**
* Fetch with the specific fields
*
* @param paths fields to return
* @return results
/*
* (non-Javadoc)
* @see com.querydsl.core.Fetchable#fetch()
*/
public List<K> fetch(Path<?>... paths) {
getQueryMixin().setProjection(paths);
return fetch();
}
@Override
public List<K> 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 mongoOperations.query(entityClass).matching(createQuery()).oneValue();
return find.matching(createQuery()).oneValue();
}
/*
* (non-Javadoc)
* @see com.querydsl.core.Fetchable#fetchResults()
*/
@Override
public QueryResults<K> 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<K> fetchResults(Path<?>... paths) {
getQueryMixin().setProjection(paths);
return fetchResults();
public <T> QuerydslJoinBuilder<Q, K, T> join(Path<T> ref, Path<T> target) {
return new QuerydslJoinBuilder<>(getQueryMixin(), ref, target);
}
@Override
public QueryResults<K> 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 <T> QuerydslJoinBuilder<Q, K, T> join(CollectionPathBase<?, T, ?> ref, Path<T> 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 <T> QuerydslAnyEmbeddedBuilder<Q, K> anyEmbedded(Path<? extends Collection<T>> collection, Path<T> 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<OrderSpecifier<?>> orderBy) {
protected org.springframework.data.mongodb.core.query.Query createQuery(@Nullable Predicate filter,
@Nullable Expression<?> projection, QueryModifiers modifiers, List<OrderSpecifier<?>> 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 @@ abstract class FetchableMongodbQuery<K, Q extends FetchableMongodbQuery<K, Q>> e
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 @@ abstract class FetchableMongodbQuery<K, Q extends FetchableMongodbQuery<K, Q>> e
@SuppressWarnings("unchecked")
@Nullable
protected Predicate createJoinFilter(QueryMetadata metadata) {
Multimap<Expression<?>, Predicate> predicates = HashMultimap.create();
LinkedMultiValueMap<Expression<?>, Predicate> predicates = new LinkedMultiValueMap<>();
List<JoinExpression> 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<Predicate> extraFilters = predicates.get(target.getRoot());
Predicate filter = ExpressionUtils.allOf(join.getCondition(), allOf(extraFilters));
List<? extends Object> 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<Object>) path, ids));
predicates.add(source.getRoot(), ExpressionUtils.in((Path<Object>) path, ids));
}
Path<?> source = (Path) ((Operation) joins.get(0).getTarget()).getArg(0);
return allOf(predicates.get(source.getRoot()));
}
@@ -252,10 +257,16 @@ abstract class FetchableMongodbQuery<K, Q extends FetchableMongodbQuery<K, Q>> e
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<Object> 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);
}
}

View File

@@ -22,28 +22,44 @@ import com.querydsl.core.types.Path;
import com.querydsl.core.types.Predicate;
/**
* {@code JoinBuilder} is a builder for join constraints
* {@code QuerydslJoinBuilder} is a builder for join constraints.
* <p>
* Original implementation source {@link com.querydsl.mongodb.JoinBuilder} by {@literal The Querydsl Team}
* (<a href="http://www.querydsl.com/team">http://www.querydsl.com/team</a>) licensed under the Apache License, Version
* 2.0.
* </p>
* Modified for usage with {@link QuerydslAbstractMongodbQuery}.
*
* @author Mark Paluch
* @param <Q>
* @param <T>
* @author tiwe
* @author Mark Paluch
* @author Christoph Strobl
* @since 2.1
*/
class JoinBuilder<Q extends AbstractMongodbQuery<Q>, T> {
public class QuerydslJoinBuilder<Q extends QuerydslAbstractMongodbQuery<K, Q>, K, T> {
private final QueryMixin<Q> queryMixin;
private final Path<?> ref;
private final Path<T> target;
public JoinBuilder(QueryMixin<Q> queryMixin, Path<?> ref, Path<T> target) {
QuerydslJoinBuilder(QueryMixin<Q> queryMixin, Path<?> ref, Path<T> 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();

View File

@@ -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<K> extends Fetchable<K>, SimpleQuery<SimpleFetchableQuery<K>> {}
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;
}
}

View File

@@ -165,7 +165,7 @@ public class QuerydslMongoPredicateExecutor<T> implements QuerydslPredicateExecu
Assert.notNull(predicate, "Predicate must not be null!");
Assert.notNull(pageable, "Pageable must not be null!");
SimpleFetchableQuery<T> query = createQueryFor(predicate);
SpringDataMongodbQuery<T> query = createQueryFor(predicate);
return PageableExecutionUtils.getPage(applyPagination(query, pageable).fetch(), pageable, query::fetchCount);
}
@@ -195,45 +195,45 @@ public class QuerydslMongoPredicateExecutor<T> implements QuerydslPredicateExecu
}
/**
* Creates a {@link AbstractMongodbQuery} for the given {@link Predicate}.
* Creates a {@link SpringDataMongodbQuery} for the given {@link Predicate}.
*
* @param predicate
* @return
*/
private SimpleFetchableQuery<T> createQueryFor(Predicate predicate) {
private SpringDataMongodbQuery<T> createQueryFor(Predicate predicate) {
return createQuery().where(predicate);
}
/**
* Creates a {@link AbstractMongodbQuery}.
* Creates a {@link SpringDataMongodbQuery}.
*
* @return
*/
private SimpleFetchableQuery<T> createQuery() {
private SpringDataMongodbQuery<T> 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<T> applyPagination(SimpleFetchableQuery<T> query, Pageable pageable) {
private SpringDataMongodbQuery<T> applyPagination(SpringDataMongodbQuery<T> 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<T> applySorting(SimpleFetchableQuery<T> query, Sort sort) {
private SpringDataMongodbQuery<T> applySorting(SpringDataMongodbQuery<T> query, Sort sort) {
// TODO: find better solution than instanceof check
if (sort instanceof QSort) {

View File

@@ -47,7 +47,7 @@ public abstract class QuerydslRepositorySupport {
}
/**
* 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 @@ public abstract class QuerydslRepositorySupport {
}
/**
* 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}

View File

@@ -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<T> implements SimpleFetchableQuery<T> {
private final OperationsMongodbQuery<T> query;
public class SpringDataMongodbQuery<T> extends QuerydslFetchableMongodbQuery<T, SpringDataMongodbQuery<T>> {
/**
* Creates a new {@link SpringDataMongodbQuery}.
@@ -58,144 +47,6 @@ public class SpringDataMongodbQuery<T> implements SimpleFetchableQuery<T> {
public SpringDataMongodbQuery(final MongoOperations operations, final Class<? extends T> type,
String collectionName) {
query = new OperationsMongodbQuery<>(new SpringDataMongodbSerializer(operations.getConverter()), type,
collectionName, operations);
}
/*
* (non-Javadoc)
* @see com.querydsl.core.Fetchable#fetch()
*/
@Override
public List<T> 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<T> iterate() {
return query.iterate();
}
/*
* (non-Javadoc)
* @see com.querydsl.core.Fetchable#fetchResults()
*/
@Override
public QueryResults<T> 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<T> limit(long limit) {
query.limit(limit);
return this;
}
/*
* (non-Javadoc)
* @see com.querydsl.core.SimpleQuery#offset(long)
*/
@Override
public SpringDataMongodbQuery<T> offset(long offset) {
query.offset(offset);
return this;
}
/*
* (non-Javadoc)
* @see com.querydsl.core.SimpleQuery#restrict(com.querydsl.core.QueryModifiers)
*/
@Override
public SpringDataMongodbQuery<T> restrict(QueryModifiers modifiers) {
query.restrict(modifiers);
return this;
}
/*
* (non-Javadoc)
* @see com.querydsl.core.SimpleQuery#orderBy(com.querydsl.core.types.OrderSpecifier[])
*/
@Override
public SpringDataMongodbQuery<T> 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 <V> SpringDataMongodbQuery<T> set(ParamExpression<V> param, V value) {
query.set(param, value);
return this;
}
/*
* (non-Javadoc)
* @see com.querydsl.core.SimpleQuery#distinct()
*/
@Override
public SpringDataMongodbQuery<T> distinct() {
query.distinct();
return this;
}
/*
* (non-Javadoc)
* @see com.querydsl.core.FilteredClause#where(com.querydsl.core.types.Predicate[])
*/
@Override
public SpringDataMongodbQuery<T> where(Predicate... o) {
query.where(o);
return this;
}
/**
* Concrete implementation of {@link FetchableMongodbQuery}.
*
* @param <T>
*/
static class OperationsMongodbQuery<T> extends FetchableMongodbQuery<T, OperationsMongodbQuery<T>> {
public OperationsMongodbQuery(MongodbDocumentSerializer serializer, Class<? extends T> entityClass,
String collection, MongoOperations mongoOperations) {
super(serializer, entityClass, collection, mongoOperations);
}
super(new SpringDataMongodbSerializer(operations.getConverter()), type, collectionName, operations);
}
}

View File

@@ -54,7 +54,7 @@ class SpringDataMongodbSerializer extends MongodbDocumentSerializer {
static {
Set<PathType> pathTypes = new HashSet<PathType>();
Set<PathType> 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 @@ class SpringDataMongodbSerializer extends MongodbDocumentSerializer {
/*
* (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 @@ class SpringDataMongodbSerializer extends MongodbDocumentSerializer {
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 @@ class SpringDataMongodbSerializer extends MongodbDocumentSerializer {
: asReference(constant.getConstant(), path);
}
@Nullable
private MongoPersistentProperty getPropertyFor(Path<?> path) {
Path<?> parent = path.getMetadata().getParent();
@@ -217,7 +217,7 @@ class SpringDataMongodbSerializer extends MongodbDocumentSerializer {
}
MongoPersistentEntity<?> entity = mappingContext.getPersistentEntity(parent.getType());
return entity != null ? entity.getRequiredPersistentProperty(path.getMetadata().getName()) : null;
return entity != null ? entity.getPersistentProperty(path.getMetadata().getName()) : null;
}
/**

View File

@@ -40,6 +40,11 @@ import org.springframework.data.mongodb.core.MongoTemplate;
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 class ApplicationContextEventTests {
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<Person, String> 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());

View File

@@ -18,6 +18,7 @@ package org.springframework.data.mongodb.repository.support;
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.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.dao.PermissionDeniedDataAccessException;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.mongodb.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 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
public class QuerydslMongoPredicateExecutorIntegrationTests {
@Autowired MongoOperations operations;
@Autowired MongoDbFactory dbFactory;
QuerydslMongoPredicateExecutor<Person> repository;
Person dave, oliver, carter;
@@ -57,7 +70,7 @@ public class QuerydslMongoPredicateExecutorIntegrationTests {
MongoRepositoryFactory factory = new MongoRepositoryFactory(operations);
MongoEntityInformation<Person, String> entityInformation = factory.getEntityInformation(Person.class);
repository = new QuerydslMongoPredicateExecutor<Person>(entityInformation, operations);
repository = new QuerydslMongoPredicateExecutor<>(entityInformation, operations);
operations.dropCollection(Person.class);
@@ -99,4 +112,114 @@ public class QuerydslMongoPredicateExecutorIntegrationTests {
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<Person> 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<Person> 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<Person> 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<Person, String> entityInformation = factory.getEntityInformation(Person.class);
repository = new QuerydslMongoPredicateExecutor<>(entityInformation, ops);
repository.findOne(person.firstname.contains("batman"));
}
}

View File

@@ -26,6 +26,7 @@ import org.bson.types.ObjectId;
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 class QuerydslRepositorySupportTests {
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<Person> queryUsingIdFieldWithinInClause = repoSupport.from(p)
.where(p.coworker.id.in(Arrays.asList(bart.getId(), lisa.getId())));
SpringDataMongodbQuery<Person> 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<Person> queryUsingIdFieldWithinInClause = repoSupport.from(p)
.where(p.coworker.id.in(Arrays.asList(bart.getId(), lisa.getId())));
SpringDataMongodbQuery<Person> 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<Outer> 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 class QuerydslRepositorySupportTests {
@Id String id;
String value;
}
}

View File

@@ -27,7 +27,6 @@ import org.bson.Document;
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 class SpringDataMongodbSerializerUnitTests {
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 {