Skip to content

Commit f911082

Browse files
Thomas Darimontodrotbohm
Thomas Darimont
authored andcommitted
DATAMONGO-407 - Fixed query mapping for updates using collection references.
When an update clause contained a collection element reference (….$.…) we failed to write the type information of the target value object as the key was not translated into a correct property path correctly. We now strip the reference literals and re-apply them when the mapped key is generated.
1 parent f301837 commit f911082

File tree

4 files changed

+235
-18
lines changed

4 files changed

+235
-18
lines changed

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

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,15 @@
2323
import org.bson.types.ObjectId;
2424
import org.springframework.core.convert.ConversionException;
2525
import org.springframework.core.convert.ConversionService;
26+
import org.springframework.core.convert.converter.Converter;
2627
import org.springframework.data.mapping.PersistentEntity;
2728
import org.springframework.data.mapping.PropertyPath;
2829
import org.springframework.data.mapping.PropertyReferenceException;
2930
import org.springframework.data.mapping.context.MappingContext;
3031
import org.springframework.data.mapping.context.PersistentPropertyPath;
3132
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
3233
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
34+
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty.PropertyToFieldNameConverter;
3335
import org.springframework.data.mongodb.core.query.Query;
3436
import org.springframework.util.Assert;
3537

@@ -102,7 +104,7 @@ public DBObject getMappedObject(DBObject query, MongoPersistentEntity<?> entity)
102104
continue;
103105
}
104106

105-
Field field = entity == null ? new Field(key) : new MetadataBackedField(key, entity, mappingContext);
107+
Field field = createPropertyField(entity, key, mappingContext);
106108

107109
Object rawValue = query.get(key);
108110
String newKey = field.getMappedKey();
@@ -118,6 +120,17 @@ public DBObject getMappedObject(DBObject query, MongoPersistentEntity<?> entity)
118120
return result;
119121
}
120122

