Skip to content

Commit a416441

Browse files
christophstroblmp911de
authored andcommitted
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.
1 parent e3ef84a commit a416441

File tree

5 files changed

+169
-0
lines changed

5 files changed

+169
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Copyright 2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.mongodb.core.aggregation;
17+
18+
import org.bson.Document;
19+
import org.springframework.data.mongodb.core.aggregation.EvaluationOperators.Expr;
20+
import org.springframework.data.mongodb.core.query.CriteriaDefinition;
21+
import org.springframework.lang.Nullable;
22+
23+
/**
24+
* A {@link CriteriaDefinition criteria} to use {@code $expr} within a
25+
* {@link org.springframework.data.mongodb.core.query.Query}.
26+
*
27+
* @author Christoph Strobl
28+
* @since 4.1
29+
*/
30+
public class AggregationExpressionCriteria implements CriteriaDefinition {
31+
32+
private final AggregationExpression expression;
33+
34+
AggregationExpressionCriteria(AggregationExpression expression) {
35+
this.expression = expression;
36+
}
37+
38+
/**
39+
* @param expression must not be {@literal null}.
40+
* @return new instance of {@link AggregationExpressionCriteria}.
41+
*/
42+
public static AggregationExpressionCriteria whereExpr(AggregationExpression expression) {
43+
return new AggregationExpressionCriteria(expression);
44+
}
45+
46+
@Override
47+
public Document getCriteriaObject() {
48+
49+
if (expression instanceof Expr expr) {
50+
return new Document(getKey(), expr.get(0));
51+
}
52+
return new Document(getKey(), expression);
53+
}
54+
55+
@Nullable
56+
@Override
57+
public String getKey() {
58+
return "$expr";
59+
}
60+
}

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java

+10
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@
4141
import org.springframework.data.mapping.context.InvalidPersistentPropertyPath;
4242
import org.springframework.data.mapping.context.MappingContext;
4343
import org.springframework.data.mongodb.MongoExpression;
44+
import org.springframework.data.mongodb.core.aggregation.Aggregation;
45+
import org.springframework.data.mongodb.core.aggregation.AggregationExpression;
46+
import org.springframework.data.mongodb.core.aggregation.RelaxedTypeBasedAggregationOperationContext;
4447
import org.springframework.data.mongodb.core.convert.MappingMongoConverter.NestedDocument;
4548
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
4649
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
@@ -560,6 +563,13 @@ protected Object convertSimpleOrDocument(Object source, @Nullable MongoPersisten
560563
return exampleMapper.getMappedExample((Example<?>) source, entity);
561564
}
562565

566+
if(source instanceof MongoExpression exr) {
567+
if(source instanceof AggregationExpression age) {
568+
return age.toDocument(new RelaxedTypeBasedAggregationOperationContext(entity.getType(), this.mappingContext, this));
569+
}
570+
return exr.toDocument();
571+
}
572+
563573
if (source instanceof List) {
564574
return delegateConvertToMongoType(source, entity);
565575
}

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Criteria.java

+32
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import org.springframework.data.geo.Point;
3838
import org.springframework.data.geo.Shape;
3939
import org.springframework.data.mongodb.InvalidMongoDbApiUsageException;
40+
import org.springframework.data.mongodb.MongoExpression;
4041
import org.springframework.data.mongodb.core.geo.GeoJson;
4142
import org.springframework.data.mongodb.core.geo.Sphere;
4243
import org.springframework.data.mongodb.core.schema.JsonSchemaObject.Type;
@@ -147,6 +148,37 @@ public static Criteria matchingDocumentStructure(MongoJsonSchema schema) {
147148
return new Criteria().andDocumentStructureMatches(schema);
148149
}
149150

