From 9c09edd59452bd29fd6e61a3f2c5cf414b874c19 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 9 Oct 2023 14:26:54 +0200 Subject: [PATCH] Polishing. Replace duplicate occurrences of _id with FieldName.ID.name(). Shorten property names to avoid repetative "field" wording. Add Javadoc to MongoField builder. See #4464 Original pull request: #4512 --- .../mongodb/core/DefaultScriptOperations.java | 4 +- .../data/mongodb/core/EntityOperations.java | 3 +- .../data/mongodb/core/MappedDocument.java | 3 +- .../data/mongodb/core/QueryOperations.java | 3 +- .../mongodb/core/ReactiveMongoTemplate.java | 15 +- .../mongodb/core/aggregation/Aggregation.java | 3 +- .../data/mongodb/core/aggregation/Fields.java | 3 +- .../core/convert/DefaultDbRefResolver.java | 5 +- .../core/convert/DocumentAccessor.java | 6 +- .../core/convert/DocumentPointerFactory.java | 3 +- .../core/convert/MappingMongoConverter.java | 9 +- .../mongodb/core/convert/MongoConverters.java | 5 +- .../core/convert/MongoExampleMapper.java | 4 +- .../mongodb/core/convert/QueryMapper.java | 23 ++- .../core/convert/ReferenceLookupDelegate.java | 4 +- .../mongodb/core/index/WildcardIndex.java | 3 +- .../mapping/BasicMongoPersistentEntity.java | 2 +- .../mapping/BasicMongoPersistentProperty.java | 21 ++- .../data/mongodb/core/mapping/Field.java | 9 +- .../data/mongodb/core/mapping/FieldName.java | 38 +++-- .../data/mongodb/core/mapping/MongoField.java | 153 +++++++++++++----- .../data/mongodb/core/query/Criteria.java | 2 +- .../repository/query/AggregationUtils.java | 3 +- .../ReactiveSpringDataMongodbQuery.java | 5 +- .../support/SpringDataMongodbQuery.java | 5 +- .../support/SpringDataMongodbSerializer.java | 3 +- .../data/mongodb/util/BsonUtils.java | 30 ++-- .../json/ParameterBindingDocumentCodec.java | 3 +- .../MappingMongoConverterUnitTests.java | 2 +- .../ROOT/pages/mongodb/mapping/mapping.adoc | 125 ++++++++------ 30 files changed, 330 insertions(+), 167 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultScriptOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultScriptOperations.java index 86e89b059..34fcba989 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultScriptOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultScriptOperations.java @@ -29,6 +29,7 @@ import java.util.Set; import org.bson.Document; import org.bson.types.ObjectId; import org.springframework.dao.DataAccessException; +import org.springframework.data.mongodb.core.mapping.FieldName; import org.springframework.data.mongodb.core.script.ExecutableMongoScript; import org.springframework.data.mongodb.core.script.NamedMongoScript; import org.springframework.util.Assert; @@ -123,7 +124,8 @@ class DefaultScriptOperations implements ScriptOperations { Assert.hasText(scriptName, "ScriptName must not be null or empty"); - return mongoOperations.exists(query(where("_id").is(scriptName)), NamedMongoScript.class, SCRIPT_COLLECTION_NAME); + return mongoOperations.exists(query(where(FieldName.ID.name()).is(scriptName)), NamedMongoScript.class, + SCRIPT_COLLECTION_NAME); } @Override diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java index ab870fc4c..8d95fd933 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java @@ -39,6 +39,7 @@ import org.springframework.data.mongodb.core.convert.MongoConverter; import org.springframework.data.mongodb.core.convert.MongoJsonSchemaMapper; import org.springframework.data.mongodb.core.convert.MongoWriter; import org.springframework.data.mongodb.core.convert.QueryMapper; +import org.springframework.data.mongodb.core.mapping.FieldName; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.core.mapping.MongoSimpleTypes; @@ -79,7 +80,7 @@ import com.mongodb.client.model.ValidationOptions; */ class EntityOperations { - private static final String ID_FIELD = "_id"; + private static final String ID_FIELD = FieldName.ID.name(); private final MappingContext, MongoPersistentProperty> context; private final QueryMapper queryMapper; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MappedDocument.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MappedDocument.java index c9165988b..d25d93fef 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MappedDocument.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MappedDocument.java @@ -20,6 +20,7 @@ import java.util.List; import org.bson.Document; import org.bson.conversions.Bson; +import org.springframework.data.mongodb.core.mapping.FieldName; import org.springframework.data.mongodb.core.query.Update; import org.springframework.data.mongodb.core.query.UpdateDefinition; import org.springframework.data.util.StreamUtils; @@ -33,7 +34,7 @@ import org.springframework.data.util.StreamUtils; */ public class MappedDocument { - private static final String ID_FIELD = "_id"; + private static final String ID_FIELD = FieldName.ID.name(); private static final Document ID_ONLY_PROJECTION = new Document(ID_FIELD, 1); private final Document document; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/QueryOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/QueryOperations.java index eabbc276d..050149ce4 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/QueryOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/QueryOperations.java @@ -48,6 +48,7 @@ import org.springframework.data.mongodb.core.aggregation.TypeBasedAggregationOpe import org.springframework.data.mongodb.core.aggregation.TypedAggregation; import org.springframework.data.mongodb.core.convert.QueryMapper; import org.springframework.data.mongodb.core.convert.UpdateMapper; +import org.springframework.data.mongodb.core.mapping.FieldName; import org.springframework.data.mongodb.core.mapping.MongoId; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; @@ -849,7 +850,7 @@ class QueryOperations { } String key = shardKey.getPropertyNames().iterator().next(); - if ("_id".equals(key)) { + if (FieldName.ID.name().equals(key)) { return true; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java index 46e240474..9b03f0820 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java @@ -17,7 +17,6 @@ package org.springframework.data.mongodb.core; import static org.springframework.data.mongodb.core.query.SerializationUtils.*; -import org.springframework.data.mongodb.core.CollectionPreparerSupport.CollectionPreparerDelegate; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.util.function.Tuple2; @@ -104,6 +103,7 @@ import org.springframework.data.mongodb.core.convert.UpdateMapper; import org.springframework.data.mongodb.core.index.MongoMappingEventPublisher; import org.springframework.data.mongodb.core.index.ReactiveIndexOperations; import org.springframework.data.mongodb.core.index.ReactiveMongoPersistentEntityIndexCreator; +import org.springframework.data.mongodb.core.mapping.FieldName; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; @@ -133,7 +133,16 @@ import com.mongodb.CursorType; import com.mongodb.MongoException; import com.mongodb.ReadPreference; import com.mongodb.WriteConcern; -import com.mongodb.client.model.*; +import com.mongodb.client.model.CountOptions; +import com.mongodb.client.model.CreateCollectionOptions; +import com.mongodb.client.model.CreateViewOptions; +import com.mongodb.client.model.DeleteOptions; +import com.mongodb.client.model.EstimatedDocumentCountOptions; +import com.mongodb.client.model.FindOneAndDeleteOptions; +import com.mongodb.client.model.FindOneAndReplaceOptions; +import com.mongodb.client.model.FindOneAndUpdateOptions; +import com.mongodb.client.model.ReturnDocument; +import com.mongodb.client.model.UpdateOptions; import com.mongodb.client.model.changestream.FullDocument; import com.mongodb.client.result.DeleteResult; import com.mongodb.client.result.InsertOneResult; @@ -826,7 +835,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati Document filter = queryContext.getMappedQuery(entityClass, this::getPersistentEntity); FindPublisher findPublisher = collectionPreparer.prepare(collection).find(filter, Document.class) - .projection(new Document("_id", 1)); + .projection(new Document(FieldName.ID.name(), 1)); if (LOGGER.isDebugEnabled()) { LOGGER.debug(String.format("exists: %s in collection: %s", serializeToJsonSafely(filter), collectionName)); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Aggregation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Aggregation.java index cb9e70dd1..a7a9c8997 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Aggregation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Aggregation.java @@ -32,6 +32,7 @@ import org.springframework.data.mongodb.core.aggregation.LookupOperation.LookupO import org.springframework.data.mongodb.core.aggregation.MergeOperation.MergeOperationBuilder; import org.springframework.data.mongodb.core.aggregation.ReplaceRootOperation.ReplaceRootDocumentOperationBuilder; import org.springframework.data.mongodb.core.aggregation.ReplaceRootOperation.ReplaceRootOperationBuilder; +import org.springframework.data.mongodb.core.mapping.FieldName; import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.CriteriaDefinition; import org.springframework.data.mongodb.core.query.NearQuery; @@ -224,7 +225,7 @@ public class Aggregation { * @return */ public static String previousOperation() { - return "_id"; + return FieldName.ID.name(); } /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Fields.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Fields.java index 027c27f9c..e20717102 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Fields.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Fields.java @@ -23,6 +23,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import org.springframework.data.mongodb.core.mapping.FieldName; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -40,7 +41,7 @@ public final class Fields implements Iterable { private static final String AMBIGUOUS_EXCEPTION = "Found two fields both using '%s' as name: %s and %s; Please " + "customize your field definitions to get to unique field names"; - public static final String UNDERSCORE_ID = "_id"; + public static final String UNDERSCORE_ID = FieldName.ID.name(); public static final String UNDERSCORE_ID_REF = "$_id"; private final List fields; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultDbRefResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultDbRefResolver.java index 39245093c..7c1266e9b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultDbRefResolver.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultDbRefResolver.java @@ -25,12 +25,12 @@ import java.util.stream.Stream; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.bson.Document; - import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.mongodb.MongoDatabaseFactory; import org.springframework.data.mongodb.MongoDatabaseUtils; import org.springframework.data.mongodb.core.convert.ReferenceLoader.DocumentReferenceQuery; import org.springframework.data.mongodb.core.mapping.BasicMongoPersistentProperty; +import org.springframework.data.mongodb.core.mapping.FieldName; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -87,7 +87,8 @@ public class DefaultDbRefResolver extends DefaultReferenceResolver implements Db @Override public Document fetch(DBRef dbRef) { - return getReferenceLoader().fetchOne(DocumentReferenceQuery.forSingleDocument(Filters.eq("_id", dbRef.getId())), + return getReferenceLoader().fetchOne( + DocumentReferenceQuery.forSingleDocument(Filters.eq(FieldName.ID.name(), dbRef.getId())), ReferenceCollection.fromDBRef(dbRef)); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentAccessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentAccessor.java index f9957ff5c..28e3551e8 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentAccessor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentAccessor.java @@ -92,7 +92,7 @@ class DocumentAccessor { Assert.notNull(prop, "MongoPersistentProperty must not be null"); - Iterator parts = Arrays.asList(prop.getMongoField().getFieldName().parts()).iterator(); + Iterator parts = Arrays.asList(prop.getMongoField().getName().parts()).iterator(); Bson document = this.document; while (parts.hasNext()) { @@ -129,7 +129,7 @@ class DocumentAccessor { */ @Nullable public Object getRawId(MongoPersistentEntity entity) { - return entity.hasIdProperty() ? get(entity.getRequiredIdProperty()) : BsonUtils.get(document, "_id"); + return entity.hasIdProperty() ? get(entity.getRequiredIdProperty()) : BsonUtils.get(document, FieldName.ID.name()); } /** @@ -148,7 +148,7 @@ class DocumentAccessor { } FieldName getFieldName(MongoPersistentProperty prop) { - return prop.getMongoField().getFieldName(); + return prop.getMongoField().getName(); } /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentPointerFactory.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentPointerFactory.java index 3c2e2dc3f..1b673740c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentPointerFactory.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentPointerFactory.java @@ -34,6 +34,7 @@ import org.springframework.data.mapping.PropertyPath; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.model.BeanWrapperPropertyAccessorFactory; import org.springframework.data.mongodb.core.mapping.DocumentPointer; +import org.springframework.data.mongodb.core.mapping.FieldName; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; @@ -232,7 +233,7 @@ class DocumentPointerFactory { attribute = attribute.substring(attribute.lastIndexOf('.') + 1); } - String fieldName = entry.getKey().equals("_id") ? "id" : entry.getKey(); + String fieldName = entry.getKey().equals(FieldName.ID.name()) ? "id" : entry.getKey(); if (!fieldName.contains(".")) { Object targetValue = propertyAccessor.getProperty(persistentEntity.getPersistentProperty(fieldName)); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java index f1e039745..ae7bea36c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java @@ -231,8 +231,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App } /** - * Configure the characters dots potentially contained in a {@link Map} shall be replaced with. By default we don't do - * any translation but rather reject a {@link Map} with keys containing dots causing the conversion for the entire + * Configure the characters dots potentially contained in a {@link Map} shall be replaced with. By default, we don't + * do any translation but rather reject a {@link Map} with keys containing dots causing the conversion for the entire * object to fail. If further customization of the translation is needed, have a look at * {@link #potentiallyEscapeMapKey(String)} as well as {@link #potentiallyUnescapeMapKey(String)}. *

