diff --git a/pom.xml b/pom.xml index 10c1adf1bf..2baa1f2b58 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 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 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..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,6 +67,19 @@ Bson getDocument() { return this.document; } + /** + * 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); + } + /** * 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..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 @@ -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(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/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/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..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; @@ -42,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; @@ -140,9 +142,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 +189,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 +219,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 +241,44 @@ 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).getRequiredLeafProperty(); + + if (property.isEmbedded() && property.isEntity()) { + + MongoPersistentEntity embeddedEntity = mappingContext.getRequiredPersistentEntity(property); + + for (MongoPersistentProperty embedded : embeddedEntity) { + + DotPath dotPath = DotPath.from(persistentPropertyPath.toDotPath()).append(embedded.getName()); + target.put(dotPath.toString(), 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 +559,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 +979,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..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 @@ -132,6 +132,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)); } 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..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 @@ -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. * @@ -26,10 +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}. + * @return index operations on the named collection + */ + default IndexOperations indexOps(String collectionName) { + return indexOps(collectionName, null); + } + + /** + * 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 */ - IndexOperations indexOps(String collectionName); + 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 224aca249b..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; @@ -135,8 +136,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( @@ -160,12 +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) { - MongoPersistentEntity entity = mappingContext.getRequiredPersistentEntity(type); + return resolveIndexForEntity(mappingContext.getRequiredPersistentEntity(type), dotPath, path, collection, 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 @@ -179,21 +185,25 @@ private List resolveIndexForClass(final TypeInformation indexes, CycleGuard guard) { - String propertyDotPath = (StringUtils.hasText(dotPath) ? dotPath + "." : "") + persistentProperty.getFieldName(); + DotPath propertyDotPath = DotPath.from(dotPath); + + if (!persistentProperty.isEmbedded()) { + propertyDotPath = propertyDotPath.append(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.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()) { @@ -206,6 +216,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)) { @@ -254,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()); @@ -275,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() { @@ -286,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()); } @@ -294,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); @@ -308,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 { @@ -321,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); } } @@ -482,7 +499,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 +510,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}. @@ -633,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/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..c319d0fde7 --- /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 + */ +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..bbfa486673 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/EmbeddedMongoPersistentEntity.java @@ -0,0 +1,283 @@ +/* + * 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; + +/** + * Embedded variant of {@link MongoPersistentEntity}. + * + * @author Christoph Strobl + * @see Embedded + */ +class EmbeddedMongoPersistentEntity implements MongoPersistentEntity { + + private final EmbeddedEntityContext context; + private final 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..88355cff63 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/EmbeddedMongoPersistentProperty.java @@ -0,0 +1,257 @@ +/* + * 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; + +/** + * Embedded variant of {@link MongoPersistentProperty}. + * + * @author Christoph Strobl + * @see Embedded + */ +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(); + } + + @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..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,8 +35,9 @@ * * @author Jon Brisbin * @author Oliver Gierke + * @author Christoph Strobl */ -public class MongoMappingContext extends AbstractMappingContext, MongoPersistentProperty> +public class MongoMappingContext extends AbstractMappingContext, MongoPersistentProperty> implements ApplicationContextAware { private static final FieldNamingStrategy DEFAULT_NAMING_STRATEGY = PropertyNameFieldNamingStrategy.INSTANCE; @@ -76,7 +77,7 @@ 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); } @@ -87,7 +88,7 @@ public MongoPersistentProperty createPersistentProperty(Property property, Basic */ @Override protected BasicMongoPersistentEntity createPersistentEntity(TypeInformation typeInformation) { - return new BasicMongoPersistentEntity(typeInformation); + return new BasicMongoPersistentEntity<>(typeInformation); } /* @@ -96,7 +97,6 @@ protected BasicMongoPersistentEntity createPersistentEntity(TypeInformati */ @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - super.setApplicationContext(applicationContext); } @@ -126,4 +126,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..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 @@ -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,12 @@ 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 53a5eb7f91..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 @@ -123,6 +123,14 @@ 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() && isAnnotationPresent(Embedded.class); + } + /** * Simple {@link Converter} implementation to transform a {@link MongoPersistentProperty} into its field name. * @@ -137,7 +145,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/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/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..119563be3a --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateEmbeddedTests.java @@ -0,0 +1,138 @@ +/* + * 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 lombok.ToString; +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; + +/** + * Integration tests for {@link Embedded}. + * + * @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 + @ToString + static class WithEmbedded { + + String id; + + @Embedded.Nullable EmbeddableType embeddableValue; + } + + @EqualsAndHashCode + @ToString + static class WithPrefixedEmbedded { + + String id; + + @Embedded.Nullable("prefix-") EmbeddableType embeddableValue; + } + + @EqualsAndHashCode + @ToString + 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/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/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..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 @@ -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,75 @@ 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)); + + 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 +1488,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/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/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/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 new file mode 100644 index 0000000000..2d83706a0a --- /dev/null +++ b/src/main/asciidoc/reference/embedded-documents.adoc @@ -0,0 +1,382 @@ +[[embedded-entities]] +== Embedded Types + +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 + +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] +---- +class User { + + @Id + String userId; + + @Embedded(onEmpty = USE_NULL) <1> + UserName name; +} + +class UserName { + + String firstname; + + 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)`. +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. +==== + +[[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] +---- +class User { + + @Id + String userId; + + @Embedded.Nullable(prefix = "u_") <1> + UserName name; + + @Embedded.Nullable(prefix = "a_") <2> + UserName name; +} + +class UserName { + + String firstname; + + String lastname; +} +---- + +[source,json] +---- +{ + "_id" : "a6a805bd-f95f", + "u_firstname" : "Jean", <1> + "u_lastname" : "Grey", + "a_firstname" : "Something", <2> + "a_lastname" : "Else" +} +---- +<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. +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> 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 `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. + +.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 using 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]] +==== 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. + +.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); <2> +} +---- +<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 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. +It is not possible to use `@Indexed` along with the `@Embedded` annotation on the owning property. + +==== +[source,java] +---- +public class User { + + @Id + private String userId; + + @Embedded(onEmpty = USE_NULL) + UserName name; <1> + + // Invalid -> InvalidDataAccessApiUsageException + @Indexed <2> + @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[]