151+
/**
152+
* Static factory method to create a {@link Criteria} matching a documents against the given {@link MongoExpression
153+
* expression}.
154+
* <p>
155+
* The {@link MongoExpression expression} can be either something that directly renders to the store native
156+
* representation like
157+
*
158+
* <pre class="code">
159+
* expr(() -> Document.parse("{ $gt : [ '$spent', '$budget'] }")))
160+
* </pre>
161+
*
162+
* or an {@link org.springframework.data.mongodb.core.aggregation.AggregationExpression} which will be subject to
163+
* context (domain type) specific field mapping.
164+
*
165+
* <pre class="code">
166+
* expr(valueOf("amountSpent").greaterThan("budget"))
167+
* </pre>
168+
*
169+
* @param expression must not be {@literal null}.
170+
* @return new instance of {@link Criteria}.
171+
* @since 4.1
172+
*/
173+
public static Criteria expr(MongoExpression expression) {
174+
175+
Assert.notNull(expression, "Expression must not be null");
176+
177+
Criteria criteria = new Criteria();
178+
criteria.criteria.put("$expr", expression);
179+
return criteria;
180+
}
181+
150182
/**
151183
* Static factory method to create a Criteria using the provided key
152184
*

spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java

+19
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
import org.springframework.data.mongodb.InvalidMongoDbApiUsageException;
6969
import org.springframework.data.mongodb.MongoDatabaseFactory;
7070
import org.springframework.data.mongodb.core.BulkOperations.BulkMode;
71+
import org.springframework.data.mongodb.core.aggregation.StringOperators;
7172
import org.springframework.data.mongodb.core.convert.LazyLoadingProxy;
7273
import org.springframework.data.mongodb.core.geo.GeoJsonPoint;
7374
import org.springframework.data.mongodb.core.index.Index;
@@ -3837,6 +3838,23 @@ public void sliceShouldLimitCollectionValues() {
38373838
assertThat(target.values).containsExactly("spring");
38383839
}
38393840

3841+
@Test // GH-2750
3842+
void shouldExecuteQueryWithExpression() {
3843+
3844+
TypeWithFieldAnnotation source1 = new TypeWithFieldAnnotation();
3845+
source1.emailAddress = "[email protected]";
3846+
3847+
TypeWithFieldAnnotation source2 = new TypeWithFieldAnnotation();
3848+
source2.emailAddress = "[email protected]";
3849+
3850+
template.insertAll(List.of(source1, source2));
3851+
3852+
TypeWithFieldAnnotation loaded = template.query(TypeWithFieldAnnotation.class)
3853+
.matching(expr(StringOperators.valueOf("emailAddress").regexFind(".*@vmware.com$", "i"))).firstValue();
3854+
3855+
assertThat(loaded).isEqualTo(source2);
3856+
}
3857+
38403858
private AtomicReference<ImmutableVersioned> createAfterSaveReference() {
38413859

38423860
AtomicReference<ImmutableVersioned> saved = new AtomicReference<>();
@@ -4158,6 +4176,7 @@ static class VersionedPerson {
41584176
@Field(write = Field.Write.ALWAYS) String lastname;
41594177
}
41604178

4179+
@EqualsAndHashCode
41614180
static class TypeWithFieldAnnotation {
41624181

41634182
@Id ObjectId id;

spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java

+48
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package org.springframework.data.mongodb.core.convert;
1717

1818
import static org.springframework.data.mongodb.core.DocumentTestUtils.*;
19+
import static org.springframework.data.mongodb.core.aggregation.AggregationExpressionCriteria.*;
1920
import static org.springframework.data.mongodb.core.query.Criteria.*;
2021
import static org.springframework.data.mongodb.core.query.Query.*;
2122
import static org.springframework.data.mongodb.test.util.Assertions.*;
@@ -43,8 +44,10 @@
4344
import org.springframework.data.geo.Point;
4445
import org.springframework.data.mongodb.core.DocumentTestUtils;
4546
import org.springframework.data.mongodb.core.Person;
47+
import org.springframework.data.mongodb.core.aggregation.ComparisonOperators;
4648
import org.springframework.data.mongodb.core.aggregation.ConditionalOperators;
4749
import org.springframework.data.mongodb.core.aggregation.EvaluationOperators;
50+
import org.springframework.data.mongodb.core.aggregation.EvaluationOperators.Expr;
4851
import org.springframework.data.mongodb.core.aggregation.TypeBasedAggregationOperationContext;
4952
import org.springframework.data.mongodb.core.geo.GeoJsonPoint;
5053
import org.springframework.data.mongodb.core.geo.GeoJsonPolygon;
@@ -1461,6 +1464,51 @@ void considersValueConverterWhenPresent() {
14611464
assertThat(mappedObject).isEqualTo(new org.bson.Document("text", "eulav"));
14621465
}
14631466

1467+
@Test // GH-2750
1468+
void mapsAggregationExpression() {
1469+
1470+
Query query = query(whereExpr(ComparisonOperators.valueOf("field").greaterThan("budget")));
1471+
org.bson.Document mappedObject = mapper.getMappedObject(query.getQueryObject(),
1472+
context.getPersistentEntity(CustomizedField.class));
1473+
assertThat(mappedObject).isEqualTo("{ $expr : { $gt : [ '$foo', '$budget'] } }");
1474+
}
1475+
1476+
@Test // GH-2750
1477+
void unwrapsAggregationExpressionExprObjectWrappedInExpressionCriteria() {
1478+
1479+
Query query = query(whereExpr(Expr.valueOf(ComparisonOperators.valueOf("field").greaterThan("budget"))));
1480+
org.bson.Document mappedObject = mapper.getMappedObject(query.getQueryObject(),
1481+
context.getPersistentEntity(CustomizedField.class));
1482+
assertThat(mappedObject).isEqualTo("{ $expr : { $gt : [ '$foo', '$budget'] } }");
1483+
}
1484+
1485+
@Test // GH-2750
1486+
void mapsMongoExpressionToFieldsIfItsAnAggregationExpression() {
1487+
1488+
Query query = query(expr(ComparisonOperators.valueOf("field").greaterThan("budget")));
1489+
org.bson.Document mappedObject = mapper.getMappedObject(query.getQueryObject(),
1490+
context.getPersistentEntity(CustomizedField.class));
1491+
assertThat(mappedObject).isEqualTo("{ $expr : { $gt : [ '$foo', '$budget'] } }");
1492+
}
1493+
1494+
@Test // GH-2750
1495+
void usageOfMongoExpressionOnCriteriaDoesNotUnwrapAnExprAggregationExpression() {
1496+
1497+
Query query = query(expr(Expr.valueOf(ComparisonOperators.valueOf("field").greaterThan("budget"))));
1498+
org.bson.Document mappedObject = mapper.getMappedObject(query.getQueryObject(),
1499+
context.getPersistentEntity(CustomizedField.class));
1500+
assertThat(mappedObject).isEqualTo("{ $expr : { $expr : { $gt : [ '$foo', '$budget'] } } }");
1501+
}
1502+
1503+
@Test // GH-2750
1504+
void usesMongoExpressionDocumentAsIsIfItIsNotAnAggregationExpression() {
1505+
1506+
Query query = query(expr(() -> org.bson.Document.parse("{ $gt : [ '$field', '$budget'] }")));
1507+
org.bson.Document mappedObject = mapper.getMappedObject(query.getQueryObject(),
1508+
context.getPersistentEntity(CustomizedField.class));
1509+
assertThat(mappedObject).isEqualTo("{ $expr : { $gt : [ '$field', '$budget'] } }");
1510+
}
1511+
14641512
class WithDeepArrayNesting {
14651513

14661514
List<WithNestedArray> level0;

0 commit comments

Comments
 (0)