Skip to content

Commit a173d87

Browse files
christophstroblmp911de
authored andcommitted
Add support for mapping document fields with . (dot) in the field name.
This commit introduces support for mapping (read/write) fields that contain dots in their name, preserving the name as is instead of considering the dot being a deliminator within a path of nested objects. Query and Update functionality remains unaffected which means no automatic rewrite for field names containing paths will NOT take place. It's in the users responsibility to pick the appropriate query/update operator (eg. $expr) to interact with the field.
1 parent 2322540 commit a173d87

File tree

19 files changed

+946
-82
lines changed

19 files changed

+946
-82
lines changed

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

+57-4
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import java.util.Arrays;
1919
import java.util.Collection;
2020
import java.util.Collections;
21+
import java.util.Map;
2122

2223
import org.bson.Document;
2324
import org.springframework.util.Assert;
@@ -52,6 +53,41 @@ public static ObjectOperatorFactory valueOf(AggregationExpression expression) {
5253
return new ObjectOperatorFactory(expression);
5354
}
5455

56+
/**
57+
* Use the value from the given {@link SystemVariable} as input for the target {@link AggregationExpression expression}.
58+
*
59+
* @param variable the {@link SystemVariable} to use (eg. {@link SystemVariable#ROOT}.
60+
* @return new instance of {@link ObjectOperatorFactory}.
61+
* @since 4.2
62+
*/
63+
public static ObjectOperatorFactory valueOf(SystemVariable variable) {
64+
return new ObjectOperatorFactory(Fields.field(variable.getName(), variable.getTarget()));
65+
}
66+
67+
/**
68+
* Get the value of the field with given name from the {@literal $$CURRENT} object.
69+
* Short version for {@code ObjectOperators.valueOf("$$CURRENT").getField(fieldName)}.
70+
*
71+
* @param fieldName the field name.
72+
* @return new instance of {@link AggregationExpression}.
73+
* @since 4.2
74+
*/
75+
public static AggregationExpression getValueOf(String fieldName) {
76+
return new ObjectOperatorFactory(SystemVariable.CURRENT).getField(fieldName);
77+
}
78+
79+
/**
80+
* Set the value of the field with given name on the {@literal $$CURRENT} object.
81+
* Short version for {@code ObjectOperators.valueOf($$CURRENT).setField(fieldName).toValue(value)}.
82+
*
83+
* @param fieldName the field name.
84+
* @return new instance of {@link AggregationExpression}.
85+
* @since 4.2
86+
*/
87+
public static AggregationExpression setValueTo(String fieldName, Object value) {
88+
return new ObjectOperatorFactory(SystemVariable.CURRENT).setField(fieldName).toValue(value);
89+
}
90+
5591
/**
5692
* @author Christoph Strobl
5793
*/
@@ -133,7 +169,7 @@ public ObjectToArray toArray() {
133169
* @since 4.0
134170
*/
135171
public GetField getField(String fieldName) {
136-
return GetField.getField(fieldName).of(value);
172+
return GetField.getField(Fields.field(fieldName)).of(value);
137173
}
138174

139175
/**
@@ -143,7 +179,7 @@ public GetField getField(String fieldName) {
143179
* @since 4.0
144180
*/
145181
public SetField setField(String fieldName) {
146-
return SetField.field(fieldName).input(value);
182+
return SetField.field(Fields.field(fieldName)).input(value);
147183
}
148184

149185
/**
@@ -340,7 +376,7 @@ public static GetField getField(String fieldName) {
340376
* @return new instance of {@link GetField}.
341377
*/
342378
public static GetField getField(Field field) {
343-
return getField(field.getTarget());
379+
return new GetField(Collections.singletonMap("field", field));
344380
}
345381

346382
/**
@@ -369,6 +405,15 @@ private GetField of(Object fieldRef) {
369405
return new GetField(append("input", fieldRef));
370406
}
371407

408+
@Override
409+
public Document toDocument(AggregationOperationContext context) {
410+
411+
if(isArgumentMap() && get("field") instanceof Field field) {
412+
return new GetField(append("field", context.getReference(field).getRaw())).toDocument(context);
413+
}
414+
return super.toDocument(context);
415+
}
416+
372417
@Override
373418
protected String getMongoMethod() {
374419
return "$getField";
@@ -405,7 +450,7 @@ public static SetField field(String fieldName) {
405450
* @return new instance of {@link SetField}.
406451
*/
407452
public static SetField field(Field field) {
408-
return field(field.getTarget());
453+
return new SetField(Collections.singletonMap("field", field));
409454
}
410455

411456
/**
@@ -472,6 +517,14 @@ public SetField toValue(Object value) {
472517
return new SetField(append("value", value));
473518
}
474519

520+
@Override
521+
public Document toDocument(AggregationOperationContext context) {
522+
if(get("field") instanceof Field field) {
523+
return new SetField(append("field", context.getReference(field).getRaw())).toDocument(context);
524+
}
525+
return super.toDocument(context);
526+
}
527+
475528
@Override
476529
protected String getMongoMethod() {
477530
return "$setField";

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

+4-10
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
import org.bson.Document;
2323
import org.bson.conversions.Bson;
24-
24+
import org.springframework.data.mongodb.core.mapping.FieldName;
2525
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
2626
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
2727
import org.springframework.data.mongodb.util.BsonUtils;
@@ -91,14 +91,8 @@ public void putAll(Document source) {
9191
public void put(MongoPersistentProperty prop, @Nullable Object value) {
9292

9393
Assert.notNull(prop, "MongoPersistentProperty must not be null");
94-
String fieldName = getFieldName(prop);
95-
96-
if (!fieldName.contains(".")) {
97-
BsonUtils.addToMap(document, fieldName, value);
98-
return;
99-
}
10094

101-
Iterator<String> parts = Arrays.asList(fieldName.split("\\.")).iterator();
95+
Iterator<String> parts = Arrays.asList(prop.getMongoField().getFieldName().parts()).iterator();
10296
Bson document = this.document;
10397

10498
while (parts.hasNext()) {
@@ -153,8 +147,8 @@ public boolean hasValue(MongoPersistentProperty property) {
153147
return BsonUtils.hasValue(document, getFieldName(property));
154148
}
155149

156-
String getFieldName(MongoPersistentProperty prop) {
157-
return prop.getFieldName();
150+
FieldName getFieldName(MongoPersistentProperty prop) {
151+
return prop.getMongoField().getFieldName();
158152
}
159153

160154
/**

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

+13-2
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
import org.springframework.data.mongodb.MongoDatabaseFactory;
7373
import org.springframework.data.mongodb.core.mapping.BasicMongoPersistentProperty;
7474
import org.springframework.data.mongodb.core.mapping.DocumentPointer;
75+
import org.springframework.data.mongodb.core.mapping.FieldName;
7576
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
7677
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
7778
import org.springframework.data.mongodb.core.mapping.PersistentPropertyTranslator;
@@ -244,6 +245,16 @@ public void setMapKeyDotReplacement(@Nullable String mapKeyDotReplacement) {
244245
this.mapKeyDotReplacement = mapKeyDotReplacement;
245246
}
246247

248+
/**
249+
* If {@link #preserveMapKeys(boolean) preserve} is set to {@literal true} the conversion will treat map keys containing {@literal .} (dot) characters as is.
250+
*
251+
* @since 4.2
252+
* @see #setMapKeyDotReplacement(String)
253+
*/
254+
public void preserveMapKeys(boolean preserve) {
255+
setMapKeyDotReplacement(preserve ? "." : null);
256+
}
257+
247258
/**
248259
* Configure a {@link CodecRegistryProvider} that provides native MongoDB {@link org.bson.codecs.Codec codecs} for
249260
* reading values.
@@ -345,8 +356,8 @@ private <R> R doReadProjection(ConversionContext context, Bson bson, EntityProje
345356
Predicates.negate(MongoPersistentProperty::hasExplicitFieldName));
346357
DocumentAccessor documentAccessor = new DocumentAccessor(bson) {
347358
@Override
348-
String getFieldName(MongoPersistentProperty prop) {
349-
return propertyTranslator.translate(prop).getFieldName();
359+
FieldName getFieldName(MongoPersistentProperty prop) {
360+
return propertyTranslator.translate(prop).getMongoField().getFieldName();
350361
}
351362
};
352363

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

+12
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
import org.springframework.data.mongodb.core.aggregation.AggregationExpression;
4848
import org.springframework.data.mongodb.core.aggregation.RelaxedTypeBasedAggregationOperationContext;
4949
import org.springframework.data.mongodb.core.convert.MappingMongoConverter.NestedDocument;
50+
import org.springframework.data.mongodb.core.mapping.FieldName.Type;
5051
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
5152
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
5253
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty.PropertyToFieldNameConverter;
@@ -168,6 +169,14 @@ public Document getMappedObject(Bson query, @Nullable MongoPersistentEntity<?> e
168169

169170
Entry<String, Object> entry = getMappedObjectForField(field, BsonUtils.get(query, key));
170171

172+
/*
173+
* Note to future self:
174+
* ----
175+
* This could be the place to plug in a query rewrite mechanism that allows to transform comparison
176+
* against field that has a dot in its name (like 'a.b') into an $expr so that { "a.b" : "some value" }
177+
* eventually becomes { $expr : { $eq : [ { $getField : "a.b" }, "some value" ] } }
178+
* ----
179+
*/
171180
result.put(entry.getKey(), entry.getValue());
172181
}
173182
} catch (InvalidPersistentPropertyPath invalidPathException) {
@@ -1213,6 +1222,9 @@ public Class<?> getFieldType() {
12131222

12141223
@Override
12151224
public String getMappedKey() {
1225+
if(getProperty() != null && getProperty().getMongoField().getFieldName().isOfType(Type.KEY)) {
1226+
return getProperty().getFieldName();
1227+
}
12161228
return path == null ? name : path.toDotPath(isAssociation() ? getAssociationConverter() : getPropertyConverter());
12171229
}
12181230

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

+63-31
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,15 @@
2626
import org.apache.commons.logging.Log;
2727
import org.apache.commons.logging.LogFactory;
2828
import org.bson.types.ObjectId;
29-
3029
import org.springframework.data.mapping.Association;
3130
import org.springframework.data.mapping.MappingException;
3231
import org.springframework.data.mapping.model.AnnotationBasedPersistentProperty;
3332
import org.springframework.data.mapping.model.FieldNamingStrategy;
3433
import org.springframework.data.mapping.model.Property;
3534
import org.springframework.data.mapping.model.PropertyNameFieldNamingStrategy;
3635
import org.springframework.data.mapping.model.SimpleTypeHolder;
36+
import org.springframework.data.mongodb.core.mapping.FieldName.Type;
37+
import org.springframework.data.mongodb.core.mapping.MongoField.MongoFieldBuilder;
3738
import org.springframework.data.mongodb.util.encryption.EncryptionUtils;
3839
import org.springframework.data.util.Lazy;
3940
import org.springframework.expression.EvaluationContext;
@@ -131,30 +132,7 @@ public boolean isExplicitIdProperty() {
131132
* @return
132133
*/
133134
public String getFieldName() {
134-
135-
if (isIdProperty()) {
136-
137-
if (getOwner().getIdProperty() == null) {
138-
return ID_FIELD_NAME;
139-
}
140-
141-
if (getOwner().isIdProperty(this)) {
142-
return ID_FIELD_NAME;
143-
}
144-
}
145-
146-
if (hasExplicitFieldName()) {
147-
return getAnnotatedFieldName();
148-
}
149-
150-
String fieldName = fieldNamingStrategy.getFieldName(this);
151-
152-
if (!StringUtils.hasText(fieldName)) {
153-
throw new MappingException(String.format("Invalid (null or empty) field name returned for property %s by %s",
154-
this, fieldNamingStrategy.getClass()));
155-
}
156-
157-
return fieldName;
135+
return getMongoField().getFieldName().name();
158136
}
159137

160138
@Override
@@ -175,7 +153,7 @@ public Class<?> getFieldType() {
175153
return FieldType.OBJECT_ID.getJavaClass();
176154
}
177155

178-
FieldType fieldType = fieldAnnotation.targetType();
156+
FieldType fieldType = getMongoField().getFieldType();
179157
if (fieldType == FieldType.IMPLICIT) {
180158

181159
if (isEntity()) {
@@ -207,11 +185,7 @@ private String getAnnotatedFieldName() {
207185
}
208186

209187
public int getFieldOrder() {
210-
211-
org.springframework.data.mongodb.core.mapping.Field annotation = findAnnotation(
212-
org.springframework.data.mongodb.core.mapping.Field.class);
213-
214-
return annotation != null ? annotation.order() : Integer.MAX_VALUE;
188+
return getMongoField().getFieldOrder();
215189
}
216190

217191
@Override
@@ -278,6 +252,11 @@ public EvaluationContext getEvaluationContext(@Nullable Object rootObject) {
278252
return rootObject != null ? new StandardEvaluationContext(rootObject) : new StandardEvaluationContext();
279253
}
280254

255+
@Override
256+
public MongoField getMongoField() {
257+
return doGetMongoField();
258+
}
259+
281260
@Override
282261
public Collection<Object> getEncryptionKeyIds() {
283262

@@ -302,4 +281,57 @@ public Collection<Object> getEncryptionKeyIds() {
302281
}
303282
return target;
304283
}
284+
285+
protected MongoField doGetMongoField() {
286+
287+
MongoFieldBuilder builder = MongoField.builder();
288+
if (isAnnotationPresent(Field.class) && Type.KEY.equals(findAnnotation(Field.class).nameType())) {
289+
builder.fieldName(doGetFieldName());
290+
} else {
291+
builder.fieldPath(doGetFieldName());
292+
}
293+
builder.fieldType(doGetFieldType());
294+
builder.fieldOrderNumber(doGetFieldOrder());
295+
return builder.build();
296+
}
297+
298+
private String doGetFieldName() {
299+
300+
if (isIdProperty()) {
301+
302+
if (getOwner().getIdProperty() == null) {
303+
return ID_FIELD_NAME;
304+
}
305+
306+
if (getOwner().isIdProperty(this)) {
307+
return ID_FIELD_NAME;
308+
}
309+
}
310+
311+
if (hasExplicitFieldName()) {
312+
return getAnnotatedFieldName();
313+
}
314+
315+
String fieldName = fieldNamingStrategy.getFieldName(this);
316+
317+
if (!StringUtils.hasText(fieldName)) {
318+
throw new MappingException(String.format("Invalid (null or empty) field name returned for property %s by %s",
319+
this, fieldNamingStrategy.getClass()));
320+
}
321+
322+
return fieldName;
323+
}
324+
325+
private FieldType doGetFieldType() {
326+
327+
Field fieldAnnotation = findAnnotation(Field.class);
328+
return fieldAnnotation != null ? fieldAnnotation.targetType() : FieldType.IMPLICIT;
329+
}
330+
331+
private int doGetFieldOrder() {
332+
333+
Field annotation = findAnnotation(Field.class);
334+
return annotation != null ? annotation.order() : Integer.MAX_VALUE;
335+
}
336+
305337
}

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

+8
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,15 @@
1818
import org.springframework.data.mapping.model.FieldNamingStrategy;
1919
import org.springframework.data.mapping.model.Property;
2020
import org.springframework.data.mapping.model.SimpleTypeHolder;
21+
import org.springframework.data.util.Lazy;
2122
import org.springframework.lang.Nullable;
2223

2324
/**
2425
* {@link MongoPersistentProperty} caching access to {@link #isIdProperty()} and {@link #getFieldName()}.
2526
*
2627
* @author Oliver Gierke
2728
* @author Mark Paluch
29+
* @author Christoph Strobl
2830
*/
2931
public class CachingMongoPersistentProperty extends BasicMongoPersistentProperty {
3032

@@ -37,6 +39,7 @@ public class CachingMongoPersistentProperty extends BasicMongoPersistentProperty
3739
private @Nullable Class<?> fieldType;
3840
private @Nullable Boolean usePropertyAccess;
3941
private @Nullable Boolean isTransient;
42+
private @Nullable Lazy<MongoField> mongoField = Lazy.of(super::getMongoField);
4043

4144
/**
4245
* Creates a new {@link CachingMongoPersistentProperty}.
@@ -134,4 +137,9 @@ public DBRef getDBRef() {
134137

135138
return this.dbref;
136139
}
140+
141+
@Override
142+
public MongoField getMongoField() {
143+
return mongoField.get();
144+
}
137145
}

0 commit comments

Comments
 (0)