@@ -246,7 +246,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App } /** - * If {@link #preserveMapKeys(boolean) preserve} is set to {@literal true} the conversion will treat map keys containing {@literal .} (dot) characters as is. + * If {@link #preserveMapKeys(boolean) preserve} is set to {@literal true} the conversion will treat map keys + * containing dot ({@literal .}) characters as is. * * @since 4.2 * @see #setMapKeyDotReplacement(String) @@ -357,7 +358,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App DocumentAccessor documentAccessor = new DocumentAccessor(bson) { @Override FieldName getFieldName(MongoPersistentProperty prop) { - return propertyTranslator.translate(prop).getMongoField().getFieldName(); + return propertyTranslator.translate(prop).getMongoField().getName(); } }; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverters.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverters.java index 734469797..dd6224472 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverters.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverters.java @@ -51,6 +51,7 @@ import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.ConverterFactory; import org.springframework.data.convert.ReadingConverter; import org.springframework.data.convert.WritingConverter; +import org.springframework.data.mongodb.core.mapping.FieldName; import org.springframework.data.mongodb.core.query.Term; import org.springframework.data.mongodb.core.script.NamedMongoScript; import org.springframework.util.Assert; @@ -300,7 +301,7 @@ abstract class MongoConverters { return null; } - String id = source.get("_id").toString(); + String id = source.get(FieldName.ID.name()).toString(); Object rawValue = source.get("value"); return new NamedMongoScript(id, ((Code) rawValue).getCode()); @@ -320,7 +321,7 @@ abstract class MongoConverters { Document document = new Document(); - document.put("_id", source.getName()); + document.put(FieldName.ID.name(), source.getName()); document.put("value", new Code(source.getCode())); return document; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoExampleMapper.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoExampleMapper.java index 69757b22e..f93afcee8 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoExampleMapper.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoExampleMapper.java @@ -34,6 +34,7 @@ import org.springframework.data.domain.ExampleMatcher.PropertyValueTransformer; import org.springframework.data.domain.ExampleMatcher.StringMatcher; import org.springframework.data.mapping.PropertyHandler; import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.mongodb.core.mapping.FieldName; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.core.query.MongoRegexCreator; @@ -278,7 +279,8 @@ public class MongoExampleMapper { } private static boolean isEmptyIdProperty(Entry entry) { - return entry.getKey().equals("_id") && (entry.getValue() == null || entry.getValue().equals(Optional.empty())); + return entry.getKey().equals(FieldName.ID.name()) + && (entry.getValue() == null || entry.getValue().equals(Optional.empty())); } private static void applyStringMatcher(Map.Entry entry, StringMatcher stringMatcher, diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java index b2baf20ca..156108c56 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java @@ -47,7 +47,7 @@ import org.springframework.data.mongodb.MongoExpression; import org.springframework.data.mongodb.core.aggregation.AggregationExpression; import org.springframework.data.mongodb.core.aggregation.RelaxedTypeBasedAggregationOperationContext; import org.springframework.data.mongodb.core.convert.MappingMongoConverter.NestedDocument; -import org.springframework.data.mongodb.core.mapping.FieldName.Type; +import org.springframework.data.mongodb.core.mapping.FieldName; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty.PropertyToFieldNameConverter; @@ -81,7 +81,7 @@ public class QueryMapper { protected static final Log LOGGER = LogFactory.getLog(QueryMapper.class); - private static final List DEFAULT_ID_NAMES = Arrays.asList("id", "_id"); + private static final List DEFAULT_ID_NAMES = Arrays.asList("id", FieldName.ID.name()); private static final Document META_TEXT_SCORE = new Document("$meta", "textScore"); static final TypeInformation NESTED_DOCUMENT = TypeInformation.of(NestedDocument.class); @@ -367,7 +367,7 @@ public class QueryMapper { return new Field(key); } - if (Field.ID_KEY.equals(key)) { + if (FieldName.ID.name().equals(key)) { return new MetadataBackedField(key, entity, mappingContext, entity.getIdProperty()); } @@ -387,8 +387,7 @@ public class QueryMapper { if (keyword.isOrOrNor() || (keyword.hasIterableValue() && !keyword.isGeometry())) { Iterable conditions = keyword.getValue(); - List newConditions = conditions instanceof Collection collection - ? new ArrayList<>(collection.size()) + List newConditions = conditions instanceof Collection collection ? new ArrayList<>(collection.size()) : new ArrayList<>(); for (Object condition : conditions) { @@ -624,7 +623,7 @@ public class QueryMapper { String key = ObjectUtils.nullSafeToString(converter.convertToMongoType(it.getKey())); - if (it.getValue() instanceof Document document) { + if (it.getValue()instanceof Document document) { map.put(key, getMappedObject(document, entity)); } else { map.put(key, delegateConvertToMongoType(it.getValue(), entity)); @@ -977,8 +976,6 @@ public class QueryMapper { protected static final Pattern POSITIONAL_OPERATOR = Pattern.compile("\\$\\[.*\\]"); - private static final String ID_KEY = "_id"; - protected final String name; /** @@ -1008,7 +1005,7 @@ public class QueryMapper { * @return */ public boolean isIdField() { - return ID_KEY.equals(name); + return FieldName.ID.name().equals(name); } /** @@ -1053,7 +1050,7 @@ public class QueryMapper { * @return */ public String getMappedKey() { - return isIdField() ? ID_KEY : name; + return isIdField() ? FieldName.ID.name() : name; } /** @@ -1222,9 +1219,11 @@ public class QueryMapper { @Override public String getMappedKey() { - if(getProperty() != null && getProperty().getMongoField().getFieldName().isOfType(Type.KEY)) { + + if (getProperty() != null && getProperty().getMongoField().getName().isKey()) { return getProperty().getFieldName(); } + return path == null ? name : path.toDotPath(isAssociation() ? getAssociationConverter() : getPropertyConverter()); } @@ -1355,7 +1354,7 @@ public class QueryMapper { /* always start from a property, so we can skip the first segment. from there remove any position placeholder */ - for(int i=1; i < segments.length; i++) { + for (int i = 1; i < segments.length; i++) { String segment = segments[i]; if (segment.startsWith("[") && segment.endsWith("]")) { continue; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceLookupDelegate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceLookupDelegate.java index 045f14f7a..e4d94d64f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceLookupDelegate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceLookupDelegate.java @@ -37,6 +37,7 @@ import org.springframework.data.mongodb.core.convert.ReferenceLoader.DocumentRef import org.springframework.data.mongodb.core.convert.ReferenceResolver.MongoEntityReader; import org.springframework.data.mongodb.core.convert.ReferenceResolver.ReferenceCollection; import org.springframework.data.mongodb.core.mapping.DocumentReference; +import org.springframework.data.mongodb.core.mapping.FieldName; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.util.BsonUtils; @@ -63,7 +64,8 @@ import com.mongodb.client.MongoCollection; */ public final class ReferenceLookupDelegate { - private static final Document NO_RESULTS_PREDICATE = new Document("_id", new Document("$exists", false)); + private static final Document NO_RESULTS_PREDICATE = new Document(FieldName.ID.name(), + new Document("$exists", false)); private final MappingContext, MongoPersistentProperty> mappingContext; private final SpELContext spELContext; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/WildcardIndex.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/WildcardIndex.java index f7b86b322..8e4186f59 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/WildcardIndex.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/WildcardIndex.java @@ -21,6 +21,7 @@ import java.util.Map; import java.util.concurrent.TimeUnit; import org.bson.Document; +import org.springframework.data.mongodb.core.mapping.FieldName; import org.springframework.lang.Nullable; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; @@ -77,7 +78,7 @@ public class WildcardIndex extends Index { */ public WildcardIndex includeId() { - wildcardProjection.put("_id", 1); + wildcardProjection.put(FieldName.ID.name(), 1); return this; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntity.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntity.java index 9b87e0545..048104965 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntity.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntity.java @@ -116,7 +116,7 @@ public class BasicMongoPersistentEntity extends BasicPersistentEntity> SUPPORTED_ID_TYPES = new HashSet>(); private static final Set SUPPORTED_ID_PROPERTY_NAMES = new HashSet(); @@ -70,7 +70,7 @@ public class BasicMongoPersistentProperty extends AnnotationBasedPersistentPrope SUPPORTED_ID_TYPES.add(BigInteger.class); SUPPORTED_ID_PROPERTY_NAMES.add("id"); - SUPPORTED_ID_PROPERTY_NAMES.add("_id"); + SUPPORTED_ID_PROPERTY_NAMES.add(ID_FIELD_NAME); } private final FieldNamingStrategy fieldNamingStrategy; @@ -131,8 +131,9 @@ public class BasicMongoPersistentProperty extends AnnotationBasedPersistentPrope * * @return */ + @Override public String getFieldName() { - return getMongoField().getFieldName().name(); + return getMongoField().getName().name(); } @Override @@ -171,6 +172,7 @@ public class BasicMongoPersistentProperty extends AnnotationBasedPersistentPrope * {@link org.springframework.data.mongodb.core.mapping.Field#value()} present. * @since 1.7 */ + @Override public boolean hasExplicitFieldName() { return StringUtils.hasText(getAnnotatedFieldName()); } @@ -184,8 +186,9 @@ public class BasicMongoPersistentProperty extends AnnotationBasedPersistentPrope return annotation != null ? annotation.value() : null; } + @Override public int getFieldOrder() { - return getMongoField().getFieldOrder(); + return getMongoField().getOrder(); } @Override @@ -199,9 +202,10 @@ public class BasicMongoPersistentProperty extends AnnotationBasedPersistentPrope @Override protected Association createAssociation() { - return new Association(this, null); + return new Association<>(this, null); } + @Override public boolean isDbReference() { return isAnnotationPresent(DBRef.class); } @@ -211,6 +215,7 @@ public class BasicMongoPersistentProperty extends AnnotationBasedPersistentPrope return isAnnotationPresent(DocumentReference.class); } + @Override @Nullable public DBRef getDBRef() { return findAnnotation(DBRef.class); @@ -286,12 +291,12 @@ public class BasicMongoPersistentProperty extends AnnotationBasedPersistentPrope MongoFieldBuilder builder = MongoField.builder(); if (isAnnotationPresent(Field.class) && Type.KEY.equals(findAnnotation(Field.class).nameType())) { - builder.fieldName(doGetFieldName()); + builder.name(doGetFieldName()); } else { - builder.fieldPath(doGetFieldName()); + builder.path(doGetFieldName()); } builder.fieldType(doGetFieldType()); - builder.fieldOrderNumber(doGetFieldOrder()); + builder.order(doGetFieldOrder()); return builder.build(); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/Field.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/Field.java index f5c38eafa..97e90562a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/Field.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/Field.java @@ -47,9 +47,8 @@ public @interface Field { /** * The key to be used to store the field inside the document. Alias for {@link #value()}. The name may contain MongoDB - * special characters like {@literal .} (dot). In this case the name is by default treated as a - * {@link Type#PATH path}. To preserve dots within the name set the {@link #nameType()} attribute to - * {@link Type#KEY}. + * special characters like dot ({@literal .}). In this case the name is by default treated as a {@link Type#PATH + * path}. To preserve dots within the name set the {@link #nameType()} attribute to {@link Type#KEY}. * * @return an empty {@link String} by default. * @since 2.2 @@ -58,8 +57,8 @@ public @interface Field { String name() default ""; /** - * The used {@link Type type} has impact on how a given {@link #name()} is treated if it contains - * {@literal .} (dot) characters. + * The used {@link Type type} has impact on how a given {@link #name()} is treated if it contains dot ({@literal .}) + * characters. * * @return {@link Type#PATH} by default. * @since 4.2 diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/FieldName.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/FieldName.java index 99a71da12..211eb0be8 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/FieldName.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/FieldName.java @@ -19,14 +19,18 @@ import org.springframework.util.ObjectUtils; /** * Value Object representing a field name that should be used to read/write fields within the MongoDB document. - * {@link FieldName Field names} field names may contain special characters (such as {@literal .} (dot)) but may be - * treated differently depending ob their {@link Type type}. - * + * {@link FieldName Field names} field names may contain special characters (such as dot ({@literal .})) but may be + * treated differently depending on their {@link Type type}. + * * @author Christoph Strobl * @since 4.2 */ public record FieldName(String name, Type type) { + private static final String ID_KEY = "_id"; + + public static final FieldName ID = new FieldName(ID_KEY, Type.KEY); + /** * Create a new {@link FieldName} that treats the given {@literal value} as is. * @@ -39,7 +43,7 @@ public record FieldName(String name, Type type) { /** * Create a new {@link FieldName} that treats the given {@literal value} as a path. If the {@literal value} contains - * {@literal .} (dot) characters, they are considered deliminators in a path. + * dot ({@literal .}) characters, they are considered deliminators in a path. * * @param value must not be {@literal null}. * @return new instance of {@link FieldName}. @@ -50,14 +54,14 @@ public record FieldName(String name, Type type) { /** * Get the parts the field name consists of. If the {@link FieldName} is a {@link Type#KEY} or a {@link Type#PATH} - * that does not contain {@literal .} (dot) characters an array containing a single element is returned. Otherwise the - * {@link #name()} is split into segments using {@literal .} (dot) as a deliminator. - * + * that does not contain dot ({@literal .}) characters an array containing a single element is returned. Otherwise the + * {@link #name()} is split into segments using dot ({@literal .}) as a separator. + * * @return never {@literal null}. */ public String[] parts() { - if (isOfType(Type.KEY)) { + if (isKey()) { return new String[] { name }; } @@ -72,9 +76,23 @@ public record FieldName(String name, Type type) { return ObjectUtils.nullSafeEquals(type(), type); } + /** + * @return whether the field name represents a key (i.e. as-is name). + */ + public boolean isKey() { + return isOfType(Type.KEY); + } + + /** + * @return whether the field name represents a path (i.e. dot-path). + */ + public boolean isPath() { + return isOfType(Type.PATH); + } + @Override public String toString() { - return "FieldName{%s=%s}".formatted(isOfType(Type.KEY) ? "key" : "path", name); + return "FieldName{%s=%s}".formatted(isKey() ? "key" : "path", name); } @Override @@ -106,7 +124,7 @@ public record FieldName(String name, Type type) { public enum Type { /** - * {@literal .} (dot) characters are treated as deliminators in a path. + * Dot ({@literal .}) characters are treated as separators for segments in a path. */ PATH, diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoField.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoField.java index 069c3ff73..29d3676e1 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoField.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoField.java @@ -16,18 +16,31 @@ package org.springframework.data.mongodb.core.mapping; import org.springframework.data.mongodb.core.mapping.FieldName.Type; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; /** * Value Object for representing a field to read/write within a MongoDB {@link org.bson.Document}. - * + * * @author Christoph Strobl * @since 4.2 */ public class MongoField { - private final FieldName fieldName; + private final FieldName name; private final FieldType fieldType; - private final int fieldOrder; + private final int order; + + protected MongoField(FieldName name, Class targetFieldType, int fieldOrder) { + this(name, FieldType.valueOf(targetFieldType.getSimpleName()), fieldOrder); + } + + protected MongoField(FieldName name, FieldType fieldType, int fieldOrder) { + + this.name = name; + this.fieldType = fieldType; + this.order = fieldOrder; + } /** * Create a new {@link MongoField} with given {@literal name}. @@ -35,8 +48,18 @@ public class MongoField { * @param name the name to be used as is (with all its potentially special characters). * @return new instance of {@link MongoField}. */ - public static MongoField just(String name) { - return builder().fieldName(name).build(); + public static MongoField fromKey(String name) { + return builder().name(name).build(); + } + + /** + * Create a new {@link MongoField} with given {@literal name}. + * + * @param name the name to be used path expression. + * @return new instance of {@link MongoField}. + */ + public static MongoField fromPath(String name) { + return builder().path(name).build(); } /** @@ -46,22 +69,11 @@ public class MongoField { return new MongoFieldBuilder(); } - protected MongoField(FieldName fieldName, Class targetFieldType, int fieldOrder) { - this(fieldName, FieldType.valueOf(targetFieldType.getSimpleName()), fieldOrder); - } - - protected MongoField(FieldName fieldName, FieldType fieldType, int fieldOrder) { - - this.fieldName = fieldName; - this.fieldType = fieldType; - this.fieldOrder = fieldOrder; - } - /** * @return never {@literal null}. */ - public FieldName getFieldName() { - return fieldName; + public FieldName getName() { + return name; } /** @@ -69,8 +81,8 @@ public class MongoField { * * @return {@link Integer#MAX_VALUE} if undefined. */ - public int getFieldOrder() { - return fieldOrder; + public int getOrder() { + return order; } /** @@ -78,53 +90,122 @@ public class MongoField { * @return new instance of {@link MongoField} with prefix appended to current field name. */ MongoField withPrefix(String prefix) { - return new MongoField(new FieldName(prefix + fieldName.name(), fieldName.type()), fieldType, fieldOrder); + return new MongoField(new FieldName(prefix + name.name(), name.type()), fieldType, order); } /** * Get the fields target type if defined. - * + * * @return never {@literal null}. */ public FieldType getFieldType() { return fieldType; } + @Override + public boolean equals(Object o) { + + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + MongoField that = (MongoField) o; + + if (order != that.order) + return false; + if (!ObjectUtils.nullSafeEquals(name, that.name)) { + return false; + } + return fieldType == that.fieldType; + } + + @Override + public int hashCode() { + + int result = ObjectUtils.nullSafeHashCode(name); + result = 31 * result + ObjectUtils.nullSafeHashCode(fieldType); + result = 31 * result + order; + return result; + } + + @Override + public String toString() { + return name.toString(); + } + + /** + * Builder for {@link MongoField}. + */ public static class MongoFieldBuilder { - private String fieldName; - private FieldType fieldType = FieldType.IMPLICIT; - private int orderNumber = Integer.MAX_VALUE; - private Type type = Type.PATH; + private String name; + private Type nameType = Type.PATH; + private FieldType type = FieldType.IMPLICIT; + private int order = Integer.MAX_VALUE; + /** + * Configure the field type. + * + * @param fieldType + * @return + */ public MongoFieldBuilder fieldType(FieldType fieldType) { - this.fieldType = fieldType; + this.type = fieldType; return this; } - public MongoFieldBuilder fieldName(String fieldName) { + /** + * Configure the field name as key. Key field names are used as-is without applying path segmentation splitting + * rules. + * + * @param fieldName + * @return + */ + public MongoFieldBuilder name(String fieldName) { - this.fieldName = fieldName; - this.type = Type.KEY; + Assert.hasText(fieldName, "Field name must not be empty"); + + this.name = fieldName; + this.nameType = Type.KEY; return this; } - public MongoFieldBuilder fieldOrderNumber(int orderNumber) { + /** + * Configure the field name as path. Path field names are applied as paths potentially pointing into subdocuments. + * + * @param path + * @return + */ + public MongoFieldBuilder path(String path) { - this.orderNumber = orderNumber; + Assert.hasText(path, "Field path (name) must not be empty"); + + this.name = path; + this.nameType = Type.PATH; return this; } - public MongoFieldBuilder fieldPath(String path) { + /** + * Configure the field order, defaulting to {@link Integer#MAX_VALUE} (undefined). + * + * @param order + * @return + */ + public MongoFieldBuilder order(int order) { - this.fieldName = path; - this.type = Type.PATH; + this.order = order; return this; } + /** + * Build a new {@link MongoField}. + * + * @return a new {@link MongoField}. + */ public MongoField build() { - return new MongoField(new FieldName(fieldName, type), fieldType, orderNumber); + return new MongoField(new FieldName(name, nameType), type, order); } } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Criteria.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Criteria.java index dd13cb5bc..276b0c4b8 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Criteria.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Criteria.java @@ -122,7 +122,7 @@ public class Criteria implements CriteriaDefinition { * By default the {@link Example} uses typed matching restricting it to probe assignable types. For example, when * sticking with the default type key ({@code _class}), the query has restrictions such as * _class : { $in : [com.acme.Person] } .
- * To avoid the above mentioned type restriction use an {@link UntypedExampleMatcher} with + * To avoid the above-mentioned type restriction use an {@link UntypedExampleMatcher} with * {@link Example#of(Object, org.springframework.data.domain.ExampleMatcher)}. * * @param example must not be {@literal null}. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AggregationUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AggregationUtils.java index 7b1e70993..e57286f76 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AggregationUtils.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AggregationUtils.java @@ -27,6 +27,7 @@ import org.springframework.data.mongodb.core.aggregation.Aggregation; import org.springframework.data.mongodb.core.aggregation.AggregationOptions; import org.springframework.data.mongodb.core.aggregation.AggregationPipeline; import org.springframework.data.mongodb.core.convert.MongoConverter; +import org.springframework.data.mongodb.core.mapping.FieldName; import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.core.query.Meta; import org.springframework.data.mongodb.core.query.Query; @@ -208,7 +209,7 @@ abstract class AggregationUtils { } Document intermediate = new Document(source); - intermediate.remove("_id"); + intermediate.remove(FieldName.ID.name()); if (intermediate.size() == 1) { return getPotentiallyConvertedSimpleTypeValue(converter, intermediate.values().iterator().next(), targetType); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveSpringDataMongodbQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveSpringDataMongodbQuery.java index e29341425..789dc1265 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveSpringDataMongodbQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveSpringDataMongodbQuery.java @@ -26,11 +26,12 @@ import java.util.function.Consumer; import org.bson.Document; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Window; import org.springframework.data.domain.ScrollPosition; +import org.springframework.data.domain.Window; import org.springframework.data.mongodb.core.MongoOperations; import org.springframework.data.mongodb.core.ReactiveFindOperation; import org.springframework.data.mongodb.core.ReactiveMongoOperations; +import org.springframework.data.mongodb.core.mapping.FieldName; import org.springframework.data.mongodb.core.query.BasicQuery; import org.springframework.data.mongodb.core.query.Query; import org.springframework.lang.Nullable; @@ -264,7 +265,7 @@ class ReactiveSpringDataMongodbQuery extends SpringDataMongodbQuerySupport getJoinIds(Class targetType, @Nullable Predicate condition) { return createQuery(Mono.justOrEmpty(condition), null, QueryModifiers.EMPTY, Collections.emptyList()) - .flatMapMany(query -> mongoOperations.findDistinct(query, "_id", targetType, Object.class)); + .flatMapMany(query -> mongoOperations.findDistinct(query, FieldName.ID.name(), targetType, Object.class)); } @Override diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbQuery.java index 0ecff3958..c2bd4d248 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbQuery.java @@ -25,10 +25,11 @@ import org.bson.Document; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Window; import org.springframework.data.domain.ScrollPosition; +import org.springframework.data.domain.Window; import org.springframework.data.mongodb.core.ExecutableFindOperation; import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.data.mongodb.core.mapping.FieldName; import org.springframework.data.mongodb.core.query.BasicQuery; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.support.PageableExecutionUtils; @@ -258,7 +259,7 @@ public class SpringDataMongodbQuery extends SpringDataMongodbQuerySupport getIds(Class targetType, Predicate condition) { Query query = createQuery(condition, null, QueryModifiers.EMPTY, Collections.emptyList()); - return mongoOperations.findDistinct(query, "_id", targetType, Object.class); + return mongoOperations.findDistinct(query, FieldName.ID.name(), targetType, Object.class); } private static T handleException(RuntimeException e, T defaultValue) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbSerializer.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbSerializer.java index 7d14ef0b4..7be427b10 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbSerializer.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbSerializer.java @@ -25,6 +25,7 @@ import org.bson.Document; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mongodb.core.convert.MongoConverter; import org.springframework.data.mongodb.core.convert.QueryMapper; +import org.springframework.data.mongodb.core.mapping.FieldName; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.lang.Nullable; @@ -50,7 +51,7 @@ import com.querydsl.mongodb.document.MongodbDocumentSerializer; */ class SpringDataMongodbSerializer extends MongodbDocumentSerializer { - private static final String ID_KEY = "_id"; + private static final String ID_KEY = FieldName.ID.name(); private static final Set PATH_TYPES; static { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/BsonUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/BsonUtils.java index 4eb5b162d..eebd948a1 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/BsonUtils.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/BsonUtils.java @@ -246,11 +246,11 @@ public class BsonUtils { */ public static boolean removeNullId(Bson bson) { - if (!contains(bson, "_id", null)) { + if (!contains(bson, FieldName.ID.name(), null)) { return false; } - removeFrom(bson, "_id"); + removeFrom(bson, FieldName.ID.name()); return true; } @@ -520,11 +520,10 @@ public class BsonUtils { } /** - * Resolve the value for a given {@link FieldName field name}. - * If the given name is a {@link Type#KEY} the value is obtained from the target {@link Bson} immediately. - * If the given fieldName is a {@link Type#PATH} maybe using the dot ({@code .}) notation it will try to resolve the path by - * inspecting the individual parts. If one of the intermediate ones is {@literal null} or cannot be inspected further - * (wrong) type, {@literal null} is returned. + * Resolve the value for a given {@link FieldName field name}. If the given name is a {@link Type#KEY} the value is + * obtained from the target {@link Bson} immediately. If the given fieldName is a {@link Type#PATH} maybe using the + * dot ({@code .}) notation it will try to resolve the path by inspecting the individual parts. If one of the + * intermediate ones is {@literal null} or cannot be inspected further (wrong) type, {@literal null} is returned. * * @param bson the source to inspect. Must not be {@literal null}. * @param fieldName the name to lookup. Must not be {@literal null}. @@ -536,11 +535,10 @@ public class BsonUtils { } /** - * Resolve the value for a given {@link FieldName field name}. - * If the given name is a {@link Type#KEY} the value is obtained from the target {@link Bson} immediately. - * If the given fieldName is a {@link Type#PATH} maybe using the dot ({@code .}) notation it will try to resolve the path by - * inspecting the individual parts. If one of the intermediate ones is {@literal null} or cannot be inspected further - * (wrong) type, {@literal null} is returned. + * Resolve the value for a given {@link FieldName field name}. If the given name is a {@link Type#KEY} the value is + * obtained from the target {@link Bson} immediately. If the given fieldName is a {@link Type#PATH} maybe using the + * dot ({@code .}) notation it will try to resolve the path by inspecting the individual parts. If one of the + * intermediate ones is {@literal null} or cannot be inspected further (wrong) type, {@literal null} is returned. * * @param source the source to inspect. Must not be {@literal null}. * @param fieldName the key to lookup. Must not be {@literal null}. @@ -550,7 +548,7 @@ public class BsonUtils { @Nullable public static Object resolveValue(Map source, FieldName fieldName) { - if(fieldName.isOfType(Type.KEY)) { + if (fieldName.isKey()) { return source.get(fieldName.name()); } @@ -584,7 +582,7 @@ public class BsonUtils { @Nullable public static Object resolveValue(Map source, String key) { - if(source.containsKey(key)) { + if (source.containsKey(key)) { return source.get(key); } @@ -594,11 +592,11 @@ public class BsonUtils { public static boolean hasValue(Bson bson, FieldName fieldName) { Map source = asMap(bson); - if(fieldName.isOfType(Type.KEY)) { + if (fieldName.isKey()) { return source.get(fieldName.name()) != null; } - String [] parts = fieldName.parts(); + String[] parts = fieldName.parts(); Object result; for (int i = 1; i < parts.length; i++) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingDocumentCodec.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingDocumentCodec.java index 9fac4c7d0..fb85255b2 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingDocumentCodec.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingDocumentCodec.java @@ -41,6 +41,7 @@ import org.bson.codecs.*; import org.bson.codecs.configuration.CodecRegistry; import org.bson.json.JsonParseException; import org.springframework.data.mapping.model.SpELExpressionEvaluator; +import org.springframework.data.mongodb.core.mapping.FieldName; import org.springframework.data.spel.EvaluationContextProvider; import org.springframework.data.spel.ExpressionDependencies; import org.springframework.expression.ExpressionParser; @@ -66,7 +67,7 @@ import org.springframework.util.StringUtils; */ public class ParameterBindingDocumentCodec implements CollectibleCodec { - private static final String ID_FIELD_NAME = "_id"; + private static final String ID_FIELD_NAME = FieldName.ID.name(); private static final CodecRegistry DEFAULT_REGISTRY = fromProviders( asList(new ValueCodecProvider(), new BsonValueCodecProvider(), new DocumentCodecProvider())); private static final BsonTypeClassMap DEFAULT_BSON_TYPE_CLASS_MAP = new BsonTypeClassMap(); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java index 97ffd280e..0e3e6416b 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java @@ -2640,7 +2640,7 @@ class MappingMongoConverterUnitTests { DocumentAccessor accessor = new DocumentAccessor(new org.bson.Document()); MongoPersistentProperty persistentProperty = mock(MongoPersistentProperty.class); when(persistentProperty.isAssociation()).thenReturn(true); - when(persistentProperty.getMongoField()).thenReturn(MongoField.just("pName")); + when(persistentProperty.getMongoField()).thenReturn(MongoField.fromKey("pName")); doReturn(TypeInformation.of(Person.class)).when(persistentProperty).getTypeInformation(); doReturn(Person.class).when(persistentProperty).getType(); doReturn(Person.class).when(persistentProperty).getRawType(); diff --git a/src/main/antora/modules/ROOT/pages/mongodb/mapping/mapping.adoc b/src/main/antora/modules/ROOT/pages/mongodb/mapping/mapping.adoc index b10befc7b..9283ad9be 100644 --- a/src/main/antora/modules/ROOT/pages/mongodb/mapping/mapping.adoc +++ b/src/main/antora/modules/ROOT/pages/mongodb/mapping/mapping.adoc @@ -36,7 +36,7 @@ The following outlines what field will be mapped to the `_id` document field: * A field without an annotation but named `id` will be mapped to the `_id` field. * The default field name for identifiers is `_id` and can be customized via the `@Field` annotation. -[cols="1,2", options="header"] +[cols="1,2",options="header"] .Examples for the translation of `_id` field definitions |=== | Field definition @@ -60,9 +60,16 @@ The following outlines what field will be mapped to the `_id` document field: The following outlines what type conversion, if any, will be done on the property mapped to the _id document field. -* If a field named `id` is declared as a String or BigInteger in the Java class it will be converted to and stored as an ObjectId if possible. ObjectId as a field type is also valid. If you specify a value for `id` in your application, the conversion to an ObjectId is detected to the MongoDB driver. If the specified `id` value cannot be converted to an ObjectId, then the value will be stored as is in the document's _id field. This also applies if the field is annotated with `@Id`. -* If a field is annotated with `@MongoId` in the Java class it will be converted to and stored as using its actual type. No further conversion happens unless `@MongoId` declares a desired field type. If no value is provided for the `id` field, a new `ObjectId` will be created and converted to the properties type. -* If a field is annotated with `@MongoId(FieldType.…)` in the Java class it will be attempted to convert the value to the declared `FieldType`. If no value is provided for the `id` field, a new `ObjectId` will be created and converted to the declared type. +* If a field named `id` is declared as a String or BigInteger in the Java class it will be converted to and stored as an ObjectId if possible. +ObjectId as a field type is also valid. +If you specify a value for `id` in your application, the conversion to an ObjectId is detected to the MongoDB driver. +If the specified `id` value cannot be converted to an ObjectId, then the value will be stored as is in the document's _id field. +This also applies if the field is annotated with `@Id`. +* If a field is annotated with `@MongoId` in the Java class it will be converted to and stored as using its actual type. +No further conversion happens unless `@MongoId` declares a desired field type. +If no value is provided for the `id` field, a new `ObjectId` will be created and converted to the properties type. +* If a field is annotated with `@MongoId(FieldType.…)` in the Java class it will be attempted to convert the value to the declared `FieldType`. +If no value is provided for the `id` field, a new `ObjectId` will be created and converted to the declared type. * If a field named `id` id field is not declared as a String, BigInteger, or ObjectID in the Java class then you should assign it a value in your application so it can be stored 'as-is' in the document's _id field. * If no field named `id` is present in the Java class then an implicit `_id` file will be generated by the driver but not mapped to a property or field of the Java class. @@ -79,7 +86,7 @@ See xref:mongodb/mapping/custom-conversions.adoc[Custom Conversions - Overriding .Built in Type conversions: [%collapsible] ==== -[cols="3,1,6", options="header"] +[cols="3,1,6",options="header"] .Type |=== | Type @@ -250,9 +257,9 @@ calling `get()` before the actual conversion | `GeoJsonMultiPolygon` | converter | `{"geoJsonMultiPolygon" : { "type" : "MultiPolygon", "coordinates" : [ - [ [ [ -73.958 , 40.8003 ] , [ -73.9498 , 40.7968 ] ] ], - [ [ [ -73.973 , 40.7648 ] , [ -73.9588 , 40.8003 ] ] ] - ] }}` +[ [ [ -73.958 , 40.8003 ] , [ -73.9498 , 40.7968 ] ] ], +[ [ [ -73.973 , 40.7648 ] , [ -73.9588 , 40.8003 ] ] ] +] }}` | `GeoJsonLineString` | converter @@ -261,19 +268,22 @@ calling `get()` before the actual conversion | `GeoJsonMultiLineString` | converter | `{"geoJsonLineString" : { "type" : "MultiLineString", coordinates: [ - [ [ -73.97162 , 40.78205 ], [ -73.96374 , 40.77715 ] ], - [ [ -73.97880 , 40.77247 ], [ -73.97036 , 40.76811 ] ] - ] }}` +[ [ -73.97162 , 40.78205 ], [ -73.96374 , 40.77715 ] ], +[ [ -73.97880 , 40.77247 ], [ -73.97036 , 40.76811 ] ] +] }}` |=== ==== [[mapping-configuration]] == Mapping Configuration -Unless explicitly configured, an instance of `MappingMongoConverter` is created by default when you create a `MongoTemplate`. You can create your own instance of the `MappingMongoConverter`. Doing so lets you dictate where in the classpath your domain classes can be found, so that Spring Data MongoDB can extract metadata and construct indexes. Also, by creating your own instance, you can register Spring converters to map specific classes to and from the database. - -You can configure the `MappingMongoConverter` as well as `com.mongodb.client.MongoClient` and MongoTemplate by using either Java-based or XML-based metadata. The following example shows the configuration: +Unless explicitly configured, an instance of `MappingMongoConverter` is created by default when you create a `MongoTemplate`. +You can create your own instance of the `MappingMongoConverter`. +Doing so lets you dictate where in the classpath your domain classes can be found, so that Spring Data MongoDB can extract metadata and construct indexes. +Also, by creating your own instance, you can register Spring converters to map specific classes to and from the database. +You can configure the `MappingMongoConverter` as well as `com.mongodb.client.MongoClient` and MongoTemplate by using either Java-based or XML-based metadata. +The following example shows the configuration: [tabs] ====== @@ -309,7 +319,9 @@ public class MongoConfig extends AbstractMongoClientConfiguration { } } ---- -<1> The mapping base package defines the root path used to scan for entities used to pre initialize the `MappingContext`. By default the configuration classes package is used. + +<1> The mapping base package defines the root path used to scan for entities used to pre initialize the `MappingContext`. +By default the configuration classes package is used. <2> Configure additional custom converters for specific domain types that replace the default mapping procedure for those types with your custom implementation. XML:: @@ -363,7 +375,6 @@ Also shown in the preceding example is a `LoggingEventListener`, which logs `Mon [TIP] ==== .Java Time Types - We recommend using MongoDB's native JSR-310 support via `MongoConverterConfigurationAdapter.useNativeDriverJavaTimeCodecs()` as described above as it is using an `UTC` based approach. The default JSR-310 support for `java.time` types inherited from Spring Data Commons uses the local machine timezone as reference and should only be used for backwards compatibility. ==== @@ -418,26 +429,38 @@ WARNING: Auto index creation is **disabled** by default and needs to be enabled [[mapping-usage-annotations]] === Mapping Annotation Overview -The MappingMongoConverter can use metadata to drive the mapping of objects to documents. The following annotations are available: +The MappingMongoConverter can use metadata to drive the mapping of objects to documents. +The following annotations are available: * `@Id`: Applied at the field level to mark the field used for identity purpose. -* `@MongoId`: Applied at the field level to mark the field used for identity purpose. Accepts an optional `FieldType` to customize id conversion. -* `@Document`: Applied at the class level to indicate this class is a candidate for mapping to the database. You can specify the name of the collection where the data will be stored. +* `@MongoId`: Applied at the field level to mark the field used for identity purpose. +Accepts an optional `FieldType` to customize id conversion. +* `@Document`: Applied at the class level to indicate this class is a candidate for mapping to the database. +You can specify the name of the collection where the data will be stored. * `@DBRef`: Applied at the field to indicate it is to be stored using a com.mongodb.DBRef. -* `@DocumentReference`: Applied at the field to indicate it is to be stored as a pointer to another document. This can be a single value (the _id_ by default), or a `Document` provided via a converter. +* `@DocumentReference`: Applied at the field to indicate it is to be stored as a pointer to another document. +This can be a single value (the _id_ by default), or a `Document` provided via a converter. * `@Indexed`: Applied at the field level to describe how to index the field. * `@CompoundIndex` (repeatable): Applied at the type level to declare Compound Indexes. * `@GeoSpatialIndexed`: Applied at the field level to describe how to geoindex the field. * `@TextIndexed`: Applied at the field level to mark the field to be included in the text index. * `@HashIndexed`: Applied at the field level for usage within a hashed index to partition data across a sharded cluster. * `@Language`: Applied at the field level to set the language override property for text index. -* `@Transient`: By default, all fields are mapped to the document. This annotation excludes the field where it is applied from being stored in the database. Transient properties cannot be used within a persistence constructor as the converter cannot materialize a value for the constructor argument. -* `@PersistenceConstructor`: Marks a given constructor - even a package protected one - to use when instantiating the object from the database. Constructor arguments are mapped by name to the key values in the retrieved Document. -* `@Value`: This annotation is part of the Spring Framework . Within the mapping framework it can be applied to constructor arguments. This lets you use a Spring Expression Language statement to transform a key's value retrieved in the database before it is used to construct a domain object. In order to reference a property of a given document one has to use expressions like: `@Value("#root.myProperty")` where `root` refers to the root of the given document. +* `@Transient`: By default, all fields are mapped to the document. +This annotation excludes the field where it is applied from being stored in the database. +Transient properties cannot be used within a persistence constructor as the converter cannot materialize a value for the constructor argument. +* `@PersistenceConstructor`: Marks a given constructor - even a package protected one - to use when instantiating the object from the database. +Constructor arguments are mapped by name to the key values in the retrieved Document. +* `@Value`: This annotation is part of the Spring Framework . Within the mapping framework it can be applied to constructor arguments. +This lets you use a Spring Expression Language statement to transform a key's value retrieved in the database before it is used to construct a domain object. +In order to reference a property of a given document one has to use expressions like: `@Value("#root.myProperty")` where `root` refers to the root of the given document. * `@Field`: Applied at the field level it allows to describe the name and type of the field as it will be represented in the MongoDB BSON document thus allowing the name and type to be different than the fieldname of the class as well as the property type. -* `@Version`: Applied at field level is used for optimistic locking and checked for modification on save operations. The initial value is `zero` (`one` for primitive types) which is bumped automatically on every update. +* `@Version`: Applied at field level is used for optimistic locking and checked for modification on save operations. +The initial value is `zero` (`one` for primitive types) which is bumped automatically on every update. -The mapping metadata infrastructure is defined in a separate spring-data-commons project that is technology agnostic. Specific subclasses are using in the MongoDB support to support annotation based metadata. Other strategies are also possible to put in place if there is demand. +The mapping metadata infrastructure is defined in a separate spring-data-commons project that is technology agnostic. +Specific subclasses are using in the MongoDB support to support annotation based metadata. +Other strategies are also possible to put in place if there is demand. .Here is an example of a more complex mapping [%collapsible] @@ -500,9 +523,9 @@ public class Person { [TIP] ==== -`@Field(targetType=...)` can come in handy when the native MongoDB type inferred by the mapping infrastructure does not -match the expected one. Like for `BigDecimal`, which is represented as `String` instead of `Decimal128`, just because earlier -versions of MongoDB Server did not have support for it. +`@Field(targetType=...)` can come in handy when the native MongoDB type inferred by the mapping infrastructure does not match the expected one. +Like for `BigDecimal`, which is represented as `String` instead of `Decimal128`, just because earlier versions of MongoDB Server did not have support for it. + [source,java] ---- public class Balance { @@ -537,22 +560,22 @@ public class Balance { === Special Field Names -Generally speaking MongoDB uses the `.` (dot) sign as a path deliminator for nested objects. -This means that in a query or update statement a key like `a.b.c` targets an object structure as outlined below. +Generally speaking MongoDB uses the dot (`.`) character as a path separator for nested documents or arrays. +This means that in a query (or update statement) a key like `a.b.c` targets an object structure as outlined below: [source,json] ---- { 'a' : { 'b' : { - 'c' : ... + 'c' : … } } } ---- -Therefore up until MongoDB 5.0 field names must not contain `.` (dot). + -Using a `MappingMongoConverter#setMapKeyDotReplacement` allowed circumvent some of the limitations when storing `Map` structures by substituting `.` (dots) on write with another character. +Therefore, up until MongoDB 5.0 field names must not contain dots (`.`). + +Using a `MappingMongoConverter#setMapKeyDotReplacement` allowed circumvent some of the limitations when storing `Map` structures by substituting dots on write with another character. [source,java] ---- @@ -565,16 +588,18 @@ converter.write(source,...) // -> map : { 'key-with-dot', 'value' } With the release of MongoDB 5.0 this restriction on `Document` field names containing special characters was lifted. We highly recommend reading more about limitations on using dots in field names in the https://www.mongodb.com/docs/manual/core/dot-dollar-considerations/[MongoDB Reference]. + -To allow `.` (dots) in `Map` structures please set `preserveMapKeys` on the `MappingMongoConverter`. +To allow dots in `Map` structures please set `preserveMapKeys` on the `MappingMongoConverter`. -Using `@Field` allows to customize the field name to consider `.` (dots) in two ways. +Using `@Field` allows customizing the field name to consider dots in two ways. -. `@Field(name = "a.b")`: The name is considered to be a path. Writes will create nested objects such as `{ `a` : { `b` : ... } }`. -. `@Field(name = "a.b", fieldNameType = KEY)`: The names is considered a name as is. Writes will create a field with the given value as `{ 'a.b' : ... }` +. `@Field(name = "a.b")`: The name is considered to be a path. +Operations expect a structure of nested objects such as `{ a : { b : … } }`. +. `@Field(name = "a.b", fieldNameType = KEY)`: The names is considered a name as-is. +Operations expect a field with the given value as `{ 'a.b' : ….. }` [WARNING] ==== -Due to the special nature of the `.` (dot) sign in both MongoDB query and update statements field names containing `.` cannot be targeted directly and therefore are excluded from being used in derived query methods. +Due to the special nature of the dot character in both MongoDB query and update statements field names containing dots cannot be targeted directly and therefore are excluded from being used in derived query methods. Consider the following `Item` having a `categoryId` property that is mapped to the field named `cat.id`. [source,java] @@ -588,7 +613,8 @@ public class Item { } ---- -It's raw representation will look like +Its raw representation will look like + [source,json] ---- { @@ -599,7 +625,7 @@ It's raw representation will look like Since we cannot target the `cat.id` field directly (as this would be interpreted as a path) we need the help of the xref:mongodb/aggregation-framework.adoc#mongo.aggregation[Aggregation Framework]. -.Query fields with `.` (dot) in name +.Query fields with a dot in its name [source,java] ---- template.query(Item.class) @@ -607,9 +633,11 @@ template.query(Item.class) .matching(expr(ComparisonOperators.valueOf(ObjectOperators.getValueOf("value")).equalToValue("5b28b5e7-52c2"))) <1> .all(); ---- -<1> The mapping layer takes care of translating the property name `value` into the actual field name. It is absolutely valid to use the target field name here as well. -.Update fields with `.` (dot) in name +<1> The mapping layer takes care of translating the property name `value` into the actual field name. +It is absolutely valid to use the target field name here as well. + +.Update fields with a dot in its name [source,java] ---- template.update(Item.class) @@ -618,18 +646,23 @@ template.update(Item.class) .apply(AggregationUpdate.newUpdate(ReplaceWithOperation.replaceWithValue(ObjectOperators.setValueTo("value", "af29-f87f4e933f97")))) <1> .first(); ---- -<1> The mapping layer takes care of translating the property name `value` into the actual field name. It is absolutely valid to use the target field name here as well. -The above shows a simple example where the special field is present on the top document level. Increased levels of nesting increase the complexity of the aggregation expression required to interact with the field. +<1> The mapping layer takes care of translating the property name `value` into the actual field name. +It is absolutely valid to use the target field name here as well. + +The above shows a simple example where the special field is present on the top document level. +Increased levels of nesting increase the complexity of the aggregation expression required to interact with the field. ==== [[mapping-custom-object-construction]] === Customized Object Construction -The mapping subsystem allows the customization of the object construction by annotating a constructor with the `@PersistenceConstructor` annotation. The values to be used for the constructor parameters are resolved in the following way: +The mapping subsystem allows the customization of the object construction by annotating a constructor with the `@PersistenceConstructor` annotation. +The values to be used for the constructor parameters are resolved in the following way: * If a parameter is annotated with the `@Value` annotation, the given expression is evaluated and the result is used as the parameter value. -* If the Java type has a property whose name matches the given field of the input document, then it's property information is used to select the appropriate constructor parameter to pass the input field value to. This works only if the parameter name information is present in the java `.class` files which can be achieved by compiling the source with debug information or using the new `-parameters` command-line switch for javac in Java 8. +* If the Java type has a property whose name matches the given field of the input document, then it's property information is used to select the appropriate constructor parameter to pass the input field value to. +This works only if the parameter name information is present in the java `.class` files which can be achieved by compiling the source with debug information or using the new `-parameters` command-line switch for javac in Java 8. * Otherwise, a `MappingException` will be thrown indicating that the given constructor parameter could not be bound. [source,java]