From c3cb5d1b32b802b58ad609d4b2e5cab5947b5f94 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Mon, 30 Nov 2020 13:19:58 +0100 Subject: [PATCH 1/3] DATAMONGO-1902 - Prepare issue branch. --- pom.xml | 4 ++-- spring-data-mongodb-benchmarks/pom.xml | 2 +- spring-data-mongodb-distribution/pom.xml | 2 +- spring-data-mongodb/pom.xml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pom.xml b/pom.xml index 10c1adf1bf..98746696eb 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 3.2.0-SNAPSHOT + 3.2.0-DATAMONGO-1902-SNAPSHOT pom Spring Data MongoDB @@ -26,7 +26,7 @@ multi spring-data-mongodb - 2.5.0-SNAPSHOT + 2.5.0-DATACMNS-1699-SNAPSHOT 4.1.1 ${mongo} 1.19 diff --git a/spring-data-mongodb-benchmarks/pom.xml b/spring-data-mongodb-benchmarks/pom.xml index f0fbb601c8..f1988062fe 100644 --- a/spring-data-mongodb-benchmarks/pom.xml +++ b/spring-data-mongodb-benchmarks/pom.xml @@ -7,7 +7,7 @@ org.springframework.data spring-data-mongodb-parent - 3.2.0-SNAPSHOT + 3.2.0-DATAMONGO-1902-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml index 1a17321782..c932559b82 100644 --- a/spring-data-mongodb-distribution/pom.xml +++ b/spring-data-mongodb-distribution/pom.xml @@ -14,7 +14,7 @@ org.springframework.data spring-data-mongodb-parent - 3.2.0-SNAPSHOT + 3.2.0-DATAMONGO-1902-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index 0248517caf..a47a7642d2 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -11,7 +11,7 @@ org.springframework.data spring-data-mongodb-parent - 3.2.0-SNAPSHOT + 3.2.0-DATAMONGO-1902-SNAPSHOT ../pom.xml From 2a94e42801665fde7cf625dfd6e46eb9a2b3fab0 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Wed, 2 Dec 2020 11:06:56 +0100 Subject: [PATCH 2/3] 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" } --- pom.xml | 2 +- .../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 + 41 files changed, 2391 insertions(+), 62 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/pom.xml b/pom.xml index 98746696eb..2baa1f2b58 100644 --- a/pom.xml +++ b/pom.xml @@ -26,7 +26,7 @@ multi spring-data-mongodb - 2.5.0-DATACMNS-1699-SNAPSHOT + 2.5.0-SNAPSHOT 4.1.1 ${mongo} 1.19 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 0bab23dead..4ca69116df 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 0591484c86..bd7637d3d3 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 void dropCollection(String collectionName) { }); } + + @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 IndexOperations indexOps(String collectionName) { * @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 13234cc3ab..44c5f75f28 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 @@ Bson getDocument() { 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 92f7617833..2e0f1e6bad 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.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 @@ private void readProperties(MongoPersistentEntity entity, PersistentPropertyA 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 @@ private void readAssociation(Association association, P 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 @@ protected void writePropertyInternal(@Nullable Object obj, DocumentAccessor acce 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 Object convertToMongoType(@Nullable Object obj, TypeInformation typeIn 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 5bf48cd3f5..8887a3bd03 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.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 @@ default Object convertId(@Nullable Object id, Class targetType) { 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 d3115a4400..0f64177bca 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 @@ 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 @@ default Object convertToMongoType(@Nullable Object obj) { @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 0fb8723d11..e54deca25f 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.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 Document getMappedObject(Bson query, @Nullable MongoPersistentEntity e 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 Document getMappedSort(Document sortObject, @Nullable MongoPersistentEnti 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 Document getMappedFields(Document fieldsObject, @Nullable MongoPersistent 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 @@ private void mapMetaAttributes(Document source, @Nullable MongoPersistentEntity< } } + 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 @@ protected Object convertSimpleOrDocument(Object source, @Nullable MongoPersisten */ @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 boolean isMap() { 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 c824148ddb..51d7badaaf 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.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 static boolean isUpdateObject(@Nullable Document updateObj) { */ @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 @@ protected Entry getMappedObjectForField(Field field, Object rawV 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 4f003e2075..a912ae56c3 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 @@ @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 224aca249b..8c158c34bd 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 @@ private void potentiallyAddIndexForProperty(MongoPersistentEntity root, Mongo 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 @@ private void potentiallyAddIndexForProperty(MongoPersistentEntity root, Mongo 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 @@ private List resolveIndexForClass(final TypeInformation 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 @@ private List createIndexDefinitionHolderForProperty(Strin 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 @@ protected IndexDefinitionHolder createIndexDefinition(String dotPath, String col 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 @@ private PartialIndexFilter evaluatePartialFilter(String filterExpression, Persis 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 44daa6bcaa..7bf8214aeb 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 boolean hasTextScoreProperty() { @Override public org.springframework.data.mongodb.core.query.Collation getCollation() { - Object collationValue = collationExpression != null ? collationExpression.getValue(getEvaluationContext(null), String.class) + Object collationValue = collationExpression != null + ? collationExpression.getValue(getEvaluationContext(null), String.class) : this.collation; if (collationValue == null) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/Embedded.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/Embedded.java new file mode 100644 index 0000000000..3eecf56e5c --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/Embedded.java @@ -0,0 +1,136 @@ +/* + * 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.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import javax.annotation.meta.When; + +import org.springframework.core.annotation.AliasFor; + +/** + * The annotation to configure a value object as embedded (flattened out) in the target document. + *

