Skip to content

Commit 8ee33b2

Browse files
christophstroblmp911de
authored andcommitted
Generate and convert id on insert if explicitly defined.
We now make sure to provide an id value that matches the desired target type when no id is set, and the property defines an explicit conversion target. Previously a new ObjectId would have been generated which leads to type inconsistencies when querying for _id. Closes #4026 Original pull request: #4057.
1 parent 5f33987 commit 8ee33b2

File tree

8 files changed

+267
-17
lines changed

8 files changed

+267
-17
lines changed

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

+10
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,16 @@ public Document getDocument() {
9797
return this.document;
9898
}
9999

100+
/**
101+
* Updates the documents {@link #ID_FIELD}.
102+
*
103+
* @param value the {@literal _id} value to set.
104+
* @since 3.4.3
105+
*/
106+
public void updateId(Object value) {
107+
document.put(ID_FIELD, value);
108+
}
109+
100110
/**
101111
* An {@link UpdateDefinition} that indicates that the {@link #getUpdateObject() update object} has already been
102112
* mapped to the specific domain type.

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

+10-5
Original file line numberDiff line numberDiff line change
@@ -1543,18 +1543,21 @@ protected Object insertDocument(String collectionName, Document document, Class<
15431543
collectionName));
15441544
}
15451545

1546+
MappedDocument mappedDocument = queryOperations.createInsertContext(MappedDocument.of(document))
1547+
.prepareId(entityClass);
1548+
15461549
return execute(collectionName, collection -> {
15471550
MongoAction mongoAction = new MongoAction(writeConcern, MongoActionOperation.INSERT, collectionName, entityClass,
1548-
document, null);
1551+
mappedDocument.getDocument(), null);
15491552
WriteConcern writeConcernToUse = prepareWriteConcern(mongoAction);
15501553

15511554
if (writeConcernToUse == null) {
1552-
collection.insertOne(document);
1555+
collection.insertOne(mappedDocument.getDocument());
15531556
} else {
1554-
collection.withWriteConcern(writeConcernToUse).insertOne(document);
1557+
collection.withWriteConcern(writeConcernToUse).insertOne(mappedDocument.getDocument());
15551558
}
15561559

1557-
return operations.forEntity(document).getId();
1560+
return operations.forEntity(mappedDocument.getDocument()).getId();
15581561
});
15591562
}
15601563

@@ -1605,7 +1608,9 @@ protected Object saveDocument(String collectionName, Document dbDoc, Class<?> en
16051608
: collection.withWriteConcern(writeConcernToUse);
16061609

16071610
if (!mapped.hasId()) {
1608-
collectionToUse.insertOne(dbDoc);
1611+
1612+
mapped = queryOperations.createInsertContext(mapped).prepareId(mappingContext.getPersistentEntity(entityClass));
1613+
collectionToUse.insertOne(mapped.getDocument());
16091614
} else {
16101615

16111616
MongoPersistentEntity<?> entity = mappingContext.getPersistentEntity(entityClass);

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

+63-5
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
import org.bson.BsonValue;
2929
import org.bson.Document;
3030
import org.bson.codecs.Codec;
31-
31+
import org.bson.types.ObjectId;
3232
import org.springframework.data.mapping.PropertyPath;
3333
import org.springframework.data.mapping.PropertyReferenceException;
3434
import org.springframework.data.mapping.context.MappingContext;
@@ -46,6 +46,7 @@
4646
import org.springframework.data.mongodb.core.aggregation.TypedAggregation;
4747
import org.springframework.data.mongodb.core.convert.QueryMapper;
4848
import org.springframework.data.mongodb.core.convert.UpdateMapper;
49+
import org.springframework.data.mongodb.core.mapping.MongoId;
4950
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
5051
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
5152
import org.springframework.data.mongodb.core.mapping.ShardKey;
@@ -107,6 +108,14 @@ class QueryOperations {
107108
this.aggregationUtil = new AggregationUtil(queryMapper, mappingContext);
108109
}
109110

111+
InsertContext createInsertContext(Document source) {
112+
return createInsertContext(MappedDocument.of(source));
113+
}
114+
115+
InsertContext createInsertContext(MappedDocument mappedDocument) {
116+
return new InsertContext(mappedDocument);
117+
}
118+
110119
/**
111120
* Create a new {@link QueryContext} instance.
112121
*
@@ -227,6 +236,57 @@ AggregationDefinition createAggregation(Aggregation aggregation,
227236
return new AggregationDefinition(aggregation, aggregationOperationContext);
228237
}
229238

239+
/**
240+
* {@link InsertContext} encapsulates common tasks required to interact with {@link Document} to be inserted.
241+
*
242+
* @since 3.4.3
243+
*/
244+
class InsertContext {
245+
246+
private final MappedDocument source;
247+
248+
private InsertContext(MappedDocument source) {
249+
this.source = source;
250+
}
251+
252+
/**
253+
* Prepare the {@literal _id} field. May generate a new {@literal id} value and convert it to the id properties
254+
* {@link MongoPersistentProperty#getFieldType() target type}.
255+
*
256+
* @param type must not be {@literal null}.
257+
* @param <T>
258+
* @return the {@link MappedDocument} containing the changes.
259+
* @see #prepareId(MongoPersistentEntity)
260+
*/
261+
<T> MappedDocument prepareId(Class<T> type) {
262+
return prepareId(mappingContext.getPersistentEntity(type));
263+
}
264+
265+
/**
266+
* Prepare the {@literal _id} field. May generate a new {@literal id} value and convert it to the id properties
267+
* {@link MongoPersistentProperty#getFieldType() target type}.
268+
*
269+
* @param entity can be {@literal null}.
270+
* @param <T>
271+
* @return the {@link MappedDocument} containing the changes.
272+
*/
273+
<T> MappedDocument prepareId(@Nullable MongoPersistentEntity<T> entity) {
274+
275+
if (entity == null) {
276+
return source;
277+
}
278+
279+
MongoPersistentProperty idProperty = entity.getIdProperty();
280+
if (idProperty != null
281+
&& (idProperty.hasExplicitWriteTarget() || idProperty.isAnnotationPresent(MongoId.class))) {
282+
if (!ClassUtils.isAssignable(ObjectId.class, idProperty.getFieldType())) {
283+
source.updateId(queryMapper.convertId(new ObjectId(), idProperty.getFieldType()));
284+
}
285+
}
286+
return source;
287+
}
288+
}
289+
230290
/**
231291
* {@link QueryContext} encapsulates common tasks required to convert a {@link Query} into its MongoDB document
232292
* representation, mapping field names, as well as determining and applying {@link Collation collations}.
@@ -288,8 +348,7 @@ <T> Document getMappedQuery(@Nullable MongoPersistentEntity<T> entity) {
288348
return queryMapper.getMappedObject(getQueryObject(), entity);
289349
}
290350

291-
Document getMappedFields(@Nullable MongoPersistentEntity<?> entity,
292-
EntityProjection<?, ?> projection) {
351+
Document getMappedFields(@Nullable MongoPersistentEntity<?> entity, EntityProjection<?, ?> projection) {
293352

294353
Document fields = evaluateFields(entity);
295354

@@ -402,8 +461,7 @@ private DistinctQueryContext(@Nullable Object query, String fieldName) {
402461
}
403462

404463
@Override
405-
Document getMappedFields(@Nullable MongoPersistentEntity<?> entity,
406-
EntityProjection<?, ?> projection) {
464+
Document getMappedFields(@Nullable MongoPersistentEntity<?> entity, EntityProjection<?, ?> projection) {
407465
return getMappedFields(entity);
408466
}
409467

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

+5-4
Original file line numberDiff line numberDiff line change
@@ -1686,7 +1686,8 @@ protected Mono<Object> insertDocument(String collectionName, Document dbDoc, Cla
16861686
.format("Inserting Document containing fields: " + dbDoc.keySet() + " in collection: " + collectionName));
16871687
}
16881688

1689-
Document document = new Document(dbDoc);
1689+
MappedDocument document = MappedDocument.of(dbDoc);
1690+
queryOperations.createInsertContext(document).prepareId(entityClass);
16901691

16911692
Flux<InsertOneResult> execute = execute(collectionName, collection -> {
16921693

@@ -1696,10 +1697,10 @@ protected Mono<Object> insertDocument(String collectionName, Document dbDoc, Cla
16961697

16971698
MongoCollection<Document> collectionToUse = prepareCollection(collection, writeConcernToUse);
16981699

1699-
return collectionToUse.insertOne(document);
1700+
return collectionToUse.insertOne(document.getDocument());
17001701
});
17011702

1702-
return Flux.from(execute).last().map(success -> MappedDocument.of(document).getId());
1703+
return Flux.from(execute).last().map(success -> document.getId());
17031704
}
17041705

17051706
protected Flux<ObjectId> insertDocumentList(String collectionName, List<Document> dbDocList) {
@@ -1763,7 +1764,7 @@ protected Mono<Object> saveDocument(String collectionName, Document document, Cl
17631764

17641765
Publisher<?> publisher;
17651766
if (!mapped.hasId()) {
1766-
publisher = collectionToUse.insertOne(document);
1767+
publisher = collectionToUse.insertOne(queryOperations.createInsertContext(mapped).prepareId(entityClass).getDocument());
17671768
} else {
17681769

17691770
MongoPersistentEntity<?> entity = mappingContext.getPersistentEntity(entityClass);

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

+33
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import java.util.stream.Collectors;
4040
import java.util.stream.IntStream;
4141

42+
import org.bson.Document;
4243
import org.bson.types.ObjectId;
4344
import org.joda.time.DateTime;
4445
import org.junit.jupiter.api.AfterEach;
@@ -3658,6 +3659,38 @@ public void saveAndLoadStringThatIsAnObjectIdAsString() {
36583659
assertThat(target).isEqualTo(source);
36593660
}
36603661

3662+
@Test // GH-4026
3663+
void saveShouldGenerateNewIdOfTypeIfExplicitlyDefined() {
3664+
3665+
RawStringId source = new RawStringId();
3666+
source.value = "new value";
3667+
3668+
template.save(source);
3669+
3670+
template.execute(RawStringId.class, collection -> {
3671+
3672+
org.bson.Document first = collection.find(new org.bson.Document()).first();
3673+
assertThat(first.get("_id")).isInstanceOf(String.class);
3674+
return null;
3675+
});
3676+
}
3677+
3678+
@Test // GH-4026
3679+
void insertShouldGenerateNewIdOfTypeIfExplicitlyDefined() {
3680+
3681+
RawStringId source = new RawStringId();
3682+
source.value = "new value";
3683+
3684+
template.insert(source);
3685+
3686+
template.execute(RawStringId.class, collection -> {
3687+
3688+
org.bson.Document first = collection.find(new org.bson.Document()).first();
3689+
assertThat(first.get("_id")).isInstanceOf(String.class);
3690+
return null;
3691+
});
3692+
}
3693+
36613694
@Test // DATAMONGO-2193
36623695
public void shouldNotConvertStringToObjectIdForNonIdField() {
36633696

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

+97
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
import static org.assertj.core.api.Assertions.*;
1919
import static org.mockito.Mockito.*;
2020

21+
import org.bson.Document;
22+
import org.bson.types.ObjectId;
2123
import org.junit.jupiter.api.BeforeEach;
2224
import org.junit.jupiter.api.Test;
2325
import org.junit.jupiter.api.extension.ExtendWith;
@@ -32,7 +34,10 @@
3234
import org.springframework.data.mongodb.core.aggregation.TypeBasedAggregationOperationContext;
3335
import org.springframework.data.mongodb.core.convert.QueryMapper;
3436
import org.springframework.data.mongodb.core.convert.UpdateMapper;
37+
import org.springframework.data.mongodb.core.mapping.MongoId;
3538
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
39+
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
40+
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
3641

3742
/**
3843
* Unit tests for {@link QueryOperations}.
@@ -109,7 +114,99 @@ void createAggregationContextUsesStrictlyTypedContextForTypedAggregationsWhenReq
109114
assertThat(ctx.getAggregationOperationContext()).isInstanceOf(TypeBasedAggregationOperationContext.class);
110115
}
111116

117+
@Test // GH-4026
118+
void insertContextDoesNotAddIdIfNoPersistentEntityCanBeFound() {
119+
120+
assertThat(queryOperations.createInsertContext(new Document("value", "one")).prepareId(Person.class).getDocument())//
121+
.satisfies(result -> {
122+
assertThat(result).isEqualTo(new Document("value", "one"));
123+
});
124+
}
125+
126+
@Test // GH-4026
127+
void insertContextDoesNotAddIdIfNoIdPropertyCanBeFound() {
128+
129+
MongoPersistentEntity<Person> entity = mock(MongoPersistentEntity.class);
130+
when(entity.getIdProperty()).thenReturn(null);
131+
when(mappingContext.getPersistentEntity(eq(Person.class))).thenReturn((MongoPersistentEntity) entity);
132+
133+
assertThat(queryOperations.createInsertContext(new Document("value", "one")).prepareId(Person.class).getDocument())//
134+
.satisfies(result -> {
135+
assertThat(result).isEqualTo(new Document("value", "one"));
136+
});
137+
}
138+
139+
@Test // GH-4026
140+
void insertContextDoesNotAddConvertedIdForNonExplicitFieldTypes() {
141+
142+
MongoPersistentEntity<Person> entity = mock(MongoPersistentEntity.class);
143+
MongoPersistentProperty property = mock(MongoPersistentProperty.class);
144+
when(entity.getIdProperty()).thenReturn(property);
145+
when(property.hasExplicitWriteTarget()).thenReturn(false);
146+
doReturn(entity).when(mappingContext).getPersistentEntity(eq(Person.class));
147+
148+
assertThat(queryOperations.createInsertContext(new Document("value", "one")).prepareId(Person.class).getDocument())//
149+
.satisfies(result -> {
150+
assertThat(result).isEqualTo(new Document("value", "one"));
151+
});
152+
}
153+
154+
@Test // GH-4026
155+
void insertContextAddsConvertedIdForExplicitFieldTypes() {
156+
157+
MongoPersistentEntity<Person> entity = mock(MongoPersistentEntity.class);
158+
MongoPersistentProperty property = mock(MongoPersistentProperty.class);
159+
when(entity.getIdProperty()).thenReturn(property);
160+
when(property.hasExplicitWriteTarget()).thenReturn(true);
161+
doReturn(String.class).when(property).getFieldType();
162+
doReturn(entity).when(mappingContext).getPersistentEntity(eq(Person.class));
163+
164+
when(queryMapper.convertId(any(), eq(String.class))).thenReturn("&#9774;");
165+
166+
assertThat(queryOperations.createInsertContext(new Document("value", "one")).prepareId(Person.class).getDocument())//
167+
.satisfies(result -> {
168+
assertThat(result).isEqualTo(new Document("value", "one").append("_id", "&#9774;"));
169+
});
170+
}
171+
172+
@Test // GH-4026
173+
void insertContextAddsConvertedIdForMongoIdTypes() {
174+
175+
MongoPersistentEntity<Person> entity = mock(MongoPersistentEntity.class);
176+
MongoPersistentProperty property = mock(MongoPersistentProperty.class);
177+
when(entity.getIdProperty()).thenReturn(property);
178+
when(property.hasExplicitWriteTarget()).thenReturn(false);
179+
when(property.isAnnotationPresent(eq(MongoId.class))).thenReturn(true);
180+
doReturn(String.class).when(property).getFieldType();
181+
doReturn(entity).when(mappingContext).getPersistentEntity(eq(Person.class));
182+
183+
when(queryMapper.convertId(any(), eq(String.class))).thenReturn("&#9774;");
184+
185+
assertThat(queryOperations.createInsertContext(new Document("value", "one")).prepareId(Person.class).getDocument())//
186+
.satisfies(result -> {
187+
assertThat(result).isEqualTo(new Document("value", "one").append("_id", "&#9774;"));
188+
});
189+
}
190+
191+
@Test // GH-4026
192+
void insertContextDoesNotAddConvertedIdForMongoIdTypesTargetingObjectId() {
193+
194+
MongoPersistentEntity<Person> entity = mock(MongoPersistentEntity.class);
195+
MongoPersistentProperty property = mock(MongoPersistentProperty.class);
196+
when(entity.getIdProperty()).thenReturn(property);
197+
when(property.hasExplicitWriteTarget()).thenReturn(false);
198+
when(property.isAnnotationPresent(eq(MongoId.class))).thenReturn(true);
199+
doReturn(ObjectId.class).when(property).getFieldType();
200+
doReturn(entity).when(mappingContext).getPersistentEntity(eq(Person.class));
201+
202+
assertThat(queryOperations.createInsertContext(new Document("value", "one")).prepareId(Person.class).getDocument())//
203+
.satisfies(result -> {
204+
assertThat(result).isEqualTo(new Document("value", "one"));
205+
});
206+
}
207+
112208
static class Person {
113209

114210
}
211+
115212
}

0 commit comments

Comments
 (0)