Skip to content

Commit af6d1ef

Browse files
christophstroblmp911de
authored andcommitted
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.
1 parent 191993c commit af6d1ef

File tree

10 files changed

+627
-7
lines changed

10 files changed

+627
-7
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
/*
2+
* Copyright 2021 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;
17+
18+
import org.bson.Document;
19+
import org.bson.codecs.DocumentCodec;
20+
import org.bson.codecs.configuration.CodecRegistry;
21+
import org.springframework.data.mongodb.util.json.ParameterBindingDocumentCodec;
22+
import org.springframework.data.util.Lazy;
23+
import org.springframework.lang.Nullable;
24+
import org.springframework.util.ObjectUtils;
25+
import org.springframework.util.StringUtils;
26+
27+
/**
28+
* A {@link MongoExpression} using the {@link ParameterBindingDocumentCodec} for parsing a raw ({@literal json})
29+
* expression. The expression will be wrapped within <code>{ ... }</code> if necessary. The actual parsing and parameter
30+
* binding of placeholders like {@code ?0} is delayed upon first call on the the target {@link Document} via
31+
* {@link #toDocument()}.
32+
* <p />
33+
*
34+
* <pre class="code">
35+
* $toUpper : $name -> { '$toUpper' : '$name' }
36+
*
37+
* { '$toUpper' : '$name' } -> { '$toUpper' : '$name' }
38+
*
39+
* { '$toUpper' : '?0' }, "$name" -> { '$toUpper' : '$name' }
40+
* </pre>
41+
*
42+
* Some types might require a special {@link org.bson.codecs.Codec}. If so, make sure to provide a {@link CodecRegistry}
43+
* containing the required {@link org.bson.codecs.Codec codec} via {@link #withCodecRegistry(CodecRegistry)}.
44+
*
45+
* @author Christoph Strobl
46+
* @since 3.2
47+
*/
48+
public class BindableMongoExpression implements MongoExpression {
49+
50+
private final String expressionString;
51+
52+
@Nullable //
53+
private final CodecRegistryProvider codecRegistryProvider;
54+
55+
@Nullable //
56+
private final Object[] args;
57+
58+
private final Lazy<Document> target;
59+
60+
/**
61+
* Create a new instance of {@link BindableMongoExpression}.
62+
*
63+
* @param expression must not be {@literal null}.
64+
* @param args can be {@literal null}.
65+
*/
66+
public BindableMongoExpression(String expression, @Nullable Object[] args) {
67+
this(expression, null, args);
68+
}
69+
70+
/**
71+
* Create a new instance of {@link BindableMongoExpression}.
72+
*
73+
* @param expression must not be {@literal null}.
74+
* @param codecRegistryProvider can be {@literal null}.
75+
* @param args can be {@literal null}.
76+
*/
77+
public BindableMongoExpression(String expression, @Nullable CodecRegistryProvider codecRegistryProvider,
78+
@Nullable Object[] args) {
79+
80+
this.expressionString = expression;
81+
this.codecRegistryProvider = codecRegistryProvider;
82+
this.args = args;
83+
this.target = Lazy.of(this::parse);
84+
}
85+
86+
/**
87+
* Provide the {@link CodecRegistry} used to convert expressions.
88+
*
89+
* @param codecRegistry must not be {@literal null}.
90+
* @return new instance of {@link BindableMongoExpression}.
91+
*/
92+
public BindableMongoExpression withCodecRegistry(CodecRegistry codecRegistry) {
93+
return new BindableMongoExpression(expressionString, () -> codecRegistry, args);
94+
}
95+
96+
/**
97+
* Provide the arguments to bind to the placeholders via their index.
98+
*
99+
* @param args must not be {@literal null}.
100+
* @return new instance of {@link BindableMongoExpression}.
101+
*/
102+
public BindableMongoExpression bind(Object... args) {
103+
return new BindableMongoExpression(expressionString, codecRegistryProvider, args);
104+
}
105+
106+
/*
107+
* (non-Javadoc)
108+
* @see org.springframework.data.mongodb.MongoExpression#toDocument()
109+
*/
110+
@Override
111+
public Document toDocument() {
112+
return target.get();
113+
}
114+
115+
/*
116+
* (non-Javadoc)
117+
* @see java.lang.Object#toString()
118+
*/
119+
@Override
120+
public String toString() {
121+
return "BindableMongoExpression{" + "expressionString='" + expressionString + '\'' + ", args=" + args + '}';
122+
}
123+
124+
private String wrapJsonIfNecessary(String json) {
125+
126+
if (StringUtils.hasText(json) && (json.startsWith("{") && json.endsWith("}"))) {
127+
return json;
128+
}
129+
130+
return "{" + json + "}";
131+
}
132+
133+
private Document parse() {
134+
135+
String expression = wrapJsonIfNecessary(expressionString);
136+
137+
if (ObjectUtils.isEmpty(args)) {
138+
139+
if (codecRegistryProvider == null) {
140+
return Document.parse(expression);
141+
}
142+
143+
return Document.parse(expression, codecRegistryProvider.getCodecFor(Document.class)
144+
.orElseGet(() -> new DocumentCodec(codecRegistryProvider.getCodecRegistry())));
145+
}
146+
147+
ParameterBindingDocumentCodec codec = codecRegistryProvider == null ? new ParameterBindingDocumentCodec()
148+
: new ParameterBindingDocumentCodec(codecRegistryProvider.getCodecRegistry());
149+
return codec.decode(expression, args);
150+
}
151+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Copyright 2021 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;
17+
18+
/**
19+
* Wrapper object for MongoDB expressions like {@code $toUpper : $name} that manifest as {@link org.bson.Document} when
20+
* passed on to the driver.
21+
* <p />
22+
* A set of predefined {@link MongoExpression expressions}, including a
23+
* {@link org.springframework.data.mongodb.core.aggregation.AggregationSpELExpression SpEL based variant} for method
24+
* like expressions (eg. {@code toUpper(name)}) are available via the
25+
* {@link org.springframework.data.mongodb.core.aggregation Aggregation API}.
26+
*
27+
* @author Christoph Strobl
28+
* @since 3.2
29+
* @see org.springframework.data.mongodb.core.aggregation.ArithmeticOperators
30+
* @see org.springframework.data.mongodb.core.aggregation.ArrayOperators
31+
* @see org.springframework.data.mongodb.core.aggregation.ComparisonOperators
32+
* @see org.springframework.data.mongodb.core.aggregation.ConditionalOperators
33+
* @see org.springframework.data.mongodb.core.aggregation.ConvertOperators
34+
* @see org.springframework.data.mongodb.core.aggregation.DateOperators
35+
* @see org.springframework.data.mongodb.core.aggregation.ObjectOperators
36+
* @see org.springframework.data.mongodb.core.aggregation.SetOperators
37+
* @see org.springframework.data.mongodb.core.aggregation.StringOperators
38+
*/
39+
@FunctionalInterface
40+
public interface MongoExpression {
41+
42+
/**
43+
* Obtain the native {@link org.bson.Document} representation.
44+
*
45+
* @return never {@literal null}.
46+
*/
47+
org.bson.Document toDocument();
48+
49+
/**
50+
* Create a new {@link MongoExpression} from plain {@link String} (eg. {@code $toUpper : $name}). <br />
51+
* The given expression will be wrapped with <code>{ ... }</code> to match an actual MongoDB {@link org.bson.Document}
52+
* if necessary.
53+
*
54+
* @param expression must not be {@literal null}.
55+
* @return new instance of {@link MongoExpression}.
56+
*/
57+
static MongoExpression create(String expression) {
58+
return new BindableMongoExpression(expression, null);
59+
}
60+
61+
/**
62+
* Create a new {@link MongoExpression} from plain {@link String} containing placeholders (eg. {@code $toUpper : ?0})
63+
* that will be resolved on first call of {@link #toDocument()}. <br />
64+
* The given expression will be wrapped with <code>{ ... }</code> to match an actual MongoDB {@link org.bson.Document}
65+
* if necessary.
66+
*
67+
* @param expression must not be {@literal null}.
68+
* @return new instance of {@link MongoExpression}.
69+
*/
70+
static MongoExpression create(String expression, Object... args) {
71+
return new BindableMongoExpression(expression, args);
72+
}
73+
}

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

+18-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import java.util.List;
1919
import java.util.Map;
20+
import java.util.Map.Entry;
2021
import java.util.Optional;
2122
import java.util.Set;
2223
import java.util.concurrent.ConcurrentHashMap;
@@ -31,8 +32,10 @@
3132
import org.springframework.data.mapping.PropertyReferenceException;
3233
import org.springframework.data.mapping.context.MappingContext;
3334
import org.springframework.data.mongodb.CodecRegistryProvider;
35+
import org.springframework.data.mongodb.MongoExpression;
3436
import org.springframework.data.mongodb.core.MappedDocument.MappedUpdate;
3537
import org.springframework.data.mongodb.core.aggregation.Aggregation;
38+
import org.springframework.data.mongodb.core.aggregation.AggregationExpression;
3639
import org.springframework.data.mongodb.core.aggregation.AggregationOperationContext;
3740
import org.springframework.data.mongodb.core.aggregation.AggregationOptions;
3841
import org.springframework.data.mongodb.core.aggregation.AggregationPipeline;
@@ -288,7 +291,21 @@ <T> Document getMappedQuery(@Nullable MongoPersistentEntity<T> entity) {
288291
Document getMappedFields(@Nullable MongoPersistentEntity<?> entity, Class<?> targetType,
289292
ProjectionFactory projectionFactory) {
290293

291-
Document fields = query.getFieldsObject();
294+
Document fields = new Document();
295+
296+
for (Entry<String, Object> entry : query.getFieldsObject().entrySet()) {
297+
298+
if (entry.getValue() instanceof MongoExpression) {
299+
300+
AggregationOperationContext ctx = entity == null ? Aggregation.DEFAULT_CONTEXT
301+
: new RelaxedTypeBasedAggregationOperationContext(entity.getType(), mappingContext, queryMapper);
302+
303+
fields.put(entry.getKey(), AggregationExpression.from((MongoExpression) entry.getValue()).toDocument(ctx));
304+
} else {
305+
fields.put(entry.getKey(), entry.getValue());
306+
}
307+
}
308+
292309
Document mappedFields = fields;
293310

294311
if (entity == null) {

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

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

1818
import org.bson.Document;
19+
import org.springframework.data.mongodb.MongoExpression;
1920

2021
/**
2122
* An {@link AggregationExpression} can be used with field expressions in aggregation pipeline stages like
@@ -25,7 +26,37 @@
2526
* @author Oliver Gierke
2627
* @author Christoph Strobl
2728
*/
28-
public interface AggregationExpression {
29+
public interface AggregationExpression extends MongoExpression {
30+
31+
/**
32+
* Obtain the as is (unmapped) representation of the {@link AggregationExpression}. Use
33+
* {@link #toDocument(AggregationOperationContext)} with a matching {@link AggregationOperationContext context} to
34+
* engage domain type mapping including field name resolution.
35+
*
36+
* @see org.springframework.data.mongodb.MongoExpression#toDocument()
37+
*/
38+
@Override
39+
default Document toDocument() {
40+
return toDocument(Aggregation.DEFAULT_CONTEXT);
41+
}
42+
43+
/**
44+
* Create an {@link AggregationExpression} out of a given {@link MongoExpression} to ensure the resulting
45+
* {@link MongoExpression#toDocument() Document} is mapped against the {@link AggregationOperationContext}. <br />
46+
* If the given expression is already an {@link AggregationExpression} the very same instance is returned.
47+
*
48+
* @param expression must not be {@literal null}.
49+
* @return never {@literal null}.
50+
* @since 3.2
51+
*/
52+
static AggregationExpression from(MongoExpression expression) {
53+
54+
if (expression instanceof AggregationExpression) {
55+
return AggregationExpression.class.cast(expression);
56+
}
57+
58+
return (context) -> context.getMappedObject(expression.toDocument());
59+
}
2960

3061
/**
3162
* Turns the {@link AggregationExpression} into a {@link Document} within the given

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

+16-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
import org.bson.Document;
2525
import org.bson.conversions.Bson;
2626
import org.bson.types.ObjectId;
27-
2827
import org.springframework.core.convert.ConversionService;
2928
import org.springframework.core.convert.converter.Converter;
3029
import org.springframework.data.domain.Example;
@@ -37,6 +36,7 @@
3736
import org.springframework.data.mapping.PropertyReferenceException;
3837
import org.springframework.data.mapping.context.InvalidPersistentPropertyPath;
3938
import org.springframework.data.mapping.context.MappingContext;
39+
import org.springframework.data.mongodb.MongoExpression;
4040
import org.springframework.data.mongodb.core.convert.MappingMongoConverter.NestedDocument;
4141
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
4242
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
@@ -295,6 +295,10 @@ protected Entry<String, Object> getMappedObjectForField(Field field, Object rawV
295295
String key = field.getMappedKey();
296296
Object value;
297297

298+
if (rawValue instanceof MongoExpression) {
299+
return createMapEntry(key, getMappedObject(((MongoExpression) rawValue).toDocument(), field.getEntity()));
300+
}
301+
298302
if (isNestedKeyword(rawValue) && !field.isIdField()) {
299303
Keyword keyword = new Keyword((Document) rawValue);
300304
value = getMappedKeyword(field, keyword);
@@ -934,6 +938,11 @@ public MongoPersistentEntity<?> getPropertyEntity() {
934938
return null;
935939
}
936940

941+
@Nullable
942+
MongoPersistentEntity<?> getEntity() {
943+
return null;
944+
}
945+
937946
/**
938947
* Returns whether the field represents an association.
939948
*
@@ -1086,6 +1095,12 @@ public MongoPersistentEntity<?> getPropertyEntity() {
10861095
return property == null ? null : mappingContext.getPersistentEntity(property);
10871096
}
10881097

1098+
@Nullable
1099+
@Override
1100+
public MongoPersistentEntity<?> getEntity() {
1101+
return entity;
1102+
}
1103+
10891104
/*
10901105
* (non-Javadoc)
10911106
* @see org.springframework.data.mongodb.core.convert.QueryMapper.Field#isAssociation()

0 commit comments

Comments
 (0)