From bd985a6589ed8dc5b6f18dc153e5dd10eedf2175 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Wed, 2 Dec 2020 11:06:56 +0100 Subject: [PATCH] Add support for embedded types. We now support embedded types in the sense of unwrapping nested objects into their parent Document to flatten out domain models where needed. A domain class of: public class User { @Id private String userId; @Embedded(onEmpty = USE_NULL) private UserName name; } public class UserName { private String firstname; private String lastname; } renders: { "_id" : "1da2ba06-3ba7", "firstname" : "Emma", "lastname" : "Frost" } Resolves #2803. Original pull request: #896. --- .../core/DefaultIndexOperationsProvider.java | 4 +- .../data/mongodb/core/MongoTemplate.java | 12 +- .../core/convert/DocumentAccessor.java | 4 + .../core/convert/MappingMongoConverter.java | 42 ++ .../mongodb/core/convert/MongoConverter.java | 5 +- .../mongodb/core/convert/MongoWriter.java | 6 +- .../mongodb/core/convert/QueryMapper.java | 72 +++- .../mongodb/core/convert/UpdateMapper.java | 10 + .../core/index/IndexOperationsProvider.java | 16 +- .../MongoPersistentEntityIndexResolver.java | 29 +- .../mapping/BasicMongoPersistentEntity.java | 3 +- .../data/mongodb/core/mapping/Embedded.java | 136 +++++++ .../core/mapping/EmbeddedEntityContext.java | 33 ++ .../EmbeddedMongoPersistentEntity.java | 281 +++++++++++++ .../EmbeddedMongoPersistentProperty.java | 259 ++++++++++++ .../core/mapping/MongoMappingContext.java | 22 +- .../core/mapping/MongoPersistentEntity.java | 7 +- .../core/mapping/MongoPersistentProperty.java | 27 +- .../IndexEnsuringQueryCreationListener.java | 24 +- .../support/MongoRepositoryFactoryBean.java | 2 +- .../ReactiveMongoRepositoryFactoryBean.java | 2 +- .../AbstractMongoConfigurationUnitTests.java | 3 +- ...ctReactiveMongoConfigurationUnitTests.java | 3 +- .../core/MongoTemplateEmbeddedTests.java | 132 +++++++ ...dAggregationOperationContextUnitTests.java | 121 +++++- .../MappingMongoConverterUnitTests.java | 239 +++++++++++ .../convert/MongoExampleMapperUnitTests.java | 82 +++- .../core/convert/QueryMapperUnitTests.java | 228 ++++++++++- .../core/convert/UpdateMapperUnitTests.java | 106 ++++- ...ersistentEntityIndexResolverUnitTests.java | 88 ++++- .../BasicMongoPersistentEntityUnitTests.java | 15 +- ...BasicMongoPersistentPropertyUnitTests.java | 2 +- .../mapping/MongoMappingContextUnitTests.java | 8 +- ...tractPersonRepositoryIntegrationTests.java | 37 +- .../data/mongodb/repository/Person.java | 12 + .../mongodb/repository/PersonRepository.java | 4 + ...positoryIndexCreationIntegrationTests.java | 1 + ...nsuringQueryCreationListenerUnitTests.java | 2 +- .../reference/embedded-documents.adoc | 370 ++++++++++++++++++ src/main/asciidoc/reference/mapping.adoc | 2 + 40 files changed, 2390 insertions(+), 61 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/Embedded.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/EmbeddedEntityContext.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/EmbeddedMongoPersistentEntity.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/EmbeddedMongoPersistentProperty.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateEmbeddedTests.java create mode 100644 src/main/asciidoc/reference/embedded-documents.adoc diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultIndexOperationsProvider.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultIndexOperationsProvider.java index 0bab23dea..4ca69116d 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultIndexOperationsProvider.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultIndexOperationsProvider.java @@ -47,7 +47,7 @@ class DefaultIndexOperationsProvider implements IndexOperationsProvider { * @see org.springframework.data.mongodb.core.index.IndexOperationsProvider#reactiveIndexOps(java.lang.String) */ @Override - public IndexOperations indexOps(String collectionName) { - return new DefaultIndexOperations(mongoDbFactory, collectionName, mapper); + public IndexOperations indexOps(String collectionName, Class type) { + return new DefaultIndexOperations(mongoDbFactory, collectionName, mapper, type); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java index 0591484c8..bd7637d3d 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java @@ -715,12 +715,18 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, }); } + + @Override + public IndexOperations indexOps(String collectionName) { + return indexOps(collectionName, null); + } + /* * (non-Javadoc) * @see org.springframework.data.mongodb.core.ExecutableInsertOperation#indexOps(java.lang.String) */ - public IndexOperations indexOps(String collectionName) { - return new DefaultIndexOperations(this, collectionName, null); + public IndexOperations indexOps(String collectionName, @Nullable Class type) { + return new DefaultIndexOperations(this, collectionName, type); } /* @@ -728,7 +734,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, * @see org.springframework.data.mongodb.core.ExecutableInsertOperation#indexOps(java.lang.Class) */ public IndexOperations indexOps(Class entityClass) { - return new DefaultIndexOperations(this, getCollectionName(entityClass), entityClass); + return indexOps(getCollectionName(entityClass), entityClass); } /* 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 13234cc3a..44c5f75f2 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 @@ -67,6 +67,10 @@ class DocumentAccessor { return this.document; } + public void putAll(MongoPersistentProperty prop, Document value) { + value.entrySet().forEach(entry -> BsonUtils.asMap(document).put(entry.getKey(), entry.getValue())); + } + /** * Puts the given value into the backing {@link Document} based on the coordinates defined through the given * {@link MongoPersistentProperty}. By default this will be the plain field name. But field names might also consist 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 92f761783..2e0f1e6ba 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 @@ -62,6 +62,8 @@ import org.springframework.data.mapping.model.SpELExpressionEvaluator; import org.springframework.data.mapping.model.SpELExpressionParameterValueProvider; import org.springframework.data.mongodb.CodecRegistryProvider; import org.springframework.data.mongodb.MongoDatabaseFactory; +import org.springframework.data.mongodb.core.mapping.Embedded; +import org.springframework.data.mongodb.core.mapping.Embedded.OnEmpty; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.core.mapping.event.AfterConvertCallback; @@ -427,6 +429,13 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App continue; } + if (prop.isEmbedded()) { + + accessor.setProperty(prop, + readEmbedded(documentAccessor, currentPath, prop, mappingContext.getPersistentEntity(prop))); + continue; + } + // We skip the id property since it was already set if (entity.isIdProperty(prop)) { @@ -472,6 +481,22 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App accessor.setProperty(property, dbRefResolver.resolveDbRef(property, dbref, callback, handler)); } + @Nullable + private Object readEmbedded(DocumentAccessor documentAccessor, ObjectPath currentPath, MongoPersistentProperty prop, + MongoPersistentEntity embeddedEntity) { + + if (prop.findAnnotation(Embedded.class).onEmpty().equals(OnEmpty.USE_EMPTY)) { + return read(embeddedEntity, (Document) documentAccessor.getDocument(), currentPath); + } + + for (MongoPersistentProperty persistentProperty : embeddedEntity) { + if (documentAccessor.hasValue(persistentProperty)) { + return read(embeddedEntity, (Document) documentAccessor.getDocument(), currentPath); + } + } + return null; + } + /* * (non-Javadoc) * @see org.springframework.data.mongodb.core.convert.MongoWriter#toDBRef(java.lang.Object, org.springframework.data.mongodb.core.mapping.MongoPersistentProperty) @@ -642,6 +667,15 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App TypeInformation valueType = ClassTypeInformation.from(obj.getClass()); TypeInformation type = prop.getTypeInformation(); + if (prop.isEmbedded()) { + + Document target = new Document(); + writeInternal(obj, target, mappingContext.getPersistentEntity(prop)); + + accessor.putAll(prop, target); + return; + } + if (valueType.isCollectionLike()) { List collectionInternal = createCollection(asCollection(obj), prop); accessor.put(prop, collectionInternal); @@ -1352,6 +1386,14 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App return !obj.getClass().equals(typeInformation.getType()) ? newDocument : removeTypeInfo(newDocument, true); } + @Nullable + @Override + public Object convertToMongoType(@Nullable Object obj, MongoPersistentEntity entity) { + Document newDocument = new Document(); + writeInternal(obj, newDocument, entity); + return newDocument; + } + public List maybeConvertList(Iterable source, TypeInformation typeInformation) { List newDbl = new ArrayList<>(); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverter.java index 5bf48cd3f..8887a3bd0 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverter.java @@ -26,6 +26,7 @@ import org.springframework.data.convert.TypeMapper; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.util.BsonUtils; +import org.springframework.data.util.TypeInformation; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -144,9 +145,9 @@ public interface MongoConverter try { return getConversionService().canConvert(id.getClass(), targetType) ? getConversionService().convert(id, targetType) - : convertToMongoType(id, null); + : convertToMongoType(id, (TypeInformation) null); } catch (ConversionException o_O) { - return convertToMongoType(id, null); + return convertToMongoType(id,(TypeInformation) null); } } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoWriter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoWriter.java index d3115a440..0f64177bc 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoWriter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoWriter.java @@ -17,6 +17,7 @@ package org.springframework.data.mongodb.core.convert; import org.bson.conversions.Bson; import org.springframework.data.convert.EntityWriter; +import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.util.TypeInformation; import org.springframework.lang.Nullable; @@ -43,7 +44,7 @@ public interface MongoWriter extends EntityWriter { */ @Nullable default Object convertToMongoType(@Nullable Object obj) { - return convertToMongoType(obj, null); + return convertToMongoType(obj, (TypeInformation) null); } /** @@ -57,6 +58,9 @@ public interface MongoWriter extends EntityWriter { @Nullable Object convertToMongoType(@Nullable Object obj, @Nullable TypeInformation typeInformation); + default Object convertToMongoType(@Nullable Object obj, MongoPersistentEntity entity) { + return convertToMongoType(obj, entity.getTypeInformation()); + } /** * Creates a {@link DBRef} to refer to the given object. * 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 0fb8723d1..e54deca25 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 @@ -32,6 +32,7 @@ import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.PersistentPropertyPath; +import org.springframework.data.mapping.PropertyHandler; import org.springframework.data.mapping.PropertyPath; import org.springframework.data.mapping.PropertyReferenceException; import org.springframework.data.mapping.context.InvalidPersistentPropertyPath; @@ -140,9 +141,23 @@ public class QueryMapper { try { Field field = createPropertyField(entity, key, mappingContext); - Entry entry = getMappedObjectForField(field, BsonUtils.get(query, key)); - result.put(entry.getKey(), entry.getValue()); + // TODO: move to dedicated method + if (field.getProperty() != null && field.getProperty().isEmbedded()) { + + Object theNestedObject = BsonUtils.get(query, key); + Document mappedValue = (Document) getMappedValue(field, theNestedObject); + if (!StringUtils.hasText(field.getMappedKey())) { + result.putAll(mappedValue); + } else { + result.put(field.getMappedKey(), mappedValue); + } + } else { + + Entry entry = getMappedObjectForField(field, BsonUtils.get(query, key)); + + result.put(entry.getKey(), entry.getValue()); + } } catch (InvalidPersistentPropertyPath invalidPathException) { // in case the object has not already been mapped @@ -173,10 +188,16 @@ public class QueryMapper { return new Document(); } + sortObject = filterEmbeddedObjects(sortObject, entity); + Document mappedSort = new Document(); for (Map.Entry entry : BsonUtils.asMap(sortObject).entrySet()) { Field field = createPropertyField(entity, entry.getKey(), mappingContext); + if (field.getProperty() != null && field.getProperty().isEmbedded()) { + continue; + } + mappedSort.put(field.getMappedKey(), entry.getValue()); } @@ -197,7 +218,9 @@ public class QueryMapper { Assert.notNull(fieldsObject, "FieldsObject must not be null!"); - Document mappedFields = fieldsObject.isEmpty() ? new Document() : getMappedObject(fieldsObject, entity); + fieldsObject = filterEmbeddedObjects(fieldsObject, entity); + + Document mappedFields = getMappedObject(fieldsObject, entity); mapMetaAttributes(mappedFields, entity, MetaMapping.FORCE); return mappedFields; } @@ -217,6 +240,43 @@ public class QueryMapper { } } + private Document filterEmbeddedObjects(Document fieldsObject, @Nullable MongoPersistentEntity entity) { + + if (fieldsObject.isEmpty() || entity == null) { + return fieldsObject; + } + + Document target = new Document(); + + for (Entry field : fieldsObject.entrySet()) { + + try { + + PropertyPath path = PropertyPath.from(field.getKey(), entity.getTypeInformation()); + PersistentPropertyPath persistentPropertyPath = mappingContext + .getPersistentPropertyPath(path); + MongoPersistentProperty property = mappingContext.getPersistentPropertyPath(path).getLeafProperty(); + + if (property.isEmbedded() && property.isEntity()) { + + mappingContext.getPersistentEntity(property) + .doWithProperties((PropertyHandler) embedded -> { + + String dotPath = persistentPropertyPath.toDotPath(); + dotPath = dotPath + (StringUtils.hasText(dotPath) ? "." : "") + embedded.getName(); + target.put(dotPath, field.getValue()); + }); + } else { + target.put(field.getKey(), field.getValue()); + } + } catch (RuntimeException e) { + target.put(field.getKey(), field.getValue()); + } + + } + return target; + } + private Document getMappedTextScoreField(MongoPersistentProperty property) { return new Document(property.getFieldName(), META_TEXT_SCORE); } @@ -497,6 +557,11 @@ public class QueryMapper { */ @Nullable protected Object delegateConvertToMongoType(Object source, @Nullable MongoPersistentEntity entity) { + + if (entity != null && entity.isEmbedded()) { + return converter.convertToMongoType(source, entity); + } + return converter.convertToMongoType(source, entity == null ? null : entity.getTypeInformation()); } @@ -912,6 +977,7 @@ public class QueryMapper { public TypeInformation getTypeHint() { return ClassTypeInformation.OBJECT; } + } /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/UpdateMapper.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/UpdateMapper.java index c824148dd..51d7badaa 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/UpdateMapper.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/UpdateMapper.java @@ -26,6 +26,7 @@ import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Order; import org.springframework.data.mapping.Association; import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.mongodb.core.mapping.EmbeddedMongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.core.query.Query; @@ -132,6 +133,11 @@ public class UpdateMapper extends QueryMapper { */ @Override protected Object delegateConvertToMongoType(Object source, @Nullable MongoPersistentEntity entity) { + + if(entity != null && entity.isEmbedded()) { + return converter.convertToMongoType(source, entity); + } + return converter.convertToMongoType(source, entity == null ? ClassTypeInformation.OBJECT : getTypeHintForEntity(source, entity)); } @@ -158,6 +164,10 @@ public class UpdateMapper extends QueryMapper { return getMappedUpdateModifier(field, rawValue); } + if(field.getProperty() != null && field.getProperty().isEmbedded()) { + System.out.println("here we are: "); + } + return super.getMappedObjectForField(field, rawValue); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexOperationsProvider.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexOperationsProvider.java index 4f003e207..a912ae56c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexOperationsProvider.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexOperationsProvider.java @@ -15,6 +15,8 @@ */ package org.springframework.data.mongodb.core.index; +import org.springframework.lang.Nullable; + /** * Provider interface to obtain {@link IndexOperations} by MongoDB collection name. * @@ -25,11 +27,23 @@ package org.springframework.data.mongodb.core.index; @FunctionalInterface public interface IndexOperationsProvider { + /** + * Returns the operations that can be performed on indexes + * + * @param collectionName name of the MongoDB collection, must not be {@literal null}. + * @param type the type used for field mapping. Can be {@literal null}. + * @return index operations on the named collection + * @since 2.5 + */ + IndexOperations indexOps(String collectionName, @Nullable Class type); + /** * Returns the operations that can be performed on indexes * * @param collectionName name of the MongoDB collection, must not be {@literal null}. * @return index operations on the named collection */ - IndexOperations indexOps(String collectionName); + default IndexOperations indexOps(String collectionName) { + return indexOps(collectionName, null); + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java index 224aca249..8c158c34b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java @@ -135,8 +135,9 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver { try { if (persistentProperty.isEntity()) { - indexes.addAll(resolveIndexForClass(persistentProperty.getTypeInformation().getActualType(), - persistentProperty.getFieldName(), Path.of(persistentProperty), root.getCollection(), guard)); + indexes.addAll(resolveIndexForEntity(mappingContext.getPersistentEntity(persistentProperty), + persistentProperty.isEmbedded() ? "" : persistentProperty.getFieldName(), Path.of(persistentProperty), + root.getCollection(), guard)); } List indexDefinitions = createIndexDefinitionHolderForProperty( @@ -163,7 +164,11 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver { private List resolveIndexForClass(final TypeInformation type, final String dotPath, final Path path, final String collection, final CycleGuard guard) { - MongoPersistentEntity entity = mappingContext.getRequiredPersistentEntity(type); + return resolveIndexForEntity(mappingContext.getRequiredPersistentEntity(type), dotPath, path, collection, guard); + } + + private List resolveIndexForEntity(MongoPersistentEntity entity, final String dotPath, + final Path path, final String collection, final CycleGuard guard) { final List indexInformation = new ArrayList<>(); indexInformation.addAll(potentiallyCreateCompoundIndexDefinitions(dotPath, collection, entity)); @@ -179,14 +184,18 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver { private void guardAndPotentiallyAddIndexForProperty(MongoPersistentProperty persistentProperty, String dotPath, Path path, String collection, List indexes, CycleGuard guard) { - String propertyDotPath = (StringUtils.hasText(dotPath) ? dotPath + "." : "") + persistentProperty.getFieldName(); + String propertyDotPath = dotPath; + + if (!persistentProperty.isEmbedded()) { + propertyDotPath = (StringUtils.hasText(dotPath) ? dotPath + "." : "") + persistentProperty.getFieldName(); + } Path propertyPath = path.append(persistentProperty); guard.protect(persistentProperty, propertyPath); if (persistentProperty.isEntity()) { try { - indexes.addAll(resolveIndexForClass(persistentProperty.getTypeInformation().getActualType(), propertyDotPath, + indexes.addAll(resolveIndexForEntity(mappingContext.getPersistentEntity(persistentProperty), propertyDotPath, propertyPath, collection, guard)); } catch (CyclicPropertyReferenceException e) { LOGGER.info(e.getMessage()); @@ -206,6 +215,13 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver { List indices = new ArrayList<>(2); + if (persistentProperty.isEmbedded() && (persistentProperty.isAnnotationPresent(Indexed.class) + || persistentProperty.isAnnotationPresent(HashIndexed.class) + || persistentProperty.isAnnotationPresent(GeoSpatialIndexed.class))) { + throw new InvalidDataAccessApiUsageException( + String.format("Index annotation not allowed on embedded object for path '%s'.", dotPath)); + } + if (persistentProperty.isAnnotationPresent(Indexed.class)) { indices.add(createIndexDefinition(dotPath, collection, persistentProperty)); } else if (persistentProperty.isAnnotationPresent(GeoSpatialIndexed.class)) { @@ -482,7 +498,7 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver { return new IndexDefinitionHolder(dotPath, indexDefinition, collection); } - private PartialIndexFilter evaluatePartialFilter(String filterExpression, PersistentEntity entity) { + private PartialIndexFilter evaluatePartialFilter(String filterExpression, PersistentEntity entity) { Object result = evaluate(filterExpression, getEvaluationContextForProperty(entity)); @@ -493,7 +509,6 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver { return PartialIndexFilter.of(BsonUtils.parse(filterExpression, null)); } - /** * Creates {@link HashedIndex} wrapped in {@link IndexDefinitionHolder} out of {@link HashIndexed} for a given * {@link MongoPersistentProperty}. 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 44daa6bca..7bf8214ae 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 @@ -164,7 +164,8 @@ public class BasicMongoPersistentEntity extends BasicPersistentEntity + * Depending on the {@link OnEmpty value} of {@link #onEmpty()} the property is set to {@literal null} or an empty + * instance in the case all embedded values are {@literal null} when reading from the result set. + * + * @author Christoph Strobl + * @since 3.2 + */ +@Documented +@Retention(value = RetentionPolicy.RUNTIME) +@Target(value = { ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.METHOD }) +public @interface Embedded { + + /** + * Set the load strategy for the embedded object if all contained fields yield {@literal null} values. + *

