Skip to content

Commit 5d39191

Browse files
christophstroblmp911de
authored andcommitted
DATAMONGO-1798 - Introduce @mongoid annotation for fine grained id conversion control.
@mongoid allows more fine grained control over id conversion by specifying the intended id target type. This allows to skip the automatic to ObjectId conversion of values that happen to be valid ObjectId hex strings. public class PlainStringId { @mongoid String id; // treated as String no matter what } public class PlainObjectId { @mongoid ObjectId id; // treated as ObjectId } public class StringToObjectId { @mongoid(FieldType.OBJECT_ID) String id; // treated as ObjectId if the value is a valid ObjectId hex string } Original pull request: spring-projects#617.
1 parent 75b6dc7 commit 5d39191

File tree

13 files changed

+425
-31
lines changed

13 files changed

+425
-31
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright 2018 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+
* http://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 java.lang.annotation.ElementType;
19+
import java.lang.annotation.Retention;
20+
import java.lang.annotation.RetentionPolicy;
21+
import java.lang.annotation.Target;
22+
23+
import org.springframework.core.annotation.AliasFor;
24+
import org.springframework.data.annotation.Id;
25+
26+
/**
27+
* {@link MongoId} represents a MongoDB specific {@link Id} annotation that allows tweaking {@literal id} conversion. By
28+
* default {@link Object Class<Object>} will be used as the {@literal id's} target type. This means that the
29+
* actual property value is used. No conversion attempts to any other type is made. <br />
30+
* In contrast to {@link Id &#64;Id}, {@link String} {@literal id's} are stored as the such even when the actual value
31+
* represents a valid {@link org.bson.types.ObjectId#isValid(String) ObjectId hex String}. To trigger {@link String} to
32+
* {@link org.bson.types.ObjectId} conversion use {@link MongoId#targetType() &#64;MongoId(ObjectId.class)}.
33+
*
34+
* @author Christoph Strobl
35+
* @since 2.2
36+
*/
37+
@Id
38+
@Retention(RetentionPolicy.RUNTIME)
39+
@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE })
40+
public @interface MongoId {
41+
42+
/**
43+
* @return the preferred id type.
44+
* @see #targetType()
45+
*/
46+
@AliasFor("targetType")
47+
Class<?> value() default Object.class;
48+
49+
/**
50+
* Get the preferred {@literal _id} type to be used. Defaulted to {@link Object Class&lt;Object&gt;} which used the
51+
* property's type. If defined different, the given value is attempted to be converted into the desired target type
52+
* via {@link org.springframework.data.mongodb.core.convert.MongoConverter#convertId(Object, Class)}.
53+
*
54+
* @return the preferred {@literal id} type. {@link Object Class&lt;Object&gt;} by default.
55+
*/
56+
@AliasFor("value")
57+
Class<?> targetType() default Object.class;
58+
59+
}

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