123+
/**
124+
* @param entity
125+
* @param key
126+
* @param mappingContext
127+
* @return
128+
*/
129+
protected Field createPropertyField(MongoPersistentEntity<?> entity, String key,
130+
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext) {
131+
return entity == null ? new Field(key) : new MetadataBackedField(key, entity, mappingContext);
132+
}
133+
121134
/**
122135
* Returns the given {@link DBObject} representing a keyword by mapping the keyword's value.
123136
*
@@ -400,7 +413,7 @@ public <T> T getValue() {
400413
*
401414
* @author Oliver Gierke
402415
*/
403-
private static class Field {
416+
protected static class Field {
404417

405418
private static final String ID_KEY = "_id";
406419

@@ -477,12 +490,14 @@ public String getMappedKey() {
477490
* Extension of {@link DocumentField} to be backed with mapping metadata.
478491
*
479492
* @author Oliver Gierke
493+
* @author Thomas Darimont
480494
*/
481-
private static class MetadataBackedField extends Field {
495+
protected static class MetadataBackedField extends Field {
482496

483497
private final MongoPersistentEntity<?> entity;
484498
private final MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext;
485499
private final MongoPersistentProperty property;
500+
private final PersistentPropertyPath<MongoPersistentProperty> path;
486501

487502
/**
488503
* Creates a new {@link MetadataBackedField} with the given name, {@link MongoPersistentEntity} and
@@ -502,7 +517,7 @@ public MetadataBackedField(String name, MongoPersistentEntity<?> entity,
502517
this.entity = entity;
503518
this.mappingContext = context;
504519

505-
PersistentPropertyPath<MongoPersistentProperty> path = getPath(name);
520+
this.path = getPath(name);
506521
this.property = path == null ? null : path.getLeafProperty();
507522
}
508523

@@ -567,19 +582,33 @@ public boolean isAssociation() {
567582
*/
568583
@Override
569584
public String getMappedKey() {
570-
571-
PersistentPropertyPath<MongoPersistentProperty> path = getPath(name);
572-
return path == null ? name : path.toDotPath(MongoPersistentProperty.PropertyToFieldNameConverter.INSTANCE);
585+
return path == null ? name : path.toDotPath(getPropertyConverter());
573586
}
574587

575-
private PersistentPropertyPath<MongoPersistentProperty> getPath(String name) {
588+
/**
589+
* Returns the {@link PersistentPropertyPath} for the given <code>pathExpression</code>.
590+
*
591+
* @param pathExpression
592+
* @return
593+
*/
594+
private PersistentPropertyPath<MongoPersistentProperty> getPath(String pathExpression) {
576595

577596
try {
578-
PropertyPath path = PropertyPath.from(name, entity.getTypeInformation());
597+
PropertyPath path = PropertyPath.from(pathExpression, entity.getTypeInformation());
579598
return mappingContext.getPersistentPropertyPath(path);
580599
} catch (PropertyReferenceException e) {
581600
return null;
582601
}
583602
}
603+
604+
/**
605+
* Return the {@link Converter} to be used to created the mapped key. Default implementation will use
606+
* {@link PropertyToFieldNameConverter}.
607+
*
608+
* @return
609+
*/
610+
protected Converter<MongoPersistentProperty, String> getPropertyConverter() {
611+
return PropertyToFieldNameConverter.INSTANCE;
612+
}
584613
}
585614
}

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

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2013 the original author or authors.
2+
* Copyright 2013-2014 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -15,12 +15,21 @@
1515
*/
1616
package org.springframework.data.mongodb.core.convert;
1717

18+
import java.util.Arrays;
19+
import java.util.Iterator;
20+
21+
import org.springframework.core.convert.converter.Converter;
22+
import org.springframework.data.mapping.context.MappingContext;
1823
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
24+
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
25+
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty.PropertyToFieldNameConverter;
26+
import org.springframework.util.Assert;
1927

2028
/**
2129
* A subclass of {@link QueryMapper} that retains type information on the mongo types.
2230
*
2331
* @author Thomas Darimont
32+
* @author Oliver Gierke
2433
*/
2534
public class UpdateMapper extends QueryMapper {
2635

@@ -49,4 +58,90 @@ protected Object delegateConvertToMongoType(Object source, MongoPersistentEntity
4958
return entity == null ? super.delegateConvertToMongoType(source, null) : converter.convertToMongoType(source,
5059
entity.getTypeInformation());
5160
}
61+
62+
/*
63+
* (non-Javadoc)
64+
* @see org.springframework.data.mongodb.core.convert.QueryMapper#createPropertyField(org.springframework.data.mongodb.core.mapping.MongoPersistentEntity, java.lang.String, org.springframework.data.mapping.context.MappingContext)
65+
*/
66+
@Override
67+
protected Field createPropertyField(MongoPersistentEntity<?> entity, String key,
68+
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext) {
69+
70+
return entity == null ? super.createPropertyField(entity, key, mappingContext) : //
71+
new MetadataBackedUpdateField(entity, key, mappingContext);
72+
}
73+
74+
/**
75+
* {@link MetadataBackedField} that handles {@literal $} paths inside a field key. We clean up an update key
76+
* containing a {@literal $} before handing it to the super class to make sure property lookups and transformations
77+
* continue to work as expected. We provide a custom property converter to re-applied the cleaned up {@literal $}s
78+
* when constructing the mapped key.
79+
*
80+
* @author Thomas Darimont
81+
* @author Oliver Gierke
82+
*/
83+
private static class MetadataBackedUpdateField extends MetadataBackedField {
84+
85+
private final String key;
86+
87+
/**
88+
* Creates a new {@link MetadataBackedField} with the given {@link MongoPersistentEntity}, key and
89+
* {@link MappingContext}. We clean up the key before handing it up to the super class to make sure it continues to
90+
* work as expected.
91+
*
92+
* @param entity must not be {@literal null}.
93+
* @param key must not be {@literal null} or empty.
94+
* @param mappingContext must not be {@literal null}.
95+
*/
96+
public MetadataBackedUpdateField(MongoPersistentEntity<?> entity, String key,
97+
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext) {
98+
99+
super(key.replaceAll("\\.\\$", ""), entity, mappingContext);
100+
this.key = key;
101+
}
102+
103+
/*
104+
* (non-Javadoc)
105+
* @see org.springframework.data.mongodb.core.convert.QueryMapper.MetadataBackedField#getPropertyConverter()
106+
*/
107+
@Override
108+
protected Converter<MongoPersistentProperty, String> getPropertyConverter() {
109+
return new UpdatePropertyConverter(key);
110+
}
111+
112+
/**
113+
* Special {@link Converter} for {@link MongoPersistentProperty} instances that will concatenate the {@literal $}
114+
* contained in the source update key.
115+
*
116+
* @author Oliver Gierke
117+
*/
118+
private static class UpdatePropertyConverter implements Converter<MongoPersistentProperty, String> {
119+
120+
private final Iterator<String> iterator;
121+
122+
/**
123+
* Creates a new {@link UpdatePropertyConverter} with the given update key.
124+
*
125+
* @param updateKey must not be {@literal null} or empty.
126+
*/
127+
public UpdatePropertyConverter(String updateKey) {
128+
129+
Assert.hasText(updateKey, "Update key must not be null or empty!");
130+
131+
this.iterator = Arrays.asList(updateKey.split("\\.")).iterator();
132+
this.iterator.next();
133+
}
134+
135+
/*
136+
* (non-Javadoc)
137+
* @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object)
138+
*/
139+
@Override
140+
public String convert(MongoPersistentProperty property) {
141+
142+
String mappedName = PropertyToFieldNameConverter.INSTANCE.convert(property);
143+
return iterator.hasNext() && iterator.next().equals("$") ? String.format("%s.$", mappedName) : mappedName;
144+
}
145+
}
146+
}
52147
}

spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ protected void cleanDb() {
167167
template.dropCollection(ObjectWith3AliasedFieldsAndNestedAddress.class);
168168
template.dropCollection(BaseDoc.class);
169169
template.dropCollection(ObjectWithEnumValue.class);
170+
template.dropCollection(DocumentWithCollection.class);
170171
}
171172

172173
@Test
@@ -2205,6 +2206,40 @@ public void findAndModifyShouldRetrainTypeInformationWithinUpdatedType() {
22052206
Assert.assertThat(retrieved.model.value(), equalTo("value2"));
22062207
}
22072208

2209+
/**
2210+
* @see DATAMONGO-407
2211+
*/
2212+
@Test
2213+
public void updatesShouldRetainTypeInformationEvenForCollections() {
2214+
2215+
DocumentWithCollection doc = new DocumentWithCollection();
2216+
doc.id = "4711";
2217+
doc.model = new ArrayList<Model>();
2218+
doc.model.add(new ModelA("foo"));
2219+
template.insert(doc);
2220+
2221+
Query query = new Query(Criteria.where("id").is(doc.id));
2222+
query.addCriteria(where("model.value").is("foo"));
2223+
String newModelValue = "bar";
2224+
Update update = Update.update("model.$", new ModelA(newModelValue));
2225+
template.updateFirst(query, update, DocumentWithCollection.class);
2226+
2227+
Query findQuery = new Query(Criteria.where("id").is(doc.id));
2228+
DocumentWithCollection result = template.findOne(findQuery, DocumentWithCollection.class);
2229+
2230+
assertThat(result, is(notNullValue()));
2231+
assertThat(result.id, is(doc.id));
2232+
assertThat(result.model, is(notNullValue()));
2233+
assertThat(result.model, hasSize(1));
2234+
assertThat(result.model.get(0).value(), is(newModelValue));
2235+
}
2236+
2237+
static class DocumentWithCollection {
2238+
2239+
@Id public String id;
2240+
public List<Model> model;
2241+
}
2242+
22082243
static interface Model {
22092244
String value();
22102245
}
@@ -2221,7 +2256,6 @@ static class ModelA implements Model {
22212256
public String value() {
22222257
return this.value;
22232258
}
2224-
22252259
}
22262260

22272261
static class Document {

0 commit comments

Comments
 (0)