Add support for embedded types.

We now support embedded types in the sense of unwrapping nested objects into their parent Document to flatten out domain models where needed.

A domain class of:

public class User {
    @Id
    private String userId;

    @Embedded(onEmpty = USE_NULL)
    private UserName name;
}

public class UserName {
    private String firstname;
    private String lastname;
}

renders:

{
  "_id" : "1da2ba06-3ba7",
  "firstname" : "Emma",
  "lastname" : "Frost"
}

Resolves #2803.
Original pull request: #896.
This commit is contained in:
Christoph Strobl
2020-12-02 11:06:56 +01:00
committed by Mark Paluch
parent c1417c4e4b
commit bd985a6589
40 changed files with 2390 additions and 61 deletions

View File

@@ -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);
}
}

View File

@@ -715,12 +715,18 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
});
}
@Override
public IndexOperations indexOps(String collectionName) {
return indexOps(collectionName, null);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.ExecutableInsertOperation#indexOps(java.lang.String)
*/
public IndexOperations indexOps(String collectionName) {
return new DefaultIndexOperations(this, collectionName, null);
public IndexOperations indexOps(String collectionName, @Nullable Class<?> type) {
return new DefaultIndexOperations(this, collectionName, type);
}
/*
@@ -728,7 +734,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
* @see org.springframework.data.mongodb.core.ExecutableInsertOperation#indexOps(java.lang.Class)
*/
public IndexOperations indexOps(Class<?> entityClass) {
return new DefaultIndexOperations(this, getCollectionName(entityClass), entityClass);
return indexOps(getCollectionName(entityClass), entityClass);
}
/*

View File

@@ -67,6 +67,10 @@ class DocumentAccessor {
return this.document;
}
public void putAll(MongoPersistentProperty prop, Document value) {
value.entrySet().forEach(entry -> BsonUtils.asMap(document).put(entry.getKey(), entry.getValue()));
}
/**
* Puts the given value into the backing {@link Document} based on the coordinates defined through the given
* {@link MongoPersistentProperty}. By default this will be the plain field name. But field names might also consist

View File

@@ -62,6 +62,8 @@ import org.springframework.data.mapping.model.SpELExpressionEvaluator;
import org.springframework.data.mapping.model.SpELExpressionParameterValueProvider;
import org.springframework.data.mongodb.CodecRegistryProvider;
import org.springframework.data.mongodb.MongoDatabaseFactory;
import org.springframework.data.mongodb.core.mapping.Embedded;
import org.springframework.data.mongodb.core.mapping.Embedded.OnEmpty;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.data.mongodb.core.mapping.event.AfterConvertCallback;
@@ -427,6 +429,13 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
continue;
}
if (prop.isEmbedded()) {
accessor.setProperty(prop,
readEmbedded(documentAccessor, currentPath, prop, mappingContext.getPersistentEntity(prop)));
continue;
}
// We skip the id property since it was already set
if (entity.isIdProperty(prop)) {
@@ -472,6 +481,22 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
accessor.setProperty(property, dbRefResolver.resolveDbRef(property, dbref, callback, handler));
}
@Nullable
private Object readEmbedded(DocumentAccessor documentAccessor, ObjectPath currentPath, MongoPersistentProperty prop,
MongoPersistentEntity<?> embeddedEntity) {
if (prop.findAnnotation(Embedded.class).onEmpty().equals(OnEmpty.USE_EMPTY)) {
return read(embeddedEntity, (Document) documentAccessor.getDocument(), currentPath);
}
for (MongoPersistentProperty persistentProperty : embeddedEntity) {
if (documentAccessor.hasValue(persistentProperty)) {
return read(embeddedEntity, (Document) documentAccessor.getDocument(), currentPath);
}
}
return null;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.convert.MongoWriter#toDBRef(java.lang.Object, org.springframework.data.mongodb.core.mapping.MongoPersistentProperty)
@@ -642,6 +667,15 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
TypeInformation<?> valueType = ClassTypeInformation.from(obj.getClass());
TypeInformation<?> type = prop.getTypeInformation();
if (prop.isEmbedded()) {
Document target = new Document();
writeInternal(obj, target, mappingContext.getPersistentEntity(prop));
accessor.putAll(prop, target);
return;
}
if (valueType.isCollectionLike()) {
List<Object> collectionInternal = createCollection(asCollection(obj), prop);
accessor.put(prop, collectionInternal);
@@ -1352,6 +1386,14 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
return !obj.getClass().equals(typeInformation.getType()) ? newDocument : removeTypeInfo(newDocument, true);
}
@Nullable
@Override
public Object convertToMongoType(@Nullable Object obj, MongoPersistentEntity entity) {
Document newDocument = new Document();
writeInternal(obj, newDocument, entity);
return newDocument;
}
public List<Object> maybeConvertList(Iterable<?> source, TypeInformation<?> typeInformation) {
List<Object> newDbl = new ArrayList<>();

View File

@@ -26,6 +26,7 @@ import org.springframework.data.convert.TypeMapper;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.data.mongodb.util.BsonUtils;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
@@ -144,9 +145,9 @@ public interface MongoConverter
try {
return getConversionService().canConvert(id.getClass(), targetType)
? getConversionService().convert(id, targetType)
: convertToMongoType(id, null);
: convertToMongoType(id, (TypeInformation<?>) null);
} catch (ConversionException o_O) {
return convertToMongoType(id, null);
return convertToMongoType(id,(TypeInformation<?>) null);
}
}
}

View File

@@ -17,6 +17,7 @@ package org.springframework.data.mongodb.core.convert;
import org.bson.conversions.Bson;
import org.springframework.data.convert.EntityWriter;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable;
@@ -43,7 +44,7 @@ public interface MongoWriter<T> extends EntityWriter<T, Bson> {
*/
@Nullable
default Object convertToMongoType(@Nullable Object obj) {
return convertToMongoType(obj, null);
return convertToMongoType(obj, (TypeInformation<?>) null);
}
/**
@@ -57,6 +58,9 @@ public interface MongoWriter<T> extends EntityWriter<T, Bson> {
@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.
*

View File

@@ -32,6 +32,7 @@ import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.PersistentPropertyPath;
import org.springframework.data.mapping.PropertyHandler;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.mapping.PropertyReferenceException;
import org.springframework.data.mapping.context.InvalidPersistentPropertyPath;
@@ -140,9 +141,23 @@ public class QueryMapper {
try {
Field field = createPropertyField(entity, key, mappingContext);
Entry<String, Object> 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<String, Object> entry = getMappedObjectForField(field, BsonUtils.get(query, key));
result.put(entry.getKey(), entry.getValue());
}
} catch (InvalidPersistentPropertyPath invalidPathException) {
// in case the object has not already been mapped
@@ -173,10 +188,16 @@ public class QueryMapper {
return new Document();
}
sortObject = filterEmbeddedObjects(sortObject, entity);
Document mappedSort = new Document();
for (Map.Entry<String, Object> entry : BsonUtils.asMap(sortObject).entrySet()) {
Field field = createPropertyField(entity, entry.getKey(), mappingContext);
if (field.getProperty() != null && field.getProperty().isEmbedded()) {
continue;
}
mappedSort.put(field.getMappedKey(), entry.getValue());
}
@@ -197,7 +218,9 @@ public class QueryMapper {
Assert.notNull(fieldsObject, "FieldsObject must not be null!");
Document mappedFields = fieldsObject.isEmpty() ? new Document() : getMappedObject(fieldsObject, entity);
fieldsObject = filterEmbeddedObjects(fieldsObject, entity);
Document mappedFields = getMappedObject(fieldsObject, entity);
mapMetaAttributes(mappedFields, entity, MetaMapping.FORCE);
return mappedFields;
}
@@ -217,6 +240,43 @@ public class QueryMapper {
}
}
private Document filterEmbeddedObjects(Document fieldsObject, @Nullable MongoPersistentEntity<?> entity) {
if (fieldsObject.isEmpty() || entity == null) {
return fieldsObject;
}
Document target = new Document();
for (Entry<String, Object> field : fieldsObject.entrySet()) {
try {
PropertyPath path = PropertyPath.from(field.getKey(), entity.getTypeInformation());
PersistentPropertyPath<MongoPersistentProperty> persistentPropertyPath = mappingContext
.getPersistentPropertyPath(path);
MongoPersistentProperty property = mappingContext.getPersistentPropertyPath(path).getLeafProperty();
if (property.isEmbedded() && property.isEntity()) {
mappingContext.getPersistentEntity(property)
.doWithProperties((PropertyHandler<MongoPersistentProperty>) embedded -> {
String dotPath = persistentPropertyPath.toDotPath();
dotPath = dotPath + (StringUtils.hasText(dotPath) ? "." : "") + embedded.getName();
target.put(dotPath, field.getValue());
});
} else {
target.put(field.getKey(), field.getValue());
}
} catch (RuntimeException e) {
target.put(field.getKey(), field.getValue());
}
}
return target;
}
private Document getMappedTextScoreField(MongoPersistentProperty property) {
return new Document(property.getFieldName(), META_TEXT_SCORE);
}
@@ -497,6 +557,11 @@ public class QueryMapper {
*/
@Nullable
protected Object delegateConvertToMongoType(Object source, @Nullable MongoPersistentEntity<?> entity) {
if (entity != null && entity.isEmbedded()) {
return converter.convertToMongoType(source, entity);
}
return converter.convertToMongoType(source, entity == null ? null : entity.getTypeInformation());
}
@@ -912,6 +977,7 @@ public class QueryMapper {
public TypeInformation<?> getTypeHint() {
return ClassTypeInformation.OBJECT;
}
}
/**

View File

@@ -26,6 +26,7 @@ import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Order;
import org.springframework.data.mapping.Association;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mongodb.core.mapping.EmbeddedMongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.data.mongodb.core.query.Query;
@@ -132,6 +133,11 @@ public class UpdateMapper extends QueryMapper {
*/
@Override
protected Object delegateConvertToMongoType(Object source, @Nullable MongoPersistentEntity<?> entity) {
if(entity != null && entity.isEmbedded()) {
return converter.convertToMongoType(source, entity);
}
return converter.convertToMongoType(source,
entity == null ? ClassTypeInformation.OBJECT : getTypeHintForEntity(source, entity));
}
@@ -158,6 +164,10 @@ public class UpdateMapper extends QueryMapper {
return getMappedUpdateModifier(field, rawValue);
}
if(field.getProperty() != null && field.getProperty().isEmbedded()) {
System.out.println("here we are: ");
}
return super.getMappedObjectForField(field, rawValue);
}

View File

@@ -15,6 +15,8 @@
*/
package org.springframework.data.mongodb.core.index;
import org.springframework.lang.Nullable;
/**
* Provider interface to obtain {@link IndexOperations} by MongoDB collection name.
*
@@ -25,11 +27,23 @@ package org.springframework.data.mongodb.core.index;
@FunctionalInterface
public interface IndexOperationsProvider {
/**
* Returns the operations that can be performed on indexes
*
* @param collectionName name of the MongoDB collection, must not be {@literal null}.
* @param type the type used for field mapping. Can be {@literal null}.
* @return index operations on the named collection
* @since 2.5
*/
IndexOperations indexOps(String collectionName, @Nullable Class<?> type);
/**
* Returns the operations that can be performed on indexes
*
* @param collectionName name of the MongoDB collection, must not be {@literal null}.
* @return index operations on the named collection
*/
IndexOperations indexOps(String collectionName);
default IndexOperations indexOps(String collectionName) {
return indexOps(collectionName, null);
}
}