+3-2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import org.bson.Document;
2222
import org.bson.conversions.Bson;
23+
import org.bson.types.ObjectId;
2324
import org.slf4j.Logger;
2425
import org.slf4j.LoggerFactory;
2526
import org.springframework.beans.BeansException;
@@ -516,7 +517,7 @@ protected void writeInternal(@Nullable Object obj, Bson bson, @Nullable MongoPer
516517

517518
if (idProperty != null && !dbObjectAccessor.hasValue(idProperty)) {
518519

519-
Object value = idMapper.convertId(accessor.getProperty(idProperty));
520+
Object value = idMapper.convertId(accessor.getProperty(idProperty), idProperty.getIdType());
520521

521522
if (value != null) {
522523
dbObjectAccessor.put(idProperty, value);
@@ -981,7 +982,7 @@ protected DBRef createDBRef(Object target, MongoPersistentProperty property) {
981982
throw new MappingException("Cannot create a reference to an object with a NULL id.");
982983
}
983984

984-
return dbRefResolver.createDbRef(property == null ? null : property.getDBRef(), entity, idMapper.convertId(id));
985+
return dbRefResolver.createDbRef(property == null ? null : property.getDBRef(), entity, idMapper.convertId(id, idProperty != null ? idProperty.getIdType() : ObjectId.class));
985986
}
986987

987988
throw new MappingException("No id property found on class " + entity.getType());

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

+46-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
import org.bson.BsonValue;
1919
import org.bson.Document;
2020
import org.bson.conversions.Bson;
21+
import org.bson.types.ObjectId;
22+
import org.springframework.core.convert.ConversionException;
2123
import org.springframework.data.convert.EntityConverter;
2224
import org.springframework.data.convert.EntityReader;
2325
import org.springframework.data.convert.TypeMapper;
@@ -83,7 +85,18 @@ default <S, T> T mapValueToTargetType(S source, Class<T> targetType, DbRefResolv
8385

8486
if (sourceDocument.containsKey("$ref") && sourceDocument.containsKey("$id")) {
8587

86-
sourceDocument = dbRefResolver.fetch(new DBRef(sourceDocument.getString("$ref"), sourceDocument.get("$id")));
88+
Object id = sourceDocument.get("$id");
89+
String collection = sourceDocument.getString("$ref");
90+
91+
MongoPersistentEntity<?> entity = getMappingContext().getPersistentEntity(targetType);
92+
if (entity.getIdType() != null) {
93+
id = convertId(id, entity.getIdType());
94+
}
95+
96+
DBRef ref = sourceDocument.containsKey("$db") ? new DBRef(sourceDocument.getString("$db"), collection, id)
97+
: new DBRef(collection, id);
98+
99+
sourceDocument = dbRefResolver.fetch(ref);
87100
if (sourceDocument == null) {
88101
return null;
89102
}
@@ -102,4 +115,36 @@ default <S, T> T mapValueToTargetType(S source, Class<T> targetType, DbRefResolv
102115
}
103116
return getConversionService().convert(source, targetType);
104117
}
118+
119+
/**
120+
* Converts the given raw id value into either {@link ObjectId} or {@link String}.
121+
*
122+
* @param id
123+
* @return {@literal null} if source {@literal id} is already {@literal null}.
124+
* @since 2.2
125+
*/
126+
@Nullable
127+
default Object convertId(@Nullable Object id, Class<?> targetType) {
128+
129+
if (id == null) {
130+
return null;
131+
}
132+
133+
if (ClassUtils.isAssignable(ObjectId.class, targetType)) {
134+
135+
if (id instanceof String) {
136+
137+
if (ObjectId.isValid(id.toString())) {
138+
return new ObjectId(id.toString());
139+
}
140+
}
141+
}
142+
143+
try {
144+
return getConversionService().canConvert(id.getClass(), targetType)
145+
? getConversionService().convert(id, targetType) : convertToMongoType(id, null);
146+
} catch (ConversionException o_O) {
147+
return convertToMongoType(id, null);
148+
}
149+
}
105150
}

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

+35-21
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@
2828
import org.bson.Document;
2929
import org.bson.conversions.Bson;
3030
import org.bson.types.ObjectId;
31-
import org.springframework.core.convert.ConversionException;
3231
import org.springframework.core.convert.ConversionService;
3332
import org.springframework.core.convert.converter.Converter;
3433
import org.springframework.data.domain.Example;
@@ -50,6 +49,7 @@
5049
import org.springframework.data.util.TypeInformation;
5150
import org.springframework.lang.Nullable;
5251
import org.springframework.util.Assert;
52+
import org.springframework.util.StringUtils;
5353

5454
import com.mongodb.BasicDBList;
5555
import com.mongodb.BasicDBObject;
@@ -322,11 +322,11 @@ protected Object getMappedValue(Field documentField, Object value) {
322322
String inKey = valueDbo.containsField("$in") ? "$in" : "$nin";
323323
List<Object> ids = new ArrayList<Object>();
324324
for (Object id : (Iterable<?>) valueDbo.get(inKey)) {
325-
ids.add(convertId(id));
325+
ids.add(convertId(id, getIdTypeForField(documentField)));
326326
}
327327
resultDbo.put(inKey, ids);
328328
} else if (valueDbo.containsField("$ne")) {
329-
resultDbo.put("$ne", convertId(valueDbo.get("$ne")));
329+
resultDbo.put("$ne", convertId(valueDbo.get("$ne"), getIdTypeForField(documentField)));
330330
} else {
331331
return getMappedObject(resultDbo, Optional.empty());
332332
}
@@ -341,18 +341,18 @@ else if (isDocument(value)) {
341341
String inKey = valueDbo.containsKey("$in") ? "$in" : "$nin";
342342
List<Object> ids = new ArrayList<Object>();
343343
for (Object id : (Iterable<?>) valueDbo.get(inKey)) {
344-
ids.add(convertId(id));
344+
ids.add(convertId(id, getIdTypeForField(documentField)));
345345
}
346346
resultDbo.put(inKey, ids);
347347
} else if (valueDbo.containsKey("$ne")) {
348-
resultDbo.put("$ne", convertId(valueDbo.get("$ne")));
348+
resultDbo.put("$ne", convertId(valueDbo.get("$ne"), getIdTypeForField(documentField)));
349349
} else {
350350
return getMappedObject(resultDbo, Optional.empty());
351351
}
352352
return resultDbo;
353353

354354
} else {
355-
return convertId(value);
355+
return convertId(value, getIdTypeForField(documentField));
356356
}
357357
}
358358

@@ -367,6 +367,14 @@ else if (isDocument(value)) {
367367
return convertSimpleOrDocument(value, documentField.getPropertyEntity());
368368
}
369369

