Support $expr via criteria query.
This commit introduces AggregationExpressionCriteria to be used along with Query to run an $expr operator within the find query.
query(whereExpr(valueOf("spent").greaterThan("budget")))
Closes: #2750
Original pull request: #4316.
This commit is contained in:
committed by
Mark Paluch
parent
e3ef84a56c
commit
a416441427
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.mongodb.core.aggregation;
|
||||
|
||||
import org.bson.Document;
|
||||
import org.springframework.data.mongodb.core.aggregation.EvaluationOperators.Expr;
|
||||
import org.springframework.data.mongodb.core.query.CriteriaDefinition;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* A {@link CriteriaDefinition criteria} to use {@code $expr} within a
|
||||
* {@link org.springframework.data.mongodb.core.query.Query}.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 4.1
|
||||
*/
|
||||
public class AggregationExpressionCriteria implements CriteriaDefinition {
|
||||
|
||||
private final AggregationExpression expression;
|
||||
|
||||
AggregationExpressionCriteria(AggregationExpression expression) {
|
||||
this.expression = expression;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param expression must not be {@literal null}.
|
||||
* @return new instance of {@link AggregationExpressionCriteria}.
|
||||
*/
|
||||
public static AggregationExpressionCriteria whereExpr(AggregationExpression expression) {
|
||||
return new AggregationExpressionCriteria(expression);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Document getCriteriaObject() {
|
||||
|
||||
if (expression instanceof Expr expr) {
|
||||
return new Document(getKey(), expr.get(0));
|
||||
}
|
||||
return new Document(getKey(), expression);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public String getKey() {
|
||||
return "$expr";
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,9 @@ import org.springframework.data.mapping.PropertyReferenceException;
|
||||
import org.springframework.data.mapping.context.InvalidPersistentPropertyPath;
|
||||
import org.springframework.data.mapping.context.MappingContext;
|
||||
import org.springframework.data.mongodb.MongoExpression;
|
||||
import org.springframework.data.mongodb.core.aggregation.Aggregation;
|
||||
import org.springframework.data.mongodb.core.aggregation.AggregationExpression;
|
||||
import org.springframework.data.mongodb.core.aggregation.RelaxedTypeBasedAggregationOperationContext;
|
||||
import org.springframework.data.mongodb.core.convert.MappingMongoConverter.NestedDocument;
|
||||
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
|
||||
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
|
||||
@@ -560,6 +563,13 @@ public class QueryMapper {
|
||||
return exampleMapper.getMappedExample((Example<?>) source, entity);
|
||||
}
|
||||
|
||||
if(source instanceof MongoExpression exr) {
|
||||
if(source instanceof AggregationExpression age) {
|
||||
return age.toDocument(new RelaxedTypeBasedAggregationOperationContext(entity.getType(), this.mappingContext, this));
|
||||
}
|
||||
return exr.toDocument();
|
||||
}
|
||||
|
||||
if (source instanceof List) {
|
||||
return delegateConvertToMongoType(source, entity);
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ import org.springframework.data.geo.Circle;
|
||||
import org.springframework.data.geo.Point;
|
||||
import org.springframework.data.geo.Shape;
|
||||
import org.springframework.data.mongodb.InvalidMongoDbApiUsageException;
|
||||
import org.springframework.data.mongodb.MongoExpression;
|
||||
import org.springframework.data.mongodb.core.geo.GeoJson;
|
||||
import org.springframework.data.mongodb.core.geo.Sphere;
|
||||
import org.springframework.data.mongodb.core.schema.JsonSchemaObject.Type;
|
||||
@@ -147,6 +148,37 @@ public class Criteria implements CriteriaDefinition {
|
||||
return new Criteria().andDocumentStructureMatches(schema);
|
||||
}
|
||||
|
||||
/**
|
||||
* Static factory method to create a {@link Criteria} matching a documents against the given {@link MongoExpression
|
||||
* expression}.
|
||||
* <p>
|
||||
* The {@link MongoExpression expression} can be either something that directly renders to the store native
|
||||
* representation like
|
||||
*
|
||||
* <pre class="code">
|
||||
* expr(() -> Document.parse("{ $gt : [ '$spent', '$budget'] }")))
|
||||
* </pre>
|
||||
*
|
||||
* or an {@link org.springframework.data.mongodb.core.aggregation.AggregationExpression} which will be subject to
|
||||
* context (domain type) specific field mapping.
|
||||
*
|
||||
* <pre class="code">
|
||||
* expr(valueOf("amountSpent").greaterThan("budget"))
|
||||
* </pre>
|
||||
*
|
||||
* @param expression must not be {@literal null}.
|
||||
* @return new instance of {@link Criteria}.
|
||||
* @since 4.1
|
||||
*/
|
||||
public static Criteria expr(MongoExpression expression) {
|
||||
|
||||
Assert.notNull(expression, "Expression must not be null");
|
||||
|
||||
Criteria criteria = new Criteria();
|
||||
criteria.criteria.put("$expr", expression);
|
||||
return criteria;
|
||||
}
|
||||
|
||||
/**
|
||||
* Static factory method to create a Criteria using the provided key
|
||||
*
|
||||
|
||||
@@ -68,6 +68,7 @@ import org.springframework.data.mapping.context.PersistentEntities;
|
||||
import org.springframework.data.mongodb.InvalidMongoDbApiUsageException;
|
||||
import org.springframework.data.mongodb.MongoDatabaseFactory;
|
||||
import org.springframework.data.mongodb.core.BulkOperations.BulkMode;
|
||||
import org.springframework.data.mongodb.core.aggregation.StringOperators;
|
||||
import org.springframework.data.mongodb.core.convert.LazyLoadingProxy;
|
||||
import org.springframework.data.mongodb.core.geo.GeoJsonPoint;
|
||||
import org.springframework.data.mongodb.core.index.Index;
|
||||
@@ -3837,6 +3838,23 @@ public class MongoTemplateTests {
|
||||
assertThat(target.values).containsExactly("spring");
|
||||
}
|
||||
|
||||
@Test // GH-2750
|
||||
void shouldExecuteQueryWithExpression() {
|
||||
|
||||
TypeWithFieldAnnotation source1 = new TypeWithFieldAnnotation();
|
||||
source1.emailAddress = "spring.data@pivotal.com";
|
||||
|
||||
TypeWithFieldAnnotation source2 = new TypeWithFieldAnnotation();
|
||||
source2.emailAddress = "spring.data@vmware.com";
|
||||
|
||||
template.insertAll(List.of(source1, source2));
|
||||
|
||||
TypeWithFieldAnnotation loaded = template.query(TypeWithFieldAnnotation.class)
|
||||
.matching(expr(StringOperators.valueOf("emailAddress").regexFind(".*@vmware.com$", "i"))).firstValue();
|
||||
|
||||
assertThat(loaded).isEqualTo(source2);
|
||||
}
|
||||
|
||||
private AtomicReference<ImmutableVersioned> createAfterSaveReference() {
|
||||
|
||||
AtomicReference<ImmutableVersioned> saved = new AtomicReference<>();
|
||||
@@ -4158,6 +4176,7 @@ public class MongoTemplateTests {
|
||||
@Field(write = Field.Write.ALWAYS) String lastname;
|
||||
}
|
||||
|
||||
@EqualsAndHashCode
|
||||
static class TypeWithFieldAnnotation {
|
||||
|
||||
@Id ObjectId id;
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
package org.springframework.data.mongodb.core.convert;
|
||||
|
||||
import static org.springframework.data.mongodb.core.DocumentTestUtils.*;
|
||||
import static org.springframework.data.mongodb.core.aggregation.AggregationExpressionCriteria.*;
|
||||
import static org.springframework.data.mongodb.core.query.Criteria.*;
|
||||
import static org.springframework.data.mongodb.core.query.Query.*;
|
||||
import static org.springframework.data.mongodb.test.util.Assertions.*;
|
||||
@@ -43,8 +44,10 @@ import org.springframework.data.domain.Sort.Direction;
|
||||
import org.springframework.data.geo.Point;
|
||||
import org.springframework.data.mongodb.core.DocumentTestUtils;
|
||||
import org.springframework.data.mongodb.core.Person;
|
||||
import org.springframework.data.mongodb.core.aggregation.ComparisonOperators;
|
||||
import org.springframework.data.mongodb.core.aggregation.ConditionalOperators;
|
||||
import org.springframework.data.mongodb.core.aggregation.EvaluationOperators;
|
||||
import org.springframework.data.mongodb.core.aggregation.EvaluationOperators.Expr;
|
||||
import org.springframework.data.mongodb.core.aggregation.TypeBasedAggregationOperationContext;
|
||||
import org.springframework.data.mongodb.core.geo.GeoJsonPoint;
|
||||
import org.springframework.data.mongodb.core.geo.GeoJsonPolygon;
|
||||
@@ -1461,6 +1464,51 @@ public class QueryMapperUnitTests {
|
||||
assertThat(mappedObject).isEqualTo(new org.bson.Document("text", "eulav"));
|
||||
}
|
||||
|
||||
@Test // GH-2750
|
||||
void mapsAggregationExpression() {
|
||||
|
||||
Query query = query(whereExpr(ComparisonOperators.valueOf("field").greaterThan("budget")));
|
||||
org.bson.Document mappedObject = mapper.getMappedObject(query.getQueryObject(),
|
||||
context.getPersistentEntity(CustomizedField.class));
|
||||
assertThat(mappedObject).isEqualTo("{ $expr : { $gt : [ '$foo', '$budget'] } }");
|
||||
}
|
||||
|
||||
@Test // GH-2750
|
||||
void unwrapsAggregationExpressionExprObjectWrappedInExpressionCriteria() {
|
||||
|
||||
Query query = query(whereExpr(Expr.valueOf(ComparisonOperators.valueOf("field").greaterThan("budget"))));
|
||||
org.bson.Document mappedObject = mapper.getMappedObject(query.getQueryObject(),
|
||||
context.getPersistentEntity(CustomizedField.class));
|
||||
assertThat(mappedObject).isEqualTo("{ $expr : { $gt : [ '$foo', '$budget'] } }");
|
||||
}
|
||||
|
||||
@Test // GH-2750
|
||||
void mapsMongoExpressionToFieldsIfItsAnAggregationExpression() {
|
||||
|
||||
Query query = query(expr(ComparisonOperators.valueOf("field").greaterThan("budget")));
|
||||
org.bson.Document mappedObject = mapper.getMappedObject(query.getQueryObject(),
|
||||
context.getPersistentEntity(CustomizedField.class));
|
||||
assertThat(mappedObject).isEqualTo("{ $expr : { $gt : [ '$foo', '$budget'] } }");
|
||||
}
|
||||
|
||||
@Test // GH-2750
|
||||
void usageOfMongoExpressionOnCriteriaDoesNotUnwrapAnExprAggregationExpression() {
|
||||
|
||||
Query query = query(expr(Expr.valueOf(ComparisonOperators.valueOf("field").greaterThan("budget"))));
|
||||
org.bson.Document mappedObject = mapper.getMappedObject(query.getQueryObject(),
|
||||
context.getPersistentEntity(CustomizedField.class));
|
||||
assertThat(mappedObject).isEqualTo("{ $expr : { $expr : { $gt : [ '$foo', '$budget'] } } }");
|
||||
}
|
||||
|
||||
@Test // GH-2750
|
||||
void usesMongoExpressionDocumentAsIsIfItIsNotAnAggregationExpression() {
|
||||
|
||||
Query query = query(expr(() -> org.bson.Document.parse("{ $gt : [ '$field', '$budget'] }")));
|
||||
org.bson.Document mappedObject = mapper.getMappedObject(query.getQueryObject(),
|
||||
context.getPersistentEntity(CustomizedField.class));
|
||||
assertThat(mappedObject).isEqualTo("{ $expr : { $gt : [ '$field', '$budget'] } }");
|
||||
}
|
||||
|
||||
class WithDeepArrayNesting {
|
||||
|
||||
List<WithNestedArray> level0;
|
||||
|
||||
Reference in New Issue
Block a user