Support expressions in query field projections.
// explicit via dedicated AggregationExpression
query.fields()
.project(StringOperators.valueOf("name").toUpper())
.as("name");
// with a user provided expression parsed from a String
query.fields().project(MongoExpression.create("'$toUpper' : '$name'"))
.as("name")
// using SpEL support
query.fields().project(AggregationSpELExpression.expressionOf("toUpper(name)"))
.as("name");
// with parameter binding
query.fields().project(
MongoExpression.create("'$toUpper' : '?0'").bind("$name")
).as("name")
// via the @Query annotation on repositories
@Query(value = "{ 'id' : ?0 }", fields = "{ 'name': { '$toUpper': '$name' } }")
Closes: #3583
Original pull request: #3585.
This commit is contained in:
committed by
Mark Paluch
parent
191993caef
commit
af6d1eff0c
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* Copyright 2021 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;
|
||||
|
||||
import org.bson.Document;
|
||||
import org.bson.codecs.DocumentCodec;
|
||||
import org.bson.codecs.configuration.CodecRegistry;
|
||||
import org.springframework.data.mongodb.util.json.ParameterBindingDocumentCodec;
|
||||
import org.springframework.data.util.Lazy;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* A {@link MongoExpression} using the {@link ParameterBindingDocumentCodec} for parsing a raw ({@literal json})
|
||||
* expression. The expression will be wrapped within <code>{ ... }</code> if necessary. The actual parsing and parameter
|
||||
* binding of placeholders like {@code ?0} is delayed upon first call on the the target {@link Document} via
|
||||
* {@link #toDocument()}.
|
||||
* <p />
|
||||
*
|
||||
* <pre class="code">
|
||||
* $toUpper : $name -> { '$toUpper' : '$name' }
|
||||
*
|
||||
* { '$toUpper' : '$name' } -> { '$toUpper' : '$name' }
|
||||
*
|
||||
* { '$toUpper' : '?0' }, "$name" -> { '$toUpper' : '$name' }
|
||||
* </pre>
|
||||
*
|
||||
* Some types might require a special {@link org.bson.codecs.Codec}. If so, make sure to provide a {@link CodecRegistry}
|
||||
* containing the required {@link org.bson.codecs.Codec codec} via {@link #withCodecRegistry(CodecRegistry)}.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 3.2
|
||||
*/
|
||||
public class BindableMongoExpression implements MongoExpression {
|
||||
|
||||
private final String expressionString;
|
||||
|
||||
@Nullable //
|
||||
private final CodecRegistryProvider codecRegistryProvider;
|
||||
|
||||
@Nullable //
|
||||
private final Object[] args;
|
||||
|
||||
private final Lazy<Document> target;
|
||||
|
||||
/**
|
||||
* Create a new instance of {@link BindableMongoExpression}.
|
||||
*
|
||||
* @param expression must not be {@literal null}.
|
||||
* @param args can be {@literal null}.
|
||||
*/
|
||||
public BindableMongoExpression(String expression, @Nullable Object[] args) {
|
||||
this(expression, null, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance of {@link BindableMongoExpression}.
|
||||
*
|
||||
* @param expression must not be {@literal null}.
|
||||
* @param codecRegistryProvider can be {@literal null}.
|
||||
* @param args can be {@literal null}.
|
||||
*/
|
||||
public BindableMongoExpression(String expression, @Nullable CodecRegistryProvider codecRegistryProvider,
|
||||
@Nullable Object[] args) {
|
||||
|
||||
this.expressionString = expression;
|
||||
this.codecRegistryProvider = codecRegistryProvider;
|
||||
this.args = args;
|
||||
this.target = Lazy.of(this::parse);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide the {@link CodecRegistry} used to convert expressions.
|
||||
*
|
||||
* @param codecRegistry must not be {@literal null}.
|
||||
* @return new instance of {@link BindableMongoExpression}.
|
||||
*/
|
||||
public BindableMongoExpression withCodecRegistry(CodecRegistry codecRegistry) {
|
||||
return new BindableMongoExpression(expressionString, () -> codecRegistry, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide the arguments to bind to the placeholders via their index.
|
||||
*
|
||||
* @param args must not be {@literal null}.
|
||||
* @return new instance of {@link BindableMongoExpression}.
|
||||
*/
|
||||
public BindableMongoExpression bind(Object... args) {
|
||||
return new BindableMongoExpression(expressionString, codecRegistryProvider, args);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.mongodb.MongoExpression#toDocument()
|
||||
*/
|
||||
@Override
|
||||
public Document toDocument() {
|
||||
return target.get();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#toString()
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
return "BindableMongoExpression{" + "expressionString='" + expressionString + '\'' + ", args=" + args + '}';
|
||||
}
|
||||
|
||||
private String wrapJsonIfNecessary(String json) {
|
||||
|
||||
if (StringUtils.hasText(json) && (json.startsWith("{") && json.endsWith("}"))) {
|
||||
return json;
|
||||
}
|
||||
|
||||
return "{" + json + "}";
|
||||
}
|
||||
|
||||
private Document parse() {
|
||||
|
||||
String expression = wrapJsonIfNecessary(expressionString);
|
||||
|
||||
if (ObjectUtils.isEmpty(args)) {
|
||||
|
||||
if (codecRegistryProvider == null) {
|
||||
return Document.parse(expression);
|
||||
}
|
||||
|
||||
return Document.parse(expression, codecRegistryProvider.getCodecFor(Document.class)
|
||||
.orElseGet(() -> new DocumentCodec(codecRegistryProvider.getCodecRegistry())));
|
||||
}
|
||||
|
||||
ParameterBindingDocumentCodec codec = codecRegistryProvider == null ? new ParameterBindingDocumentCodec()
|
||||
: new ParameterBindingDocumentCodec(codecRegistryProvider.getCodecRegistry());
|
||||
return codec.decode(expression, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright 2021 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;
|
||||
|
||||
/**
|
||||
* Wrapper object for MongoDB expressions like {@code $toUpper : $name} that manifest as {@link org.bson.Document} when
|
||||
* passed on to the driver.
|
||||
* <p />
|
||||
* A set of predefined {@link MongoExpression expressions}, including a
|
||||
* {@link org.springframework.data.mongodb.core.aggregation.AggregationSpELExpression SpEL based variant} for method
|
||||
* like expressions (eg. {@code toUpper(name)}) are available via the
|
||||
* {@link org.springframework.data.mongodb.core.aggregation Aggregation API}.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 3.2
|
||||
* @see org.springframework.data.mongodb.core.aggregation.ArithmeticOperators
|
||||
* @see org.springframework.data.mongodb.core.aggregation.ArrayOperators
|
||||
* @see org.springframework.data.mongodb.core.aggregation.ComparisonOperators
|
||||
* @see org.springframework.data.mongodb.core.aggregation.ConditionalOperators
|
||||
* @see org.springframework.data.mongodb.core.aggregation.ConvertOperators
|
||||
* @see org.springframework.data.mongodb.core.aggregation.DateOperators
|
||||
* @see org.springframework.data.mongodb.core.aggregation.ObjectOperators
|
||||
* @see org.springframework.data.mongodb.core.aggregation.SetOperators
|
||||
* @see org.springframework.data.mongodb.core.aggregation.StringOperators
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface MongoExpression {
|
||||
|
||||
/**
|
||||
* Obtain the native {@link org.bson.Document} representation.
|
||||
*
|
||||
* @return never {@literal null}.
|
||||
*/
|
||||
org.bson.Document toDocument();
|
||||
|
||||
/**
|
||||
* Create a new {@link MongoExpression} from plain {@link String} (eg. {@code $toUpper : $name}). <br />
|
||||
* The given expression will be wrapped with <code>{ ... }</code> to match an actual MongoDB {@link org.bson.Document}
|
||||
* if necessary.
|
||||
*
|
||||
* @param expression must not be {@literal null}.
|
||||
* @return new instance of {@link MongoExpression}.
|
||||
*/
|
||||
static MongoExpression create(String expression) {
|
||||
return new BindableMongoExpression(expression, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link MongoExpression} from plain {@link String} containing placeholders (eg. {@code $toUpper : ?0})
|
||||
* that will be resolved on first call of {@link #toDocument()}. <br />
|
||||
* The given expression will be wrapped with <code>{ ... }</code> to match an actual MongoDB {@link org.bson.Document}
|
||||
* if necessary.
|
||||
*
|
||||
* @param expression must not be {@literal null}.
|
||||
* @return new instance of {@link MongoExpression}.
|
||||
*/
|
||||
static MongoExpression create(String expression, Object... args) {
|
||||
return new BindableMongoExpression(expression, args);
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ package org.springframework.data.mongodb.core;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
@@ -31,8 +32,10 @@ import org.springframework.data.mapping.PropertyPath;
|
||||
import org.springframework.data.mapping.PropertyReferenceException;
|
||||
import org.springframework.data.mapping.context.MappingContext;
|
||||
import org.springframework.data.mongodb.CodecRegistryProvider;
|
||||
import org.springframework.data.mongodb.MongoExpression;
|
||||
import org.springframework.data.mongodb.core.MappedDocument.MappedUpdate;
|
||||
import org.springframework.data.mongodb.core.aggregation.Aggregation;
|
||||
import org.springframework.data.mongodb.core.aggregation.AggregationExpression;
|
||||
import org.springframework.data.mongodb.core.aggregation.AggregationOperationContext;
|
||||
import org.springframework.data.mongodb.core.aggregation.AggregationOptions;
|
||||
import org.springframework.data.mongodb.core.aggregation.AggregationPipeline;
|
||||
@@ -288,7 +291,21 @@ class QueryOperations {
|
||||
Document getMappedFields(@Nullable MongoPersistentEntity<?> entity, Class<?> targetType,
|
||||
ProjectionFactory projectionFactory) {
|
||||
|
||||
Document fields = query.getFieldsObject();
|
||||
Document fields = new Document();
|
||||
|
||||
for (Entry<String, Object> entry : query.getFieldsObject().entrySet()) {
|
||||
|
||||
if (entry.getValue() instanceof MongoExpression) {
|
||||
|
||||
AggregationOperationContext ctx = entity == null ? Aggregation.DEFAULT_CONTEXT
|
||||
: new RelaxedTypeBasedAggregationOperationContext(entity.getType(), mappingContext, queryMapper);
|
||||
|
||||
fields.put(entry.getKey(), AggregationExpression.from((MongoExpression) entry.getValue()).toDocument(ctx));
|
||||
} else {
|
||||
fields.put(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
Document mappedFields = fields;
|
||||
|
||||
if (entity == null) {
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
package org.springframework.data.mongodb.core.aggregation;
|
||||
|
||||
import org.bson.Document;
|
||||
import org.springframework.data.mongodb.MongoExpression;
|
||||
|
||||
/**
|
||||
* An {@link AggregationExpression} can be used with field expressions in aggregation pipeline stages like
|
||||
@@ -25,7 +26,37 @@ import org.bson.Document;
|
||||
* @author Oliver Gierke
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
public interface AggregationExpression {
|
||||
public interface AggregationExpression extends MongoExpression {
|
||||
|
||||
/**
|
||||
* Obtain the as is (unmapped) representation of the {@link AggregationExpression}. Use
|
||||
* {@link #toDocument(AggregationOperationContext)} with a matching {@link AggregationOperationContext context} to
|
||||
* engage domain type mapping including field name resolution.
|
||||
*
|
||||
* @see org.springframework.data.mongodb.MongoExpression#toDocument()
|
||||
*/
|
||||
@Override
|
||||
default Document toDocument() {
|
||||
return toDocument(Aggregation.DEFAULT_CONTEXT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an {@link AggregationExpression} out of a given {@link MongoExpression} to ensure the resulting
|
||||
* {@link MongoExpression#toDocument() Document} is mapped against the {@link AggregationOperationContext}. <br />
|
||||
* If the given expression is already an {@link AggregationExpression} the very same instance is returned.
|
||||
*
|
||||
* @param expression must not be {@literal null}.
|
||||
* @return never {@literal null}.
|
||||
* @since 3.2
|
||||
*/
|
||||
static AggregationExpression from(MongoExpression expression) {
|
||||
|
||||
if (expression instanceof AggregationExpression) {
|
||||
return AggregationExpression.class.cast(expression);
|
||||
}
|
||||
|
||||
return (context) -> context.getMappedObject(expression.toDocument());
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns the {@link AggregationExpression} into a {@link Document} within the given
|
||||
|
||||
@@ -24,7 +24,6 @@ import org.bson.BsonValue;
|
||||
import org.bson.Document;
|
||||
import org.bson.conversions.Bson;
|
||||
import org.bson.types.ObjectId;
|
||||
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.data.domain.Example;
|
||||
@@ -37,6 +36,7 @@ import org.springframework.data.mapping.PropertyPath;
|
||||
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.convert.MappingMongoConverter.NestedDocument;
|
||||
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
|
||||
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
|
||||
@@ -295,6 +295,10 @@ public class QueryMapper {
|
||||
String key = field.getMappedKey();
|
||||
Object value;
|
||||
|
||||
if (rawValue instanceof MongoExpression) {
|
||||
return createMapEntry(key, getMappedObject(((MongoExpression) rawValue).toDocument(), field.getEntity()));
|
||||
}
|
||||
|
||||
if (isNestedKeyword(rawValue) && !field.isIdField()) {
|
||||
Keyword keyword = new Keyword((Document) rawValue);
|
||||
value = getMappedKeyword(field, keyword);
|
||||
@@ -934,6 +938,11 @@ public class QueryMapper {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
MongoPersistentEntity<?> getEntity() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the field represents an association.
|
||||
*
|
||||
@@ -1086,6 +1095,12 @@ public class QueryMapper {
|
||||
return property == null ? null : mappingContext.getPersistentEntity(property);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public MongoPersistentEntity<?> getEntity() {
|
||||
return entity;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.mongodb.core.convert.QueryMapper.Field#isAssociation()
|
||||
|
||||
@@ -20,7 +20,7 @@ import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import org.bson.Document;
|
||||
|
||||
import org.springframework.data.mongodb.MongoExpression;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
@@ -37,7 +37,7 @@ import org.springframework.util.ObjectUtils;
|
||||
*/
|
||||
public class Field {
|
||||
|
||||
private final Map<String, Integer> criteria = new HashMap<>();
|
||||
private final Map<String, Object> criteria = new HashMap<>();
|
||||
private final Map<String, Object> slices = new HashMap<>();
|
||||
private final Map<String, Criteria> elemMatchs = new HashMap<>();
|
||||
private @Nullable String positionKey;
|
||||
@@ -58,6 +58,62 @@ public class Field {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Project a given {@link MongoExpression} to a {@link FieldProjectionExpression#as(String) field} included in the
|
||||
* result.
|
||||
*
|
||||
* <pre class="code">
|
||||
*
|
||||
* // { 'name' : { '$toUpper' : '$name' } }
|
||||
*
|
||||
* // native MongoDB expression
|
||||
* .project(MongoExpression.expressionFromString("'$toUpper' : '$name'")).as("name");
|
||||
*
|
||||
* // Aggregation Framework expression
|
||||
* .project(StringOperators.valueOf("name").toUpper()).as("name");
|
||||
*
|
||||
* // Aggregation Framework SpEL expression
|
||||
* .project(AggregationSpELExpression.expressionOf("toUpper(name)")).as("name");
|
||||
* </pre>
|
||||
*
|
||||
* @param expression must not be {@literal null}.
|
||||
* @return new instance of {@link FieldProjectionExpression} - you still need to define the target field name via
|
||||
* {@link FieldProjectionExpression#as(String) as(String)}.
|
||||
* @since 3.2
|
||||
*/
|
||||
public FieldProjectionExpression project(MongoExpression expression) {
|
||||
return field -> Field.this.projectAs(expression, field);
|
||||
}
|
||||
|
||||
/**
|
||||
* Project a given {@link MongoExpression} to a {@link FieldProjectionExpression#as(String) field} included in the
|
||||
* result.
|
||||
*
|
||||
* <pre class="code">
|
||||
*
|
||||
* // { 'name' : { '$toUpper' : '$name' } }
|
||||
*
|
||||
* // native MongoDB expression
|
||||
* .projectAs(MongoExpression.expressionFromString("'$toUpper' : '$name'"), "name");
|
||||
*
|
||||
* // Aggregation Framework expression
|
||||
* .projectAs(StringOperators.valueOf("name").toUpper(), "name");
|
||||
*
|
||||
* // Aggregation Framework SpEL expression
|
||||
* .projectAs(AggregationSpELExpression.expressionOf("toUpper(name)"), "name");
|
||||
* </pre>
|
||||
*
|
||||
* @param expression must not be {@literal null}.
|
||||
* @param field the field name used in the result.
|
||||
* @return new instance of {@link FieldProjectionExpression}.
|
||||
* @since 3.2
|
||||
*/
|
||||
public Field projectAs(MongoExpression expression, String field) {
|
||||
|
||||
criteria.put(field, expression);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Include one or more {@code fields} to be returned by the query operation.
|
||||
*
|
||||
@@ -166,8 +222,7 @@ public class Field {
|
||||
|
||||
public Document getFieldsObject() {
|
||||
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
Document document = new Document((Map) criteria);
|
||||
Document document = new Document(criteria);
|
||||
|
||||
for (Entry<String, Object> entry : slices.entrySet()) {
|
||||
document.put(entry.getKey(), new Document("$slice", entry.getValue()));
|
||||
@@ -219,4 +274,21 @@ public class Field {
|
||||
result = 31 * result + positionValue;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Intermediate builder part for projecting a {@link MongoExpression} to a result field.
|
||||
*
|
||||
* @since 3.2
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
public interface FieldProjectionExpression {
|
||||
|
||||
/**
|
||||
* Set the name to be used in the result.
|
||||
*
|
||||
* @param name must not be {@literal null}.
|
||||
* @return the calling instance {@link Field}.
|
||||
*/
|
||||
Field as(String name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
/*
|
||||
* Copyright 2021 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;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.mongodb.MongoExpression;
|
||||
import org.springframework.data.mongodb.core.aggregation.AggregationSpELExpression;
|
||||
import org.springframework.data.mongodb.core.aggregation.StringOperators;
|
||||
import org.springframework.data.mongodb.core.mapping.Embedded;
|
||||
import org.springframework.data.mongodb.core.mapping.Field;
|
||||
import org.springframework.data.mongodb.core.query.Criteria;
|
||||
import org.springframework.data.mongodb.core.query.Query;
|
||||
import org.springframework.data.mongodb.test.util.MongoTemplateExtension;
|
||||
import org.springframework.data.mongodb.test.util.MongoTestTemplate;
|
||||
import org.springframework.data.mongodb.test.util.Template;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
@ExtendWith(MongoTemplateExtension.class)
|
||||
class MongoTemplateFieldProjectionTests {
|
||||
|
||||
private static @Template MongoTestTemplate template;
|
||||
|
||||
private Person luke;
|
||||
|
||||
@BeforeEach
|
||||
void beforeEach() {
|
||||
|
||||
luke = new Person();
|
||||
luke.id = "luke";
|
||||
luke.firstname = "luke";
|
||||
luke.lastname = "skywalker";
|
||||
|
||||
template.save(luke);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void afterEach() {
|
||||
template.flush(Person.class, Wrapper.class);
|
||||
}
|
||||
|
||||
@Test // GH-3583
|
||||
void usesMongoExpressionAsIs() {
|
||||
|
||||
Person result = findLuke(fields -> {
|
||||
fields.include("firstname").project(MongoExpression.create("'$toUpper' : '$last_name'"))
|
||||
.as("last_name");
|
||||
});
|
||||
|
||||
assertThat(result).isEqualTo(luke.upperCaseLastnameClone());
|
||||
}
|
||||
|
||||
@Test // GH-3583
|
||||
void usesMongoExpressionWithPlaceholdersAsIs() {
|
||||
|
||||
Person result = findLuke(fields -> {
|
||||
fields.include("firstname").project(MongoExpression.create("'$toUpper' : '$?0'", "last_name"))
|
||||
.as("last_name");
|
||||
});
|
||||
|
||||
assertThat(result).isEqualTo(luke.upperCaseLastnameClone());
|
||||
}
|
||||
|
||||
@Test // GH-3583
|
||||
void mapsAggregationExpressionToDomainType() {
|
||||
|
||||
Person result = findLuke(fields -> {
|
||||
fields.include("firstname").project(StringOperators.valueOf("lastname").toUpper()).as("last_name");
|
||||
});
|
||||
|
||||
assertThat(result).isEqualTo(luke.upperCaseLastnameClone());
|
||||
}
|
||||
|
||||
@Test // GH-3583
|
||||
void mapsAggregationSpELExpressionToDomainType() {
|
||||
|
||||
Person result = findLuke(fields -> {
|
||||
fields.include("firstname").project(AggregationSpELExpression.expressionOf("toUpper(lastname)")).as("last_name");
|
||||
});
|
||||
|
||||
assertThat(result).isEqualTo(luke.upperCaseLastnameClone());
|
||||
}
|
||||
|
||||
@Test // GH-3583
|
||||
void mapsNestedPathAggregationExpressionToDomainType() {
|
||||
|
||||
Wrapper wrapper = new Wrapper();
|
||||
wrapper.id = "wrapper";
|
||||
wrapper.person = luke;
|
||||
|
||||
template.save(wrapper);
|
||||
|
||||
Query query = Query.query(Criteria.where("id").is(wrapper.id));
|
||||
query.fields().include("person.firstname", "person.id")
|
||||
.project(StringOperators.valueOf("person.lastname").toUpper()).as("person.last_name");
|
||||
|
||||
Wrapper result = template.findOne(query, Wrapper.class);
|
||||
assertThat(result.person).isEqualTo(luke.upperCaseLastnameClone());
|
||||
}
|
||||
|
||||
@Test // GH-3583
|
||||
void mapsProjectionOnEmbedded() {
|
||||
|
||||
luke.address = new Address();
|
||||
luke.address.planet = "tatoine";
|
||||
|
||||
template.save(luke);
|
||||
|
||||
Person result = findLuke(fields -> {
|
||||
fields.project(StringOperators.valueOf("address.planet").toUpper()).as("planet");
|
||||
});
|
||||
|
||||
assertThat(result.address.planet).isEqualTo("TATOINE");
|
||||
}
|
||||
|
||||
private Person findLuke(Consumer<org.springframework.data.mongodb.core.query.Field> projection) {
|
||||
|
||||
Query query = Query.query(Criteria.where("id").is(luke.id));
|
||||
projection.accept(query.fields());
|
||||
return template.findOne(query, Person.class);
|
||||
}
|
||||
|
||||
@EqualsAndHashCode
|
||||
@ToString
|
||||
static class Wrapper {
|
||||
@Id String id;
|
||||
Person person;
|
||||
}
|
||||
|
||||
@EqualsAndHashCode
|
||||
@ToString
|
||||
static class Person {
|
||||
|
||||
@Id String id;
|
||||
String firstname;
|
||||
|
||||
@Field("last_name") //
|
||||
String lastname;
|
||||
|
||||
@Embedded.Nullable Address address;
|
||||
|
||||
Person toUpperCaseLastnameClone(Person source) {
|
||||
|
||||
Person target = new Person();
|
||||
target.id = source.id;
|
||||
target.firstname = source.firstname;
|
||||
target.lastname = source.lastname.toUpperCase();
|
||||
target.address = source.address;
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
Person upperCaseLastnameClone() {
|
||||
return toUpperCaseLastnameClone(this);
|
||||
}
|
||||
}
|
||||
|
||||
@EqualsAndHashCode
|
||||
@ToString
|
||||
static class Address {
|
||||
String planet;
|
||||
}
|
||||
}
|
||||
@@ -1413,4 +1413,11 @@ public abstract class AbstractPersonRepositoryIntegrationTests {
|
||||
void caseSensitiveInClauseIgnoresExpressions() {
|
||||
assertThat(repository.findByFirstnameIn(".*")).isEmpty();
|
||||
}
|
||||
|
||||
@Test // GH-23583
|
||||
void annotatedQueryShouldAllowAggregationInProjection() {
|
||||
|
||||
Person target = repository.findWithAggregationInProjection(alicia.getId());
|
||||
assertThat(target.getFirstname()).isEqualTo(alicia.getFirstname().toUpperCase());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -366,6 +366,9 @@ public interface PersonRepository extends MongoRepository<Person, String>, Query
|
||||
@Query(value = "{ 'id' : ?0 }", fields = "{ 'fans': { '$slice': [ ?1, ?2 ] } }")
|
||||
Person findWithSliceInProjection(String id, int skip, int limit);
|
||||
|
||||
@Query(value = "{ 'id' : ?0 }", fields = "{ 'firstname': { '$toUpper': '$firstname' } }")
|
||||
Person findWithAggregationInProjection(String id);
|
||||
|
||||
@Query(value = "{ 'shippingAddresses' : { '$elemMatch' : { 'city' : { '$eq' : 'lnz' } } } }",
|
||||
fields = "{ 'shippingAddresses.$': ?0 }")
|
||||
Person findWithArrayPositionInProjection(int position);
|
||||
|
||||
@@ -1247,6 +1247,69 @@ The `Query` class has some additional methods that provide options for the query
|
||||
* `Query` *skip* `(int skip)` used to skip the provided number of documents in the results (used for paging)
|
||||
* `Query` *with* `(Sort sort)` used to provide sort definition for the results
|
||||
|
||||
[[mongo-template.querying.field-selection]]
|
||||
==== Selecting fields
|
||||
|
||||
MongoDB supports https://docs.mongodb.com/manual/tutorial/project-fields-from-query-results/[projecting fields] returned by a query.
|
||||
A projection can in- & exclude fields (the `_id` field is always included unless explicitly excluded) based on their name.
|
||||
|
||||
.Selecting result fields
|
||||
====
|
||||
[source,java]
|
||||
----
|
||||
public class Person {
|
||||
|
||||
@Id String id;
|
||||
String firstname;
|
||||
|
||||
@Field("last_name")
|
||||
String lastname;
|
||||
|
||||
Address address;
|
||||
}
|
||||
|
||||
query.fields().include("lastname"); <1>
|
||||
|
||||
query.fields().exclude("id").include("lastname") <2>
|
||||
|
||||
query.fields().include("address") <3>
|
||||
|
||||
query.fields().include("address.city") <4>
|
||||
|
||||
|
||||
----
|
||||
<1> Result will contain both `_id` and `last_name` via `{ "last_name" : 1 }`.
|
||||
<2> Result will only contain the `last_name` via `{ "_id" : 0, "last_name" : 1 }`.
|
||||
<3> Result will contain the `_id` and entire `address` object via `{ "address" : 1 }`.
|
||||
<4> Result will contain the `_id` and and `address` object that only contains the `city` field via `{ "address.city" : 1 }`.
|
||||
====
|
||||
|
||||
Starting with MongoDB 4.4 it is possible to use the aggregation expressions syntax for field projections as shown below.
|
||||
|
||||
.Computing result fields with expressions
|
||||
====
|
||||
[source,java]
|
||||
----
|
||||
query.fields()
|
||||
.project(MongoExpression.create("'$toUpper' : '$last_name'")) <1>
|
||||
.as("last_name"); <2>
|
||||
|
||||
query.fields()
|
||||
.project(StringOperators.valueOf("lastname").toUpper()) <3>
|
||||
.as("last_name");
|
||||
|
||||
query.fields()
|
||||
.project(AggregationSpELExpression.expressionOf("toUpper(lastname)")) <4>
|
||||
.as("last_name");
|
||||
----
|
||||
<1> Use a native expression. The used field names must refer to the ones of the document within the database.
|
||||
<2> Assign the field name that shall hold the expression result in the target document. The resulting field name will never be mapped against the domain model.
|
||||
<3> Use an `AggregationExpression`. Other than native `MongoExpression`, field names are mapped to the ones used in the domain model.
|
||||
<4> Use SpEL along with an `AggregationExpression` to invoke expression functions. Field names are mapped to the ones used in the domain model.
|
||||
====
|
||||
|
||||
`@Query(fields='...')` allows usage of expression field projections at `Repository` level as described in <<mongodb.repositories.queries.json-based>>.
|
||||
|
||||
[[mongo-template.querying]]
|
||||
=== Methods for Querying for Documents
|
||||
|
||||
|
||||
Reference in New Issue
Block a user