+ * {@link Nullable @Embedded.Nullable} and {@link Empty @Embedded.Empty} offer shortcuts for this. + * + * @return never {@link} null. + */ + OnEmpty onEmpty(); + + /** + * @return prefix for columns in the embedded value object. An empty {@link String} by default. + */ + String prefix() default ""; + + /** + * Load strategy to be used {@link Embedded#onEmpty()}. + * + * @author Christoph Strobl + */ + enum OnEmpty { + USE_NULL, USE_EMPTY + } + + /** + * Shortcut for a nullable embedded property. + * + *

+	 * @Embedded.Nullable private Address address;
+	 * 
+ * + * as alternative to the more verbose + * + *
+	 * @Embedded(onEmpty = USE_NULL) @javax.annotation.Nonnull(when = When.MAYBE) private Address address;
+	 * 
+ * + * @author Christoph Strobl + * @see Embedded#onEmpty() + */ + @Embedded(onEmpty = OnEmpty.USE_NULL) + @Documented + @Retention(RetentionPolicy.RUNTIME) + @Target({ ElementType.FIELD, ElementType.METHOD }) + @javax.annotation.Nonnull(when = When.MAYBE) + @interface Nullable { + + /** + * @return prefix for columns in the embedded value object. An empty {@link String} by default. + */ + @AliasFor(annotation = Embedded.class, attribute = "prefix") + String prefix() default ""; + + /** + * @return value for columns in the embedded value object. An empty {@link String} by default. + */ + @AliasFor(annotation = Embedded.class, attribute = "prefix") + String value() default ""; + } + + /** + * Shortcut for an empty embedded property. + * + *
+	 * @Embedded.Empty private Address address;
+	 * 
+ * + * as alternative to the more verbose + * + *
+	 * @Embedded(onEmpty = USE_EMPTY) @javax.annotation.Nonnull(when = When.NEVER) private Address address;
+	 * 
+ * + * @author Christoph Strobl + * @see Embedded#onEmpty() + */ + @Embedded(onEmpty = OnEmpty.USE_EMPTY) + @Documented + @Retention(RetentionPolicy.RUNTIME) + @Target({ ElementType.FIELD, ElementType.METHOD }) + @javax.annotation.Nonnull(when = When.NEVER) + @interface Empty { + + /** + * @return prefix for columns in the embedded value object. An empty {@link String} by default. + */ + @AliasFor(annotation = Embedded.class, attribute = "prefix") + String prefix() default ""; + + /** + * @return value for columns in the embedded value object. An empty {@link String} by default. + */ + @AliasFor(annotation = Embedded.class, attribute = "prefix") + String value() default ""; + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/EmbeddedEntityContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/EmbeddedEntityContext.java new file mode 100644 index 000000000..a3390e8e5 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/EmbeddedEntityContext.java @@ -0,0 +1,33 @@ +/* + * Copyright 2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.core.mapping; + +/** + * @author Christoph Strobl + * @since 3.2 + */ +public class EmbeddedEntityContext { + + private final MongoPersistentProperty property; + + public EmbeddedEntityContext(MongoPersistentProperty property) { + this.property = property; + } + + public MongoPersistentProperty getProperty() { + return property; + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/EmbeddedMongoPersistentEntity.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/EmbeddedMongoPersistentEntity.java new file mode 100644 index 000000000..9029cfc2f --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/EmbeddedMongoPersistentEntity.java @@ -0,0 +1,281 @@ +/* + * Copyright 2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.core.mapping; + +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Spliterator; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import org.springframework.data.mapping.*; +import org.springframework.data.mapping.model.PersistentPropertyAccessorFactory; +import org.springframework.data.mongodb.core.query.Collation; +import org.springframework.data.spel.EvaluationContextProvider; +import org.springframework.data.util.Streamable; +import org.springframework.data.util.TypeInformation; +import org.springframework.lang.Nullable; + +/** + * @author Christoph Strobl + * @since 2020/12 + */ +public class EmbeddedMongoPersistentEntity implements MongoPersistentEntity { + + private EmbeddedEntityContext context; + private MongoPersistentEntity delegate; + + public EmbeddedMongoPersistentEntity(MongoPersistentEntity delegate, EmbeddedEntityContext context) { + + this.context = context; + this.delegate = delegate; + } + + public String getCollection() { + return delegate.getCollection(); + } + + public String getLanguage() { + return delegate.getLanguage(); + } + + @Nullable + public MongoPersistentProperty getTextScoreProperty() { + return delegate.getTextScoreProperty(); + } + + public boolean hasTextScoreProperty() { + return delegate.hasTextScoreProperty(); + } + + @Nullable + public Collation getCollation() { + return delegate.getCollation(); + } + + public boolean hasCollation() { + return delegate.hasCollation(); + } + + public ShardKey getShardKey() { + return delegate.getShardKey(); + } + + public boolean isSharded() { + return delegate.isSharded(); + } + + public String getName() { + return delegate.getName(); + } + + @Nullable + public PreferredConstructor getPersistenceConstructor() { + return delegate.getPersistenceConstructor(); + } + + public boolean isConstructorArgument(PersistentProperty property) { + return delegate.isConstructorArgument(property); + } + + public boolean isIdProperty(PersistentProperty property) { + return delegate.isIdProperty(property); + } + + public boolean isVersionProperty(PersistentProperty property) { + return delegate.isVersionProperty(property); + } + + @Nullable + public MongoPersistentProperty getIdProperty() { + return delegate.getIdProperty(); + } + + public MongoPersistentProperty getRequiredIdProperty() { + return delegate.getRequiredIdProperty(); + } + + @Nullable + public MongoPersistentProperty getVersionProperty() { + return delegate.getVersionProperty(); + } + + public MongoPersistentProperty getRequiredVersionProperty() { + return delegate.getRequiredVersionProperty(); + } + + @Nullable + public MongoPersistentProperty getPersistentProperty(String name) { + return wrap(delegate.getPersistentProperty(name)); + } + + public MongoPersistentProperty getRequiredPersistentProperty(String name) { + + MongoPersistentProperty persistentProperty = getPersistentProperty(name); + if (persistentProperty != null) { + return persistentProperty; + } + + throw new RuntimeException(":kladjnf"); + } + + @Nullable + public MongoPersistentProperty getPersistentProperty(Class annotationType) { + return wrap(delegate.getPersistentProperty(annotationType)); + } + + public Iterable getPersistentProperties(Class annotationType) { + return Streamable.of(delegate.getPersistentProperties(annotationType)).stream().map(this::wrap) + .collect(Collectors.toList()); + } + + public boolean hasIdProperty() { + return delegate.hasIdProperty(); + } + + public boolean hasVersionProperty() { + return delegate.hasVersionProperty(); + } + + public Class getType() { + return delegate.getType(); + } + + public Alias getTypeAlias() { + return delegate.getTypeAlias(); + } + + public TypeInformation getTypeInformation() { + return delegate.getTypeInformation(); + } + + public void doWithProperties(PropertyHandler handler) { + + delegate.doWithProperties((PropertyHandler) property -> { + handler.doWithPersistentProperty(wrap(property)); + }); + } + + public void doWithProperties(SimplePropertyHandler handler) { + + delegate.doWithProperties((SimplePropertyHandler) property -> { + if (property instanceof MongoPersistentProperty) { + handler.doWithPersistentProperty(wrap((MongoPersistentProperty) property)); + } else { + handler.doWithPersistentProperty(property); + } + }); + } + + public void doWithAssociations(AssociationHandler handler) { + delegate.doWithAssociations(handler); + } + + public void doWithAssociations(SimpleAssociationHandler handler) { + delegate.doWithAssociations(handler); + } + + @Nullable + public A findAnnotation(Class annotationType) { + return delegate.findAnnotation(annotationType); + } + + public A getRequiredAnnotation(Class annotationType) throws IllegalStateException { + return delegate.getRequiredAnnotation(annotationType); + } + + public boolean isAnnotationPresent(Class annotationType) { + return delegate.isAnnotationPresent(annotationType); + } + + public PersistentPropertyAccessor getPropertyAccessor(B bean) { + return delegate.getPropertyAccessor(bean); + } + + public PersistentPropertyPathAccessor getPropertyPathAccessor(B bean) { + return delegate.getPropertyPathAccessor(bean); + } + + public IdentifierAccessor getIdentifierAccessor(Object bean) { + return delegate.getIdentifierAccessor(bean); + } + + public boolean isNew(Object bean) { + return delegate.isNew(bean); + } + + public boolean isImmutable() { + return delegate.isImmutable(); + } + + public boolean requiresPropertyPopulation() { + return delegate.requiresPropertyPopulation(); + } + + public Iterator iterator() { + + List target = new ArrayList<>(); + delegate.iterator().forEachRemaining(it -> target.add(wrap(it))); + return target.iterator(); + } + + public void forEach(Consumer action) { + delegate.forEach(it -> action.accept(wrap(it))); + } + + public Spliterator spliterator() { + return delegate.spliterator(); + } + + private MongoPersistentProperty wrap(MongoPersistentProperty source) { + if (source == null) { + return source; + } + return new EmbeddedMongoPersistentProperty(source, context); + } + + @Override + public void addPersistentProperty(MongoPersistentProperty property) { + + } + + @Override + public void addAssociation(Association association) { + + } + + @Override + public void verify() throws MappingException { + + } + + @Override + public void setPersistentPropertyAccessorFactory(PersistentPropertyAccessorFactory factory) { + + } + + @Override + public void setEvaluationContextProvider(EvaluationContextProvider provider) { + + } + + @Override + public boolean isEmbedded() { + return context.getProperty().isEmbedded(); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/EmbeddedMongoPersistentProperty.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/EmbeddedMongoPersistentProperty.java new file mode 100644 index 000000000..e307de451 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/EmbeddedMongoPersistentProperty.java @@ -0,0 +1,259 @@ +/* + * Copyright 2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.core.mapping; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +import org.springframework.data.mapping.Association; +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.util.TypeInformation; +import org.springframework.lang.Nullable; + +/** + * @author Christoph Strobl + * @since 2020/12 + */ +public class EmbeddedMongoPersistentProperty implements MongoPersistentProperty { + + private final MongoPersistentProperty delegate; + private final EmbeddedEntityContext context; + + public EmbeddedMongoPersistentProperty(MongoPersistentProperty delegate, EmbeddedEntityContext context) { + + this.delegate = delegate; + this.context = context; + } + + public String getFieldName() { + + if (!context.getProperty().isEmbedded()) { + return delegate.getFieldName(); + } + + return context.getProperty().findAnnotation(Embedded.class).prefix() + delegate.getFieldName(); + } + + public Class getFieldType() { + return delegate.getFieldType(); + } + + public int getFieldOrder() { + return delegate.getFieldOrder(); + } + + public boolean isDbReference() { + return delegate.isDbReference(); + } + + public boolean isExplicitIdProperty() { + return delegate.isExplicitIdProperty(); + } + + public boolean isLanguageProperty() { + return delegate.isLanguageProperty(); + } + + public boolean isExplicitLanguageProperty() { + return delegate.isExplicitLanguageProperty(); + } + + public boolean isTextScoreProperty() { + return delegate.isTextScoreProperty(); + } + + @Nullable + public DBRef getDBRef() { + return delegate.getDBRef(); + } + + public boolean usePropertyAccess() { + return delegate.usePropertyAccess(); + } + + public boolean hasExplicitWriteTarget() { + return delegate.hasExplicitWriteTarget(); + } + + public PersistentEntity getOwner() { + return delegate.getOwner(); + } + + public String getName() { + return delegate.getName(); + } + + public Class getType() { + return delegate.getType(); + } + + public TypeInformation getTypeInformation() { + return delegate.getTypeInformation(); + } + + public Iterable> getPersistentEntityTypes() { + return delegate.getPersistentEntityTypes(); + } + + @Nullable + public Method getGetter() { + return delegate.getGetter(); + } + + public Method getRequiredGetter() { + return delegate.getRequiredGetter(); + } + + @Nullable + public Method getSetter() { + return delegate.getSetter(); + } + + public Method getRequiredSetter() { + return delegate.getRequiredSetter(); + } + + @Nullable + public Method getWither() { + return delegate.getWither(); + } + + public Method getRequiredWither() { + return delegate.getRequiredWither(); + } + + @Nullable + public Field getField() { + return delegate.getField(); + } + + public Field getRequiredField() { + return delegate.getRequiredField(); + } + + @Nullable + public String getSpelExpression() { + return delegate.getSpelExpression(); + } + + @Nullable + public Association getAssociation() { + return delegate.getAssociation(); + } + + public Association getRequiredAssociation() { + return delegate.getRequiredAssociation(); + } + + public boolean isEntity() { + return delegate.isEntity(); + } + + public boolean isIdProperty() { + return delegate.isIdProperty(); + } + + public boolean isVersionProperty() { + return delegate.isVersionProperty(); + } + + public boolean isCollectionLike() { + return delegate.isCollectionLike(); + } + + public boolean isMap() { + return delegate.isMap(); + } + + public boolean isArray() { + return delegate.isArray(); + } + + public boolean isTransient() { + return delegate.isTransient(); + } + + public boolean isWritable() { + return delegate.isWritable(); + } + + public boolean isImmutable() { + return delegate.isImmutable(); + } + + public boolean isAssociation() { + return delegate.isAssociation(); + } + + public boolean isEmbedded() { + return delegate.isEmbedded(); + } + + public boolean isNullable() { + return delegate.isNullable(); + } + + @Nullable + public Class getComponentType() { + return delegate.getComponentType(); + } + + public Class getRawType() { + return delegate.getRawType(); + } + + @Nullable + public Class getMapValueType() { + return delegate.getMapValueType(); + } + + public Class getActualType() { + return delegate.getActualType(); + } + + @Nullable + public A findAnnotation(Class annotationType) { + return delegate.findAnnotation(annotationType); + } + + public A getRequiredAnnotation(Class annotationType) throws IllegalStateException { + return delegate.getRequiredAnnotation(annotationType); + } + + @Nullable + public A findPropertyOrOwnerAnnotation(Class annotationType) { + return delegate.findPropertyOrOwnerAnnotation(annotationType); + } + + public boolean isAnnotationPresent(Class annotationType) { + return delegate.isAnnotationPresent(annotationType); + } + + public boolean hasActualTypeAnnotation(Class annotationType) { + return delegate.hasActualTypeAnnotation(annotationType); + } + + @Nullable + public Class getAssociationTargetType() { + return delegate.getAssociationTargetType(); + } + + public PersistentPropertyAccessor getAccessorForOwner(T owner) { + return delegate.getAccessorForOwner(owner); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoMappingContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoMappingContext.java index 938f16d0f..0c7bea76d 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoMappingContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoMappingContext.java @@ -36,7 +36,7 @@ import org.springframework.lang.Nullable; * @author Jon Brisbin * @author Oliver Gierke */ -public class MongoMappingContext extends AbstractMappingContext, MongoPersistentProperty> +public class MongoMappingContext extends AbstractMappingContext, MongoPersistentProperty> implements ApplicationContextAware { private static final FieldNamingStrategy DEFAULT_NAMING_STRATEGY = PropertyNameFieldNamingStrategy.INSTANCE; @@ -76,11 +76,16 @@ public class MongoMappingContext extends AbstractMappingContext owner, + public MongoPersistentProperty createPersistentProperty(Property property, MongoPersistentEntity owner, SimpleTypeHolder simpleTypeHolder) { return new CachingMongoPersistentProperty(property, owner, simpleTypeHolder, fieldNamingStrategy); } +// @Override +// protected MongoPersistentProperty createPersistentProperty(Property property, MongoPersistentEntity owner, SimpleTypeHolder simpleTypeHolder) { +// return null; +// } + /* * (non-Javadoc) * @see org.springframework.data.mapping.BasicMappingContext#createPersistentEntity(org.springframework.data.util.TypeInformation, org.springframework.data.mapping.model.MappingContext) @@ -126,4 +131,17 @@ public class MongoMappingContext extends AbstractMappingContext getPersistentEntity(MongoPersistentProperty persistentProperty) { + + MongoPersistentEntity entity = super.getPersistentEntity(persistentProperty); + if(entity == null || !persistentProperty.isEmbedded()) { + return entity; + } + + return new EmbeddedMongoPersistentEntity(entity, new EmbeddedEntityContext(persistentProperty)); + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentEntity.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentEntity.java index 9a0d60100..1f18f984e 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentEntity.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentEntity.java @@ -16,6 +16,7 @@ package org.springframework.data.mongodb.core.mapping; import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.model.MutablePersistentEntity; import org.springframework.lang.Nullable; /** @@ -24,7 +25,7 @@ import org.springframework.lang.Nullable; * @author Oliver Gierke * @author Christoph Strobl */ -public interface MongoPersistentEntity extends PersistentEntity { +public interface MongoPersistentEntity extends MutablePersistentEntity { /** * Returns the collection the entity shall be persisted to. @@ -93,4 +94,8 @@ public interface MongoPersistentEntity extends PersistentEntity metadata = query.getQueryMethod().getEntityInformation(); try { - indexOperationsProvider.indexOps(metadata.getCollectionName()).ensureIndex(index); + indexOperationsProvider.indexOps(metadata.getCollectionName(), metadata.getJavaType()).ensureIndex(index); } catch (UncategorizedMongoDbException e) { if (e.getCause() instanceof MongoException) { @@ -129,6 +138,19 @@ class IndexEnsuringQueryCreationListener implements QueryCreationListener, S, ID exten if (createIndexesForQueryMethods) { factory.addQueryCreationListener( - new IndexEnsuringQueryCreationListener(collectionName -> operations.indexOps(collectionName))); + new IndexEnsuringQueryCreationListener((collectionName, javaType) -> operations.indexOps(javaType))); } return factory; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactoryBean.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactoryBean.java index f694100ef..4e8232714 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactoryBean.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactoryBean.java @@ -97,7 +97,7 @@ public class ReactiveMongoRepositoryFactoryBean, S, if (createIndexesForQueryMethods) { factory.addQueryCreationListener(new IndexEnsuringQueryCreationListener( - collectionName -> IndexOperationsAdapter.blocking(operations.indexOps(collectionName)))); + (collectionName, javaType) -> IndexOperationsAdapter.blocking(operations.indexOps(javaType)))); } return factory; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/config/AbstractMongoConfigurationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/config/AbstractMongoConfigurationUnitTests.java index 37764ff9a..1ec266bac 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/config/AbstractMongoConfigurationUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/config/AbstractMongoConfigurationUnitTests.java @@ -39,6 +39,7 @@ import org.springframework.data.mongodb.core.convert.MongoTypeMapper; import org.springframework.data.mongodb.core.mapping.BasicMongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; +import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.spel.EvaluationContextProvider; import org.springframework.data.spel.ExtensionAwareEvaluationContextProvider; import org.springframework.test.util.ReflectionTestUtils; @@ -103,7 +104,7 @@ public class AbstractMongoConfigurationUnitTests { AbstractApplicationContext context = new AnnotationConfigApplicationContext(SampleMongoConfiguration.class); MongoMappingContext mappingContext = context.getBean(MongoMappingContext.class); - BasicMongoPersistentEntity entity = mappingContext.getRequiredPersistentEntity(Entity.class); + MongoPersistentEntity entity = mappingContext.getRequiredPersistentEntity(Entity.class); EvaluationContextProvider provider = (EvaluationContextProvider) ReflectionTestUtils.getField(entity, "evaluationContextProvider"); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/config/AbstractReactiveMongoConfigurationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/config/AbstractReactiveMongoConfigurationUnitTests.java index 39d998f10..abbb7e028 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/config/AbstractReactiveMongoConfigurationUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/config/AbstractReactiveMongoConfigurationUnitTests.java @@ -40,6 +40,7 @@ import org.springframework.data.mongodb.core.convert.MongoTypeMapper; import org.springframework.data.mongodb.core.mapping.BasicMongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; +import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.test.util.MongoTestUtils; import org.springframework.data.spel.EvaluationContextProvider; import org.springframework.data.spel.ExtensionAwareEvaluationContextProvider; @@ -103,7 +104,7 @@ public class AbstractReactiveMongoConfigurationUnitTests { AbstractApplicationContext context = new AnnotationConfigApplicationContext(SampleMongoConfiguration.class); MongoMappingContext mappingContext = context.getBean(MongoMappingContext.class); - BasicMongoPersistentEntity entity = mappingContext.getRequiredPersistentEntity(Entity.class); + MongoPersistentEntity entity = mappingContext.getRequiredPersistentEntity(Entity.class); EvaluationContextProvider provider = (EvaluationContextProvider) ReflectionTestUtils.getField(entity, "evaluationContextProvider"); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateEmbeddedTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateEmbeddedTests.java new file mode 100644 index 000000000..486a48784 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateEmbeddedTests.java @@ -0,0 +1,132 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.core; + +import static org.assertj.core.api.Assertions.*; +import static org.springframework.data.mongodb.core.query.Criteria.*; +import static org.springframework.data.mongodb.core.query.Query.*; + +import lombok.EqualsAndHashCode; + +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.data.mongodb.core.mapping.Embedded; +import org.springframework.data.mongodb.core.mapping.Field; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.test.util.MongoTemplateExtension; +import org.springframework.data.mongodb.test.util.Template; + +/** + * @author Christoph Strobl + */ +@ExtendWith(MongoTemplateExtension.class) +class MongoTemplateEmbeddedTests { + + private static @Template MongoTemplate template; + + @Test // DATAMONGO-1902 + void readWrite() { + + WithEmbedded source = new WithEmbedded(); + source.id = "id-1"; + source.embeddableValue = new EmbeddableType(); + source.embeddableValue.stringValue = "string-val"; + source.embeddableValue.listValue = Arrays.asList("list-val-1", "list-val-2"); + source.embeddableValue.atFieldAnnotatedValue = "@Field"; + + template.save(source); + + assertThat(template.findOne(query(where("id").is(source.id)), WithEmbedded.class)).isEqualTo(source); + } + + @Test // DATAMONGO-1902 + void filterOnEmbeddedValue() { + + WithEmbedded source = new WithEmbedded(); + source.id = "id-1"; + source.embeddableValue = new EmbeddableType(); + source.embeddableValue.stringValue = "string-val"; + source.embeddableValue.listValue = Arrays.asList("list-val-1", "list-val-2"); + source.embeddableValue.atFieldAnnotatedValue = "@Field"; + + template.save(source); + + assertThat(template.findOne( + Query.query(where("embeddableValue.stringValue").is(source.embeddableValue.stringValue)), WithEmbedded.class)) + .isEqualTo(source); + } + + @Test // DATAMONGO-1902 + void readWritePrefixed() { + + WithPrefixedEmbedded source = new WithPrefixedEmbedded(); + source.id = "id-1"; + source.embeddableValue = new EmbeddableType(); + source.embeddableValue.stringValue = "string-val"; + source.embeddableValue.listValue = Arrays.asList("list-val-1", "list-val-2"); + source.embeddableValue.atFieldAnnotatedValue = "@Field"; + + template.save(source); + + assertThat(template.findOne(query(where("id").is(source.id)), WithPrefixedEmbedded.class)).isEqualTo(source); + } + + @Test // DATAMONGO-1902 + void filterOnPrefixedEmbeddedValue() { + + WithPrefixedEmbedded source = new WithPrefixedEmbedded(); + source.id = "id-1"; + source.embeddableValue = new EmbeddableType(); + source.embeddableValue.stringValue = "string-val"; + source.embeddableValue.listValue = Arrays.asList("list-val-1", "list-val-2"); + source.embeddableValue.atFieldAnnotatedValue = "@Field"; + + template.save(source); + + assertThat( + template.findOne(Query.query(where("embeddableValue.stringValue").is(source.embeddableValue.stringValue)), + WithPrefixedEmbedded.class)).isEqualTo(source); + } + + @EqualsAndHashCode + static class WithEmbedded { + + String id; + + @Embedded.Nullable EmbeddableType embeddableValue; + } + + @EqualsAndHashCode + static class WithPrefixedEmbedded { + + String id; + + @Embedded.Nullable("prefix-") EmbeddableType embeddableValue; + } + + @EqualsAndHashCode + static class EmbeddableType { + + String stringValue; + List listValue; + + @Field("with-at-field-annotation") // + String atFieldAnnotatedValue; + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/TypeBasedAggregationOperationContextUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/TypeBasedAggregationOperationContextUnitTests.java index 1b40cfc04..0e4c600e1 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/TypeBasedAggregationOperationContextUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/TypeBasedAggregationOperationContextUnitTests.java @@ -32,7 +32,6 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; - import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.support.GenericConversionService; import org.springframework.data.annotation.Id; @@ -46,6 +45,7 @@ import org.springframework.data.mongodb.core.convert.DbRefResolver; import org.springframework.data.mongodb.core.convert.MappingMongoConverter; import org.springframework.data.mongodb.core.convert.MongoCustomConversions; import org.springframework.data.mongodb.core.convert.QueryMapper; +import org.springframework.data.mongodb.core.mapping.Embedded; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.data.mongodb.core.query.Criteria; @@ -357,6 +357,103 @@ public class TypeBasedAggregationOperationContextUnitTests { .isEqualTo(new Document("val", new Document("$add", Arrays.asList("$nested1.value1", "$field2.nestedValue2")))); } + @Test // DATAMONGO-1902 + void rendersProjectOnEmbeddedFieldCorrectly() { + + AggregationOperationContext context = getContext(WithEmbedded.class); + + Document agg = newAggregation(WithEmbedded.class, project().and("embeddableType.stringValue").as("val")) + .toDocument("collection", context); + + assertThat(getPipelineElementFromAggregationAt(agg, 0).get("$project")) + .isEqualTo(new Document("val", "$stringValue")); + } + + @Test // DATAMONGO-1902 + void rendersProjectOnEmbeddedFieldWithAtFieldAnnotationCorrectly() { + + AggregationOperationContext context = getContext(WithEmbedded.class); + + Document agg = newAggregation(WithEmbedded.class, project().and("embeddableType.atFieldAnnotatedValue").as("val")) + .toDocument("collection", context); + + assertThat(getPipelineElementFromAggregationAt(agg, 0).get("$project")) + .isEqualTo(new Document("val", "$with-at-field-annotation")); + } + + @Test // DATAMONGO-1902 + void rendersProjectOnPrefixedEmbeddedFieldCorrectly() { + + AggregationOperationContext context = getContext(WithEmbedded.class); + + Document agg = newAggregation(WithEmbedded.class, project().and("prefixedEmbeddableValue.stringValue").as("val")) + .toDocument("collection", context); + + assertThat(getPipelineElementFromAggregationAt(agg, 0).get("$project")) + .isEqualTo(new Document("val", "$prefix-stringValue")); + } + + @Test // DATAMONGO-1902 + void rendersProjectOnPrefixedEmbeddedFieldWithAtFieldAnnotationCorrectly() { + + AggregationOperationContext context = getContext(WithEmbedded.class); + + Document agg = newAggregation(WithEmbedded.class, + project().and("prefixedEmbeddableValue.atFieldAnnotatedValue").as("val")).toDocument("collection", context); + + assertThat(getPipelineElementFromAggregationAt(agg, 0).get("$project")) + .isEqualTo(new Document("val", "$prefix-with-at-field-annotation")); + } + + @Test // DATAMONGO-1902 + void rendersProjectOnNestedEmbeddedFieldCorrectly() { + + AggregationOperationContext context = getContext(WrapperAroundWithEmbedded.class); + + Document agg = newAggregation(WrapperAroundWithEmbedded.class, + project().and("withEmbedded.embeddableType.stringValue").as("val")).toDocument("collection", context); + + assertThat(getPipelineElementFromAggregationAt(agg, 0).get("$project")) + .isEqualTo(new Document("val", "$withEmbedded.stringValue")); + } + + @Test // DATAMONGO-1902 + void rendersProjectOnNestedEmbeddedFieldWithAtFieldAnnotationCorrectly() { + + AggregationOperationContext context = getContext(WrapperAroundWithEmbedded.class); + + Document agg = newAggregation(WrapperAroundWithEmbedded.class, + project().and("withEmbedded.embeddableType.atFieldAnnotatedValue").as("val")).toDocument("collection", context); + + assertThat(getPipelineElementFromAggregationAt(agg, 0).get("$project")) + .isEqualTo(new Document("val", "$withEmbedded.with-at-field-annotation")); + } + + @Test // DATAMONGO-1902 + void rendersProjectOnNestedPrefixedEmbeddedFieldCorrectly() { + + AggregationOperationContext context = getContext(WrapperAroundWithEmbedded.class); + + Document agg = newAggregation(WrapperAroundWithEmbedded.class, + project().and("withEmbedded.prefixedEmbeddableValue.stringValue").as("val")).toDocument("collection", context); + + assertThat(getPipelineElementFromAggregationAt(agg, 0).get("$project")) + .isEqualTo(new Document("val", "$withEmbedded.prefix-stringValue")); + } + + @Test // DATAMONGO-1902 + void rendersProjectOnNestedPrefixedEmbeddedFieldWithAtFieldAnnotationCorrectly() { + + AggregationOperationContext context = getContext(WrapperAroundWithEmbedded.class); + + Document agg = newAggregation(WrapperAroundWithEmbedded.class, + project().and("withEmbedded.prefixedEmbeddableValue.atFieldAnnotatedValue").as("val")).toDocument("collection", + context); + + assertThat(getPipelineElementFromAggregationAt(agg, 0).get("$project")) + .isEqualTo(new Document("val", "$withEmbedded.prefix-with-at-field-annotation")); + } + @org.springframework.data.mongodb.core.mapping.Document(collection = "person") @AllArgsConstructor public static class FooPerson { @@ -433,4 +530,26 @@ public class TypeBasedAggregationOperationContextUnitTests { String value1; @org.springframework.data.mongodb.core.mapping.Field("nestedValue2") String value2; } + + static class WrapperAroundWithEmbedded { + + String id; + WithEmbedded withEmbedded; + } + + static class WithEmbedded { + + String id; + + @Embedded.Nullable EmbeddableType embeddableType; + @Embedded.Nullable("prefix-") EmbeddableType prefixedEmbeddableValue; + } + + static class EmbeddableType { + + String stringValue; + + @org.springframework.data.mongodb.core.mapping.Field("with-at-field-annotation") // + String atFieldAnnotatedValue; + } } 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 fdaf46692..198a5f8ae 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 @@ -51,6 +51,7 @@ import org.springframework.core.convert.ConverterNotFoundException; import org.springframework.core.convert.converter.Converter; import org.springframework.data.annotation.Id; import org.springframework.data.annotation.PersistenceConstructor; +import org.springframework.data.annotation.Transient; import org.springframework.data.annotation.TypeAlias; import org.springframework.data.convert.CustomConversions; import org.springframework.data.convert.ReadingConverter; @@ -71,6 +72,7 @@ import org.springframework.data.mongodb.core.convert.DocumentAccessorUnitTests.P import org.springframework.data.mongodb.core.convert.MappingMongoConverterUnitTests.ClassWithMapUsingEnumAsKey.FooBarEnum; import org.springframework.data.mongodb.core.geo.Sphere; import org.springframework.data.mongodb.core.mapping.Document; +import org.springframework.data.mongodb.core.mapping.Embedded; import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.mapping.FieldType; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; @@ -2179,6 +2181,197 @@ public class MappingMongoConverterUnitTests { assertThat(((LinkedHashMap) result.get("cluster")).get("_id")).isEqualTo(100L); } + @Test // DATAMONGO-1902 + void writeFlattensEmbeddedType() { + + WithNullableEmbedded source = new WithNullableEmbedded(); + source.id = "id-1"; + source.embeddableValue = new EmbeddableType(); + source.embeddableValue.listValue = Arrays.asList("list-val-1", "list-val-2"); + source.embeddableValue.stringValue = "string-val"; + source.embeddableValue.transientValue = "must-not-be-written"; + source.embeddableValue.atFieldAnnotatedValue = "@Field"; + + org.bson.Document target = new org.bson.Document(); + converter.write(source, target); + + assertThat(target).containsEntry("_id", "id-1") // + .containsEntry("stringValue", "string-val") // + .containsEntry("listValue", Arrays.asList("list-val-1", "list-val-2")) // + .containsEntry("with-at-field-annotation", "@Field") // + .doesNotContainKey("embeddableValue") // + .doesNotContainKey("transientValue"); + } + + @Test // DATAMONGO-1902 + void writePrefixesEmbeddedType() { + + WithPrefixedNullableEmbedded source = new WithPrefixedNullableEmbedded(); + source.id = "id-1"; + source.embeddableValue = new EmbeddableType(); + source.embeddableValue.listValue = Arrays.asList("list-val-1", "list-val-2"); + source.embeddableValue.stringValue = "string-val"; + source.embeddableValue.transientValue = "must-not-be-written"; + source.embeddableValue.atFieldAnnotatedValue = "@Field"; + + org.bson.Document target = new org.bson.Document(); + converter.write(source, target); + + assertThat(target).containsEntry("_id", "id-1") // + .containsEntry("prefix-stringValue", "string-val") // + .containsEntry("prefix-listValue", Arrays.asList("list-val-1", "list-val-2")) // + .containsEntry("prefix-with-at-field-annotation", "@Field") // + .doesNotContainKey("embeddableValue") // + .doesNotContainKey("transientValue") // + .doesNotContainKey("prefix-transientValue"); + } + + @Test // DATAMONGO-1902 + void writeNullEmbeddedType() { + + WithNullableEmbedded source = new WithNullableEmbedded(); + source.id = "id-1"; + source.embeddableValue = null; + + org.bson.Document target = new org.bson.Document(); + converter.write(source, target); + + assertThat(target) // + .doesNotContainKey("prefix-stringValue").doesNotContainKey("prefix-listValue") + .doesNotContainKey("embeddableValue"); + } + + @Test // DATAMONGO-1902 + void writeDeepNestedEmbeddedType() { + + WrapperAroundWithEmbedded source = new WrapperAroundWithEmbedded(); + source.someValue = "root-level-value"; + source.nullableEmbedded = new WithNullableEmbedded(); + source.nullableEmbedded.id = "id-1"; + source.nullableEmbedded.embeddableValue = new EmbeddableType(); + source.nullableEmbedded.embeddableValue.listValue = Arrays.asList("list-val-1", "list-val-2"); + source.nullableEmbedded.embeddableValue.stringValue = "string-val"; + source.nullableEmbedded.embeddableValue.transientValue = "must-not-be-written"; + source.nullableEmbedded.embeddableValue.atFieldAnnotatedValue = "@Field"; + + org.bson.Document target = new org.bson.Document(); + converter.write(source, target); + + assertThat(target).containsEntry("someValue", "root-level-value") // + .containsEntry("nullableEmbedded", new org.bson.Document("_id", "id-1").append("stringValue", "string-val") // + .append("listValue", Arrays.asList("list-val-1", "list-val-2")) // + .append("with-at-field-annotation", "@Field")); // + } + + @Test // DATAMONGO-1902 + void readEmbeddedType() { + + org.bson.Document source = new org.bson.Document("_id", "id-1") // + .append("stringValue", "string-val") // + .append("listValue", Arrays.asList("list-val-1", "list-val-2")) // + .append("with-at-field-annotation", "@Field"); + + EmbeddableType embeddableValue = new EmbeddableType(); + embeddableValue.stringValue = "string-val"; + embeddableValue.listValue = Arrays.asList("list-val-1", "list-val-2"); + embeddableValue.atFieldAnnotatedValue = "@Field"; + + WithNullableEmbedded target = converter.read(WithNullableEmbedded.class, source); + assertThat(target.embeddableValue).isEqualTo(embeddableValue); + } + + @Test // DATAMONGO-1902 + void readPrefixedEmbeddedType() { + + org.bson.Document source = new org.bson.Document("_id", "id-1") // + .append("prefix-stringValue", "string-val") // + .append("prefix-listValue", Arrays.asList("list-val-1", "list-val-2")) // + .append("prefix-with-at-field-annotation", "@Field"); + + EmbeddableType embeddableValue = new EmbeddableType(); + embeddableValue.stringValue = "string-val"; + embeddableValue.listValue = Arrays.asList("list-val-1", "list-val-2"); + embeddableValue.atFieldAnnotatedValue = "@Field"; + + WithPrefixedNullableEmbedded target = converter.read(WithPrefixedNullableEmbedded.class, source); + assertThat(target.embeddableValue).isEqualTo(embeddableValue); + } + + @Test // DATAMONGO-1902 + void readNullableEmbeddedTypeWhenSourceDoesNotContainValues() { + + org.bson.Document source = new org.bson.Document("_id", "id-1"); + + WithNullableEmbedded target = converter.read(WithNullableEmbedded.class, source); + assertThat(target.embeddableValue).isNull(); + } + + @Test // DATAMONGO-1902 + void readEmptyEmbeddedTypeWhenSourceDoesNotContainValues() { + + org.bson.Document source = new org.bson.Document("_id", "id-1"); + + WithEmptyEmbeddedType target = converter.read(WithEmptyEmbeddedType.class, source); + assertThat(target.embeddableValue).isNotNull(); + } + + @Test // DATAMONGO-1902 + void readDeepNestedEmbeddedType() { + + org.bson.Document source = new org.bson.Document("someValue", "root-level-value").append("nullableEmbedded", + new org.bson.Document("_id", "id-1").append("stringValue", "string-val") // + .append("listValue", Arrays.asList("list-val-1", "list-val-2")) // + .append("with-at-field-annotation", "@Field")); + + WrapperAroundWithEmbedded target = converter.read(WrapperAroundWithEmbedded.class, source); + + EmbeddableType embeddableValue = new EmbeddableType(); + embeddableValue.stringValue = "string-val"; + embeddableValue.listValue = Arrays.asList("list-val-1", "list-val-2"); + embeddableValue.atFieldAnnotatedValue = "@Field"; + + assertThat(target.someValue).isEqualTo("root-level-value"); + assertThat(target.nullableEmbedded).isNotNull(); + assertThat(target.nullableEmbedded.embeddableValue).isEqualTo(embeddableValue); + } + + @Test // DATAMONGO-1902 + void readEmbeddedTypeWithComplexValue() { + + org.bson.Document source = new org.bson.Document("_id", "id-1").append("address", + new org.bson.Document("street", "1007 Mountain Drive").append("city", "Gotham")); + + WithNullableEmbedded target = converter.read(WithNullableEmbedded.class, source); + + Address expected = new Address(); + expected.city = "Gotham"; + expected.street = "1007 Mountain Drive"; + + assertThat(target.embeddableValue.address) // + .isEqualTo(expected); + } + + @Test // DATAMONGO-1902 + void writeEmbeddedTypeWithComplexValue() { + + WithNullableEmbedded source = new WithNullableEmbedded(); + source.id = "id-1"; + source.embeddableValue = new EmbeddableType(); + source.embeddableValue.address = new Address(); + source.embeddableValue.address.city = "Gotham"; + source.embeddableValue.address.street = "1007 Mountain Drive"; + + org.bson.Document target = new org.bson.Document(); + converter.write(source, target); + + assertThat(target) // + .containsEntry("address", new org.bson.Document("street", "1007 Mountain Drive").append("city", "Gotham")) // + .doesNotContainKey("street") // + .doesNotContainKey("address.street") // + .doesNotContainKey("city") // + .doesNotContainKey("address.city"); + } + static class GenericType { T content; } @@ -2208,6 +2401,7 @@ public class MappingMongoConverterUnitTests { } + @EqualsAndHashCode static class Address implements InterfaceType { String street; String city; @@ -2641,6 +2835,50 @@ public class MappingMongoConverterUnitTests { Date dateAsObjectId; } + static class WrapperAroundWithEmbedded { + + String someValue; + WithNullableEmbedded nullableEmbedded; + WithEmptyEmbeddedType emptyEmbedded; + WithPrefixedNullableEmbedded prefixedEmbedded; + } + + static class WithNullableEmbedded { + + String id; + + @Embedded.Nullable EmbeddableType embeddableValue; + } + + static class WithPrefixedNullableEmbedded { + + String id; + + @Embedded.Nullable("prefix-") EmbeddableType embeddableValue; + } + + static class WithEmptyEmbeddedType { + + String id; + + @Embedded.Empty EmbeddableType embeddableValue; + } + + @EqualsAndHashCode + static class EmbeddableType { + + String stringValue; + List listValue; + + @Field("with-at-field-annotation") // + String atFieldAnnotatedValue; + + @Transient // + String transientValue; + + Address address; + } + static class ReturningAfterConvertCallback implements AfterConvertCallback { @Override @@ -2649,4 +2887,5 @@ public class MappingMongoConverterUnitTests { return entity; } } + } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MongoExampleMapperUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MongoExampleMapperUnitTests.java index d0eb09a40..5ef5198ae 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MongoExampleMapperUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MongoExampleMapperUnitTests.java @@ -32,7 +32,6 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; - import org.springframework.data.annotation.Id; import org.springframework.data.domain.Example; import org.springframework.data.domain.ExampleMatcher; @@ -41,8 +40,10 @@ import org.springframework.data.geo.Point; import org.springframework.data.mongodb.MongoDatabaseFactory; import org.springframework.data.mongodb.core.convert.QueryMapperUnitTests.ClassWithGeoTypes; import org.springframework.data.mongodb.core.convert.QueryMapperUnitTests.WithDBRef; +import org.springframework.data.mongodb.core.index.Indexed; import org.springframework.data.mongodb.core.mapping.DBRef; import org.springframework.data.mongodb.core.mapping.Document; +import org.springframework.data.mongodb.core.mapping.Embedded; import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.data.mongodb.core.query.UntypedExampleMatcher; @@ -454,6 +455,60 @@ public class MongoExampleMapperUnitTests { assertThat(document).doesNotContainKey("_class"); } + @Test // DATAMONGO-1902 + void mapsEmbeddedType() { + + WithEmbedded probe = new WithEmbedded(); + probe.embeddableType = new EmbeddableType(); + probe.embeddableType.atFieldAnnotatedValue = "@Field"; + probe.embeddableType.stringValue = "string-value"; + + org.bson.Document document = mapper.getMappedExample(Example.of(probe, UntypedExampleMatcher.matching())); + assertThat(document).containsEntry("stringValue", "string-value").containsEntry("with-at-field-annotation", + "@Field"); + } + + @Test // DATAMONGO-1902 + void mapsPrefixedEmbeddedType() { + + WithEmbedded probe = new WithEmbedded(); + probe.prefixedEmbeddableValue = new EmbeddableType(); + probe.prefixedEmbeddableValue.atFieldAnnotatedValue = "@Field"; + probe.prefixedEmbeddableValue.stringValue = "string-value"; + + org.bson.Document document = mapper.getMappedExample(Example.of(probe, UntypedExampleMatcher.matching())); + assertThat(document).containsEntry("prefix-stringValue", "string-value") + .containsEntry("prefix-with-at-field-annotation", "@Field"); + } + + @Test // DATAMONGO-1902 + void mapsNestedEmbeddedType() { + + WrapperAroundWithEmbedded probe = new WrapperAroundWithEmbedded(); + probe.withEmbedded = new WithEmbedded(); + probe.withEmbedded.embeddableType = new EmbeddableType(); + probe.withEmbedded.embeddableType.atFieldAnnotatedValue = "@Field"; + probe.withEmbedded.embeddableType.stringValue = "string-value"; + + org.bson.Document document = mapper.getMappedExample(Example.of(probe, UntypedExampleMatcher.matching())); + assertThat(document).containsEntry("withEmbedded.stringValue", "string-value") + .containsEntry("withEmbedded.with-at-field-annotation", "@Field"); + } + + @Test // DATAMONGO-1902 + void mapsNestedPrefixedEmbeddedType() { + + WrapperAroundWithEmbedded probe = new WrapperAroundWithEmbedded(); + probe.withEmbedded = new WithEmbedded(); + probe.withEmbedded.prefixedEmbeddableValue = new EmbeddableType(); + probe.withEmbedded.prefixedEmbeddableValue.atFieldAnnotatedValue = "@Field"; + probe.withEmbedded.prefixedEmbeddableValue.stringValue = "string-value"; + + org.bson.Document document = mapper.getMappedExample(Example.of(probe, UntypedExampleMatcher.matching())); + assertThat(document).containsEntry("withEmbedded.prefix-stringValue", "string-value") + .containsEntry("withEmbedded.prefix-with-at-field-annotation", "@Field"); + } + static class FlatDocument { @Id String id; @@ -481,4 +536,29 @@ public class MongoExampleMapperUnitTests { @Id String id; String value; } + + @Document + static class WrapperAroundWithEmbedded { + + String id; + WithEmbedded withEmbedded; + } + + @Document + static class WithEmbedded { + + String id; + + @Embedded.Nullable EmbeddableType embeddableType; + @Embedded.Nullable("prefix-") EmbeddableType prefixedEmbeddableValue; + } + + static class EmbeddableType { + + @Indexed String stringValue; + + @Indexed // + @Field("with-at-field-annotation") // + String atFieldAnnotatedValue; + } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java index 162693cd6..ce29c2a42 100755 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java @@ -37,6 +37,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.Transient; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Direction; import org.springframework.data.geo.Point; @@ -45,9 +46,9 @@ import org.springframework.data.mongodb.core.DocumentTestUtils; import org.springframework.data.mongodb.core.Person; import org.springframework.data.mongodb.core.geo.GeoJsonPoint; import org.springframework.data.mongodb.core.geo.GeoJsonPolygon; -import org.springframework.data.mongodb.core.mapping.BasicMongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.DBRef; import org.springframework.data.mongodb.core.mapping.Document; +import org.springframework.data.mongodb.core.mapping.Embedded; import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.mapping.FieldType; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; @@ -389,7 +390,7 @@ public class QueryMapperUnitTests { Query query = query(where("reference").exists(false)); - BasicMongoPersistentEntity entity = context.getRequiredPersistentEntity(WithDBRef.class); + MongoPersistentEntity entity = context.getRequiredPersistentEntity(WithDBRef.class); org.bson.Document mappedObject = mapper.getMappedObject(query.getQueryObject(), entity); org.bson.Document reference = getAsDocument(mappedObject, "reference"); @@ -405,7 +406,7 @@ public class QueryMapperUnitTests { Query query = query(where("someString").is("foo").andOperator(where("reference").in(reference))); - BasicMongoPersistentEntity entity = context.getRequiredPersistentEntity(WithDBRef.class); + MongoPersistentEntity entity = context.getRequiredPersistentEntity(WithDBRef.class); org.bson.Document mappedObject = mapper.getMappedObject(query.getQueryObject(), entity); assertThat(mappedObject).containsEntry("someString", "foo"); @@ -446,7 +447,7 @@ public class QueryMapperUnitTests { Query query = query(where("someString").is("foo")); query.fields().exclude("reference"); - BasicMongoPersistentEntity entity = context.getRequiredPersistentEntity(WithDBRef.class); + MongoPersistentEntity entity = context.getRequiredPersistentEntity(WithDBRef.class); org.bson.Document queryResult = mapper.getMappedObject(query.getQueryObject(), entity); org.bson.Document fieldsResult = mapper.getMappedObject(query.getFieldsObject(), entity); @@ -457,7 +458,7 @@ public class QueryMapperUnitTests { @Test // DATAMONGO-686 void queryMapperShouldNotChangeStateInGivenQueryObjectWhenIdConstrainedByInList() { - BasicMongoPersistentEntity persistentEntity = context.getRequiredPersistentEntity(Sample.class); + MongoPersistentEntity persistentEntity = context.getRequiredPersistentEntity(Sample.class); String idPropertyName = persistentEntity.getIdProperty().getName(); org.bson.Document queryObject = query(where(idPropertyName).in("42")).getQueryObject(); @@ -515,7 +516,7 @@ public class QueryMapperUnitTests { @Test // DATAMONGO-773 void queryMapperShouldBeAbleToProcessQueriesThatIncludeDbRefFields() { - BasicMongoPersistentEntity persistentEntity = context.getRequiredPersistentEntity(WithDBRef.class); + MongoPersistentEntity persistentEntity = context.getRequiredPersistentEntity(WithDBRef.class); Query qry = query(where("someString").is("abc")); qry.fields().include("reference"); @@ -781,7 +782,8 @@ public class QueryMapperUnitTests { Query query = query(byExample(probe).and("listOfItems").exists(true)); org.bson.Document document = mapper.getMappedObject(query.getQueryObject(), context.getPersistentEntity(Foo.class)); - assertThat(document).containsEntry("embedded\\._id", "conflux").containsEntry("my_items", new org.bson.Document("$exists", true)); + assertThat(document).containsEntry("embedded\\._id", "conflux").containsEntry("my_items", + new org.bson.Document("$exists", true)); } @Test // DATAMONGO-1988 @@ -1011,6 +1013,184 @@ public class QueryMapperUnitTests { assertThat(target).isEqualTo(org.bson.Document.parse("{\"$text\" : { \"$search\" : \"test\" }}")); } + @Test // DATAMONGO-1902 + void rendersQueryOnEmbeddedObjectCorrectly() { + + EmbeddableType embeddableType = new EmbeddableType(); + embeddableType.stringValue = "test"; + + Query source = query(Criteria.where("embeddableValue").is(embeddableType)); + + org.bson.Document target = mapper.getMappedObject(source.getQueryObject(), + context.getPersistentEntity(WithEmbedded.class)); + + assertThat(target).isEqualTo(new org.bson.Document("stringValue", "test")); + } + + @Test // DATAMONGO-1902 + void rendersQueryOnEmbeddedCorrectly() { + + Query source = query(Criteria.where("embeddableValue.stringValue").is("test")); + + org.bson.Document target = mapper.getMappedObject(source.getQueryObject(), + context.getPersistentEntity(WithEmbedded.class)); + + assertThat(target).isEqualTo(new org.bson.Document("stringValue", "test")); + } + + @Test // DATAMONGO-1902 + void rendersQueryOnPrefixedEmbeddedCorrectly() { + + Query source = query(Criteria.where("embeddableValue.stringValue").is("test")); + + org.bson.Document target = mapper.getMappedObject(source.getQueryObject(), + context.getPersistentEntity(WithPrefixedEmbedded.class)); + + assertThat(target).isEqualTo(new org.bson.Document("prefix-stringValue", "test")); + } + + @Test // DATAMONGO-1902 + void rendersQueryOnNestedEmbeddedObjectCorrectly() { + + EmbeddableType embeddableType = new EmbeddableType(); + embeddableType.stringValue = "test"; + Query source = query(Criteria.where("withEmbedded.embeddableValue").is(embeddableType)); + + org.bson.Document target = mapper.getMappedObject(source.getQueryObject(), + context.getPersistentEntity(WrapperAroundWithEmbedded.class)); + + assertThat(target).isEqualTo(new org.bson.Document("withEmbedded", new org.bson.Document("stringValue", "test"))); + } + + @Test // DATAMONGO-1902 + void rendersQueryOnNestedPrefixedEmbeddedObjectCorrectly() { + + EmbeddableType embeddableType = new EmbeddableType(); + embeddableType.stringValue = "test"; + Query source = query(Criteria.where("withPrefixedEmbedded.embeddableValue").is(embeddableType)); + + org.bson.Document target = mapper.getMappedObject(source.getQueryObject(), + context.getPersistentEntity(WrapperAroundWithEmbedded.class)); + + assertThat(target) + .isEqualTo(new org.bson.Document("withPrefixedEmbedded", new org.bson.Document("prefix-stringValue", "test"))); + } + + @Test // DATAMONGO-1902 + void rendersQueryOnNestedEmbeddedCorrectly() { + + Query source = query(Criteria.where("withEmbedded.embeddableValue.stringValue").is("test")); + + org.bson.Document target = mapper.getMappedObject(source.getQueryObject(), + context.getPersistentEntity(WrapperAroundWithEmbedded.class)); + + assertThat(target).isEqualTo(new org.bson.Document("withEmbedded.stringValue", "test")); + } + + @Test // DATAMONGO-1902 + void rendersQueryOnNestedPrefixedEmbeddedCorrectly() { + + Query source = query(Criteria.where("withPrefixedEmbedded.embeddableValue.stringValue").is("test")); + + org.bson.Document target = mapper.getMappedObject(source.getQueryObject(), + context.getPersistentEntity(WrapperAroundWithEmbedded.class)); + + assertThat(target).isEqualTo(new org.bson.Document("withPrefixedEmbedded.prefix-stringValue", "test")); + } + + @Test // DATAMONGO-1902 + void sortByEmbeddableIsEmpty() { + + Query query = new Query().with(Sort.by("embeddableValue")); + + org.bson.Document document = mapper.getMappedSort(query.getSortObject(), + context.getPersistentEntity(WithEmbedded.class)); + + assertThat(document).isEqualTo( + new org.bson.Document("stringValue", 1).append("listValue", 1).append("with-at-field-annotation", 1)); + } + + @Test // DATAMONGO-1902 + void sortByEmbeddableValue() { + + // atFieldAnnotatedValue + Query query = new Query().with(Sort.by("embeddableValue.stringValue")); + + org.bson.Document document = mapper.getMappedSort(query.getSortObject(), + context.getPersistentEntity(WithEmbedded.class)); + + assertThat(document).isEqualTo(new org.bson.Document("stringValue", 1)); + } + + @Test // DATAMONGO-1902 + void sortByEmbeddableValueWithFieldAnnotation() { + + Query query = new Query().with(Sort.by("embeddableValue.atFieldAnnotatedValue")); + + org.bson.Document document = mapper.getMappedSort(query.getSortObject(), + context.getPersistentEntity(WithEmbedded.class)); + + assertThat(document).isEqualTo(new org.bson.Document("with-at-field-annotation", 1)); + } + + @Test // DATAMONGO-1902 + void sortByPrefixedEmbeddableValueWithFieldAnnotation() { + + Query query = new Query().with(Sort.by("embeddableValue.atFieldAnnotatedValue")); + + org.bson.Document document = mapper.getMappedSort(query.getSortObject(), + context.getPersistentEntity(WithPrefixedEmbedded.class)); + + assertThat(document).isEqualTo(new org.bson.Document("prefix-with-at-field-annotation", 1)); + } + + @Test // DATAMONGO-1902 + void sortByNestedEmbeddableValueWithFieldAnnotation() { + + Query query = new Query().with(Sort.by("withEmbedded.embeddableValue.atFieldAnnotatedValue")); + + org.bson.Document document = mapper.getMappedSort(query.getSortObject(), + context.getPersistentEntity(WrapperAroundWithEmbedded.class)); + + assertThat(document).isEqualTo(new org.bson.Document("withEmbedded.with-at-field-annotation", 1)); + } + + @Test // DATAMONGO-1902 + void sortByNestedPrefixedEmbeddableValueWithFieldAnnotation() { + + Query query = new Query().with(Sort.by("withPrefixedEmbedded.embeddableValue.atFieldAnnotatedValue")); + + org.bson.Document document = mapper.getMappedSort(query.getSortObject(), + context.getPersistentEntity(WrapperAroundWithEmbedded.class)); + + assertThat(document).isEqualTo(new org.bson.Document("withPrefixedEmbedded.prefix-with-at-field-annotation", 1)); + } + + @Test // DATAMONGO-1902 + void projectOnEmbeddableUsesFields() { + + Query query = new Query(); + query.fields().include("embeddableValue"); + + org.bson.Document document = mapper.getMappedFields(query.getFieldsObject(), + context.getPersistentEntity(WithEmbedded.class)); + + assertThat(document).isEqualTo( + new org.bson.Document("stringValue", 1).append("listValue", 1).append("with-at-field-annotation", 1)); + } + + @Test // DATAMONGO-1902 + void projectOnEmbeddableValue() { + + Query query = new Query(); + query.fields().include("embeddableValue.stringValue"); + + org.bson.Document document = mapper.getMappedFields(query.getFieldsObject(), + context.getPersistentEntity(WithEmbedded.class)); + + assertThat(document).isEqualTo(new org.bson.Document("stringValue", 1)); + } + class WithDeepArrayNesting { List level0; @@ -1194,4 +1374,38 @@ public class QueryMapperUnitTests { this.value = value; } } + + static class WrapperAroundWithEmbedded { + + String someValue; + WithEmbedded withEmbedded; + WithPrefixedEmbedded withPrefixedEmbedded; + } + + static class WithEmbedded { + + String id; + + @Embedded.Nullable EmbeddableType embeddableValue; + } + + static class WithPrefixedEmbedded { + + String id; + + @Embedded.Nullable("prefix-") EmbeddableType embeddableValue; + } + + static class EmbeddableType { + + String stringValue; + List listValue; + + @Field("with-at-field-annotation") // + String atFieldAnnotatedValue; + + @Transient // + String transientValue; + } + } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/UpdateMapperUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/UpdateMapperUnitTests.java index df0b8e68a..57eb620a7 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/UpdateMapperUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/UpdateMapperUnitTests.java @@ -37,9 +37,9 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; - import org.springframework.core.convert.converter.Converter; import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.Transient; import org.springframework.data.convert.CustomConversions; import org.springframework.data.convert.WritingConverter; import org.springframework.data.domain.Sort; @@ -48,6 +48,7 @@ import org.springframework.data.domain.Sort.Order; import org.springframework.data.mapping.MappingException; import org.springframework.data.mongodb.MongoDatabaseFactory; import org.springframework.data.mongodb.core.DocumentTestUtils; +import org.springframework.data.mongodb.core.mapping.Embedded; import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.data.mongodb.core.query.Criteria; @@ -1089,6 +1090,76 @@ class UpdateMapperUnitTests { assertThat(mappedUpdate).isEqualTo(new Document("$set", new Document("aliased.$[element].value", 10))); } + @Test // DATAMONGO-1902 + void mappingShouldConsiderValueOfEmbeddedType() { + + Update update = new Update().set("embeddableValue.stringValue", "updated"); + + Document mappedUpdate = mapper.getMappedObject(update.getUpdateObject(), + context.getPersistentEntity(WithEmbedded.class)); + + assertThat(mappedUpdate).isEqualTo(new Document("$set", new Document("stringValue", "updated"))); + } + + @Test // DATAMONGO-1902 + void mappingShouldConsiderEmbeddedType() { + + EmbeddableType embeddableType = new EmbeddableType(); + embeddableType.stringValue = "updated"; + embeddableType.listValue = Arrays.asList("val-1", "val-2"); + Update update = new Update().set("embeddableValue", embeddableType); + + Document mappedUpdate = mapper.getMappedObject(update.getUpdateObject(), + context.getPersistentEntity(WithEmbedded.class)); + + assertThat(mappedUpdate).isEqualTo(new Document("$set", + new Document("stringValue", "updated").append("listValue", Arrays.asList("val-1", "val-2")))); + } + + @Test // DATAMONGO-1902 + void mappingShouldConsiderValueOfPrefixedEmbeddedType() { + + Update update = new Update().set("embeddableValue.stringValue", "updated"); + + Document mappedUpdate = mapper.getMappedObject(update.getUpdateObject(), + context.getPersistentEntity(WithPrefixedEmbedded.class)); + + assertThat(mappedUpdate).isEqualTo(new Document("$set", new Document("prefix-stringValue", "updated"))); + } + + @Test // DATAMONGO-1902 + void mappingShouldConsiderPrefixedEmbeddedType() { + + EmbeddableType embeddableType = new EmbeddableType(); + embeddableType.stringValue = "updated"; + embeddableType.listValue = Arrays.asList("val-1", "val-2"); + + Update update = new Update().set("embeddableValue", embeddableType); + + Document mappedUpdate = mapper.getMappedObject(update.getUpdateObject(), + context.getPersistentEntity(WithPrefixedEmbedded.class)); + + assertThat(mappedUpdate).isEqualTo(new Document("$set", + new Document("prefix-stringValue", "updated").append("prefix-listValue", Arrays.asList("val-1", "val-2")))); + } + + @Test // DATAMONGO-1902 + void mappingShouldConsiderNestedPrefixedEmbeddedType() { + + EmbeddableType embeddableType = new EmbeddableType(); + embeddableType.stringValue = "updated"; + embeddableType.listValue = Arrays.asList("val-1", "val-2"); + + Update update = new Update().set("withPrefixedEmbedded.embeddableValue", embeddableType); + + Document mappedUpdate = mapper.getMappedObject(update.getUpdateObject(), + context.getPersistentEntity(WrapperAroundWithEmbedded.class)); + + System.out.println("mappedUpdate.toJson(): " + mappedUpdate.toJson()); + assertThat(mappedUpdate).isEqualTo(new Document("$set", new Document("withPrefixedEmbedded", + new Document("prefix-stringValue", "updated").append("prefix-listValue", Arrays.asList("val-1", "val-2"))))); + } + static class DomainTypeWrappingConcreteyTypeHavingListOfInterfaceTypeAttributes { ListModelWrapper concreteTypeWithListAttributeOfInterfaceType; } @@ -1418,4 +1489,37 @@ class UpdateMapperUnitTests { } + static class WrapperAroundWithEmbedded { + + String someValue; + WithEmbedded withEmbedded; + WithPrefixedEmbedded withPrefixedEmbedded; + } + + static class WithEmbedded { + + String id; + + @Embedded.Nullable EmbeddableType embeddableValue; + } + + static class WithPrefixedEmbedded { + + String id; + + @Embedded.Nullable("prefix-") EmbeddableType embeddableValue; + } + + static class EmbeddableType { + + String stringValue; + List listValue; + + @Field("with-at-field-annotation") // + String atFieldAnnotatedValue; + + @Transient // + String transientValue; + } + } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolverUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolverUnitTests.java index 1076e0ac1..7f14080d6 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolverUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolverUnitTests.java @@ -22,7 +22,8 @@ import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import java.util.Collections; +import java.util.Arrays; +import java.util.LinkedHashSet; import java.util.List; import org.junit.Test; @@ -30,6 +31,7 @@ import org.junit.runner.RunWith; import org.junit.runners.Suite; import org.junit.runners.Suite.SuiteClasses; import org.springframework.core.annotation.AliasFor; +import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.annotation.Id; import org.springframework.data.geo.Point; import org.springframework.data.mongodb.core.DocumentTestUtils; @@ -42,6 +44,7 @@ import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexRes import org.springframework.data.mongodb.core.mapping.BasicMongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.DBRef; import org.springframework.data.mongodb.core.mapping.Document; +import org.springframework.data.mongodb.core.mapping.Embedded; import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.mapping.Language; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; @@ -1282,6 +1285,44 @@ public class MongoPersistentEntityIndexResolverUnitTests { }); } + @Test // DATAMONGO-1902 + public void resolvedIndexOnEmbeddedType() { + + List indexDefinitions = prepareMappingContextAndResolveIndexForType(WithEmbedded.class, + EmbeddableType.class); + + assertThat(indexDefinitions).hasSize(2); + assertThat(indexDefinitions.get(0)).satisfies(it -> { + assertThat(it.getIndexKeys()).containsEntry("stringValue", 1); + }); + assertThat(indexDefinitions.get(1)).satisfies(it -> { + assertThat(it.getIndexKeys()).containsEntry("with-at-field-annotation", 1); + }); + } + + @Test // DATAMONGO-1902 + public void resolvedIndexOnNestedEmbeddedType() { + + List indexDefinitions = prepareMappingContextAndResolveIndexForType( + WrapperAroundWithEmbedded.class, WithEmbedded.class, EmbeddableType.class); + + assertThat(indexDefinitions).hasSize(2); + assertThat(indexDefinitions.get(0)).satisfies(it -> { + assertThat(it.getIndexKeys()).containsEntry("withEmbedded.stringValue", 1); + }); + assertThat(indexDefinitions.get(1)).satisfies(it -> { + assertThat(it.getIndexKeys()).containsEntry("withEmbedded.with-at-field-annotation", 1); + }); + } + + @Test // DATAMONGO-1902 + public void errorsOnIndexOnEmbedded() { + + assertThatExceptionOfType(InvalidDataAccessApiUsageException.class) + .isThrownBy(() -> prepareMappingContextAndResolveIndexForType(InvalidIndexOnEmbedded.class)); + + } + @Document class MixedIndexRoot { @@ -1472,6 +1513,41 @@ public class MongoPersistentEntityIndexResolverUnitTests { AlternatePathToNoCycleButIndenticallNamedPropertiesDeeplyNestedDocument path2; } + @Document + static class WrapperAroundWithEmbedded { + + String id; + WithEmbedded withEmbedded; + } + + @Document + static class WithEmbedded { + + String id; + + @Embedded.Nullable EmbeddableType embeddableType; + } + + @Document + class InvalidIndexOnEmbedded { + + @Indexed // + @Embedded.Nullable // + EmbeddableType embeddableType; + + } + + static class EmbeddableType { + + @Indexed String stringValue; + + List listValue; + + @Indexed // + @Field("with-at-field-annotation") // + String atFieldAnnotatedValue; + } + static class AlternatePathToNoCycleButIndenticallNamedPropertiesDeeplyNestedDocument { NoCycleButIndenticallNamedPropertiesDeeplyNested propertyWithIndexedStructure; } @@ -1521,17 +1597,17 @@ public class MongoPersistentEntityIndexResolverUnitTests { } } - private static List prepareMappingContextAndResolveIndexForType(Class type) { + private static List prepareMappingContextAndResolveIndexForType(Class... types) { - MongoMappingContext mappingContext = prepareMappingContext(type); + MongoMappingContext mappingContext = prepareMappingContext(types); MongoPersistentEntityIndexResolver resolver = new MongoPersistentEntityIndexResolver(mappingContext); - return resolver.resolveIndexForEntity(mappingContext.getRequiredPersistentEntity(type)); + return resolver.resolveIndexForEntity(mappingContext.getRequiredPersistentEntity(types[0])); } - private static MongoMappingContext prepareMappingContext(Class type) { + private static MongoMappingContext prepareMappingContext(Class... types) { MongoMappingContext mappingContext = new MongoMappingContext(); - mappingContext.setInitialEntitySet(Collections.singleton(type)); + mappingContext.setInitialEntitySet(new LinkedHashSet<>(Arrays.asList(types))); mappingContext.initialize(); return mappingContext; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntityUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntityUnitTests.java index 0fb76ddb0..28d512350 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntityUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntityUnitTests.java @@ -30,10 +30,11 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; - import org.springframework.context.ApplicationContext; import org.springframework.core.annotation.AliasFor; +import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.mapping.MappingException; +import org.springframework.data.mongodb.core.index.Indexed; import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.spel.ExtensionAwareEvaluationContextProvider; import org.springframework.data.spel.spi.EvaluationContextExtension; @@ -63,8 +64,7 @@ public class BasicMongoPersistentEntityUnitTests { @Test void evaluatesSpELExpression() { - MongoPersistentEntity entity = new BasicMongoPersistentEntity<>( - ClassTypeInformation.from(Company.class)); + MongoPersistentEntity entity = new BasicMongoPersistentEntity<>(ClassTypeInformation.from(Company.class)); assertThat(entity.getCollection()).isEqualTo("35"); } @@ -364,16 +364,13 @@ public class BasicMongoPersistentEntityUnitTests { class WithDocumentCollation {} @Sharded - private - class WithDefaultShardKey {} + private class WithDefaultShardKey {} @Sharded("country") - private - class WithSingleShardKey {} + private class WithSingleShardKey {} @Sharded({ "country", "userid" }) - private - class WithMultiShardKey {} + private class WithMultiShardKey {} static class SampleExtension implements EvaluationContextExtension { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentPropertyUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentPropertyUnitTests.java index 40fd6e214..3fb4f5908 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentPropertyUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentPropertyUnitTests.java @@ -187,7 +187,7 @@ public class BasicMongoPersistentPropertyUnitTests { public void honorsFieldOrderWhenIteratingOverProperties() { MongoMappingContext context = new MongoMappingContext(); - BasicMongoPersistentEntity entity = context.getPersistentEntity(Sample.class); + MongoPersistentEntity entity = context.getPersistentEntity(Sample.class); List properties = new ArrayList<>(); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/MongoMappingContextUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/MongoMappingContextUnitTests.java index c7d42df28..a7e454c52 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/MongoMappingContextUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/MongoMappingContextUnitTests.java @@ -111,7 +111,7 @@ public class MongoMappingContextUnitTests { void mappingContextShouldAcceptClassWithImplicitIdProperty() { MongoMappingContext context = new MongoMappingContext(); - BasicMongoPersistentEntity pe = context.getRequiredPersistentEntity(ClassWithImplicitId.class); + MongoPersistentEntity pe = context.getRequiredPersistentEntity(ClassWithImplicitId.class); assertThat(pe).isNotNull(); assertThat(pe.isIdProperty(pe.getRequiredPersistentProperty("id"))).isTrue(); @@ -121,7 +121,7 @@ public class MongoMappingContextUnitTests { void mappingContextShouldAcceptClassWithExplicitIdProperty() { MongoMappingContext context = new MongoMappingContext(); - BasicMongoPersistentEntity pe = context.getRequiredPersistentEntity(ClassWithExplicitId.class); + MongoPersistentEntity pe = context.getRequiredPersistentEntity(ClassWithExplicitId.class); assertThat(pe).isNotNull(); assertThat(pe.isIdProperty(pe.getRequiredPersistentProperty("myId"))).isTrue(); @@ -131,7 +131,7 @@ public class MongoMappingContextUnitTests { void mappingContextShouldAcceptClassWithExplicitAndImplicitIdPropertyByGivingPrecedenceToExplicitIdProperty() { MongoMappingContext context = new MongoMappingContext(); - BasicMongoPersistentEntity pe = context.getRequiredPersistentEntity(ClassWithExplicitIdAndImplicitId.class); + MongoPersistentEntity pe = context.getRequiredPersistentEntity(ClassWithExplicitIdAndImplicitId.class); assertThat(pe).isNotNull(); } @@ -166,7 +166,7 @@ public class MongoMappingContextUnitTests { MongoMappingContext context = new MongoMappingContext(); - BasicMongoPersistentEntity entity = context.getRequiredPersistentEntity(ClassWithChronoUnit.class); + MongoPersistentEntity entity = context.getRequiredPersistentEntity(ClassWithChronoUnit.class); assertThat(entity.getPersistentProperty("unit").isEntity()).isFalse(); assertThat(context.hasPersistentEntityFor(ChronoUnit.class)).isFalse(); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java index 7055a73e2..849418abf 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java @@ -1361,6 +1361,41 @@ public abstract class AbstractPersonRepositoryIntegrationTests { void spelExpressionArgumentsGetReevaluatedOnEveryInvocation() { assertThat(repository.findWithSpelByFirstnameForSpELExpressionWithParameterIndexOnly("Dave")).containsExactly(dave); - assertThat(repository.findWithSpelByFirstnameForSpELExpressionWithParameterIndexOnly("Carter")).containsExactly(carter); + assertThat(repository.findWithSpelByFirstnameForSpELExpressionWithParameterIndexOnly("Carter")) + .containsExactly(carter); + } + + @Test // DATAMONGO-1902 + void findByValueInsideEmbedded() { + + Person bart = new Person("bart", "simpson"); + User user = new User(); + user.setUsername("bartman"); + user.setId("84r1m4n"); + bart.setEmbeddedUser(user); + + operations.save(bart); + + List result = repository.findByEmbeddedUserUsername(user.getUsername()); + + assertThat(result).hasSize(1); + assertThat(result.get(0).getId().equals(bart.getId())); + } + + @Test // DATAMONGO-1902 + void findByEmbedded() { + + Person bart = new Person("bart", "simpson"); + User user = new User(); + user.setUsername("bartman"); + user.setId("84r1m4n"); + bart.setEmbeddedUser(user); + + operations.save(bart); + + List result = repository.findByEmbeddedUser(user); + + assertThat(result).hasSize(1); + assertThat(result.get(0).getId().equals(bart.getId())); } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Person.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Person.java index 2786505f5..24d42f27a 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Person.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Person.java @@ -27,6 +27,7 @@ import org.springframework.data.mongodb.core.index.GeoSpatialIndexed; import org.springframework.data.mongodb.core.index.Indexed; import org.springframework.data.mongodb.core.mapping.DBRef; import org.springframework.data.mongodb.core.mapping.Document; +import org.springframework.data.mongodb.core.mapping.Embedded; import org.springframework.data.mongodb.core.mapping.Field; /** @@ -70,6 +71,9 @@ public class Person extends Contact { Credentials credentials; + @Embedded.Nullable(prefix = "u") // + User embeddedUser; + public Person() { this(null, null); @@ -296,6 +300,14 @@ public class Person extends Contact { return skills; } + public User getEmbeddedUser() { + return embeddedUser; + } + + public void setEmbeddedUser(User embeddedUser) { + this.embeddedUser = embeddedUser; + } + /* * (non-Javadoc) * diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java index 1d340bd19..52041c270 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java @@ -401,4 +401,8 @@ public interface PersonRepository extends MongoRepository, Query Person findPersonByManyArguments(String firstname, String lastname, String email, Integer age, Sex sex, Date createdAt, List skills, String street, String zipCode, // String city, UUID uniqueId, String username, String password); + + List findByEmbeddedUserUsername(String username); + + List findByEmbeddedUser(User user); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/RepositoryIndexCreationIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/RepositoryIndexCreationIntegrationTests.java index e98dc8cb2..2955d1f3d 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/RepositoryIndexCreationIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/RepositoryIndexCreationIntegrationTests.java @@ -78,6 +78,7 @@ public class RepositoryIndexCreationIntegrationTests { assertHasIndexForField(indexInfo, "lastname"); assertHasIndexForField(indexInfo, "firstname"); + assertHasIndexForField(indexInfo, "add"); } private static void assertHasIndexForField(List indexInfo, String... fields) { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/IndexEnsuringQueryCreationListenerUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/IndexEnsuringQueryCreationListenerUnitTests.java index 103da191f..a8dcf08f8 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/IndexEnsuringQueryCreationListenerUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/IndexEnsuringQueryCreationListenerUnitTests.java @@ -66,7 +66,7 @@ class IndexEnsuringQueryCreationListenerUnitTests { partTreeQuery = mock(PartTreeMongoQuery.class, Answers.RETURNS_MOCKS); when(partTreeQuery.getTree()).thenReturn(partTree); - when(provider.indexOps(anyString())).thenReturn(indexOperations); + when(provider.indexOps(anyString(), any())).thenReturn(indexOperations); when(queryMethod.getEntityInformation()).thenReturn(entityInformation); when(entityInformation.getCollectionName()).thenReturn("persons"); } diff --git a/src/main/asciidoc/reference/embedded-documents.adoc b/src/main/asciidoc/reference/embedded-documents.adoc new file mode 100644 index 000000000..1cfc91e64 --- /dev/null +++ b/src/main/asciidoc/reference/embedded-documents.adoc @@ -0,0 +1,370 @@ +[[embedded-entities]] +== Embedded Types + +Embedded entities are used to design value objects in your Java domain model whose properties are flattened out into the MongoDB Document. + +[[embedded-entities.mapping]] +=== Embedded Types Mapping + +In the example below you see, that `User.name` is annotated with `@Embedded`. +The consequence of this is that all properties of `UserName` are folded into the `user` document. + +.Sample Code of embedding objects +==== +[source,java] +---- +public class User { + + @Id + private String userId; + + @Embedded(onEmpty = USE_NULL) <1> + UserName name; +} + +public class UserName { + private String firstname; + private String lastname; +} +---- + +[source,json] +---- +{ + "_id" : "1da2ba06-3ba7", + "firstname" : "Emma", + "lastname" : "Frost" +} +---- +<1> When loading the `name` property its value is set to `null` if both `firstname` and `lastname` are either `null` or not present. +By using `onEmpty=USE_EMPTY` an empty `UserName`, with potential `null` value for its properties, will be created. +==== + +For less verbose embeddable type declarations use `@Embedded.Nullable` and `@Embedded.Empty` instead `@Embedded(onEmpty = USE_NULL)` and `@Embedded(onEmpty = USE_EMPTY)`. +Using those annotations simultaneously set JSR-305 `@javax.annotation.Nonnull` accordingly. + +[WARNING] +==== +It is possible to use complex types within an embedded object. +However those must not be, nor contain embedded fields themselves. +==== + +[[embedded-entities.mapping.field-names]] +=== Embedded Types field names + +A value object can be embedded multiple times by using the optional `prefix` attribute of the `@Embedded` annotation. +By dosing so the chosen prefix is prepended to each property or `@Field("...")` name in the embedded object. +Please note that values will overwrite each other if multiple properties render to the same field name. + +.Sample Code of embedded object with name prefix +==== +[source,java] +---- +public class User { + + @Id + private String userId; + + @Embedded.Nullable(prefix = "u") <1> + UserName name; +} + +public class UserName { + private String firstname; + private String lastname; +} +---- + +[source,json] +---- +{ + "_id" : "a6a805bd-f95f", + "ufirstname" : "Jean", + "ulastname" : "Grey" +} +---- +<1> The prefix `u` is prepended to all properties of `UserName`. +==== + +While combining the `@Field` annotation with `@Embedded` on the very same property does not make sense and therefore leads to an error. +It is a totally valid approach to use `@Field` on any of the embedded types properties. + +.Sample Code embedded object with `@Field` annotation +==== +[source,java] +---- +public class User { + + @Id + private String userId; + + @Embedded.Nullable(prefix = "u-") <1> + UserName name; +} + +public class UserName { + + @Field("first-name") <2> + private String firstname; + + @Field("last-name") + private String lastname; +} +---- + +[source,json] +---- +{ + "_id" : "2647f7b9-89da", + "u-first-name" : "Barbara", <2> + "u-last-name" : "Gordon" +} +---- +<1> The prefix `u-` is prepended to all properties of `UserName`. +<2> The field name is the result of the combination of the annotated field name an the chosen prefix. +==== + +[[embedded-entities.queries]] +=== Query on Embedded Objects + +Defining queries on embedded properties is possible on type as well as field level as the provided `Critieria` is matched against the domain type. +Prefixes and potential custom field names will be considered when rendering the actual query. +Use the property name of the embedded object to match against all contained fields as shown in the sample below. + +.Query on embedded object +==== +[source,java] +---- +UserName userName = new UserName("Carol", "Danvers") +Query findByUserName = query(where("name").is(userName)); +User user = template.findOne(findByUserName, User.class); +---- + +[source,json] +---- +db.collection.find({ + "firstname" : "Carol", + "lastname" : "Danvers" +}) +---- +==== + +It is also possible to address any field of the embedded object directly via its property name as shown in the snippet below. + +.Query on field of embedded object +==== +[source,java] +---- +Query findByUserFirstName = query(where("name.firstname").is("Shuri")); +List users = template.findAll(findByUserFirstName, User.class); +---- + +[source,json] +---- +db.collection.find({ + "firstname" : "Shuri" +}) +---- +==== + +[[embedded-entities.queries.sort]] +==== Sort by embedded field. + +Fields of embedded objects can be used for sorting via their property path as shown in the sample below. + +.Sort on embedded field +==== +[source,java] +---- +Query findByUserLastName = query(where("name.lastname").is("Romanoff")); +List user = template.findAll(findByUserName.withSort(Sort.by("name.firstname")), User.class); +---- + +[source,json] +---- +db.collection.find({ + "lastname" : "Romanoff" +}).sort({ "firstname" : 1 }) +---- +==== + +[NOTE] +==== +Though possible, using the embedded object itself as sort criteria includes all of its fields in unpredictable order and may result in inaccurate ordering. +==== + +[[embedded-entities.queries.project]] +==== Project on embedded object + +Fields of embedded objects can be subject for projection either as a whole or via single fields as shown in the samples below. + +.Project on embedded object. +==== +[source,java] +---- +Query findByUserLastName = query(where("name.firstname").is("Gamora")); +findByUserLastName.fields().include("name"); <1> +List user = template.findAll(findByUserName, User.class); +---- + +[source,json] +---- +db.collection.find({ + "lastname" : "Gamora" +}, +{ + "firstname" : 1, + "lastname" : 1 +}) +---- +<1> A field projection on an embedded object includes all of its properties. +==== + +.Project on a field of an embedded object. +==== +[source,java] +---- +Query findByUserLastName = query(where("name.lastname").is("Smoak")); +findByUserLastName.fields().include("name.firstname"); <1> +List user = template.findAll(findByUserName, User.class); +---- + +[source,json] +---- +db.collection.find({ + "lastname" : "Smoak" +}, +{ + "firstname" : 1 +}) +---- +<1> A field projection on an embedded object includes all of its properties. +==== + +[[embedded-entities.queries.by-example]] +==== Query By Example on embedded object. + +Embedded objects can be used within an `Example` probe just as any other type. +Please review the <> section, to learn more about this feature. + +[[embedded-entities.queries.repository]] +==== Repository Queries on embedded objects. + +The `Repository` abstraction allows deriving queries on fields of embedded objects as well as the entire object. + +.Repository queries on embedded objects. +==== +[source,java] +---- +interface UserRepository extends CrudRepository { + + List findByName(UserName username); <1> + + List findByNameFirstname(String firstname); <1> +} +---- +<1> Matches against all fields of the embedded object. +<2> Matches against the `firstname`. +==== + +[NOTE] +==== +Index creation for embedded objects is suspended even if the repository `create-query-indexes` namespace attribute is set to `true`. +==== + +[[embedded-entities.update]] +=== Update on Embedded Objects + +Embedded objects can be updated as any other object that is part of the domain model. +The mapping layer takes care of flattening embedded structures into their surroundings. +It is possible to update single attributes of the embedded object as well as the entire value as shown in the examples below. + +.Update a single field of an embedded object. +==== +[source,java] +---- +Update update = new Update().set("name.firstname", "Janet"); +template.update(User.class).matching(where("id").is("Wasp")) + .apply(update).first() +---- + +[source,json] +---- +db.collection.update({ + "_id" : "Wasp" +}, +{ + "$set" { "firstname" : "Janet" } +}, +{ ... } +) +---- +==== + +.Update an embedded object. +==== +[source,java] +---- +Update update = new Update().set("name", new Name("Janet", "van Dyne")); +template.update(User.class).matching(where("id").is("Wasp")) + .apply(update).first() +---- + +[source,json] +---- +db.collection.update({ + "_id" : "Wasp" +}, +{ + "$set" { + "firstname" : "Janet", + "lastname" : "van Dyne", + } +}, +{ ... } +) +---- +==== + +[[embedded-entities.aggregations]] +=== Aggregations on Embedded Objects + +The <> will attempt to map embedded values of typed aggregations. +Please make sure to work with the properties path including the embedded wrapper object when referencing one of it's values. +Other than that no special action is required. + +[[embedded-entities.indexes]] +=== Index on Embedded Objects + +It is possible to attach the `@Indexed` annotation to properties of an embedded type just as it is done with regular objects. +However it is not possible to use `@Indexed` along with the `@Embedded` annotation on the very same property of an object. + +==== +[source,java] +---- +public class User { + + @Id + private String userId; + + @Embedded(onEmpty = USE_NULL) + UserName name; <1> + + @Indexed <2> // Invalid -> InvalidDataAccessApiUsageException + @Embedded(onEmpty = USE_Empty) + Address address; +} + +public class UserName { + + private String firstname; + + @Indexed + private String lastname; <1> +} +---- +<1> Index created for `lastname` in `users` collection. +<2> Invalid `@Indexed` usage along with `@Embedded` +==== + + diff --git a/src/main/asciidoc/reference/mapping.adoc b/src/main/asciidoc/reference/mapping.adoc index 98ecf074d..697eba8d2 100644 --- a/src/main/asciidoc/reference/mapping.adoc +++ b/src/main/asciidoc/reference/mapping.adoc @@ -833,4 +833,6 @@ Events are fired throughout the lifecycle of the mapping process. This is descri Declaring these beans in your Spring ApplicationContext causes them to be invoked whenever the event is dispatched. +include::embedded-documents.adoc[] + include::mongo-custom-conversions.adoc[]