View File

@@ -135,8 +135,9 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
try {
if (persistentProperty.isEntity()) {
indexes.addAll(resolveIndexForClass(persistentProperty.getTypeInformation().getActualType(),
persistentProperty.getFieldName(), Path.of(persistentProperty), root.getCollection(), guard));
indexes.addAll(resolveIndexForEntity(mappingContext.getPersistentEntity(persistentProperty),
persistentProperty.isEmbedded() ? "" : persistentProperty.getFieldName(), Path.of(persistentProperty),
root.getCollection(), guard));
}
List<IndexDefinitionHolder> indexDefinitions = createIndexDefinitionHolderForProperty(
@@ -163,7 +164,11 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
private List<IndexDefinitionHolder> resolveIndexForClass(final TypeInformation<?> type, final String dotPath,
final Path path, final String collection, final CycleGuard guard) {
MongoPersistentEntity<?> entity = mappingContext.getRequiredPersistentEntity(type);
return resolveIndexForEntity(mappingContext.getRequiredPersistentEntity(type), dotPath, path, collection, guard);
}
private List<IndexDefinitionHolder> resolveIndexForEntity(MongoPersistentEntity<?> entity, final String dotPath,
final Path path, final String collection, final CycleGuard guard) {
final List<IndexDefinitionHolder> indexInformation = new ArrayList<>();
indexInformation.addAll(potentiallyCreateCompoundIndexDefinitions(dotPath, collection, entity));
@@ -179,14 +184,18 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
private void guardAndPotentiallyAddIndexForProperty(MongoPersistentProperty persistentProperty, String dotPath,
Path path, String collection, List<IndexDefinitionHolder> indexes, CycleGuard guard) {
String propertyDotPath = (StringUtils.hasText(dotPath) ? dotPath + "." : "") + persistentProperty.getFieldName();
String propertyDotPath = dotPath;
if (!persistentProperty.isEmbedded()) {
propertyDotPath = (StringUtils.hasText(dotPath) ? dotPath + "." : "") + persistentProperty.getFieldName();
}
Path propertyPath = path.append(persistentProperty);
guard.protect(persistentProperty, propertyPath);
if (persistentProperty.isEntity()) {
try {
indexes.addAll(resolveIndexForClass(persistentProperty.getTypeInformation().getActualType(), propertyDotPath,
indexes.addAll(resolveIndexForEntity(mappingContext.getPersistentEntity(persistentProperty), propertyDotPath,
propertyPath, collection, guard));
} catch (CyclicPropertyReferenceException e) {
LOGGER.info(e.getMessage());
@@ -206,6 +215,13 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
List<IndexDefinitionHolder> indices = new ArrayList<>(2);
if (persistentProperty.isEmbedded() && (persistentProperty.isAnnotationPresent(Indexed.class)
|| persistentProperty.isAnnotationPresent(HashIndexed.class)
|| persistentProperty.isAnnotationPresent(GeoSpatialIndexed.class))) {
throw new InvalidDataAccessApiUsageException(
String.format("Index annotation not allowed on embedded object for path '%s'.", dotPath));
}
if (persistentProperty.isAnnotationPresent(Indexed.class)) {
indices.add(createIndexDefinition(dotPath, collection, persistentProperty));
} else if (persistentProperty.isAnnotationPresent(GeoSpatialIndexed.class)) {
@@ -482,7 +498,7 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
return new IndexDefinitionHolder(dotPath, indexDefinition, collection);
}
private PartialIndexFilter evaluatePartialFilter(String filterExpression, PersistentEntity<?,?> entity) {
private PartialIndexFilter evaluatePartialFilter(String filterExpression, PersistentEntity<?, ?> entity) {
Object result = evaluate(filterExpression, getEvaluationContextForProperty(entity));
@@ -493,7 +509,6 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
return PartialIndexFilter.of(BsonUtils.parse(filterExpression, null));
}
/**
* Creates {@link HashedIndex} wrapped in {@link IndexDefinitionHolder} out of {@link HashIndexed} for a given
* {@link MongoPersistentProperty}.

View File

@@ -164,7 +164,8 @@ public class BasicMongoPersistentEntity<T> extends BasicPersistentEntity<T, Mong
@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) {

View File

@@ -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.
* <p />
* 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.
* <p />
* {@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.
*
* <pre class="code">
* &#64;Embedded.Nullable private Address address;
* </pre>
*
* as alternative to the more verbose
*
* <pre class="code">
* &#64;Embedded(onEmpty = USE_NULL) &#64;javax.annotation.Nonnull(when = When.MAYBE) private Address address;
* </pre>
*
* @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.
*
* <pre class="code">
* &#64;Embedded.Empty private Address address;
* </pre>
*
* as alternative to the more verbose
*
* <pre class="code">
* &#64;Embedded(onEmpty = USE_EMPTY) &#64;javax.annotation.Nonnull(when = When.NEVER) private Address address;
* </pre>
*
* @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 "";
}
}

View File

@@ -0,0 +1,33 @@
/*
* Copyright 2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core.mapping;
/**
* @author Christoph Strobl
* @since 3.2
*/
public class EmbeddedEntityContext {
private final MongoPersistentProperty property;
public EmbeddedEntityContext(MongoPersistentProperty property) {
this.property = property;
}
public MongoPersistentProperty getProperty() {
return property;
}
}

View File

@@ -0,0 +1,281 @@
/*
* Copyright 2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core.mapping;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Spliterator;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import org.springframework.data.mapping.*;
import org.springframework.data.mapping.model.PersistentPropertyAccessorFactory;
import org.springframework.data.mongodb.core.query.Collation;
import org.springframework.data.spel.EvaluationContextProvider;
import org.springframework.data.util.Streamable;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable;
/**
* @author Christoph Strobl
* @since 2020/12
*/
public class EmbeddedMongoPersistentEntity<T> implements MongoPersistentEntity<T> {
private EmbeddedEntityContext context;
private MongoPersistentEntity<T> delegate;
public EmbeddedMongoPersistentEntity(MongoPersistentEntity<T> 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<T, MongoPersistentProperty> 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<? extends Annotation> annotationType) {
return wrap(delegate.getPersistentProperty(annotationType));
}
public Iterable<MongoPersistentProperty> getPersistentProperties(Class<? extends Annotation> 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<T> getType() {
return delegate.getType();
}
public Alias getTypeAlias() {
return delegate.getTypeAlias();
}
public TypeInformation<T> getTypeInformation() {
return delegate.getTypeInformation();
}
public void doWithProperties(PropertyHandler<MongoPersistentProperty> handler) {
delegate.doWithProperties((PropertyHandler<MongoPersistentProperty>) 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<MongoPersistentProperty> handler) {
delegate.doWithAssociations(handler);
}
public void doWithAssociations(SimpleAssociationHandler handler) {
delegate.doWithAssociations(handler);
}
@Nullable
public <A extends Annotation> A findAnnotation(Class<A> annotationType) {
return delegate.findAnnotation(annotationType);
}
public <A extends Annotation> A getRequiredAnnotation(Class<A> annotationType) throws IllegalStateException {
return delegate.getRequiredAnnotation(annotationType);
}
public <A extends Annotation> boolean isAnnotationPresent(Class<A> annotationType) {
return delegate.isAnnotationPresent(annotationType);
}
public <B> PersistentPropertyAccessor<B> getPropertyAccessor(B bean) {
return delegate.getPropertyAccessor(bean);
}
public <B> PersistentPropertyPathAccessor<B> 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<MongoPersistentProperty> iterator() {
List<MongoPersistentProperty> target = new ArrayList<>();
delegate.iterator().forEachRemaining(it -> target.add(wrap(it)));
return target.iterator();
}
public void forEach(Consumer<? super MongoPersistentProperty> action) {
delegate.forEach(it -> action.accept(wrap(it)));
}
public Spliterator<MongoPersistentProperty> 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<MongoPersistentProperty> 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();
}
}

View File

@@ -0,0 +1,259 @@
/*
* Copyright 2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core.mapping;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import org.springframework.data.mapping.Association;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable;
/**
* @author Christoph Strobl
* @since 2020/12
*/
public class EmbeddedMongoPersistentProperty implements MongoPersistentProperty {
private final MongoPersistentProperty delegate;
private final EmbeddedEntityContext context;
public EmbeddedMongoPersistentProperty(MongoPersistentProperty delegate, EmbeddedEntityContext context) {
this.delegate = delegate;
this.context = context;
}
public String getFieldName() {
if (!context.getProperty().isEmbedded()) {
return delegate.getFieldName();
}
return context.getProperty().findAnnotation(Embedded.class).prefix() + delegate.getFieldName();
}
public Class<?> getFieldType() {
return delegate.getFieldType();
}
public int getFieldOrder() {
return delegate.getFieldOrder();
}
public boolean isDbReference() {
return delegate.isDbReference();
}
public boolean isExplicitIdProperty() {
return delegate.isExplicitIdProperty();
}
public boolean isLanguageProperty() {
return delegate.isLanguageProperty();
}
public boolean isExplicitLanguageProperty() {
return delegate.isExplicitLanguageProperty();
}
public boolean isTextScoreProperty() {
return delegate.isTextScoreProperty();
}
@Nullable
public DBRef getDBRef() {
return delegate.getDBRef();
}
public boolean usePropertyAccess() {
return delegate.usePropertyAccess();
}
public boolean hasExplicitWriteTarget() {
return delegate.hasExplicitWriteTarget();
}
public PersistentEntity<?, MongoPersistentProperty> getOwner() {
return delegate.getOwner();
}
public String getName() {
return delegate.getName();
}
public Class<?> getType() {
return delegate.getType();
}
public TypeInformation<?> getTypeInformation() {
return delegate.getTypeInformation();
}
public Iterable<? extends TypeInformation<?>> 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<MongoPersistentProperty> getAssociation() {
return delegate.getAssociation();
}
public Association<MongoPersistentProperty> getRequiredAssociation() {
return delegate.getRequiredAssociation();
}
public boolean isEntity() {
return delegate.isEntity();
}
public boolean isIdProperty() {
return delegate.isIdProperty();
}
public boolean isVersionProperty() {
return delegate.isVersionProperty();
}
public boolean isCollectionLike() {
return delegate.isCollectionLike();
}
public boolean isMap() {
return delegate.isMap();
}
public boolean isArray() {
return delegate.isArray();
}
public boolean isTransient() {
return delegate.isTransient();
}
public boolean isWritable() {
return delegate.isWritable();
}
public boolean isImmutable() {
return delegate.isImmutable();
}
public boolean isAssociation() {
return delegate.isAssociation();
}
public boolean isEmbedded() {
return delegate.isEmbedded();
}
public boolean isNullable() {
return delegate.isNullable();
}
@Nullable
public Class<?> getComponentType() {
return delegate.getComponentType();
}
public Class<?> getRawType() {
return delegate.getRawType();
}
@Nullable
public Class<?> getMapValueType() {
return delegate.getMapValueType();
}
public Class<?> getActualType() {
return delegate.getActualType();
}
@Nullable
public <A extends Annotation> A findAnnotation(Class<A> annotationType) {
return delegate.findAnnotation(annotationType);
}
public <A extends Annotation> A getRequiredAnnotation(Class<A> annotationType) throws IllegalStateException {
return delegate.getRequiredAnnotation(annotationType);
}
@Nullable
public <A extends Annotation> A findPropertyOrOwnerAnnotation(Class<A> annotationType) {
return delegate.findPropertyOrOwnerAnnotation(annotationType);
}
public boolean isAnnotationPresent(Class<? extends Annotation> annotationType) {
return delegate.isAnnotationPresent(annotationType);
}
public boolean hasActualTypeAnnotation(Class<? extends Annotation> annotationType) {
return delegate.hasActualTypeAnnotation(annotationType);
}
@Nullable
public Class<?> getAssociationTargetType() {
return delegate.getAssociationTargetType();
}
public <T> PersistentPropertyAccessor<T> getAccessorForOwner(T owner) {
return delegate.getAccessorForOwner(owner);
}
}

View File

@@ -36,7 +36,7 @@ import org.springframework.lang.Nullable;
* @author Jon Brisbin
* @author Oliver Gierke
*/
public class MongoMappingContext extends AbstractMappingContext<BasicMongoPersistentEntity<?>, MongoPersistentProperty>
public class MongoMappingContext extends AbstractMappingContext<MongoPersistentEntity<?>, MongoPersistentProperty>
implements ApplicationContextAware {
private static final FieldNamingStrategy DEFAULT_NAMING_STRATEGY = PropertyNameFieldNamingStrategy.INSTANCE;
@@ -76,11 +76,16 @@ public class MongoMappingContext extends AbstractMappingContext<BasicMongoPersis
* @see org.springframework.data.mapping.AbstractMappingContext#createPersistentProperty(java.lang.reflect.Field, java.beans.PropertyDescriptor, org.springframework.data.mapping.MutablePersistentEntity, org.springframework.data.mapping.SimpleTypeHolder)
*/
@Override
public MongoPersistentProperty createPersistentProperty(Property property, BasicMongoPersistentEntity<?> owner,
public MongoPersistentProperty createPersistentProperty(Property property, MongoPersistentEntity<?> owner,
SimpleTypeHolder simpleTypeHolder) {
return new CachingMongoPersistentProperty(property, owner, simpleTypeHolder, fieldNamingStrategy);
}
// @Override
// protected MongoPersistentProperty createPersistentProperty(Property property, MongoPersistentEntity<?> owner, SimpleTypeHolder simpleTypeHolder) {
// return null;
// }
/*
* (non-Javadoc)
* @see org.springframework.data.mapping.BasicMappingContext#createPersistentEntity(org.springframework.data.util.TypeInformation, org.springframework.data.mapping.model.MappingContext)
@@ -126,4 +131,17 @@ public class MongoMappingContext extends AbstractMappingContext<BasicMongoPersis
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));
}
}

View File

@@ -16,6 +16,7 @@
package org.springframework.data.mongodb.core.mapping;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.model.MutablePersistentEntity;
import org.springframework.lang.Nullable;
/**
@@ -24,7 +25,7 @@ import org.springframework.lang.Nullable;
* @author Oliver Gierke
* @author Christoph Strobl
*/
public interface MongoPersistentEntity<T> extends PersistentEntity<T, MongoPersistentProperty> {
public interface MongoPersistentEntity<T> extends MutablePersistentEntity<T, MongoPersistentProperty> {
/**
* Returns the collection the entity shall be persisted to.
@@ -93,4 +94,8 @@ public interface MongoPersistentEntity<T> extends PersistentEntity<T, MongoPersi
return getShardKey().isSharded();
}
default boolean isEmbedded() {
return false;
}
}

View File

@@ -15,10 +15,14 @@
*/
package org.springframework.data.mongodb.core.mapping;
import java.lang.annotation.ElementType;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.annotation.Id;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mongodb.core.mapping.Embedded.OnEmpty;
import org.springframework.data.util.NullableUtils;
import org.springframework.lang.Nullable;
/**
@@ -123,6 +127,24 @@ public interface MongoPersistentProperty extends PersistentProperty<MongoPersist
return field != null ? !FieldType.IMPLICIT.equals(field.targetType()) : false;
}
/**
* @return {@literal true} if the property should be embedded.
* @since 3.2
*/
default boolean isEmbedded() {
return isEntity() && findAnnotation(Embedded.class) != null;
}
/**
* @return {@literal true} if the property generally allows {@literal null} values;
* @since 3.2
*/
default boolean isNullable() {
return (isEmbedded() && findAnnotation(Embedded.class).onEmpty().equals(OnEmpty.USE_NULL))
&& !NullableUtils.isNonNull(getField(), ElementType.FIELD);
}
/**
* Simple {@link Converter} implementation to transform a {@link MongoPersistentProperty} into its field name.
*
@@ -137,7 +159,10 @@ public interface MongoPersistentProperty extends PersistentProperty<MongoPersist
* @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object)
*/
public String convert(MongoPersistentProperty source) {
return source.getFieldName();
if (!source.isEmbedded()) {
return source.getFieldName();
}
return "";
}
}
}

View File

@@ -15,12 +15,14 @@
*/
package org.springframework.data.mongodb.repository.support;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.domain.Sort.Order;
@@ -28,6 +30,7 @@ import org.springframework.data.mongodb.UncategorizedMongoDbException;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.index.Index;
import org.springframework.data.mongodb.core.index.IndexOperationsProvider;
import org.springframework.data.mongodb.core.mapping.Embedded;
import org.springframework.data.mongodb.core.query.Collation;
import org.springframework.data.mongodb.repository.query.MongoEntityMetadata;
import org.springframework.data.mongodb.repository.query.PartTreeMongoQuery;
@@ -36,6 +39,7 @@ import org.springframework.data.repository.query.parser.Part;
import org.springframework.data.repository.query.parser.Part.Type;
import org.springframework.data.repository.query.parser.PartTree;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
import com.mongodb.MongoException;
@@ -82,9 +86,14 @@ class IndexEnsuringQueryCreationListener implements QueryCreationListener<PartTr
Sort sort = tree.getSort();
for (Part part : tree.getParts()) {
if (GEOSPATIAL_TYPES.contains(part.getType())) {
return;
}
if (isIndexOnEmbeddedType(part)) {
return;
}
String property = part.getProperty().toDotPath();
Direction order = toDirection(sort, property);
index.on(property, order);
@@ -107,7 +116,7 @@ class IndexEnsuringQueryCreationListener implements QueryCreationListener<PartTr
MongoEntityMetadata<?> metadata = query.getQueryMethod().getEntityInformation();
try {
indexOperationsProvider.indexOps(metadata.getCollectionName()).ensureIndex(index);
indexOperationsProvider.indexOps(metadata.getCollectionName(), metadata.getJavaType()).ensureIndex(index);
} catch (UncategorizedMongoDbException e) {
if (e.getCause() instanceof MongoException) {
@@ -129,6 +138,19 @@ class IndexEnsuringQueryCreationListener implements QueryCreationListener<PartTr
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()) {

View File

@@ -90,7 +90,7 @@ public class MongoRepositoryFactoryBean<T extends Repository<S, ID>, S, ID exten
if (createIndexesForQueryMethods) {
factory.addQueryCreationListener(
new IndexEnsuringQueryCreationListener(collectionName -> operations.indexOps(collectionName)));
new IndexEnsuringQueryCreationListener((collectionName, javaType) -> operations.indexOps(javaType)));
}
return factory;

View File

@@ -97,7 +97,7 @@ public class ReactiveMongoRepositoryFactoryBean<T extends Repository<S, ID>, S,
if (createIndexesForQueryMethods) {
factory.addQueryCreationListener(new IndexEnsuringQueryCreationListener(
collectionName -> IndexOperationsAdapter.blocking(operations.indexOps(collectionName))));
(collectionName, javaType) -> IndexOperationsAdapter.blocking(operations.indexOps(javaType))));
}
return factory;

View File

@@ -39,6 +39,7 @@ import org.springframework.data.mongodb.core.convert.MongoTypeMapper;
import org.springframework.data.mongodb.core.mapping.BasicMongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.spel.EvaluationContextProvider;
import org.springframework.data.spel.ExtensionAwareEvaluationContextProvider;
import org.springframework.test.util.ReflectionTestUtils;
@@ -103,7 +104,7 @@ public class AbstractMongoConfigurationUnitTests {
AbstractApplicationContext context = new AnnotationConfigApplicationContext(SampleMongoConfiguration.class);
MongoMappingContext mappingContext = context.getBean(MongoMappingContext.class);
BasicMongoPersistentEntity<?> entity = mappingContext.getRequiredPersistentEntity(Entity.class);
MongoPersistentEntity<?> entity = mappingContext.getRequiredPersistentEntity(Entity.class);
EvaluationContextProvider provider = (EvaluationContextProvider) ReflectionTestUtils.getField(entity,
"evaluationContextProvider");

View File

@@ -40,6 +40,7 @@ import org.springframework.data.mongodb.core.convert.MongoTypeMapper;
import org.springframework.data.mongodb.core.mapping.BasicMongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.test.util.MongoTestUtils;
import org.springframework.data.spel.EvaluationContextProvider;
import org.springframework.data.spel.ExtensionAwareEvaluationContextProvider;
@@ -103,7 +104,7 @@ public class AbstractReactiveMongoConfigurationUnitTests {
AbstractApplicationContext context = new AnnotationConfigApplicationContext(SampleMongoConfiguration.class);
MongoMappingContext mappingContext = context.getBean(MongoMappingContext.class);
BasicMongoPersistentEntity<?> entity = mappingContext.getRequiredPersistentEntity(Entity.class);
MongoPersistentEntity<?> entity = mappingContext.getRequiredPersistentEntity(Entity.class);
EvaluationContextProvider provider = (EvaluationContextProvider) ReflectionTestUtils.getField(entity,
"evaluationContextProvider");

View File

@@ -0,0 +1,132 @@
/*
* Copyright 2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core;
import static org.assertj.core.api.Assertions.*;
import static org.springframework.data.mongodb.core.query.Criteria.*;
import static org.springframework.data.mongodb.core.query.Query.*;
import lombok.EqualsAndHashCode;
import java.util.Arrays;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.data.mongodb.core.mapping.Embedded;
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.test.util.MongoTemplateExtension;
import org.springframework.data.mongodb.test.util.Template;
/**
* @author Christoph Strobl
*/
@ExtendWith(MongoTemplateExtension.class)
class MongoTemplateEmbeddedTests {
private static @Template MongoTemplate template;
@Test // DATAMONGO-1902
void readWrite() {
WithEmbedded source = new WithEmbedded();
source.id = "id-1";
source.embeddableValue = new EmbeddableType();
source.embeddableValue.stringValue = "string-val";
source.embeddableValue.listValue = Arrays.asList("list-val-1", "list-val-2");
source.embeddableValue.atFieldAnnotatedValue = "@Field";
template.save(source);
assertThat(template.findOne(query(where("id").is(source.id)), WithEmbedded.class)).isEqualTo(source);
}
@Test // DATAMONGO-1902
void filterOnEmbeddedValue() {
WithEmbedded source = new WithEmbedded();
source.id = "id-1";
source.embeddableValue = new EmbeddableType();
source.embeddableValue.stringValue = "string-val";
source.embeddableValue.listValue = Arrays.asList("list-val-1", "list-val-2");
source.embeddableValue.atFieldAnnotatedValue = "@Field";
template.save(source);
assertThat(template.findOne(
Query.query(where("embeddableValue.stringValue").is(source.embeddableValue.stringValue)), WithEmbedded.class))
.isEqualTo(source);
}
@Test // DATAMONGO-1902
void readWritePrefixed() {
WithPrefixedEmbedded source = new WithPrefixedEmbedded();
source.id = "id-1";
source.embeddableValue = new EmbeddableType();
source.embeddableValue.stringValue = "string-val";
source.embeddableValue.listValue = Arrays.asList("list-val-1", "list-val-2");
source.embeddableValue.atFieldAnnotatedValue = "@Field";
template.save(source);
assertThat(template.findOne(query(where("id").is(source.id)), WithPrefixedEmbedded.class)).isEqualTo(source);
}
@Test // DATAMONGO-1902
void filterOnPrefixedEmbeddedValue() {
WithPrefixedEmbedded source = new WithPrefixedEmbedded();
source.id = "id-1";
source.embeddableValue = new EmbeddableType();
source.embeddableValue.stringValue = "string-val";
source.embeddableValue.listValue = Arrays.asList("list-val-1", "list-val-2");
source.embeddableValue.atFieldAnnotatedValue = "@Field";
template.save(source);
assertThat(
template.findOne(Query.query(where("embeddableValue.stringValue").is(source.embeddableValue.stringValue)),
WithPrefixedEmbedded.class)).isEqualTo(source);
}
@EqualsAndHashCode
static class WithEmbedded {
String id;
@Embedded.Nullable EmbeddableType embeddableValue;
}
@EqualsAndHashCode
static class WithPrefixedEmbedded {
String id;
@Embedded.Nullable("prefix-") EmbeddableType embeddableValue;
}
@EqualsAndHashCode
static class EmbeddableType {
String stringValue;
List<String> listValue;
@Field("with-at-field-annotation") //
String atFieldAnnotatedValue;
}
}

View File

@@ -32,7 +32,6 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.support.GenericConversionService;
import org.springframework.data.annotation.Id;
@@ -46,6 +45,7 @@ import org.springframework.data.mongodb.core.convert.DbRefResolver;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.convert.MongoCustomConversions;
import org.springframework.data.mongodb.core.convert.QueryMapper;
import org.springframework.data.mongodb.core.mapping.Embedded;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.query.Criteria;
@@ -357,6 +357,103 @@ public class TypeBasedAggregationOperationContextUnitTests {
.isEqualTo(new Document("val", new Document("$add", Arrays.asList("$nested1.value1", "$field2.nestedValue2"))));
}
@Test // DATAMONGO-1902
void rendersProjectOnEmbeddedFieldCorrectly() {
AggregationOperationContext context = getContext(WithEmbedded.class);
Document agg = newAggregation(WithEmbedded.class, project().and("embeddableType.stringValue").as("val"))
.toDocument("collection", context);
assertThat(getPipelineElementFromAggregationAt(agg, 0).get("$project"))
.isEqualTo(new Document("val", "$stringValue"));
}
@Test // DATAMONGO-1902
void rendersProjectOnEmbeddedFieldWithAtFieldAnnotationCorrectly() {
AggregationOperationContext context = getContext(WithEmbedded.class);
Document agg = newAggregation(WithEmbedded.class, project().and("embeddableType.atFieldAnnotatedValue").as("val"))
.toDocument("collection", context);
assertThat(getPipelineElementFromAggregationAt(agg, 0).get("$project"))
.isEqualTo(new Document("val", "$with-at-field-annotation"));
}
@Test // DATAMONGO-1902
void rendersProjectOnPrefixedEmbeddedFieldCorrectly() {
AggregationOperationContext context = getContext(WithEmbedded.class);
Document agg = newAggregation(WithEmbedded.class, project().and("prefixedEmbeddableValue.stringValue").as("val"))
.toDocument("collection", context);
assertThat(getPipelineElementFromAggregationAt(agg, 0).get("$project"))
.isEqualTo(new Document("val", "$prefix-stringValue"));
}
@Test // DATAMONGO-1902
void rendersProjectOnPrefixedEmbeddedFieldWithAtFieldAnnotationCorrectly() {
AggregationOperationContext context = getContext(WithEmbedded.class);
Document agg = newAggregation(WithEmbedded.class,
project().and("prefixedEmbeddableValue.atFieldAnnotatedValue").as("val")).toDocument("collection", context);
assertThat(getPipelineElementFromAggregationAt(agg, 0).get("$project"))
.isEqualTo(new Document("val", "$prefix-with-at-field-annotation"));
}
@Test // DATAMONGO-1902
void rendersProjectOnNestedEmbeddedFieldCorrectly() {
AggregationOperationContext context = getContext(WrapperAroundWithEmbedded.class);
Document agg = newAggregation(WrapperAroundWithEmbedded.class,
project().and("withEmbedded.embeddableType.stringValue").as("val")).toDocument("collection", context);
assertThat(getPipelineElementFromAggregationAt(agg, 0).get("$project"))
.isEqualTo(new Document("val", "$withEmbedded.stringValue"));
}
@Test // DATAMONGO-1902
void rendersProjectOnNestedEmbeddedFieldWithAtFieldAnnotationCorrectly() {
AggregationOperationContext context = getContext(WrapperAroundWithEmbedded.class);
Document agg = newAggregation(WrapperAroundWithEmbedded.class,
project().and("withEmbedded.embeddableType.atFieldAnnotatedValue").as("val")).toDocument("collection", context);
assertThat(getPipelineElementFromAggregationAt(agg, 0).get("$project"))
.isEqualTo(new Document("val", "$withEmbedded.with-at-field-annotation"));
}
@Test // DATAMONGO-1902
void rendersProjectOnNestedPrefixedEmbeddedFieldCorrectly() {
AggregationOperationContext context = getContext(WrapperAroundWithEmbedded.class);
Document agg = newAggregation(WrapperAroundWithEmbedded.class,
project().and("withEmbedded.prefixedEmbeddableValue.stringValue").as("val")).toDocument("collection", context);
assertThat(getPipelineElementFromAggregationAt(agg, 0).get("$project"))
.isEqualTo(new Document("val", "$withEmbedded.prefix-stringValue"));
}
@Test // DATAMONGO-1902
void rendersProjectOnNestedPrefixedEmbeddedFieldWithAtFieldAnnotationCorrectly() {
AggregationOperationContext context = getContext(WrapperAroundWithEmbedded.class);
Document agg = newAggregation(WrapperAroundWithEmbedded.class,
project().and("withEmbedded.prefixedEmbeddableValue.atFieldAnnotatedValue").as("val")).toDocument("collection",
context);
assertThat(getPipelineElementFromAggregationAt(agg, 0).get("$project"))
.isEqualTo(new Document("val", "$withEmbedded.prefix-with-at-field-annotation"));
}
@org.springframework.data.mongodb.core.mapping.Document(collection = "person")
@AllArgsConstructor
public static class FooPerson {
@@ -433,4 +530,26 @@ public class TypeBasedAggregationOperationContextUnitTests {
String value1;
@org.springframework.data.mongodb.core.mapping.Field("nestedValue2") String value2;
}
static class WrapperAroundWithEmbedded {
String id;
WithEmbedded withEmbedded;
}
static class WithEmbedded {
String id;
@Embedded.Nullable EmbeddableType embeddableType;
@Embedded.Nullable("prefix-") EmbeddableType prefixedEmbeddableValue;
}
static class EmbeddableType {
String stringValue;
@org.springframework.data.mongodb.core.mapping.Field("with-at-field-annotation") //
String atFieldAnnotatedValue;
}
}

View File

@@ -51,6 +51,7 @@ import org.springframework.core.convert.ConverterNotFoundException;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.PersistenceConstructor;
import org.springframework.data.annotation.Transient;
import org.springframework.data.annotation.TypeAlias;
import org.springframework.data.convert.CustomConversions;
import org.springframework.data.convert.ReadingConverter;
@@ -71,6 +72,7 @@ import org.springframework.data.mongodb.core.convert.DocumentAccessorUnitTests.P
import org.springframework.data.mongodb.core.convert.MappingMongoConverterUnitTests.ClassWithMapUsingEnumAsKey.FooBarEnum;
import org.springframework.data.mongodb.core.geo.Sphere;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Embedded;
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.mapping.FieldType;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
@@ -2179,6 +2181,197 @@ public class MappingMongoConverterUnitTests {
assertThat(((LinkedHashMap) result.get("cluster")).get("_id")).isEqualTo(100L);
}
@Test // DATAMONGO-1902
void writeFlattensEmbeddedType() {
WithNullableEmbedded source = new WithNullableEmbedded();
source.id = "id-1";
source.embeddableValue = new EmbeddableType();
source.embeddableValue.listValue = Arrays.asList("list-val-1", "list-val-2");
source.embeddableValue.stringValue = "string-val";
source.embeddableValue.transientValue = "must-not-be-written";
source.embeddableValue.atFieldAnnotatedValue = "@Field";
org.bson.Document target = new org.bson.Document();
converter.write(source, target);
assertThat(target).containsEntry("_id", "id-1") //
.containsEntry("stringValue", "string-val") //
.containsEntry("listValue", Arrays.asList("list-val-1", "list-val-2")) //
.containsEntry("with-at-field-annotation", "@Field") //
.doesNotContainKey("embeddableValue") //
.doesNotContainKey("transientValue");
}
@Test // DATAMONGO-1902
void writePrefixesEmbeddedType() {
WithPrefixedNullableEmbedded source = new WithPrefixedNullableEmbedded();
source.id = "id-1";
source.embeddableValue = new EmbeddableType();
source.embeddableValue.listValue = Arrays.asList("list-val-1", "list-val-2");
source.embeddableValue.stringValue = "string-val";
source.embeddableValue.transientValue = "must-not-be-written";
source.embeddableValue.atFieldAnnotatedValue = "@Field";
org.bson.Document target = new org.bson.Document();
converter.write(source, target);
assertThat(target).containsEntry("_id", "id-1") //
.containsEntry("prefix-stringValue", "string-val") //
.containsEntry("prefix-listValue", Arrays.asList("list-val-1", "list-val-2")) //
.containsEntry("prefix-with-at-field-annotation", "@Field") //
.doesNotContainKey("embeddableValue") //
.doesNotContainKey("transientValue") //
.doesNotContainKey("prefix-transientValue");
}
@Test // DATAMONGO-1902
void writeNullEmbeddedType() {
WithNullableEmbedded source = new WithNullableEmbedded();
source.id = "id-1";
source.embeddableValue = null;
org.bson.Document target = new org.bson.Document();
converter.write(source, target);
assertThat(target) //
.doesNotContainKey("prefix-stringValue").doesNotContainKey("prefix-listValue")
.doesNotContainKey("embeddableValue");
}
@Test // DATAMONGO-1902
void writeDeepNestedEmbeddedType() {
WrapperAroundWithEmbedded source = new WrapperAroundWithEmbedded();
source.someValue = "root-level-value";
source.nullableEmbedded = new WithNullableEmbedded();
source.nullableEmbedded.id = "id-1";
source.nullableEmbedded.embeddableValue = new EmbeddableType();
source.nullableEmbedded.embeddableValue.listValue = Arrays.asList("list-val-1", "list-val-2");
source.nullableEmbedded.embeddableValue.stringValue = "string-val";
source.nullableEmbedded.embeddableValue.transientValue = "must-not-be-written";
source.nullableEmbedded.embeddableValue.atFieldAnnotatedValue = "@Field";
org.bson.Document target = new org.bson.Document();
converter.write(source, target);
assertThat(target).containsEntry("someValue", "root-level-value") //
.containsEntry("nullableEmbedded", new org.bson.Document("_id", "id-1").append("stringValue", "string-val") //
.append("listValue", Arrays.asList("list-val-1", "list-val-2")) //
.append("with-at-field-annotation", "@Field")); //
}
@Test // DATAMONGO-1902
void readEmbeddedType() {
org.bson.Document source = new org.bson.Document("_id", "id-1") //
.append("stringValue", "string-val") //
.append("listValue", Arrays.asList("list-val-1", "list-val-2")) //
.append("with-at-field-annotation", "@Field");
EmbeddableType embeddableValue = new EmbeddableType();
embeddableValue.stringValue = "string-val";
embeddableValue.listValue = Arrays.asList("list-val-1", "list-val-2");
embeddableValue.atFieldAnnotatedValue = "@Field";
WithNullableEmbedded target = converter.read(WithNullableEmbedded.class, source);
assertThat(target.embeddableValue).isEqualTo(embeddableValue);
}
@Test // DATAMONGO-1902
void readPrefixedEmbeddedType() {
org.bson.Document source = new org.bson.Document("_id", "id-1") //
.append("prefix-stringValue", "string-val") //
.append("prefix-listValue", Arrays.asList("list-val-1", "list-val-2")) //
.append("prefix-with-at-field-annotation", "@Field");
EmbeddableType embeddableValue = new EmbeddableType();
embeddableValue.stringValue = "string-val";
embeddableValue.listValue = Arrays.asList("list-val-1", "list-val-2");
embeddableValue.atFieldAnnotatedValue = "@Field";
WithPrefixedNullableEmbedded target = converter.read(WithPrefixedNullableEmbedded.class, source);
assertThat(target.embeddableValue).isEqualTo(embeddableValue);
}
@Test // DATAMONGO-1902
void readNullableEmbeddedTypeWhenSourceDoesNotContainValues() {
org.bson.Document source = new org.bson.Document("_id", "id-1");
WithNullableEmbedded target = converter.read(WithNullableEmbedded.class, source);
assertThat(target.embeddableValue).isNull();
}
@Test // DATAMONGO-1902
void readEmptyEmbeddedTypeWhenSourceDoesNotContainValues() {
org.bson.Document source = new org.bson.Document("_id", "id-1");
WithEmptyEmbeddedType target = converter.read(WithEmptyEmbeddedType.class, source);
assertThat(target.embeddableValue).isNotNull();
}
@Test // DATAMONGO-1902
void readDeepNestedEmbeddedType() {
org.bson.Document source = new org.bson.Document("someValue", "root-level-value").append("nullableEmbedded",
new org.bson.Document("_id", "id-1").append("stringValue", "string-val") //
.append("listValue", Arrays.asList("list-val-1", "list-val-2")) //
.append("with-at-field-annotation", "@Field"));
WrapperAroundWithEmbedded target = converter.read(WrapperAroundWithEmbedded.class, source);
EmbeddableType embeddableValue = new EmbeddableType();
embeddableValue.stringValue = "string-val";
embeddableValue.listValue = Arrays.asList("list-val-1", "list-val-2");
embeddableValue.atFieldAnnotatedValue = "@Field";
assertThat(target.someValue).isEqualTo("root-level-value");
assertThat(target.nullableEmbedded).isNotNull();
assertThat(target.nullableEmbedded.embeddableValue).isEqualTo(embeddableValue);
}
@Test // DATAMONGO-1902
void readEmbeddedTypeWithComplexValue() {
org.bson.Document source = new org.bson.Document("_id", "id-1").append("address",
new org.bson.Document("street", "1007 Mountain Drive").append("city", "Gotham"));
WithNullableEmbedded target = converter.read(WithNullableEmbedded.class, source);
Address expected = new Address();
expected.city = "Gotham";
expected.street = "1007 Mountain Drive";
assertThat(target.embeddableValue.address) //
.isEqualTo(expected);
}
@Test // DATAMONGO-1902
void writeEmbeddedTypeWithComplexValue() {
WithNullableEmbedded source = new WithNullableEmbedded();
source.id = "id-1";
source.embeddableValue = new EmbeddableType();
source.embeddableValue.address = new Address();
source.embeddableValue.address.city = "Gotham";
source.embeddableValue.address.street = "1007 Mountain Drive";
org.bson.Document target = new org.bson.Document();
converter.write(source, target);
assertThat(target) //
.containsEntry("address", new org.bson.Document("street", "1007 Mountain Drive").append("city", "Gotham")) //
.doesNotContainKey("street") //
.doesNotContainKey("address.street") //
.doesNotContainKey("city") //
.doesNotContainKey("address.city");
}
static class GenericType<T> {
T content;
}
@@ -2208,6 +2401,7 @@ public class MappingMongoConverterUnitTests {
}
@EqualsAndHashCode
static class Address implements InterfaceType {
String street;
String city;
@@ -2641,6 +2835,50 @@ public class MappingMongoConverterUnitTests {
Date dateAsObjectId;
}
static class WrapperAroundWithEmbedded {
String someValue;
WithNullableEmbedded nullableEmbedded;
WithEmptyEmbeddedType emptyEmbedded;
WithPrefixedNullableEmbedded prefixedEmbedded;
}
static class WithNullableEmbedded {
String id;
@Embedded.Nullable EmbeddableType embeddableValue;
}
static class WithPrefixedNullableEmbedded {
String id;
@Embedded.Nullable("prefix-") EmbeddableType embeddableValue;
}
static class WithEmptyEmbeddedType {
String id;
@Embedded.Empty EmbeddableType embeddableValue;
}
@EqualsAndHashCode
static class EmbeddableType {
String stringValue;
List<String> listValue;
@Field("with-at-field-annotation") //
String atFieldAnnotatedValue;
@Transient //
String transientValue;
Address address;
}
static class ReturningAfterConvertCallback implements AfterConvertCallback<Person> {
@Override
@@ -2649,4 +2887,5 @@ public class MappingMongoConverterUnitTests {
return entity;
}
}
}

View File

@@ -32,7 +32,6 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.annotation.Id;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.ExampleMatcher;
@@ -41,8 +40,10 @@ import org.springframework.data.geo.Point;
import org.springframework.data.mongodb.MongoDatabaseFactory;
import org.springframework.data.mongodb.core.convert.QueryMapperUnitTests.ClassWithGeoTypes;
import org.springframework.data.mongodb.core.convert.QueryMapperUnitTests.WithDBRef;
import org.springframework.data.mongodb.core.index.Indexed;
import org.springframework.data.mongodb.core.mapping.DBRef;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Embedded;
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.query.UntypedExampleMatcher;
@@ -454,6 +455,60 @@ public class MongoExampleMapperUnitTests {
assertThat(document).doesNotContainKey("_class");
}
@Test // DATAMONGO-1902
void mapsEmbeddedType() {
WithEmbedded probe = new WithEmbedded();
probe.embeddableType = new EmbeddableType();
probe.embeddableType.atFieldAnnotatedValue = "@Field";
probe.embeddableType.stringValue = "string-value";
org.bson.Document document = mapper.getMappedExample(Example.of(probe, UntypedExampleMatcher.matching()));
assertThat(document).containsEntry("stringValue", "string-value").containsEntry("with-at-field-annotation",
"@Field");
}
@Test // DATAMONGO-1902
void mapsPrefixedEmbeddedType() {
WithEmbedded probe = new WithEmbedded();
probe.prefixedEmbeddableValue = new EmbeddableType();
probe.prefixedEmbeddableValue.atFieldAnnotatedValue = "@Field";
probe.prefixedEmbeddableValue.stringValue = "string-value";
org.bson.Document document = mapper.getMappedExample(Example.of(probe, UntypedExampleMatcher.matching()));
assertThat(document).containsEntry("prefix-stringValue", "string-value")
.containsEntry("prefix-with-at-field-annotation", "@Field");
}
@Test // DATAMONGO-1902
void mapsNestedEmbeddedType() {
WrapperAroundWithEmbedded probe = new WrapperAroundWithEmbedded();
probe.withEmbedded = new WithEmbedded();
probe.withEmbedded.embeddableType = new EmbeddableType();
probe.withEmbedded.embeddableType.atFieldAnnotatedValue = "@Field";
probe.withEmbedded.embeddableType.stringValue = "string-value";
org.bson.Document document = mapper.getMappedExample(Example.of(probe, UntypedExampleMatcher.matching()));
assertThat(document).containsEntry("withEmbedded.stringValue", "string-value")
.containsEntry("withEmbedded.with-at-field-annotation", "@Field");
}
@Test // DATAMONGO-1902
void mapsNestedPrefixedEmbeddedType() {
WrapperAroundWithEmbedded probe = new WrapperAroundWithEmbedded();
probe.withEmbedded = new WithEmbedded();
probe.withEmbedded.prefixedEmbeddableValue = new EmbeddableType();
probe.withEmbedded.prefixedEmbeddableValue.atFieldAnnotatedValue = "@Field";
probe.withEmbedded.prefixedEmbeddableValue.stringValue = "string-value";
org.bson.Document document = mapper.getMappedExample(Example.of(probe, UntypedExampleMatcher.matching()));
assertThat(document).containsEntry("withEmbedded.prefix-stringValue", "string-value")
.containsEntry("withEmbedded.prefix-with-at-field-annotation", "@Field");
}
static class FlatDocument {
@Id String id;
@@ -481,4 +536,29 @@ public class MongoExampleMapperUnitTests {
@Id String id;
String value;
}
@Document
static class WrapperAroundWithEmbedded {
String id;
WithEmbedded withEmbedded;
}
@Document
static class WithEmbedded {
String id;
@Embedded.Nullable EmbeddableType embeddableType;
@Embedded.Nullable("prefix-") EmbeddableType prefixedEmbeddableValue;
}
static class EmbeddableType {
@Indexed String stringValue;
@Indexed //
@Field("with-at-field-annotation") //
String atFieldAnnotatedValue;
}
}

View File

@@ -37,6 +37,7 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Transient;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.geo.Point;
@@ -45,9 +46,9 @@ import org.springframework.data.mongodb.core.DocumentTestUtils;
import org.springframework.data.mongodb.core.Person;
import org.springframework.data.mongodb.core.geo.GeoJsonPoint;
import org.springframework.data.mongodb.core.geo.GeoJsonPolygon;
import org.springframework.data.mongodb.core.mapping.BasicMongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.DBRef;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Embedded;
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.mapping.FieldType;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
@@ -389,7 +390,7 @@ public class QueryMapperUnitTests {
Query query = query(where("reference").exists(false));
BasicMongoPersistentEntity<?> entity = context.getRequiredPersistentEntity(WithDBRef.class);
MongoPersistentEntity<?> entity = context.getRequiredPersistentEntity(WithDBRef.class);
org.bson.Document mappedObject = mapper.getMappedObject(query.getQueryObject(), entity);
org.bson.Document reference = getAsDocument(mappedObject, "reference");
@@ -405,7 +406,7 @@ public class QueryMapperUnitTests {
Query query = query(where("someString").is("foo").andOperator(where("reference").in(reference)));
BasicMongoPersistentEntity<?> entity = context.getRequiredPersistentEntity(WithDBRef.class);
MongoPersistentEntity<?> entity = context.getRequiredPersistentEntity(WithDBRef.class);
org.bson.Document mappedObject = mapper.getMappedObject(query.getQueryObject(), entity);
assertThat(mappedObject).containsEntry("someString", "foo");
@@ -446,7 +447,7 @@ public class QueryMapperUnitTests {
Query query = query(where("someString").is("foo"));
query.fields().exclude("reference");
BasicMongoPersistentEntity<?> entity = context.getRequiredPersistentEntity(WithDBRef.class);
MongoPersistentEntity<?> entity = context.getRequiredPersistentEntity(WithDBRef.class);
org.bson.Document queryResult = mapper.getMappedObject(query.getQueryObject(), entity);
org.bson.Document fieldsResult = mapper.getMappedObject(query.getFieldsObject(), entity);
@@ -457,7 +458,7 @@ public class QueryMapperUnitTests {
@Test // DATAMONGO-686
void queryMapperShouldNotChangeStateInGivenQueryObjectWhenIdConstrainedByInList() {
BasicMongoPersistentEntity<?> persistentEntity = context.getRequiredPersistentEntity(Sample.class);
MongoPersistentEntity<?> persistentEntity = context.getRequiredPersistentEntity(Sample.class);
String idPropertyName = persistentEntity.getIdProperty().getName();
org.bson.Document queryObject = query(where(idPropertyName).in("42")).getQueryObject();
@@ -515,7 +516,7 @@ public class QueryMapperUnitTests {
@Test // DATAMONGO-773
void queryMapperShouldBeAbleToProcessQueriesThatIncludeDbRefFields() {
BasicMongoPersistentEntity<?> persistentEntity = context.getRequiredPersistentEntity(WithDBRef.class);
MongoPersistentEntity<?> persistentEntity = context.getRequiredPersistentEntity(WithDBRef.class);
Query qry = query(where("someString").is("abc"));
qry.fields().include("reference");
@@ -781,7 +782,8 @@ public class QueryMapperUnitTests {
Query query = query(byExample(probe).and("listOfItems").exists(true));
org.bson.Document document = mapper.getMappedObject(query.getQueryObject(), context.getPersistentEntity(Foo.class));
assertThat(document).containsEntry("embedded\\._id", "conflux").containsEntry("my_items", new org.bson.Document("$exists", true));
assertThat(document).containsEntry("embedded\\._id", "conflux").containsEntry("my_items",
new org.bson.Document("$exists", true));
}
@Test // DATAMONGO-1988
@@ -1011,6 +1013,184 @@ public class QueryMapperUnitTests {
assertThat(target).isEqualTo(org.bson.Document.parse("{\"$text\" : { \"$search\" : \"test\" }}"));
}
@Test // DATAMONGO-1902
void rendersQueryOnEmbeddedObjectCorrectly() {
EmbeddableType embeddableType = new EmbeddableType();
embeddableType.stringValue = "test";
Query source = query(Criteria.where("embeddableValue").is(embeddableType));
org.bson.Document target = mapper.getMappedObject(source.getQueryObject(),
context.getPersistentEntity(WithEmbedded.class));
assertThat(target).isEqualTo(new org.bson.Document("stringValue", "test"));
}
@Test // DATAMONGO-1902
void rendersQueryOnEmbeddedCorrectly() {
Query source = query(Criteria.where("embeddableValue.stringValue").is("test"));
org.bson.Document target = mapper.getMappedObject(source.getQueryObject(),
context.getPersistentEntity(WithEmbedded.class));
assertThat(target).isEqualTo(new org.bson.Document("stringValue", "test"));
}
@Test // DATAMONGO-1902
void rendersQueryOnPrefixedEmbeddedCorrectly() {
Query source = query(Criteria.where("embeddableValue.stringValue").is("test"));
org.bson.Document target = mapper.getMappedObject(source.getQueryObject(),
context.getPersistentEntity(WithPrefixedEmbedded.class));
assertThat(target).isEqualTo(new org.bson.Document("prefix-stringValue", "test"));
}
@Test // DATAMONGO-1902
void rendersQueryOnNestedEmbeddedObjectCorrectly() {
EmbeddableType embeddableType = new EmbeddableType();
embeddableType.stringValue = "test";
Query source = query(Criteria.where("withEmbedded.embeddableValue").is(embeddableType));
org.bson.Document target = mapper.getMappedObject(source.getQueryObject(),
context.getPersistentEntity(WrapperAroundWithEmbedded.class));
assertThat(target).isEqualTo(new org.bson.Document("withEmbedded", new org.bson.Document("stringValue", "test")));
}
@Test // DATAMONGO-1902
void rendersQueryOnNestedPrefixedEmbeddedObjectCorrectly() {
EmbeddableType embeddableType = new EmbeddableType();
embeddableType.stringValue = "test";
Query source = query(Criteria.where("withPrefixedEmbedded.embeddableValue").is(embeddableType));
org.bson.Document target = mapper.getMappedObject(source.getQueryObject(),
context.getPersistentEntity(WrapperAroundWithEmbedded.class));
assertThat(target)
.isEqualTo(new org.bson.Document("withPrefixedEmbedded", new org.bson.Document("prefix-stringValue", "test")));
}
@Test // DATAMONGO-1902
void rendersQueryOnNestedEmbeddedCorrectly() {
Query source = query(Criteria.where("withEmbedded.embeddableValue.stringValue").is("test"));
org.bson.Document target = mapper.getMappedObject(source.getQueryObject(),
context.getPersistentEntity(WrapperAroundWithEmbedded.class));
assertThat(target).isEqualTo(new org.bson.Document("withEmbedded.stringValue", "test"));
}
@Test // DATAMONGO-1902
void rendersQueryOnNestedPrefixedEmbeddedCorrectly() {
Query source = query(Criteria.where("withPrefixedEmbedded.embeddableValue.stringValue").is("test"));
org.bson.Document target = mapper.getMappedObject(source.getQueryObject(),
context.getPersistentEntity(WrapperAroundWithEmbedded.class));
assertThat(target).isEqualTo(new org.bson.Document("withPrefixedEmbedded.prefix-stringValue", "test"));
}
@Test // DATAMONGO-1902
void sortByEmbeddableIsEmpty() {
Query query = new Query().with(Sort.by("embeddableValue"));
org.bson.Document document = mapper.getMappedSort(query.getSortObject(),
context.getPersistentEntity(WithEmbedded.class));
assertThat(document).isEqualTo(
new org.bson.Document("stringValue", 1).append("listValue", 1).append("with-at-field-annotation", 1));
}
@Test // DATAMONGO-1902
void sortByEmbeddableValue() {
// atFieldAnnotatedValue
Query query = new Query().with(Sort.by("embeddableValue.stringValue"));
org.bson.Document document = mapper.getMappedSort(query.getSortObject(),
context.getPersistentEntity(WithEmbedded.class));
assertThat(document).isEqualTo(new org.bson.Document("stringValue", 1));
}
@Test // DATAMONGO-1902
void sortByEmbeddableValueWithFieldAnnotation() {
Query query = new Query().with(Sort.by("embeddableValue.atFieldAnnotatedValue"));
org.bson.Document document = mapper.getMappedSort(query.getSortObject(),
context.getPersistentEntity(WithEmbedded.class));
assertThat(document).isEqualTo(new org.bson.Document("with-at-field-annotation", 1));
}
@Test // DATAMONGO-1902
void sortByPrefixedEmbeddableValueWithFieldAnnotation() {
Query query = new Query().with(Sort.by("embeddableValue.atFieldAnnotatedValue"));
org.bson.Document document = mapper.getMappedSort(query.getSortObject(),
context.getPersistentEntity(WithPrefixedEmbedded.class));
assertThat(document).isEqualTo(new org.bson.Document("prefix-with-at-field-annotation", 1));
}
@Test // DATAMONGO-1902
void sortByNestedEmbeddableValueWithFieldAnnotation() {
Query query = new Query().with(Sort.by("withEmbedded.embeddableValue.atFieldAnnotatedValue"));
org.bson.Document document = mapper.getMappedSort(query.getSortObject(),
context.getPersistentEntity(WrapperAroundWithEmbedded.class));
assertThat(document).isEqualTo(new org.bson.Document("withEmbedded.with-at-field-annotation", 1));
}
@Test // DATAMONGO-1902
void sortByNestedPrefixedEmbeddableValueWithFieldAnnotation() {
Query query = new Query().with(Sort.by("withPrefixedEmbedded.embeddableValue.atFieldAnnotatedValue"));
org.bson.Document document = mapper.getMappedSort(query.getSortObject(),
context.getPersistentEntity(WrapperAroundWithEmbedded.class));
assertThat(document).isEqualTo(new org.bson.Document("withPrefixedEmbedded.prefix-with-at-field-annotation", 1));
}
@Test // DATAMONGO-1902
void projectOnEmbeddableUsesFields() {
Query query = new Query();
query.fields().include("embeddableValue");
org.bson.Document document = mapper.getMappedFields(query.getFieldsObject(),
context.getPersistentEntity(WithEmbedded.class));
assertThat(document).isEqualTo(
new org.bson.Document("stringValue", 1).append("listValue", 1).append("with-at-field-annotation", 1));
}
@Test // DATAMONGO-1902
void projectOnEmbeddableValue() {
Query query = new Query();
query.fields().include("embeddableValue.stringValue");
org.bson.Document document = mapper.getMappedFields(query.getFieldsObject(),
context.getPersistentEntity(WithEmbedded.class));
assertThat(document).isEqualTo(new org.bson.Document("stringValue", 1));
}
class WithDeepArrayNesting {
List<WithNestedArray> level0;
@@ -1194,4 +1374,38 @@ public class QueryMapperUnitTests {
this.value = value;
}
}
static class WrapperAroundWithEmbedded {
String someValue;
WithEmbedded withEmbedded;
WithPrefixedEmbedded withPrefixedEmbedded;
}
static class WithEmbedded {
String id;
@Embedded.Nullable EmbeddableType embeddableValue;
}
static class WithPrefixedEmbedded {
String id;
@Embedded.Nullable("prefix-") EmbeddableType embeddableValue;
}
static class EmbeddableType {
String stringValue;
List<String> listValue;
@Field("with-at-field-annotation") //
String atFieldAnnotatedValue;
@Transient //
String transientValue;
}
}

View File

@@ -37,9 +37,9 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Transient;
import org.springframework.data.convert.CustomConversions;
import org.springframework.data.convert.WritingConverter;
import org.springframework.data.domain.Sort;
@@ -48,6 +48,7 @@ import org.springframework.data.domain.Sort.Order;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mongodb.MongoDatabaseFactory;
import org.springframework.data.mongodb.core.DocumentTestUtils;
import org.springframework.data.mongodb.core.mapping.Embedded;
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.query.Criteria;
@@ -1089,6 +1090,76 @@ class UpdateMapperUnitTests {
assertThat(mappedUpdate).isEqualTo(new Document("$set", new Document("aliased.$[element].value", 10)));
}
@Test // DATAMONGO-1902
void mappingShouldConsiderValueOfEmbeddedType() {
Update update = new Update().set("embeddableValue.stringValue", "updated");
Document mappedUpdate = mapper.getMappedObject(update.getUpdateObject(),
context.getPersistentEntity(WithEmbedded.class));
assertThat(mappedUpdate).isEqualTo(new Document("$set", new Document("stringValue", "updated")));
}
@Test // DATAMONGO-1902
void mappingShouldConsiderEmbeddedType() {
EmbeddableType embeddableType = new EmbeddableType();
embeddableType.stringValue = "updated";
embeddableType.listValue = Arrays.asList("val-1", "val-2");
Update update = new Update().set("embeddableValue", embeddableType);
Document mappedUpdate = mapper.getMappedObject(update.getUpdateObject(),
context.getPersistentEntity(WithEmbedded.class));
assertThat(mappedUpdate).isEqualTo(new Document("$set",
new Document("stringValue", "updated").append("listValue", Arrays.asList("val-1", "val-2"))));
}
@Test // DATAMONGO-1902
void mappingShouldConsiderValueOfPrefixedEmbeddedType() {
Update update = new Update().set("embeddableValue.stringValue", "updated");
Document mappedUpdate = mapper.getMappedObject(update.getUpdateObject(),
context.getPersistentEntity(WithPrefixedEmbedded.class));
assertThat(mappedUpdate).isEqualTo(new Document("$set", new Document("prefix-stringValue", "updated")));
}
@Test // DATAMONGO-1902
void mappingShouldConsiderPrefixedEmbeddedType() {
EmbeddableType embeddableType = new EmbeddableType();
embeddableType.stringValue = "updated";
embeddableType.listValue = Arrays.asList("val-1", "val-2");
Update update = new Update().set("embeddableValue", embeddableType);
Document mappedUpdate = mapper.getMappedObject(update.getUpdateObject(),
context.getPersistentEntity(WithPrefixedEmbedded.class));
assertThat(mappedUpdate).isEqualTo(new Document("$set",
new Document("prefix-stringValue", "updated").append("prefix-listValue", Arrays.asList("val-1", "val-2"))));
}
@Test // DATAMONGO-1902
void mappingShouldConsiderNestedPrefixedEmbeddedType() {
EmbeddableType embeddableType = new EmbeddableType();
embeddableType.stringValue = "updated";
embeddableType.listValue = Arrays.asList("val-1", "val-2");
Update update = new Update().set("withPrefixedEmbedded.embeddableValue", embeddableType);
Document mappedUpdate = mapper.getMappedObject(update.getUpdateObject(),
context.getPersistentEntity(WrapperAroundWithEmbedded.class));
System.out.println("mappedUpdate.toJson(): " + mappedUpdate.toJson());
assertThat(mappedUpdate).isEqualTo(new Document("$set", new Document("withPrefixedEmbedded",
new Document("prefix-stringValue", "updated").append("prefix-listValue", Arrays.asList("val-1", "val-2")))));
}
static class DomainTypeWrappingConcreteyTypeHavingListOfInterfaceTypeAttributes {
ListModelWrapper concreteTypeWithListAttributeOfInterfaceType;
}
@@ -1418,4 +1489,37 @@ class UpdateMapperUnitTests {
}
static class WrapperAroundWithEmbedded {
String someValue;
WithEmbedded withEmbedded;
WithPrefixedEmbedded withPrefixedEmbedded;
}
static class WithEmbedded {
String id;
@Embedded.Nullable EmbeddableType embeddableValue;
}
static class WithPrefixedEmbedded {
String id;
@Embedded.Nullable("prefix-") EmbeddableType embeddableValue;
}
static class EmbeddableType {
String stringValue;
List<String> listValue;
@Field("with-at-field-annotation") //
String atFieldAnnotatedValue;
@Transient //
String transientValue;
}
}

View File

@@ -22,7 +22,8 @@ import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.Collections;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.List;
import org.junit.Test;
@@ -30,6 +31,7 @@ import org.junit.runner.RunWith;
import org.junit.runners.Suite;
import org.junit.runners.Suite.SuiteClasses;
import org.springframework.core.annotation.AliasFor;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.annotation.Id;
import org.springframework.data.geo.Point;
import org.springframework.data.mongodb.core.DocumentTestUtils;
@@ -42,6 +44,7 @@ import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexRes
import org.springframework.data.mongodb.core.mapping.BasicMongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.DBRef;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Embedded;
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.mapping.Language;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
@@ -1282,6 +1285,44 @@ public class MongoPersistentEntityIndexResolverUnitTests {
});
}
@Test // DATAMONGO-1902
public void resolvedIndexOnEmbeddedType() {
List<IndexDefinitionHolder> 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<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
WrapperAroundWithEmbedded.class, WithEmbedded.class, EmbeddableType.class);
assertThat(indexDefinitions).hasSize(2);
assertThat(indexDefinitions.get(0)).satisfies(it -> {
assertThat(it.getIndexKeys()).containsEntry("withEmbedded.stringValue", 1);
});
assertThat(indexDefinitions.get(1)).satisfies(it -> {
assertThat(it.getIndexKeys()).containsEntry("withEmbedded.with-at-field-annotation", 1);
});
}
@Test // DATAMONGO-1902
public void errorsOnIndexOnEmbedded() {
assertThatExceptionOfType(InvalidDataAccessApiUsageException.class)
.isThrownBy(() -> prepareMappingContextAndResolveIndexForType(InvalidIndexOnEmbedded.class));
}
@Document
class MixedIndexRoot {
@@ -1472,6 +1513,41 @@ public class MongoPersistentEntityIndexResolverUnitTests {
AlternatePathToNoCycleButIndenticallNamedPropertiesDeeplyNestedDocument path2;
}
@Document
static class WrapperAroundWithEmbedded {
String id;
WithEmbedded withEmbedded;
}
@Document
static class WithEmbedded {
String id;
@Embedded.Nullable EmbeddableType embeddableType;
}
@Document
class InvalidIndexOnEmbedded {
@Indexed //
@Embedded.Nullable //
EmbeddableType embeddableType;
}
static class EmbeddableType {
@Indexed String stringValue;
List<String> listValue;
@Indexed //
@Field("with-at-field-annotation") //
String atFieldAnnotatedValue;
}
static class AlternatePathToNoCycleButIndenticallNamedPropertiesDeeplyNestedDocument {
NoCycleButIndenticallNamedPropertiesDeeplyNested propertyWithIndexedStructure;
}
@@ -1521,17 +1597,17 @@ public class MongoPersistentEntityIndexResolverUnitTests {
}
}
private static List<IndexDefinitionHolder> prepareMappingContextAndResolveIndexForType(Class<?> type) {
private static List<IndexDefinitionHolder> 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;

View File

@@ -30,10 +30,11 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.context.ApplicationContext;
import org.springframework.core.annotation.AliasFor;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mongodb.core.index.Indexed;
import org.springframework.data.mongodb.core.query.Collation;
import org.springframework.data.spel.ExtensionAwareEvaluationContextProvider;
import org.springframework.data.spel.spi.EvaluationContextExtension;
@@ -63,8 +64,7 @@ public class BasicMongoPersistentEntityUnitTests {
@Test
void evaluatesSpELExpression() {
MongoPersistentEntity<Company> entity = new BasicMongoPersistentEntity<>(
ClassTypeInformation.from(Company.class));
MongoPersistentEntity<Company> entity = new BasicMongoPersistentEntity<>(ClassTypeInformation.from(Company.class));
assertThat(entity.getCollection()).isEqualTo("35");
}
@@ -364,16 +364,13 @@ public class BasicMongoPersistentEntityUnitTests {
class WithDocumentCollation {}
@Sharded
private
class WithDefaultShardKey {}
private class WithDefaultShardKey {}
@Sharded("country")
private
class WithSingleShardKey {}
private class WithSingleShardKey {}
@Sharded({ "country", "userid" })
private
class WithMultiShardKey {}
private class WithMultiShardKey {}
static class SampleExtension implements EvaluationContextExtension {

View File

@@ -187,7 +187,7 @@ public class BasicMongoPersistentPropertyUnitTests {
public void honorsFieldOrderWhenIteratingOverProperties() {
MongoMappingContext context = new MongoMappingContext();
BasicMongoPersistentEntity<?> entity = context.getPersistentEntity(Sample.class);
MongoPersistentEntity<?> entity = context.getPersistentEntity(Sample.class);
List<String> properties = new ArrayList<>();

View File

@@ -111,7 +111,7 @@ public class MongoMappingContextUnitTests {
void mappingContextShouldAcceptClassWithImplicitIdProperty() {
MongoMappingContext context = new MongoMappingContext();
BasicMongoPersistentEntity<?> pe = context.getRequiredPersistentEntity(ClassWithImplicitId.class);
MongoPersistentEntity<?> pe = context.getRequiredPersistentEntity(ClassWithImplicitId.class);
assertThat(pe).isNotNull();
assertThat(pe.isIdProperty(pe.getRequiredPersistentProperty("id"))).isTrue();
@@ -121,7 +121,7 @@ public class MongoMappingContextUnitTests {
void mappingContextShouldAcceptClassWithExplicitIdProperty() {
MongoMappingContext context = new MongoMappingContext();
BasicMongoPersistentEntity<?> pe = context.getRequiredPersistentEntity(ClassWithExplicitId.class);
MongoPersistentEntity<?> pe = context.getRequiredPersistentEntity(ClassWithExplicitId.class);
assertThat(pe).isNotNull();
assertThat(pe.isIdProperty(pe.getRequiredPersistentProperty("myId"))).isTrue();
@@ -131,7 +131,7 @@ public class MongoMappingContextUnitTests {
void mappingContextShouldAcceptClassWithExplicitAndImplicitIdPropertyByGivingPrecedenceToExplicitIdProperty() {
MongoMappingContext context = new MongoMappingContext();
BasicMongoPersistentEntity<?> pe = context.getRequiredPersistentEntity(ClassWithExplicitIdAndImplicitId.class);
MongoPersistentEntity<?> pe = context.getRequiredPersistentEntity(ClassWithExplicitIdAndImplicitId.class);
assertThat(pe).isNotNull();
}
@@ -166,7 +166,7 @@ public class MongoMappingContextUnitTests {
MongoMappingContext context = new MongoMappingContext();
BasicMongoPersistentEntity<?> entity = context.getRequiredPersistentEntity(ClassWithChronoUnit.class);
MongoPersistentEntity<?> entity = context.getRequiredPersistentEntity(ClassWithChronoUnit.class);
assertThat(entity.getPersistentProperty("unit").isEntity()).isFalse();
assertThat(context.hasPersistentEntityFor(ChronoUnit.class)).isFalse();

View File

@@ -1361,6 +1361,41 @@ public abstract class AbstractPersonRepositoryIntegrationTests {
void spelExpressionArgumentsGetReevaluatedOnEveryInvocation() {
assertThat(repository.findWithSpelByFirstnameForSpELExpressionWithParameterIndexOnly("Dave")).containsExactly(dave);
assertThat(repository.findWithSpelByFirstnameForSpELExpressionWithParameterIndexOnly("Carter")).containsExactly(carter);
assertThat(repository.findWithSpelByFirstnameForSpELExpressionWithParameterIndexOnly("Carter"))
.containsExactly(carter);
}
@Test // DATAMONGO-1902
void findByValueInsideEmbedded() {
Person bart = new Person("bart", "simpson");
User user = new User();
user.setUsername("bartman");
user.setId("84r1m4n");
bart.setEmbeddedUser(user);
operations.save(bart);
List<Person> 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<Person> result = repository.findByEmbeddedUser(user);
assertThat(result).hasSize(1);
assertThat(result.get(0).getId().equals(bart.getId()));
}
}

View File

@@ -27,6 +27,7 @@ import org.springframework.data.mongodb.core.index.GeoSpatialIndexed;
import org.springframework.data.mongodb.core.index.Indexed;
import org.springframework.data.mongodb.core.mapping.DBRef;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Embedded;
import org.springframework.data.mongodb.core.mapping.Field;
/**
@@ -70,6 +71,9 @@ public class Person extends Contact {
Credentials credentials;
@Embedded.Nullable(prefix = "u") //
User embeddedUser;
public Person() {
this(null, null);
@@ -296,6 +300,14 @@ public class Person extends Contact {
return skills;
}
public User getEmbeddedUser() {
return embeddedUser;
}
public void setEmbeddedUser(User embeddedUser) {
this.embeddedUser = embeddedUser;
}
/*
* (non-Javadoc)
*

View File

@@ -401,4 +401,8 @@ public interface PersonRepository extends MongoRepository<Person, String>, Query
Person findPersonByManyArguments(String firstname, String lastname, String email, Integer age, Sex sex,
Date createdAt, List<String> skills, String street, String zipCode, //
String city, UUID uniqueId, String username, String password);
List<Person> findByEmbeddedUserUsername(String username);
List<Person> findByEmbeddedUser(User user);
}

View File

@@ -78,6 +78,7 @@ public class RepositoryIndexCreationIntegrationTests {
assertHasIndexForField(indexInfo, "lastname");
assertHasIndexForField(indexInfo, "firstname");
assertHasIndexForField(indexInfo, "add");
}
private static void assertHasIndexForField(List<IndexInfo> indexInfo, String... fields) {

View File

@@ -66,7 +66,7 @@ class IndexEnsuringQueryCreationListenerUnitTests {
partTreeQuery = mock(PartTreeMongoQuery.class, Answers.RETURNS_MOCKS);
when(partTreeQuery.getTree()).thenReturn(partTree);
when(provider.indexOps(anyString())).thenReturn(indexOperations);
when(provider.indexOps(anyString(), any())).thenReturn(indexOperations);
when(queryMethod.getEntityInformation()).thenReturn(entityInformation);
when(entityInformation.getCollectionName()).thenReturn("persons");
}

View File

@@ -0,0 +1,370 @@
[[embedded-entities]]
== Embedded Types
Embedded entities are used to design value objects in your Java domain model whose properties are flattened out into the MongoDB Document.
[[embedded-entities.mapping]]
=== Embedded Types Mapping
In the example below you see, that `User.name` is annotated with `@Embedded`.
The consequence of this is that all properties of `UserName` are folded into the `user` document.
.Sample Code of embedding objects
====
[source,java]
----
public class User {
@Id
private String userId;
@Embedded(onEmpty = USE_NULL) <1>
UserName name;
}
public class UserName {
private String firstname;
private String lastname;
}
----
[source,json]
----
{
"_id" : "1da2ba06-3ba7",
"firstname" : "Emma",
"lastname" : "Frost"
}
----
<1> When loading the `name` property its value is set to `null` if both `firstname` and `lastname` are either `null` or not present.
By using `onEmpty=USE_EMPTY` an empty `UserName`, with potential `null` value for its properties, will be created.
====
For less verbose embeddable type declarations use `@Embedded.Nullable` and `@Embedded.Empty` instead `@Embedded(onEmpty = USE_NULL)` and `@Embedded(onEmpty = USE_EMPTY)`.
Using those annotations simultaneously set JSR-305 `@javax.annotation.Nonnull` accordingly.
[WARNING]
====
It is possible to use complex types within an embedded object.
However those must not be, nor contain embedded fields themselves.
====
[[embedded-entities.mapping.field-names]]
=== Embedded Types field names
A value object can be embedded multiple times by using the optional `prefix` attribute of the `@Embedded` annotation.
By dosing so the chosen prefix is prepended to each property or `@Field("...")` name in the embedded object.
Please note that values will overwrite each other if multiple properties render to the same field name.
.Sample Code of embedded object with name prefix
====
[source,java]
----
public class User {
@Id
private String userId;
@Embedded.Nullable(prefix = "u") <1>
UserName name;
}
public class UserName {
private String firstname;
private String lastname;
}
----
[source,json]
----
{
"_id" : "a6a805bd-f95f",
"ufirstname" : "Jean",
"ulastname" : "Grey"
}
----
<1> The prefix `u` is prepended to all properties of `UserName`.
====
While combining the `@Field` annotation with `@Embedded` on the very same property does not make sense and therefore leads to an error.
It is a totally valid approach to use `@Field` on any of the embedded types properties.
.Sample Code embedded object with `@Field` annotation
====
[source,java]
----
public class User {
@Id
private String userId;
@Embedded.Nullable(prefix = "u-") <1>
UserName name;
}
public class UserName {
@Field("first-name") <2>
private String firstname;
@Field("last-name")
private String lastname;
}
----
[source,json]
----
{
"_id" : "2647f7b9-89da",
"u-first-name" : "Barbara", <2>
"u-last-name" : "Gordon"
}
----
<1> The prefix `u-` is prepended to all properties of `UserName`.
<2> The field name is the result of the combination of the annotated field name an the chosen prefix.
====
[[embedded-entities.queries]]
=== Query on Embedded Objects
Defining queries on embedded properties is possible on type as well as field level as the provided `Critieria` is matched against the domain type.
Prefixes and potential custom field names will be considered when rendering the actual query.
Use the property name of the embedded object to match against all contained fields as shown in the sample below.
.Query on embedded object
====
[source,java]
----
UserName userName = new UserName("Carol", "Danvers")
Query findByUserName = query(where("name").is(userName));
User user = template.findOne(findByUserName, User.class);
----
[source,json]
----
db.collection.find({
"firstname" : "Carol",
"lastname" : "Danvers"
})
----
====
It is also possible to address any field of the embedded object directly via its property name as shown in the snippet below.
.Query on field of embedded object
====
[source,java]
----
Query findByUserFirstName = query(where("name.firstname").is("Shuri"));
List<User> 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> user = template.findAll(findByUserName.withSort(Sort.by("name.firstname")), User.class);
----
[source,json]
----
db.collection.find({
"lastname" : "Romanoff"
}).sort({ "firstname" : 1 })
----
====
[NOTE]
====
Though possible, using the embedded object itself as sort criteria includes all of its fields in unpredictable order and may result in inaccurate ordering.
====
[[embedded-entities.queries.project]]
==== Project on embedded object
Fields of embedded objects can be subject for projection either as a whole or via single fields as shown in the samples below.
.Project on embedded object.
====
[source,java]
----
Query findByUserLastName = query(where("name.firstname").is("Gamora"));
findByUserLastName.fields().include("name"); <1>
List<User> 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> 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 <<query-by-example.running,Query By Example>> 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<User, String> {
List<User> findByName(UserName username); <1>
List<User> findByNameFirstname(String firstname); <1>
}
----
<1> Matches against all fields of the embedded object.
<2> Matches against the `firstname`.
====
[NOTE]
====
Index creation for embedded objects is suspended even if the repository `create-query-indexes` namespace attribute is set to `true`.
====
[[embedded-entities.update]]
=== Update on Embedded Objects
Embedded objects can be updated as any other object that is part of the domain model.
The mapping layer takes care of flattening embedded structures into their surroundings.
It is possible to update single attributes of the embedded object as well as the entire value as shown in the examples below.
.Update a single field of an embedded object.
====
[source,java]
----
Update update = new Update().set("name.firstname", "Janet");
template.update(User.class).matching(where("id").is("Wasp"))
.apply(update).first()
----
[source,json]
----
db.collection.update({
"_id" : "Wasp"
},
{
"$set" { "firstname" : "Janet" }
},
{ ... }
)
----
====
.Update an embedded object.
====
[source,java]
----
Update update = new Update().set("name", new Name("Janet", "van Dyne"));
template.update(User.class).matching(where("id").is("Wasp"))
.apply(update).first()
----
[source,json]
----
db.collection.update({
"_id" : "Wasp"
},
{
"$set" {
"firstname" : "Janet",
"lastname" : "van Dyne",
}
},
{ ... }
)
----
====
[[embedded-entities.aggregations]]
=== Aggregations on Embedded Objects
The <<mongo.aggregation,Aggregation Framework>> will attempt to map embedded values of typed aggregations.
Please make sure to work with the properties path including the embedded wrapper object when referencing one of it's values.
Other than that no special action is required.
[[embedded-entities.indexes]]
=== Index on Embedded Objects
It is possible to attach the `@Indexed` annotation to properties of an embedded type just as it is done with regular objects.
However it is not possible to use `@Indexed` along with the `@Embedded` annotation on the very same property of an object.
====
[source,java]
----
public class User {
@Id
private String userId;
@Embedded(onEmpty = USE_NULL)
UserName name; <1>
@Indexed <2> // Invalid -> InvalidDataAccessApiUsageException
@Embedded(onEmpty = USE_Empty)
Address address;
}
public class UserName {
private String firstname;
@Indexed
private String lastname; <1>
}
----
<1> Index created for `lastname` in `users` collection.
<2> Invalid `@Indexed` usage along with `@Embedded`
====

View File

@@ -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[]