Browse Source

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.
pull/4323/head
Christoph Strobl 3 years ago committed by Mark Paluch
parent
commit
a416441427
No known key found for this signature in database
GPG Key ID: 4406B84C1661DCD1
  1. 60
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationExpressionCriteria.java
  2. 10
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java
  3. 32
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Criteria.java
  4. 19
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java
  5. 48
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java

60
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationExpressionCriteria.java

@ -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";
}
}

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

@ -41,6 +41,9 @@ import org.springframework.data.mapping.PropertyReferenceException;
import org.springframework.data.mapping.context.InvalidPersistentPropertyPath; import org.springframework.data.mapping.context.InvalidPersistentPropertyPath;
import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mongodb.MongoExpression; 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.convert.MappingMongoConverter.NestedDocument;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
@ -560,6 +563,13 @@ public class QueryMapper {
return exampleMapper.getMappedExample((Example<?>) source, entity); 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) { if (source instanceof List) {
return delegateConvertToMongoType(source, entity); return delegateConvertToMongoType(source, entity);
} }

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

@ -37,6 +37,7 @@ import org.springframework.data.geo.Circle;
import org.springframework.data.geo.Point; import org.springframework.data.geo.Point;
import org.springframework.data.geo.Shape; import org.springframework.data.geo.Shape;
import org.springframework.data.mongodb.InvalidMongoDbApiUsageException; 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.GeoJson;
import org.springframework.data.mongodb.core.geo.Sphere; import org.springframework.data.mongodb.core.geo.Sphere;
import org.springframework.data.mongodb.core.schema.JsonSchemaObject.Type; import org.springframework.data.mongodb.core.schema.JsonSchemaObject.Type;
@ -147,6 +148,37 @@ public class Criteria implements CriteriaDefinition {
return new Criteria().andDocumentStructureMatches(schema); 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 * Static factory method to create a Criteria using the provided key
* *

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

@ -68,6 +68,7 @@ import org.springframework.data.mapping.context.PersistentEntities;
import org.springframework.data.mongodb.InvalidMongoDbApiUsageException; import org.springframework.data.mongodb.InvalidMongoDbApiUsageException;
import org.springframework.data.mongodb.MongoDatabaseFactory; import org.springframework.data.mongodb.MongoDatabaseFactory;
import org.springframework.data.mongodb.core.BulkOperations.BulkMode; 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.convert.LazyLoadingProxy;
import org.springframework.data.mongodb.core.geo.GeoJsonPoint; import org.springframework.data.mongodb.core.geo.GeoJsonPoint;
import org.springframework.data.mongodb.core.index.Index; import org.springframework.data.mongodb.core.index.Index;
@ -3837,6 +3838,23 @@ public class MongoTemplateTests {
assertThat(target.values).containsExactly("spring"); 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() { private AtomicReference<ImmutableVersioned> createAfterSaveReference() {
AtomicReference<ImmutableVersioned> saved = new AtomicReference<>(); AtomicReference<ImmutableVersioned> saved = new AtomicReference<>();
@ -4158,6 +4176,7 @@ public class MongoTemplateTests {
@Field(write = Field.Write.ALWAYS) String lastname; @Field(write = Field.Write.ALWAYS) String lastname;
} }
@EqualsAndHashCode
static class TypeWithFieldAnnotation { static class TypeWithFieldAnnotation {
@Id ObjectId id; @Id ObjectId id;

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

@ -16,6 +16,7 @@
package org.springframework.data.mongodb.core.convert; package org.springframework.data.mongodb.core.convert;
import static org.springframework.data.mongodb.core.DocumentTestUtils.*; 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.Criteria.*;
import static org.springframework.data.mongodb.core.query.Query.*; import static org.springframework.data.mongodb.core.query.Query.*;
import static org.springframework.data.mongodb.test.util.Assertions.*; 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.geo.Point;
import org.springframework.data.mongodb.core.DocumentTestUtils; import org.springframework.data.mongodb.core.DocumentTestUtils;
import org.springframework.data.mongodb.core.Person; 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.ConditionalOperators;
import org.springframework.data.mongodb.core.aggregation.EvaluationOperators; 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.aggregation.TypeBasedAggregationOperationContext;
import org.springframework.data.mongodb.core.geo.GeoJsonPoint; import org.springframework.data.mongodb.core.geo.GeoJsonPoint;
import org.springframework.data.mongodb.core.geo.GeoJsonPolygon; import org.springframework.data.mongodb.core.geo.GeoJsonPolygon;
@ -1461,6 +1464,51 @@ public class QueryMapperUnitTests {
assertThat(mappedObject).isEqualTo(new org.bson.Document("text", "eulav")); 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 { class WithDeepArrayNesting {
List<WithNestedArray> level0; List<WithNestedArray> level0;

Loading…
Cancel
Save