370+
private boolean isIdField(Field documentField) {
371+
return documentField.getProperty() != null && documentField.getProperty().isIdProperty();
372+
}
373+
374+
private Class<?> getIdTypeForField(Field documentField) {
375+
return isIdField(documentField) ? documentField.getProperty().getIdType() : ObjectId.class;
376+
}
377+
370378
/**
371379
* Returns whether the given {@link Field} represents an association reference that together with the given value
372380
* requires conversion to a {@link org.springframework.data.mongodb.core.mapping.DBRef} object. We check whether the
@@ -468,7 +476,14 @@ protected Object convertAssociation(@Nullable Object source, @Nullable MongoPers
468476
if (source instanceof DBRef) {
469477

470478
DBRef ref = (DBRef) source;
471-
return new DBRef(ref.getCollectionName(), convertId(ref.getId()));
479+
Object id = convertId(ref.getId(),
480+
property != null && property.isIdProperty() ? property.getIdType() : ObjectId.class);
481+
482+
if (StringUtils.hasText(ref.getDatabaseName())) {
483+
return new DBRef(ref.getDatabaseName(), ref.getCollectionName(), id);
484+
} else {
485+
return new DBRef(ref.getCollectionName(), id);
486+
}
472487
}
473488

474489
if (source instanceof Iterable) {
@@ -549,24 +564,23 @@ private DBRef createDbRefFor(Object source, MongoPersistentProperty property) {
549564
*
550565
* @param id
551566
* @return
567+
* @since 2.2
552568
*/
553569
@Nullable
554570
public Object convertId(@Nullable Object id) {
571+
return convertId(id, ObjectId.class);
572+
}
555573

556-
if (id == null) {
557-
return null;
558-
}
559-
560-
if (id instanceof String) {
561-
return ObjectId.isValid(id.toString()) ? conversionService.convert(id, ObjectId.class) : id;
562-
}
563-
564-
try {
565-
return conversionService.canConvert(id.getClass(), ObjectId.class) ? conversionService.convert(id, ObjectId.class)
566-
: delegateConvertToMongoType(id, null);
567-
} catch (ConversionException o_O) {
568-
return delegateConvertToMongoType(id, null);
569-
}
574+
/**
575+
* Converts the given raw id value into either {@link ObjectId} or {@literal targetType}.
576+
*
577+
* @param id can be {@literal null}.
578+
* @return the converted {@literal id} or {@literal null} if the source was already {@literal null}.
579+
* @since 2.2
580+
*/
581+
@Nullable
582+
public Object convertId(@Nullable Object id, Class<?> targetType) {
583+
return converter.convertId(id, targetType);
570584
}
571585

572586
/**

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

+16
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,20 @@ public interface MongoPersistentEntity<T> extends PersistentEntity<T, MongoPersi
5959
*/
6060
boolean hasTextScoreProperty();
6161

62+
/**
63+
* Returns the entities {@literal id} type of {@literal null} if the entity has no {@literal id} property.
64+
*
65+
* @return {@literal null} if the entity does not have an {@link #hasIdProperty() id property}.
66+
* @since 2.2
67+
*/
68+
@Nullable
69+
default Class<?> getIdType() {
70+
71+
if (!hasIdProperty()) {
72+
return null;
73+
}
74+
75+
return getIdProperty().getIdType();
76+
}
77+
6278
}

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

+29
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@
1515
*/
1616
package org.springframework.data.mongodb.core.mapping;
1717

18+
import org.bson.types.ObjectId;
1819
import org.springframework.core.convert.converter.Converter;
1920
import org.springframework.data.annotation.Id;
2021
import org.springframework.data.mapping.PersistentEntity;
2122
import org.springframework.data.mapping.PersistentProperty;
23+
import org.springframework.data.mongodb.MongoId;
2224
import org.springframework.lang.Nullable;
2325

2426
/**
@@ -61,6 +63,33 @@ public interface MongoPersistentProperty extends PersistentProperty<MongoPersist
6163
*/
6264
boolean isExplicitIdProperty();
6365

66+
/**
67+
* Get the target id type to be used when writing the actual id value.
68+
*
69+
* @return The properties actual type of {@link Object Class&lt;Object&gt;} for properties using the native property
70+
* type. <br />
71+
* {@link org.bson.types.ObjectId Class&lt;ObjectId&gt;} indicates the attempt to parse the given raw value as
72+
* {@link org.bson.types.ObjectId}.
73+
* @throws IllegalStateException if the property is not considered an id property. Please make sure to check
74+
* {@link #isIdProperty()}.
75+
* @since 2.2
76+
*/
77+
default Class<?> getIdType() {
78+
79+
if (!isIdProperty()) {
80+
81+
throw new IllegalStateException(String.format("Property '%s' is not considerd an 'id' property",
82+
getField() != null ? getField().getName() : getFieldName()));
83+
}
84+
85+
MongoId idAnnotation = findAnnotation(MongoId.class);
86+
if (idAnnotation == null) {
87+
return ObjectId.class;
88+
}
89+
90+
return Object.class.equals(idAnnotation.targetType()) ? getActualType() : idAnnotation.targetType();
91+
}
92+
6493
/**
6594
* Returns true whether the property indicates the documents language either by having a {@link #getFieldName()} equal
6695
* to {@literal language} or being annotated with {@link Language}.

0 commit comments

Comments
 (0)