From 4b5f341938eaf915e4674c35a52e973908eaad7c Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Thu, 2 Feb 2023 13:20:18 +0100 Subject: [PATCH 1/3] Prepare issue branch. --- pom.xml | 2 +- spring-data-mongodb-benchmarks/pom.xml | 2 +- spring-data-mongodb-distribution/pom.xml | 2 +- spring-data-mongodb/pom.xml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index d864b2e4e6..2270ad3247 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 4.1.0-SNAPSHOT + 4.1.x-GH-2750-SNAPSHOT pom Spring Data MongoDB diff --git a/spring-data-mongodb-benchmarks/pom.xml b/spring-data-mongodb-benchmarks/pom.xml index 1b2a1390e6..440cea3541 100644 --- a/spring-data-mongodb-benchmarks/pom.xml +++ b/spring-data-mongodb-benchmarks/pom.xml @@ -7,7 +7,7 @@ org.springframework.data spring-data-mongodb-parent - 4.1.0-SNAPSHOT + 4.1.x-GH-2750-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml index 8db8d798fb..f4706ebf3b 100644 --- a/spring-data-mongodb-distribution/pom.xml +++ b/spring-data-mongodb-distribution/pom.xml @@ -15,7 +15,7 @@ org.springframework.data spring-data-mongodb-parent - 4.1.0-SNAPSHOT + 4.1.x-GH-2750-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index 9a57f7eb52..0fd1be4eff 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -13,7 +13,7 @@ org.springframework.data spring-data-mongodb-parent - 4.1.0-SNAPSHOT + 4.1.x-GH-2750-SNAPSHOT ../pom.xml From 3445ca6b2fa454ee576ced009ed60ad0d0f76003 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Thu, 2 Mar 2023 15:21:52 +0100 Subject: [PATCH 2/3] 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"))) --- .../AggregationExpressionCriteria.java | 60 +++++++++++++++++++ .../mongodb/core/convert/QueryMapper.java | 10 ++++ .../data/mongodb/core/query/Criteria.java | 32 ++++++++++ .../data/mongodb/core/MongoTemplateTests.java | 19 ++++++ .../core/convert/QueryMapperUnitTests.java | 48 +++++++++++++++ 5 files changed, 169 insertions(+) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationExpressionCriteria.java 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 0000000000..de748adc12 --- /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 9302d37cae..42f9d5e4b1 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.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 @@ protected Object convertSimpleOrDocument(Object source, @Nullable MongoPersisten 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 f4bf3e7105..584af855c5 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.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 static Criteria matchingDocumentStructure(MongoJsonSchema schema) { 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 c93c8ade66..6af8f74680 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.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 void sliceShouldLimitCollectionValues() { 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 createAfterSaveReference() { AtomicReference saved = new AtomicReference<>(); @@ -4158,6 +4176,7 @@ static class VersionedPerson { @Field(write = Field.Write.ALWAYS) String lastname; } + @EqualsAndHashCode static class TypeWithFieldAnnotation { @Id ObjectId id; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java index 24035cf921..68a77ad143 100755 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java +++ b/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; 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.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 @@ void considersValueConverterWhenPresent() { 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 level0; From 199db67d51996819c1aea3c1314443b0eb793367 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 3 Mar 2023 14:57:48 +0100 Subject: [PATCH 3/3] Polishing. Favor Base64 over deprecated Base64Utils. --- .../org/springframework/data/mongodb/core/query/Criteria.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 584af855c5..3ae5828114 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 @@ -19,6 +19,7 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.Base64; import java.util.Collection; import java.util.Iterator; import java.util.LinkedHashMap; @@ -46,7 +47,6 @@ import org.springframework.data.mongodb.util.RegexFlags; import org.springframework.lang.Nullable; import org.springframework.util.Assert; -import org.springframework.util.Base64Utils; import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -1394,7 +1394,7 @@ private Criteria stringBitmask(String operator, String bitmask) { Assert.hasText(bitmask, "Bitmask must not be null"); - target.criteria.put(operator, new Binary(Base64Utils.decodeFromString(bitmask))); + target.criteria.put(operator, new Binary(Base64.getDecoder().decode(bitmask))); return target; }