+ * 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 0000000000..a3390e8e54 --- /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 0000000000..9029cfc2f6 --- /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 0000000000..e307de4518 --- /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 938f16d0fa..0c7bea76da 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 @@ * @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 @@ protected boolean shouldCreatePersistentEntityFor(TypeInformation type) { * @see org.springframework.data.mapping.AbstractMappingContext#createPersistentProperty(java.lang.reflect.Field, java.beans.PropertyDescriptor, org.springframework.data.mapping.MutablePersistentEntity, org.springframework.data.mapping.SimpleTypeHolder) */ @Override - public MongoPersistentProperty createPersistentProperty(Property property, BasicMongoPersistentEntity 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 boolean isAutoIndexCreation() { public void setAutoIndexCreation(boolean autoCreateIndexes) { this.autoIndexCreation = autoCreateIndexes; } + + + @Nullable + @Override + public MongoPersistentEntity 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 9a0d60100b..1f18f984e9 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 @@ * @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 @@ default boolean isSharded() { return getShardKey().isSharded(); } + default boolean isEmbedded() { + return false; + } + } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentProperty.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentProperty.java index 53a5eb7f91..884319c9ab 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentProperty.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentProperty.java @@ -15,10 +15,14 @@ */ package org.springframework.data.mongodb.core.mapping; +import java.lang.annotation.ElementType; + import org.springframework.core.convert.converter.Converter; import org.springframework.data.annotation.Id; import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.mongodb.core.mapping.Embedded.OnEmpty; +import org.springframework.data.util.NullableUtils; import org.springframework.lang.Nullable; /** @@ -123,6 +127,24 @@ default boolean hasExplicitWriteTarget() { return field != null ? !FieldType.IMPLICIT.equals(field.targetType()) : false; } + /** + * @return {@literal true} if the property should be embedded. + * @since 3.2 + */ + default boolean isEmbedded() { + return isEntity() && findAnnotation(Embedded.class) != null; + } + + /** + * @return {@literal true} if the property generally allows {@literal null} values; + * @since 3.2 + */ + default boolean isNullable() { + + return (isEmbedded() && findAnnotation(Embedded.class).onEmpty().equals(OnEmpty.USE_NULL)) + && !NullableUtils.isNonNull(getField(), ElementType.FIELD); + } + /** * Simple {@link Converter} implementation to transform a {@link MongoPersistentProperty} into its field name. * @@ -137,7 +159,10 @@ enum PropertyToFieldNameConverter implements Converter 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 @@ public void onCreation(PartTreeMongoQuery query) { LOG.debug(String.format("Created %s!", index)); } + public boolean isIndexOnEmbeddedType(Part part) { + + // TODO we could do it for nested fields in the + Field field = ReflectionUtils.findField(part.getProperty().getOwningType().getType(), + part.getProperty().getSegment()); + + if (field == null) { + return false; + } + + return AnnotatedElementUtils.hasAnnotation(field, Embedded.class); + } + private static Direction toDirection(Sort sort, String property) { if (sort.isUnsorted()) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactoryBean.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactoryBean.java index 840527e4a9..ade85d3110 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactoryBean.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactoryBean.java @@ -90,7 +90,7 @@ protected RepositoryFactorySupport createRepositoryFactory() { 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 f694100ef0..4e8232714f 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 @@ protected RepositoryFactorySupport createRepositoryFactory() { 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 37764ff9a5..1ec266bacf 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.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 void lifecycleCallbacksAreInvokedInAppropriateOrder() { 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 39d998f109..abbb7e0287 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.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 void lifecycleCallbacksAreInvokedInAppropriateOrder() { 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 0000000000..486a487844 --- /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 1b40cfc046..0e4c600e10 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.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.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 void projectOperationShouldRenderNestedFieldNamesCorrectlyForTypedAggrega .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 @@ static class Nested { 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 fdaf46692d..198a5f8ae5 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.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.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 void readAndConvertDBRefNestedByMapCorrectly() { 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 @@ interface InterfaceType { } + @EqualsAndHashCode static class Address implements InterfaceType { String street; String city; @@ -2641,6 +2835,50 @@ static class WithExplicitTargetTypes { 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 Person onAfterConvert(Person entity, org.bson.Document document, String c 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 d0eb09a40a..5ef5198ae5 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.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.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 void untypedExampleShouldNotInferTypeRestriction() { 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 @@ static class ReferenceDocument { @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 162693cd6b..ce29c2a42b 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.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.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 @@ void convertsDBRefWithExistsQuery() { 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 @@ void convertsNestedDBRefsCorrectly() { 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 @@ void shouldExcludeDBRefAssociation() { 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 @@ void shouldExcludeDBRefAssociation() { @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 @@ void queryMapperShouldMapDBRefPropertyIfNestedInDocument() { @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 @@ void exampleWithCombinedCriteriaShouldBeMappedCorrectly() { 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 @@ void shouldParseNestedKeywordWithArgumentMatchingTheSourceEntitiesConstructorCor 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 WithSingleStringArgConstructor(String value) { 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 df0b8e68a0..57eb620a7e 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.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.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 @@ void mappingShouldAllowNestedPositionParameterWithIdentifierWhenFieldHasExplicit 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 @@ static class TypeWithFieldNameThatCannotBeDecapitalized { } + 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 1076e0ac13..7f14080d62 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.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.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.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 void hashedIndexAndIndexViaComposedAnnotation() { }); } + @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 @@ static class OuterDocumentReferingToIndexedPropertyViaDifferentNonCyclingPaths { 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 @@ class WithComposedHashedIndexAndIndex { } } - 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 0fb76ddb0b..28d5123502 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.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 @@ void subclassInheritsAtDocumentAnnotation() { @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 @@ class WithSimpleCollation {} 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 40fd6e2147..3fb4f59084 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 void shouldConsiderComposedAnnotationsForFields() { 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 c7d42df28a..a7e454c52a 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 @@ void doesNotConsiderOverrridenAccessorANewField() { 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 @@ void mappingContextShouldAcceptClassWithImplicitIdProperty() { 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 @@ void mappingContextShouldAcceptClassWithExplicitIdProperty() { 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 @@ void shouldNotCreateEntityForEnum() { 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 7055a73e25..849418abfd 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 @@ void findWithMoreThan10Arguments() { 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 2786505f58..24d42f27a2 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.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 enum Sex { Credentials credentials; + @Embedded.Nullable(prefix = "u") // + User embeddedUser; + public Person() { this(null, null); @@ -296,6 +300,14 @@ public List getSkills() { 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 1d340bd196..52041c270f 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 @@ Page findByCustomQueryLastnameAndAddressStreetInList(String lastname, Li 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 e98dc8cb2e..2955d1f3d3 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 void testname() { 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 103da191ff..a8dcf08f81 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 @@ void setUp() { 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 0000000000..1cfc91e64c --- /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 98ecf074d5..697eba8d20 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[] From 3e008351a0f764cae80230219f68d5bb5487774d Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 16 Feb 2021 11:10:06 +0100 Subject: [PATCH 3/3] Polishing. Reorder API methods, remove unused MongoPersistentProperty.isNullable method, reduce visibility where possible. Add Javadoc and tweak documentation wording. Introduce DotPath utility to abstract dot path concatenation. --- .../core/convert/DocumentAccessor.java | 13 ++- .../core/convert/MappingMongoConverter.java | 2 +- .../core/convert/MongoExampleMapper.java | 3 +- .../mongodb/core/convert/QueryMapper.java | 18 ++-- .../mongodb/core/convert/UpdateMapper.java | 5 -- .../core/index/IndexOperationsProvider.java | 16 ++-- .../MongoPersistentEntityIndexResolver.java | 41 ++++----- .../core/mapping/EmbeddedEntityContext.java | 2 +- .../EmbeddedMongoPersistentEntity.java | 10 ++- .../EmbeddedMongoPersistentProperty.java | 10 +-- .../core/mapping/MongoMappingContext.java | 15 ++-- .../core/mapping/MongoPersistentEntity.java | 4 + .../core/mapping/MongoPersistentProperty.java | 16 +--- .../data/mongodb/util/DotPath.java | 89 +++++++++++++++++++ .../core/MongoTemplateEmbeddedTests.java | 6 ++ .../UnionWithOperationUnitTests.java | 1 - .../core/convert/UpdateMapperUnitTests.java | 1 - .../core/mapreduce/MapReduceTests.java | 13 --- src/main/asciidoc/new-features.adoc | 5 ++ .../reference/embedded-documents.adoc | 88 ++++++++++-------- 20 files changed, 224 insertions(+), 134 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/DotPath.java 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 44c5f75f28..53dd03f06f 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,8 +67,17 @@ Bson getDocument() { return this.document; } - public void putAll(MongoPersistentProperty prop, Document value) { - value.entrySet().forEach(entry -> BsonUtils.asMap(document).put(entry.getKey(), entry.getValue())); + /** + * Copies all of the mappings from the given {@link Document} to the underlying target {@link Document}. These + * mappings will replace any mappings that the target document had for any of the keys currently in the specified map. + * + * @param source + */ + public void putAll(Document source) { + + Map target = BsonUtils.asMap(document); + + target.putAll(source); } /** 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 2e0f1e6bad..fd3a832e56 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 @@ -672,7 +672,7 @@ protected void writePropertyInternal(@Nullable Object obj, DocumentAccessor acce Document target = new Document(); writeInternal(obj, target, mappingContext.getPersistentEntity(prop)); - accessor.putAll(prop, target); + accessor.putAll(target); return; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoExampleMapper.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoExampleMapper.java index 606cb03e8b..93f935990f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoExampleMapper.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoExampleMapper.java @@ -40,6 +40,7 @@ import org.springframework.data.mongodb.core.query.MongoRegexCreator.MatchMode; import org.springframework.data.mongodb.core.query.SerializationUtils; import org.springframework.data.mongodb.core.query.UntypedExampleMatcher; +import org.springframework.data.mongodb.util.DotPath; import org.springframework.data.support.ExampleMatcherAccessor; import org.springframework.data.util.TypeInformation; import org.springframework.util.Assert; @@ -134,7 +135,7 @@ private void applyPropertySpecs(String path, Document source, Class probeType while (iter.hasNext()) { Map.Entry entry = iter.next(); - String propertyPath = StringUtils.hasText(path) ? path + "." + entry.getKey() : entry.getKey(); + String propertyPath = DotPath.from(path).append(entry.getKey()).toString(); String mappedPropertyPath = getMappedPropertyPath(propertyPath, probeType); if (isEmptyIdProperty(entry)) { 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 e54deca25f..e29397207e 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 @@ -24,6 +24,7 @@ import org.bson.Document; import org.bson.conversions.Bson; import org.bson.types.ObjectId; + import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.converter.Converter; import org.springframework.data.domain.Example; @@ -32,7 +33,6 @@ 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; @@ -43,6 +43,7 @@ import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty.PropertyToFieldNameConverter; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.util.BsonUtils; +import org.springframework.data.mongodb.util.DotPath; import org.springframework.data.util.ClassTypeInformation; import org.springframework.data.util.TypeInformation; import org.springframework.lang.Nullable; @@ -255,17 +256,18 @@ private Document filterEmbeddedObjects(Document fieldsObject, @Nullable MongoPer PropertyPath path = PropertyPath.from(field.getKey(), entity.getTypeInformation()); PersistentPropertyPath persistentPropertyPath = mappingContext .getPersistentPropertyPath(path); - MongoPersistentProperty property = mappingContext.getPersistentPropertyPath(path).getLeafProperty(); + MongoPersistentProperty property = mappingContext.getPersistentPropertyPath(path).getRequiredLeafProperty(); if (property.isEmbedded() && property.isEntity()) { - mappingContext.getPersistentEntity(property) - .doWithProperties((PropertyHandler) embedded -> { + MongoPersistentEntity embeddedEntity = mappingContext.getRequiredPersistentEntity(property); + + for (MongoPersistentProperty embedded : embeddedEntity) { + + DotPath dotPath = DotPath.from(persistentPropertyPath.toDotPath()).append(embedded.getName()); + target.put(dotPath.toString(), field.getValue()); + } - String dotPath = persistentPropertyPath.toDotPath(); - dotPath = dotPath + (StringUtils.hasText(dotPath) ? "." : "") + embedded.getName(); - target.put(dotPath, field.getValue()); - }); } else { target.put(field.getKey(), field.getValue()); } 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 51d7badaaf..85c15b7a1b 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,7 +26,6 @@ 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; @@ -164,10 +163,6 @@ protected Entry getMappedObjectForField(Field field, Object rawV 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 a912ae56c3..6ecbdd8e2f 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 @@ -28,22 +28,22 @@ public interface IndexOperationsProvider { /** - * Returns the operations that can be performed on indexes + * 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); + default IndexOperations indexOps(String collectionName) { + return indexOps(collectionName, null); + } /** - * Returns the operations that can be performed on indexes + * 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 3.2 */ - default IndexOperations indexOps(String collectionName) { - return indexOps(collectionName, null); - } + IndexOperations indexOps(String collectionName, @Nullable Class type); } 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 8c158c34bd..413ea661bd 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 @@ -47,6 +47,7 @@ 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.mongodb.util.DotPath; import org.springframework.data.spel.EvaluationContextProvider; import org.springframework.data.util.TypeInformation; import org.springframework.expression.EvaluationContext; @@ -161,16 +162,16 @@ private void potentiallyAddIndexForProperty(MongoPersistentEntity root, Mongo * @return List of {@link IndexDefinitionHolder} representing indexes for given type and its referenced property * types. Will never be {@code null}. */ - private List resolveIndexForClass(final TypeInformation type, final String dotPath, - final Path path, final String collection, final CycleGuard guard) { + private List resolveIndexForClass( TypeInformation type, String dotPath, + Path path, String collection, CycleGuard guard) { 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) { + private List resolveIndexForEntity(MongoPersistentEntity entity, String dotPath, + Path path, String collection, CycleGuard guard) { - final List indexInformation = new ArrayList<>(); + List indexInformation = new ArrayList<>(); indexInformation.addAll(potentiallyCreateCompoundIndexDefinitions(dotPath, collection, entity)); entity.doWithProperties((PropertyHandler) property -> this @@ -184,10 +185,10 @@ private List resolveIndexForEntity(MongoPersistentEntity< private void guardAndPotentiallyAddIndexForProperty(MongoPersistentProperty persistentProperty, String dotPath, Path path, String collection, List indexes, CycleGuard guard) { - String propertyDotPath = dotPath; + DotPath propertyDotPath = DotPath.from(dotPath); if (!persistentProperty.isEmbedded()) { - propertyDotPath = (StringUtils.hasText(dotPath) ? dotPath + "." : "") + persistentProperty.getFieldName(); + propertyDotPath = propertyDotPath.append(persistentProperty.getFieldName()); } Path propertyPath = path.append(persistentProperty); @@ -195,14 +196,14 @@ private void guardAndPotentiallyAddIndexForProperty(MongoPersistentProperty pers if (persistentProperty.isEntity()) { try { - indexes.addAll(resolveIndexForEntity(mappingContext.getPersistentEntity(persistentProperty), propertyDotPath, + indexes.addAll(resolveIndexForEntity(mappingContext.getPersistentEntity(persistentProperty), propertyDotPath.toString(), propertyPath, collection, guard)); } catch (CyclicPropertyReferenceException e) { LOGGER.info(e.getMessage()); } } - List indexDefinitions = createIndexDefinitionHolderForProperty(propertyDotPath, collection, + List indexDefinitions = createIndexDefinitionHolderForProperty(propertyDotPath.toString(), collection, persistentProperty); if (!indexDefinitions.isEmpty()) { @@ -270,7 +271,7 @@ private Collection potentiallyCreateTextIndexDe } try { - appendTextIndexInformation("", Path.empty(), indexDefinitionBuilder, root, + appendTextIndexInformation(DotPath.empty(), Path.empty(), indexDefinitionBuilder, root, new TextIndexIncludeOptions(IncludeStrategy.DEFAULT), new CycleGuard()); } catch (CyclicPropertyReferenceException e) { LOGGER.info(e.getMessage()); @@ -291,9 +292,9 @@ private Collection potentiallyCreateTextIndexDe } - private void appendTextIndexInformation(final String dotPath, final Path path, - final TextIndexDefinitionBuilder indexDefinitionBuilder, final MongoPersistentEntity entity, - final TextIndexIncludeOptions includeOptions, final CycleGuard guard) { + private void appendTextIndexInformation(DotPath dotPath, Path path, + TextIndexDefinitionBuilder indexDefinitionBuilder, MongoPersistentEntity entity, + TextIndexIncludeOptions includeOptions, CycleGuard guard) { entity.doWithProperties(new PropertyHandler() { @@ -302,7 +303,7 @@ public void doWithPersistentProperty(MongoPersistentProperty persistentProperty) guard.protect(persistentProperty, path); - if (persistentProperty.isExplicitLanguageProperty() && !StringUtils.hasText(dotPath)) { + if (persistentProperty.isExplicitLanguageProperty() && dotPath.isEmpty()) { indexDefinitionBuilder.withLanguageOverride(persistentProperty.getFieldName()); } @@ -310,8 +311,8 @@ public void doWithPersistentProperty(MongoPersistentProperty persistentProperty) if (includeOptions.isForce() || indexed != null || persistentProperty.isEntity()) { - String propertyDotPath = (StringUtils.hasText(dotPath) ? dotPath + "." : "") - + persistentProperty.getFieldName(); + DotPath propertyDotPath = dotPath + .append(persistentProperty.getFieldName()); Path propertyPath = path.append(persistentProperty); @@ -324,7 +325,7 @@ public void doWithPersistentProperty(MongoPersistentProperty persistentProperty) TextIndexIncludeOptions optionsForNestedType = includeOptions; if (!IncludeStrategy.FORCE.equals(includeOptions.getStrategy()) && indexed != null) { optionsForNestedType = new TextIndexIncludeOptions(IncludeStrategy.FORCE, - new TextIndexedFieldSpec(propertyDotPath, weight)); + new TextIndexedFieldSpec(propertyDotPath.toString(), weight)); } try { @@ -337,7 +338,7 @@ public void doWithPersistentProperty(MongoPersistentProperty persistentProperty) entity.getName()), e); } } else if (includeOptions.isForce() || indexed != null) { - indexDefinitionBuilder.onField(propertyDotPath, weight); + indexDefinitionBuilder.onField(propertyDotPath.toString(), weight); } } @@ -648,7 +649,7 @@ private void resolveAndAddIndexesForAssociation(Association indexDefinitions = createIndexDefinitionHolderForProperty(propertyDotPath, collection, + List indexDefinitions = createIndexDefinitionHolderForProperty(propertyDotPath.toString(), collection, property); if (!indexDefinitions.isEmpty()) { 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 index a3390e8e54..c319d0fde7 100644 --- 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 @@ -19,7 +19,7 @@ * @author Christoph Strobl * @since 3.2 */ -public class EmbeddedEntityContext { +class EmbeddedEntityContext { private final MongoPersistentProperty 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 index 9029cfc2f6..bbfa486673 100644 --- 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 @@ -32,13 +32,15 @@ import org.springframework.lang.Nullable; /** + * Embedded variant of {@link MongoPersistentEntity}. + * * @author Christoph Strobl - * @since 2020/12 + * @see Embedded */ -public class EmbeddedMongoPersistentEntity implements MongoPersistentEntity { +class EmbeddedMongoPersistentEntity implements MongoPersistentEntity { - private EmbeddedEntityContext context; - private MongoPersistentEntity delegate; + private final EmbeddedEntityContext context; + private final MongoPersistentEntity delegate; public EmbeddedMongoPersistentEntity(MongoPersistentEntity delegate, EmbeddedEntityContext context) { 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 index e307de4518..88355cff63 100644 --- 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 @@ -26,10 +26,12 @@ import org.springframework.lang.Nullable; /** + * Embedded variant of {@link MongoPersistentProperty}. + * * @author Christoph Strobl - * @since 2020/12 + * @see Embedded */ -public class EmbeddedMongoPersistentProperty implements MongoPersistentProperty { +class EmbeddedMongoPersistentProperty implements MongoPersistentProperty { private final MongoPersistentProperty delegate; private final EmbeddedEntityContext context; @@ -204,10 +206,6 @@ public boolean isEmbedded() { return delegate.isEmbedded(); } - public boolean isNullable() { - return delegate.isNullable(); - } - @Nullable public Class getComponentType() { return delegate.getComponentType(); 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 0c7bea76da..3e18fd463c 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 @@ -35,6 +35,7 @@ * * @author Jon Brisbin * @author Oliver Gierke + * @author Christoph Strobl */ public class MongoMappingContext extends AbstractMappingContext, MongoPersistentProperty> implements ApplicationContextAware { @@ -81,18 +82,13 @@ public MongoPersistentProperty createPersistentProperty(Property property, Mongo 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) */ @Override protected BasicMongoPersistentEntity createPersistentEntity(TypeInformation typeInformation) { - return new BasicMongoPersistentEntity(typeInformation); + return new BasicMongoPersistentEntity<>(typeInformation); } /* @@ -101,7 +97,6 @@ protected BasicMongoPersistentEntity createPersistentEntity(TypeInformati */ @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - super.setApplicationContext(applicationContext); } @@ -132,16 +127,16 @@ public void setAutoIndexCreation(boolean autoCreateIndexes) { this.autoIndexCreation = autoCreateIndexes; } - @Nullable @Override public MongoPersistentEntity getPersistentEntity(MongoPersistentProperty persistentProperty) { - MongoPersistentEntity entity = super.getPersistentEntity(persistentProperty); + MongoPersistentEntity entity = super.getPersistentEntity(persistentProperty); + if(entity == null || !persistentProperty.isEmbedded()) { return entity; } - return new EmbeddedMongoPersistentEntity(entity, new EmbeddedEntityContext(persistentProperty)); + 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 1f18f984e9..d2e74d4de8 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 @@ -94,6 +94,10 @@ default boolean isSharded() { return getShardKey().isSharded(); } + /** + * @return {@literal true} if the entity should be embedded. + * @since 3.2 + */ default boolean isEmbedded() { return false; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentProperty.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentProperty.java index 884319c9ab..be8a59882a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentProperty.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentProperty.java @@ -15,14 +15,10 @@ */ package org.springframework.data.mongodb.core.mapping; -import java.lang.annotation.ElementType; - import org.springframework.core.convert.converter.Converter; import org.springframework.data.annotation.Id; import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.PersistentProperty; -import org.springframework.data.mongodb.core.mapping.Embedded.OnEmpty; -import org.springframework.data.util.NullableUtils; import org.springframework.lang.Nullable; /** @@ -132,17 +128,7 @@ default boolean hasExplicitWriteTarget() { * @since 3.2 */ default boolean isEmbedded() { - return isEntity() && findAnnotation(Embedded.class) != null; - } - - /** - * @return {@literal true} if the property generally allows {@literal null} values; - * @since 3.2 - */ - default boolean isNullable() { - - return (isEmbedded() && findAnnotation(Embedded.class).onEmpty().equals(OnEmpty.USE_NULL)) - && !NullableUtils.isNonNull(getField(), ElementType.FIELD); + return isEntity() && isAnnotationPresent(Embedded.class); } /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/DotPath.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/DotPath.java new file mode 100644 index 0000000000..dd55caf53f --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/DotPath.java @@ -0,0 +1,89 @@ +/* + * 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.util; + +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +/** + * Value object representing a dot path. + * + * @author Mark Paluch + * @since 3.2 + */ +public class DotPath { + + private static final DotPath EMPTY = new DotPath(""); + + private final String path; + + private DotPath(String path) { + this.path = path; + } + + /** + * Creates a new {@link DotPath} from {@code dotPath}. + * + * @param dotPath the dot path, can be empty or {@literal null}. + * @return the {@link DotPath} representing {@code dotPath}. + */ + public static DotPath from(@Nullable String dotPath) { + + if (StringUtils.hasLength(dotPath)) { + return new DotPath(dotPath); + } + + return EMPTY; + } + + /** + * Returns an empty dotpath. + * + * @return an empty dotpath. + */ + public static DotPath empty() { + return EMPTY; + } + + /** + * Append a segment to the dotpath. If the dotpath is not empty, then segments are separated with a dot. + * + * @param segment the segment to append. + * @return + */ + public DotPath append(String segment) { + + if (isEmpty()) { + return new DotPath(segment); + } + + return new DotPath(path + "." + segment); + } + + /** + * Returns whether this dotpath is empty. + * + * @return whether this dotpath is empty. + */ + public boolean isEmpty() { + return !StringUtils.hasLength(path); + } + + @Override + public String toString() { + return path; + } +} 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 index 486a487844..119563be3a 100644 --- 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 @@ -24,6 +24,7 @@ import java.util.Arrays; import java.util.List; +import lombok.ToString; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.data.mongodb.core.mapping.Embedded; @@ -33,6 +34,8 @@ import org.springframework.data.mongodb.test.util.Template; /** + * Integration tests for {@link Embedded}. + * * @author Christoph Strobl */ @ExtendWith(MongoTemplateExtension.class) @@ -105,6 +108,7 @@ void filterOnPrefixedEmbeddedValue() { } @EqualsAndHashCode + @ToString static class WithEmbedded { String id; @@ -113,6 +117,7 @@ static class WithEmbedded { } @EqualsAndHashCode + @ToString static class WithPrefixedEmbedded { String id; @@ -121,6 +126,7 @@ static class WithPrefixedEmbedded { } @EqualsAndHashCode + @ToString static class EmbeddableType { String stringValue; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/UnionWithOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/UnionWithOperationUnitTests.java index bb77c86285..cef23de937 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/UnionWithOperationUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/UnionWithOperationUnitTests.java @@ -83,7 +83,6 @@ void doesNotMapAgainstFieldsFromAPreviousStage() { UnionWithOperation.unionWith("coll-1").pipeline(Aggregation.project().and("name").as("name"))); List pipeline = agg.toPipeline(contextFor(Supplier.class)); - System.out.println("pipeline: " + pipeline); assertThat(pipeline).containsExactly(new Document("$project", new Document("supplier", 1)), // new Document("$unionWith", new Document("coll", "coll-1").append("pipeline", Arrays.asList(new Document("$project", new Document("name", 1)))))); 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 57eb620a7e..996309dcdf 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 @@ -1155,7 +1155,6 @@ void mappingShouldConsiderNestedPrefixedEmbeddedType() { 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"))))); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapreduce/MapReduceTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapreduce/MapReduceTests.java index 32468fde17..5672ee2d65 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapreduce/MapReduceTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapreduce/MapReduceTests.java @@ -79,19 +79,6 @@ protected void cleanDb() { template.getMongoDbFactory().getMongoDatabase("jmr1-out-db").drop(); } - @Test // DATADOC-7 - @Ignore - public void testForDocs() { - - createMapReduceData(); - MapReduceResults results = mongoTemplate.mapReduce("jmr1", MAP_FUNCTION, REDUCE_FUNCTION, - ValueObject.class); - - for (ValueObject valueObject : results) { - System.out.println(valueObject); - } - } - @Test // DATAMONGO-260 public void testIssue260() { diff --git a/src/main/asciidoc/new-features.adoc b/src/main/asciidoc/new-features.adoc index 305e349cdd..9cf28cb30d 100644 --- a/src/main/asciidoc/new-features.adoc +++ b/src/main/asciidoc/new-features.adoc @@ -1,6 +1,11 @@ [[new-features]] = New & Noteworthy +[[new-features.3.2]] +== What's New in Spring Data MongoDB 3.2 + +* Support for <> to unwrap nested objects into the parent `Document`. + [[new-features.3.1]] == What's New in Spring Data MongoDB 3.1 diff --git a/src/main/asciidoc/reference/embedded-documents.adoc b/src/main/asciidoc/reference/embedded-documents.adoc index 1cfc91e64c..2d83706a0a 100644 --- a/src/main/asciidoc/reference/embedded-documents.adoc +++ b/src/main/asciidoc/reference/embedded-documents.adoc @@ -1,30 +1,33 @@ [[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 are used to design value objects in your Java domain model whose properties are flattened out into the parent's 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. +Consider the following domain model where `User.name` is annotated with `@Embedded`. +The `@Embedded` annotation signals that all properties of `UserName` should be unwrapped into the `user` document that owns the `name` property. .Sample Code of embedding objects ==== [source,java] ---- -public class User { +class User { - @Id - private String userId; + @Id + String userId; @Embedded(onEmpty = USE_NULL) <1> UserName name; } -public class UserName { - private String firstname; - private String lastname; +class UserName { + + String firstname; + + String lastname; + } ---- @@ -41,37 +44,42 @@ By using `onEmpty=USE_EMPTY` an empty `UserName`, with potential `null` value fo ==== 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. +Both annotations are meta-annotated with JSR-305 `@javax.annotation.Nonnull` to aid with nullability inspections. [WARNING] ==== It is possible to use complex types within an embedded object. -However those must not be, nor contain embedded fields themselves. +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. +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 { +class User { - @Id - private String userId; + @Id + String userId; - @Embedded.Nullable(prefix = "u") <1> + @Embedded.Nullable(prefix = "u_") <1> + UserName name; + + @Embedded.Nullable(prefix = "a_") <2> UserName name; } -public class UserName { - private String firstname; - private String lastname; +class UserName { + + String firstname; + + String lastname; } ---- @@ -79,11 +87,14 @@ public class UserName { ---- { "_id" : "a6a805bd-f95f", - "ufirstname" : "Jean", - "ulastname" : "Grey" + "u_firstname" : "Jean", <1> + "u_lastname" : "Grey", + "a_firstname" : "Something", <2> + "a_lastname" : "Else" } ---- -<1> The prefix `u` is prepended to all properties of `UserName`. +<1> All properties of `UserName` are prefixed with `u_`. +<2> All properties of `UserName` are prefixed with `a_`. ==== While combining the `@Field` annotation with `@Embedded` on the very same property does not make sense and therefore leads to an error. @@ -104,7 +115,7 @@ public class User { public class UserName { - @Field("first-name") <2> + @Field("first-name") <2> private String firstname; @Field("last-name") @@ -116,18 +127,18 @@ public class UserName { ---- { "_id" : "2647f7b9-89da", - "u-first-name" : "Barbara", <2> + "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. +<1> All properties of `UserName` are prefixed with `u-`. +<2> Final field names are a result of concatenating `@Embedded(prefix)` and `@Field(name)`. ==== [[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. +Defining queries on embedded properties is possible on type- as well as field-level as the provided `Criteria` 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. @@ -149,7 +160,7 @@ db.collection.find({ ---- ==== -It is also possible to address any field of the embedded object directly via its property name as shown in the snippet below. +It is also possible to address any field of the embedded object directly using its property name as shown in the snippet below. .Query on field of embedded object ==== @@ -194,7 +205,7 @@ Though possible, using the embedded object itself as sort criteria includes all ==== [[embedded-entities.queries.project]] -==== Project on embedded object +==== Field projection on embedded objects Fields of embedded objects can be subject for projection either as a whole or via single fields as shown in the samples below. @@ -203,7 +214,7 @@ Fields of embedded objects can be subject for projection either as a whole or vi [source,java] ---- Query findByUserLastName = query(where("name.firstname").is("Gamora")); -findByUserLastName.fields().include("name"); <1> +findByUserLastName.fields().include("name"); <1> List user = template.findAll(findByUserName, User.class); ---- @@ -225,7 +236,7 @@ db.collection.find({ [source,java] ---- Query findByUserLastName = query(where("name.lastname").is("Smoak")); -findByUserLastName.fields().include("name.firstname"); <1> +findByUserLastName.fields().include("name.firstname"); <1> List user = template.findAll(findByUserName, User.class); ---- @@ -258,9 +269,9 @@ The `Repository` abstraction allows deriving queries on fields of embedded objec ---- interface UserRepository extends CrudRepository { - List findByName(UserName username); <1> + List findByName(UserName username); <1> - List findByNameFirstname(String firstname); <1> + List findByNameFirstname(String firstname); <2> } ---- <1> Matches against all fields of the embedded object. @@ -330,14 +341,14 @@ db.collection.update({ === 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. +Please make sure to work with the property path including the embedded wrapper object when referencing one of its 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. +It is not possible to use `@Indexed` along with the `@Embedded` annotation on the owning property. ==== [source,java] @@ -348,9 +359,10 @@ public class User { private String userId; @Embedded(onEmpty = USE_NULL) - UserName name; <1> + UserName name; <1> - @Indexed <2> // Invalid -> InvalidDataAccessApiUsageException + // Invalid -> InvalidDataAccessApiUsageException + @Indexed <2> @Embedded(onEmpty = USE_Empty) Address address; } @@ -360,7 +372,7 @@ public class UserName { private String firstname; @Indexed - private String lastname; <1> + private String lastname; <1> } ---- <1> Index created for `lastname` in `users` collection.