diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationExpressionCriteria.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationExpressionCriteria.java new file mode 100644 index 000000000..de748adc1 --- /dev/null +++ b/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"; + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java index 9302d37ca..42f9d5e4b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java +++ b/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.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); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Criteria.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Criteria.java index f4bf3e710..584af855c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Criteria.java +++ b/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.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}. + *
+ * The {@link MongoExpression expression} can be either something that directly renders to the store native + * representation like + * + *
+ * expr(() -> Document.parse("{ $gt : [ '$spent', '$budget'] }")))
+ *
+ *
+ * or an {@link org.springframework.data.mongodb.core.aggregation.AggregationExpression} which will be subject to
+ * context (domain type) specific field mapping.
+ *
+ *
+ * expr(valueOf("amountSpent").greaterThan("budget"))
+ *
+ *
+ * @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
*
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java
index c93c8ade6..6af8f7468 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java
+++ b/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.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