indexDefinitions = createIndexDefinitionHolderForProperty(propertyDotPath, collection,
+ List indexDefinitions = createIndexDefinitionHolderForProperty(propertyDotPath.toString(), collection,
property);
if (!indexDefinitions.isEmpty()) {
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntity.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntity.java
index 44daa6bcaa..7bf8214aeb 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntity.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntity.java
@@ -164,7 +164,8 @@ public boolean hasTextScoreProperty() {
@Override
public org.springframework.data.mongodb.core.query.Collation getCollation() {
- Object collationValue = collationExpression != null ? collationExpression.getValue(getEvaluationContext(null), String.class)
+ Object collationValue = collationExpression != null
+ ? collationExpression.getValue(getEvaluationContext(null), String.class)
: this.collation;
if (collationValue == null) {
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/Embedded.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/Embedded.java
new file mode 100644
index 0000000000..3eecf56e5c
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/Embedded.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.mongodb.core.mapping;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import javax.annotation.meta.When;
+
+import org.springframework.core.annotation.AliasFor;
+
+/**
+ * The annotation to configure a value object as embedded (flattened out) in the target document.
+ *
+ * Depending on the {@link OnEmpty value} of {@link #onEmpty()} the property is set to {@literal null} or an empty
+ * instance in the case all embedded values are {@literal null} when reading from the result set.
+ *
+ * @author Christoph Strobl
+ * @since 3.2
+ */
+@Documented
+@Retention(value = RetentionPolicy.RUNTIME)
+@Target(value = { ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.METHOD })
+public @interface Embedded {
+
+ /**
+ * Set the load strategy for the embedded object if all contained fields yield {@literal null} values.
+ *
+ * {@link Nullable @Embedded.Nullable} and {@link Empty @Embedded.Empty} offer shortcuts for this.
+ *
+ * @return never {@link} null.
+ */
+ OnEmpty onEmpty();
+
+ /**
+ * @return prefix for columns in the embedded value object. An empty {@link String} by default.
+ */
+ String prefix() default "";
+
+ /**
+ * Load strategy to be used {@link Embedded#onEmpty()}.
+ *
+ * @author Christoph Strobl
+ */
+ enum OnEmpty {
+ USE_NULL, USE_EMPTY
+ }
+
+ /**
+ * Shortcut for a nullable embedded property.
+ *
+ *
+ * @Embedded.Nullable private Address address;
+ *
+ *
+ * as alternative to the more verbose
+ *
+ *
+ * @Embedded(onEmpty = USE_NULL) @javax.annotation.Nonnull(when = When.MAYBE) private Address address;
+ *
+ *
+ * @author Christoph Strobl
+ * @see Embedded#onEmpty()
+ */
+ @Embedded(onEmpty = OnEmpty.USE_NULL)
+ @Documented
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target({ ElementType.FIELD, ElementType.METHOD })
+ @javax.annotation.Nonnull(when = When.MAYBE)
+ @interface Nullable {
+
+ /**
+ * @return prefix for columns in the embedded value object. An empty {@link String} by default.
+ */
+ @AliasFor(annotation = Embedded.class, attribute = "prefix")
+ String prefix() default "";
+
+ /**
+ * @return value for columns in the embedded value object. An empty {@link String} by default.
+ */
+ @AliasFor(annotation = Embedded.class, attribute = "prefix")
+ String value() default "";
+ }
+
+ /**
+ * Shortcut for an empty embedded property.
+ *
+ *
+ * @Embedded.Empty private Address address;
+ *
+ *
+ * as alternative to the more verbose
+ *
+ *
+ * @Embedded(onEmpty = USE_EMPTY) @javax.annotation.Nonnull(when = When.NEVER) private Address address;
+ *
+ *
+ * @author Christoph Strobl
+ * @see Embedded#onEmpty()
+ */
+ @Embedded(onEmpty = OnEmpty.USE_EMPTY)
+ @Documented
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target({ ElementType.FIELD, ElementType.METHOD })
+ @javax.annotation.Nonnull(when = When.NEVER)
+ @interface Empty {
+
+ /**
+ * @return prefix for columns in the embedded value object. An empty {@link String} by default.
+ */
+ @AliasFor(annotation = Embedded.class, attribute = "prefix")
+ String prefix() default "";
+
+ /**
+ * @return value for columns in the embedded value object. An empty {@link String} by default.
+ */
+ @AliasFor(annotation = Embedded.class, attribute = "prefix")
+ String value() default "";
+ }
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/EmbeddedEntityContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/EmbeddedEntityContext.java
new file mode 100644
index 0000000000..c319d0fde7
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/EmbeddedEntityContext.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.mongodb.core.mapping;
+
+/**
+ * @author Christoph Strobl
+ * @since 3.2
+ */
+class EmbeddedEntityContext {
+
+ private final MongoPersistentProperty property;
+
+ public EmbeddedEntityContext(MongoPersistentProperty property) {
+ this.property = property;
+ }
+
+ public MongoPersistentProperty getProperty() {
+ return property;
+ }
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/EmbeddedMongoPersistentEntity.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/EmbeddedMongoPersistentEntity.java
new file mode 100644
index 0000000000..bbfa486673
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/EmbeddedMongoPersistentEntity.java
@@ -0,0 +1,283 @@
+/*
+ * Copyright 2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.mongodb.core.mapping;
+
+import java.lang.annotation.Annotation;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Spliterator;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+
+import org.springframework.data.mapping.*;
+import org.springframework.data.mapping.model.PersistentPropertyAccessorFactory;
+import org.springframework.data.mongodb.core.query.Collation;
+import org.springframework.data.spel.EvaluationContextProvider;
+import org.springframework.data.util.Streamable;
+import org.springframework.data.util.TypeInformation;
+import org.springframework.lang.Nullable;
+
+/**
+ * Embedded variant of {@link MongoPersistentEntity}.
+ *
+ * @author Christoph Strobl
+ * @see Embedded
+ */
+class EmbeddedMongoPersistentEntity implements MongoPersistentEntity {
+
+ private final EmbeddedEntityContext context;
+ private final MongoPersistentEntity delegate;
+
+ public EmbeddedMongoPersistentEntity(MongoPersistentEntity delegate, EmbeddedEntityContext context) {
+
+ this.context = context;
+ this.delegate = delegate;
+ }
+
+ public String getCollection() {
+ return delegate.getCollection();
+ }
+
+ public String getLanguage() {
+ return delegate.getLanguage();
+ }
+
+ @Nullable
+ public MongoPersistentProperty getTextScoreProperty() {
+ return delegate.getTextScoreProperty();
+ }
+
+ public boolean hasTextScoreProperty() {
+ return delegate.hasTextScoreProperty();
+ }
+
+ @Nullable
+ public Collation getCollation() {
+ return delegate.getCollation();
+ }
+
+ public boolean hasCollation() {
+ return delegate.hasCollation();
+ }
+
+ public ShardKey getShardKey() {
+ return delegate.getShardKey();
+ }
+
+ public boolean isSharded() {
+ return delegate.isSharded();
+ }
+
+ public String getName() {
+ return delegate.getName();
+ }
+
+ @Nullable
+ public PreferredConstructor getPersistenceConstructor() {
+ return delegate.getPersistenceConstructor();
+ }
+
+ public boolean isConstructorArgument(PersistentProperty> property) {
+ return delegate.isConstructorArgument(property);
+ }
+
+ public boolean isIdProperty(PersistentProperty> property) {
+ return delegate.isIdProperty(property);
+ }
+
+ public boolean isVersionProperty(PersistentProperty> property) {
+ return delegate.isVersionProperty(property);
+ }
+
+ @Nullable
+ public MongoPersistentProperty getIdProperty() {
+ return delegate.getIdProperty();
+ }
+
+ public MongoPersistentProperty getRequiredIdProperty() {
+ return delegate.getRequiredIdProperty();
+ }
+
+ @Nullable
+ public MongoPersistentProperty getVersionProperty() {
+ return delegate.getVersionProperty();
+ }
+
+ public MongoPersistentProperty getRequiredVersionProperty() {
+ return delegate.getRequiredVersionProperty();
+ }
+
+ @Nullable
+ public MongoPersistentProperty getPersistentProperty(String name) {
+ return wrap(delegate.getPersistentProperty(name));
+ }
+
+ public MongoPersistentProperty getRequiredPersistentProperty(String name) {
+
+ MongoPersistentProperty persistentProperty = getPersistentProperty(name);
+ if (persistentProperty != null) {
+ return persistentProperty;
+ }
+
+ throw new RuntimeException(":kladjnf");
+ }
+
+ @Nullable
+ public MongoPersistentProperty getPersistentProperty(Class extends Annotation> annotationType) {
+ return wrap(delegate.getPersistentProperty(annotationType));
+ }
+
+ public Iterable 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 getType() {
+ return delegate.getType();
+ }
+
+ public Alias getTypeAlias() {
+ return delegate.getTypeAlias();
+ }
+
+ public TypeInformation getTypeInformation() {
+ return delegate.getTypeInformation();
+ }
+
+ public void doWithProperties(PropertyHandler handler) {
+
+ delegate.doWithProperties((PropertyHandler) property -> {
+ handler.doWithPersistentProperty(wrap(property));
+ });
+ }
+
+ public void doWithProperties(SimplePropertyHandler handler) {
+
+ delegate.doWithProperties((SimplePropertyHandler) property -> {
+ if (property instanceof MongoPersistentProperty) {
+ handler.doWithPersistentProperty(wrap((MongoPersistentProperty) property));
+ } else {
+ handler.doWithPersistentProperty(property);
+ }
+ });
+ }
+
+ public void doWithAssociations(AssociationHandler handler) {
+ delegate.doWithAssociations(handler);
+ }
+
+ public void doWithAssociations(SimpleAssociationHandler handler) {
+ delegate.doWithAssociations(handler);
+ }
+
+ @Nullable
+ public A findAnnotation(Class annotationType) {
+ return delegate.findAnnotation(annotationType);
+ }
+
+ public A getRequiredAnnotation(Class annotationType) throws IllegalStateException {
+ return delegate.getRequiredAnnotation(annotationType);
+ }
+
+ public boolean isAnnotationPresent(Class annotationType) {
+ return delegate.isAnnotationPresent(annotationType);
+ }
+
+ public PersistentPropertyAccessor getPropertyAccessor(B bean) {
+ return delegate.getPropertyAccessor(bean);
+ }
+
+ public PersistentPropertyPathAccessor getPropertyPathAccessor(B bean) {
+ return delegate.getPropertyPathAccessor(bean);
+ }
+
+ public IdentifierAccessor getIdentifierAccessor(Object bean) {
+ return delegate.getIdentifierAccessor(bean);
+ }
+
+ public boolean isNew(Object bean) {
+ return delegate.isNew(bean);
+ }
+
+ public boolean isImmutable() {
+ return delegate.isImmutable();
+ }
+
+ public boolean requiresPropertyPopulation() {
+ return delegate.requiresPropertyPopulation();
+ }
+
+ public Iterator iterator() {
+
+ List target = new ArrayList<>();
+ delegate.iterator().forEachRemaining(it -> target.add(wrap(it)));
+ return target.iterator();
+ }
+
+ public void forEach(Consumer super MongoPersistentProperty> action) {
+ delegate.forEach(it -> action.accept(wrap(it)));
+ }
+
+ public Spliterator spliterator() {
+ return delegate.spliterator();
+ }
+
+ private MongoPersistentProperty wrap(MongoPersistentProperty source) {
+ if (source == null) {
+ return source;
+ }
+ return new EmbeddedMongoPersistentProperty(source, context);
+ }
+
+ @Override
+ public void addPersistentProperty(MongoPersistentProperty property) {
+
+ }
+
+ @Override
+ public void addAssociation(Association association) {
+
+ }
+
+ @Override
+ public void verify() throws MappingException {
+
+ }
+
+ @Override
+ public void setPersistentPropertyAccessorFactory(PersistentPropertyAccessorFactory factory) {
+
+ }
+
+ @Override
+ public void setEvaluationContextProvider(EvaluationContextProvider provider) {
+
+ }
+
+ @Override
+ public boolean isEmbedded() {
+ return context.getProperty().isEmbedded();
+ }
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/EmbeddedMongoPersistentProperty.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/EmbeddedMongoPersistentProperty.java
new file mode 100644
index 0000000000..88355cff63
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/EmbeddedMongoPersistentProperty.java
@@ -0,0 +1,257 @@
+/*
+ * Copyright 2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.mongodb.core.mapping;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+
+import org.springframework.data.mapping.Association;
+import org.springframework.data.mapping.PersistentEntity;
+import org.springframework.data.mapping.PersistentPropertyAccessor;
+import org.springframework.data.util.TypeInformation;
+import org.springframework.lang.Nullable;
+
+/**
+ * Embedded variant of {@link MongoPersistentProperty}.
+ *
+ * @author Christoph Strobl
+ * @see Embedded
+ */
+class EmbeddedMongoPersistentProperty implements MongoPersistentProperty {
+
+ private final MongoPersistentProperty delegate;
+ private final EmbeddedEntityContext context;
+
+ public EmbeddedMongoPersistentProperty(MongoPersistentProperty delegate, EmbeddedEntityContext context) {
+
+ this.delegate = delegate;
+ this.context = context;
+ }
+
+ public String getFieldName() {
+
+ if (!context.getProperty().isEmbedded()) {
+ return delegate.getFieldName();
+ }
+
+ return context.getProperty().findAnnotation(Embedded.class).prefix() + delegate.getFieldName();
+ }
+
+ public Class> getFieldType() {
+ return delegate.getFieldType();
+ }
+
+ public int getFieldOrder() {
+ return delegate.getFieldOrder();
+ }
+
+ public boolean isDbReference() {
+ return delegate.isDbReference();
+ }
+
+ public boolean isExplicitIdProperty() {
+ return delegate.isExplicitIdProperty();
+ }
+
+ public boolean isLanguageProperty() {
+ return delegate.isLanguageProperty();
+ }
+
+ public boolean isExplicitLanguageProperty() {
+ return delegate.isExplicitLanguageProperty();
+ }
+
+ public boolean isTextScoreProperty() {
+ return delegate.isTextScoreProperty();
+ }
+
+ @Nullable
+ public DBRef getDBRef() {
+ return delegate.getDBRef();
+ }
+
+ public boolean usePropertyAccess() {
+ return delegate.usePropertyAccess();
+ }
+
+ public boolean hasExplicitWriteTarget() {
+ return delegate.hasExplicitWriteTarget();
+ }
+
+ public PersistentEntity, 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 getAssociation() {
+ return delegate.getAssociation();
+ }
+
+ public Association getRequiredAssociation() {
+ return delegate.getRequiredAssociation();
+ }
+
+ public boolean isEntity() {
+ return delegate.isEntity();
+ }
+
+ public boolean isIdProperty() {
+ return delegate.isIdProperty();
+ }
+
+ public boolean isVersionProperty() {
+ return delegate.isVersionProperty();
+ }
+
+ public boolean isCollectionLike() {
+ return delegate.isCollectionLike();
+ }
+
+ public boolean isMap() {
+ return delegate.isMap();
+ }
+
+ public boolean isArray() {
+ return delegate.isArray();
+ }
+
+ public boolean isTransient() {
+ return delegate.isTransient();
+ }
+
+ public boolean isWritable() {
+ return delegate.isWritable();
+ }
+
+ public boolean isImmutable() {
+ return delegate.isImmutable();
+ }
+
+ public boolean isAssociation() {
+ return delegate.isAssociation();
+ }
+
+ public boolean isEmbedded() {
+ return delegate.isEmbedded();
+ }
+
+ @Nullable
+ public Class> getComponentType() {
+ return delegate.getComponentType();
+ }
+
+ public Class> getRawType() {
+ return delegate.getRawType();
+ }
+
+ @Nullable
+ public Class> getMapValueType() {
+ return delegate.getMapValueType();
+ }
+
+ public Class> getActualType() {
+ return delegate.getActualType();
+ }
+
+ @Nullable
+ public A findAnnotation(Class annotationType) {
+ return delegate.findAnnotation(annotationType);
+ }
+
+ public A getRequiredAnnotation(Class annotationType) throws IllegalStateException {
+ return delegate.getRequiredAnnotation(annotationType);
+ }
+
+ @Nullable
+ public A findPropertyOrOwnerAnnotation(Class annotationType) {
+ return delegate.findPropertyOrOwnerAnnotation(annotationType);
+ }
+
+ public boolean isAnnotationPresent(Class 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 PersistentPropertyAccessor getAccessorForOwner(T owner) {
+ return delegate.getAccessorForOwner(owner);
+ }
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoMappingContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoMappingContext.java
index 938f16d0fa..3e18fd463c 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoMappingContext.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoMappingContext.java
@@ -35,8 +35,9 @@
*
* @author Jon Brisbin
* @author Oliver Gierke
+ * @author Christoph Strobl
*/
-public class MongoMappingContext extends AbstractMappingContext, MongoPersistentProperty>
+public class MongoMappingContext extends AbstractMappingContext, MongoPersistentProperty>
implements ApplicationContextAware {
private static final FieldNamingStrategy DEFAULT_NAMING_STRATEGY = PropertyNameFieldNamingStrategy.INSTANCE;
@@ -76,7 +77,7 @@ protected boolean shouldCreatePersistentEntityFor(TypeInformation> type) {
* @see org.springframework.data.mapping.AbstractMappingContext#createPersistentProperty(java.lang.reflect.Field, java.beans.PropertyDescriptor, org.springframework.data.mapping.MutablePersistentEntity, org.springframework.data.mapping.SimpleTypeHolder)
*/
@Override
- public MongoPersistentProperty createPersistentProperty(Property property, BasicMongoPersistentEntity> owner,
+ public MongoPersistentProperty createPersistentProperty(Property property, MongoPersistentEntity> owner,
SimpleTypeHolder simpleTypeHolder) {
return new CachingMongoPersistentProperty(property, owner, simpleTypeHolder, fieldNamingStrategy);
}
@@ -87,7 +88,7 @@ public MongoPersistentProperty createPersistentProperty(Property property, Basic
*/
@Override
protected BasicMongoPersistentEntity createPersistentEntity(TypeInformation typeInformation) {
- return new BasicMongoPersistentEntity(typeInformation);
+ return new BasicMongoPersistentEntity<>(typeInformation);
}
/*
@@ -96,7 +97,6 @@ protected BasicMongoPersistentEntity createPersistentEntity(TypeInformati
*/
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
-
super.setApplicationContext(applicationContext);
}
@@ -126,4 +126,17 @@ public boolean isAutoIndexCreation() {
public void setAutoIndexCreation(boolean autoCreateIndexes) {
this.autoIndexCreation = autoCreateIndexes;
}
+
+ @Nullable
+ @Override
+ public MongoPersistentEntity> getPersistentEntity(MongoPersistentProperty persistentProperty) {
+
+ MongoPersistentEntity> entity = super.getPersistentEntity(persistentProperty);
+
+ if(entity == null || !persistentProperty.isEmbedded()) {
+ return entity;
+ }
+
+ return new EmbeddedMongoPersistentEntity<>(entity, new EmbeddedEntityContext(persistentProperty));
+ }
}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentEntity.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentEntity.java
index 9a0d60100b..d2e74d4de8 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentEntity.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentEntity.java
@@ -16,6 +16,7 @@
package org.springframework.data.mongodb.core.mapping;
import org.springframework.data.mapping.PersistentEntity;
+import org.springframework.data.mapping.model.MutablePersistentEntity;
import org.springframework.lang.Nullable;
/**
@@ -24,7 +25,7 @@
* @author Oliver Gierke
* @author Christoph Strobl
*/
-public interface MongoPersistentEntity extends PersistentEntity {
+public interface MongoPersistentEntity extends MutablePersistentEntity {
/**
* Returns the collection the entity shall be persisted to.
@@ -93,4 +94,12 @@ default boolean isSharded() {
return getShardKey().isSharded();
}
+ /**
+ * @return {@literal true} if the entity should be embedded.
+ * @since 3.2
+ */
+ default boolean isEmbedded() {
+ return false;
+ }
+
}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentProperty.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentProperty.java
index 53a5eb7f91..be8a59882a 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentProperty.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentProperty.java
@@ -123,6 +123,14 @@ default boolean hasExplicitWriteTarget() {
return field != null ? !FieldType.IMPLICIT.equals(field.targetType()) : false;
}
+ /**
+ * @return {@literal true} if the property should be embedded.
+ * @since 3.2
+ */
+ default boolean isEmbedded() {
+ return isEntity() && isAnnotationPresent(Embedded.class);
+ }
+
/**
* Simple {@link Converter} implementation to transform a {@link MongoPersistentProperty} into its field name.
*
@@ -137,7 +145,10 @@ enum PropertyToFieldNameConverter implements Converter metadata = query.getQueryMethod().getEntityInformation();
try {
- indexOperationsProvider.indexOps(metadata.getCollectionName()).ensureIndex(index);
+ indexOperationsProvider.indexOps(metadata.getCollectionName(), metadata.getJavaType()).ensureIndex(index);
} catch (UncategorizedMongoDbException e) {
if (e.getCause() instanceof MongoException) {
@@ -129,6 +138,19 @@ public void onCreation(PartTreeMongoQuery query) {
LOG.debug(String.format("Created %s!", index));
}
+ public boolean isIndexOnEmbeddedType(Part part) {
+
+ // TODO we could do it for nested fields in the
+ Field field = ReflectionUtils.findField(part.getProperty().getOwningType().getType(),
+ part.getProperty().getSegment());
+
+ if (field == null) {
+ return false;
+ }
+
+ return AnnotatedElementUtils.hasAnnotation(field, Embedded.class);
+ }
+
private static Direction toDirection(Sort sort, String property) {
if (sort.isUnsorted()) {
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactoryBean.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactoryBean.java
index 840527e4a9..ade85d3110 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactoryBean.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactoryBean.java
@@ -90,7 +90,7 @@ protected RepositoryFactorySupport createRepositoryFactory() {
if (createIndexesForQueryMethods) {
factory.addQueryCreationListener(
- new IndexEnsuringQueryCreationListener(collectionName -> operations.indexOps(collectionName)));
+ new IndexEnsuringQueryCreationListener((collectionName, javaType) -> operations.indexOps(javaType)));
}
return factory;
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactoryBean.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactoryBean.java
index f694100ef0..4e8232714f 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactoryBean.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactoryBean.java
@@ -97,7 +97,7 @@ protected RepositoryFactorySupport createRepositoryFactory() {
if (createIndexesForQueryMethods) {
factory.addQueryCreationListener(new IndexEnsuringQueryCreationListener(
- collectionName -> IndexOperationsAdapter.blocking(operations.indexOps(collectionName))));
+ (collectionName, javaType) -> IndexOperationsAdapter.blocking(operations.indexOps(javaType))));
}
return factory;
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/DotPath.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/DotPath.java
new file mode 100644
index 0000000000..dd55caf53f
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/DotPath.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.mongodb.util;
+
+import org.springframework.lang.Nullable;
+import org.springframework.util.StringUtils;
+
+/**
+ * Value object representing a dot path.
+ *
+ * @author Mark Paluch
+ * @since 3.2
+ */
+public class DotPath {
+
+ private static final DotPath EMPTY = new DotPath("");
+
+ private final String path;
+
+ private DotPath(String path) {
+ this.path = path;
+ }
+
+ /**
+ * Creates a new {@link DotPath} from {@code dotPath}.
+ *
+ * @param dotPath the dot path, can be empty or {@literal null}.
+ * @return the {@link DotPath} representing {@code dotPath}.
+ */
+ public static DotPath from(@Nullable String dotPath) {
+
+ if (StringUtils.hasLength(dotPath)) {
+ return new DotPath(dotPath);
+ }
+
+ return EMPTY;
+ }
+
+ /**
+ * Returns an empty dotpath.
+ *
+ * @return an empty dotpath.
+ */
+ public static DotPath empty() {
+ return EMPTY;
+ }
+
+ /**
+ * Append a segment to the dotpath. If the dotpath is not empty, then segments are separated with a dot.
+ *
+ * @param segment the segment to append.
+ * @return
+ */
+ public DotPath append(String segment) {
+
+ if (isEmpty()) {
+ return new DotPath(segment);
+ }
+
+ return new DotPath(path + "." + segment);
+ }
+
+ /**
+ * Returns whether this dotpath is empty.
+ *
+ * @return whether this dotpath is empty.
+ */
+ public boolean isEmpty() {
+ return !StringUtils.hasLength(path);
+ }
+
+ @Override
+ public String toString() {
+ return path;
+ }
+}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/config/AbstractMongoConfigurationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/config/AbstractMongoConfigurationUnitTests.java
index 37764ff9a5..1ec266bacf 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/config/AbstractMongoConfigurationUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/config/AbstractMongoConfigurationUnitTests.java
@@ -39,6 +39,7 @@
import org.springframework.data.mongodb.core.mapping.BasicMongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
+import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.spel.EvaluationContextProvider;
import org.springframework.data.spel.ExtensionAwareEvaluationContextProvider;
import org.springframework.test.util.ReflectionTestUtils;
@@ -103,7 +104,7 @@ public void lifecycleCallbacksAreInvokedInAppropriateOrder() {
AbstractApplicationContext context = new AnnotationConfigApplicationContext(SampleMongoConfiguration.class);
MongoMappingContext mappingContext = context.getBean(MongoMappingContext.class);
- BasicMongoPersistentEntity> entity = mappingContext.getRequiredPersistentEntity(Entity.class);
+ MongoPersistentEntity> entity = mappingContext.getRequiredPersistentEntity(Entity.class);
EvaluationContextProvider provider = (EvaluationContextProvider) ReflectionTestUtils.getField(entity,
"evaluationContextProvider");
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/config/AbstractReactiveMongoConfigurationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/config/AbstractReactiveMongoConfigurationUnitTests.java
index 39d998f109..abbb7e0287 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/config/AbstractReactiveMongoConfigurationUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/config/AbstractReactiveMongoConfigurationUnitTests.java
@@ -40,6 +40,7 @@
import org.springframework.data.mongodb.core.mapping.BasicMongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
+import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.test.util.MongoTestUtils;
import org.springframework.data.spel.EvaluationContextProvider;
import org.springframework.data.spel.ExtensionAwareEvaluationContextProvider;
@@ -103,7 +104,7 @@ public void lifecycleCallbacksAreInvokedInAppropriateOrder() {
AbstractApplicationContext context = new AnnotationConfigApplicationContext(SampleMongoConfiguration.class);
MongoMappingContext mappingContext = context.getBean(MongoMappingContext.class);
- BasicMongoPersistentEntity> entity = mappingContext.getRequiredPersistentEntity(Entity.class);
+ MongoPersistentEntity> entity = mappingContext.getRequiredPersistentEntity(Entity.class);
EvaluationContextProvider provider = (EvaluationContextProvider) ReflectionTestUtils.getField(entity,
"evaluationContextProvider");
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateEmbeddedTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateEmbeddedTests.java
new file mode 100644
index 0000000000..119563be3a
--- /dev/null
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateEmbeddedTests.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2020 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.mongodb.core;
+
+import static org.assertj.core.api.Assertions.*;
+import static org.springframework.data.mongodb.core.query.Criteria.*;
+import static org.springframework.data.mongodb.core.query.Query.*;
+
+import lombok.EqualsAndHashCode;
+
+import java.util.Arrays;
+import java.util.List;
+
+import lombok.ToString;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.springframework.data.mongodb.core.mapping.Embedded;
+import org.springframework.data.mongodb.core.mapping.Field;
+import org.springframework.data.mongodb.core.query.Query;
+import org.springframework.data.mongodb.test.util.MongoTemplateExtension;
+import org.springframework.data.mongodb.test.util.Template;
+
+/**
+ * Integration tests for {@link Embedded}.
+ *
+ * @author Christoph Strobl
+ */
+@ExtendWith(MongoTemplateExtension.class)
+class MongoTemplateEmbeddedTests {
+
+ private static @Template MongoTemplate template;
+
+ @Test // DATAMONGO-1902
+ void readWrite() {
+
+ WithEmbedded source = new WithEmbedded();
+ source.id = "id-1";
+ source.embeddableValue = new EmbeddableType();
+ source.embeddableValue.stringValue = "string-val";
+ source.embeddableValue.listValue = Arrays.asList("list-val-1", "list-val-2");
+ source.embeddableValue.atFieldAnnotatedValue = "@Field";
+
+ template.save(source);
+
+ assertThat(template.findOne(query(where("id").is(source.id)), WithEmbedded.class)).isEqualTo(source);
+ }
+
+ @Test // DATAMONGO-1902
+ void filterOnEmbeddedValue() {
+
+ WithEmbedded source = new WithEmbedded();
+ source.id = "id-1";
+ source.embeddableValue = new EmbeddableType();
+ source.embeddableValue.stringValue = "string-val";
+ source.embeddableValue.listValue = Arrays.asList("list-val-1", "list-val-2");
+ source.embeddableValue.atFieldAnnotatedValue = "@Field";
+
+ template.save(source);
+
+ assertThat(template.findOne(
+ Query.query(where("embeddableValue.stringValue").is(source.embeddableValue.stringValue)), WithEmbedded.class))
+ .isEqualTo(source);
+ }
+
+ @Test // DATAMONGO-1902
+ void readWritePrefixed() {
+
+ WithPrefixedEmbedded source = new WithPrefixedEmbedded();
+ source.id = "id-1";
+ source.embeddableValue = new EmbeddableType();
+ source.embeddableValue.stringValue = "string-val";
+ source.embeddableValue.listValue = Arrays.asList("list-val-1", "list-val-2");
+ source.embeddableValue.atFieldAnnotatedValue = "@Field";
+
+ template.save(source);
+
+ assertThat(template.findOne(query(where("id").is(source.id)), WithPrefixedEmbedded.class)).isEqualTo(source);
+ }
+
+ @Test // DATAMONGO-1902
+ void filterOnPrefixedEmbeddedValue() {
+
+ WithPrefixedEmbedded source = new WithPrefixedEmbedded();
+ source.id = "id-1";
+ source.embeddableValue = new EmbeddableType();
+ source.embeddableValue.stringValue = "string-val";
+ source.embeddableValue.listValue = Arrays.asList("list-val-1", "list-val-2");
+ source.embeddableValue.atFieldAnnotatedValue = "@Field";
+
+ template.save(source);
+
+ assertThat(
+ template.findOne(Query.query(where("embeddableValue.stringValue").is(source.embeddableValue.stringValue)),
+ WithPrefixedEmbedded.class)).isEqualTo(source);
+ }
+
+ @EqualsAndHashCode
+ @ToString
+ static class WithEmbedded {
+
+ String id;
+
+ @Embedded.Nullable EmbeddableType embeddableValue;
+ }
+
+ @EqualsAndHashCode
+ @ToString
+ static class WithPrefixedEmbedded {
+
+ String id;
+
+ @Embedded.Nullable("prefix-") EmbeddableType embeddableValue;
+ }
+
+ @EqualsAndHashCode
+ @ToString
+ static class EmbeddableType {
+
+ String stringValue;
+ List listValue;
+
+ @Field("with-at-field-annotation") //
+ String atFieldAnnotatedValue;
+ }
+}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/TypeBasedAggregationOperationContextUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/TypeBasedAggregationOperationContextUnitTests.java
index 1b40cfc046..0e4c600e10 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/TypeBasedAggregationOperationContextUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/TypeBasedAggregationOperationContextUnitTests.java
@@ -32,7 +32,6 @@
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
-
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.support.GenericConversionService;
import org.springframework.data.annotation.Id;
@@ -46,6 +45,7 @@
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.convert.MongoCustomConversions;
import org.springframework.data.mongodb.core.convert.QueryMapper;
+import org.springframework.data.mongodb.core.mapping.Embedded;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.query.Criteria;
@@ -357,6 +357,103 @@ public void projectOperationShouldRenderNestedFieldNamesCorrectlyForTypedAggrega
.isEqualTo(new Document("val", new Document("$add", Arrays.asList("$nested1.value1", "$field2.nestedValue2"))));
}
+ @Test // DATAMONGO-1902
+ void rendersProjectOnEmbeddedFieldCorrectly() {
+
+ AggregationOperationContext context = getContext(WithEmbedded.class);
+
+ Document agg = newAggregation(WithEmbedded.class, project().and("embeddableType.stringValue").as("val"))
+ .toDocument("collection", context);
+
+ assertThat(getPipelineElementFromAggregationAt(agg, 0).get("$project"))
+ .isEqualTo(new Document("val", "$stringValue"));
+ }
+
+ @Test // DATAMONGO-1902
+ void rendersProjectOnEmbeddedFieldWithAtFieldAnnotationCorrectly() {
+
+ AggregationOperationContext context = getContext(WithEmbedded.class);
+
+ Document agg = newAggregation(WithEmbedded.class, project().and("embeddableType.atFieldAnnotatedValue").as("val"))
+ .toDocument("collection", context);
+
+ assertThat(getPipelineElementFromAggregationAt(agg, 0).get("$project"))
+ .isEqualTo(new Document("val", "$with-at-field-annotation"));
+ }
+
+ @Test // DATAMONGO-1902
+ void rendersProjectOnPrefixedEmbeddedFieldCorrectly() {
+
+ AggregationOperationContext context = getContext(WithEmbedded.class);
+
+ Document agg = newAggregation(WithEmbedded.class, project().and("prefixedEmbeddableValue.stringValue").as("val"))
+ .toDocument("collection", context);
+
+ assertThat(getPipelineElementFromAggregationAt(agg, 0).get("$project"))
+ .isEqualTo(new Document("val", "$prefix-stringValue"));
+ }
+
+ @Test // DATAMONGO-1902
+ void rendersProjectOnPrefixedEmbeddedFieldWithAtFieldAnnotationCorrectly() {
+
+ AggregationOperationContext context = getContext(WithEmbedded.class);
+
+ Document agg = newAggregation(WithEmbedded.class,
+ project().and("prefixedEmbeddableValue.atFieldAnnotatedValue").as("val")).toDocument("collection", context);
+
+ assertThat(getPipelineElementFromAggregationAt(agg, 0).get("$project"))
+ .isEqualTo(new Document("val", "$prefix-with-at-field-annotation"));
+ }
+
+ @Test // DATAMONGO-1902
+ void rendersProjectOnNestedEmbeddedFieldCorrectly() {
+
+ AggregationOperationContext context = getContext(WrapperAroundWithEmbedded.class);
+
+ Document agg = newAggregation(WrapperAroundWithEmbedded.class,
+ project().and("withEmbedded.embeddableType.stringValue").as("val")).toDocument("collection", context);
+
+ assertThat(getPipelineElementFromAggregationAt(agg, 0).get("$project"))
+ .isEqualTo(new Document("val", "$withEmbedded.stringValue"));
+ }
+
+ @Test // DATAMONGO-1902
+ void rendersProjectOnNestedEmbeddedFieldWithAtFieldAnnotationCorrectly() {
+
+ AggregationOperationContext context = getContext(WrapperAroundWithEmbedded.class);
+
+ Document agg = newAggregation(WrapperAroundWithEmbedded.class,
+ project().and("withEmbedded.embeddableType.atFieldAnnotatedValue").as("val")).toDocument("collection", context);
+
+ assertThat(getPipelineElementFromAggregationAt(agg, 0).get("$project"))
+ .isEqualTo(new Document("val", "$withEmbedded.with-at-field-annotation"));
+ }
+
+ @Test // DATAMONGO-1902
+ void rendersProjectOnNestedPrefixedEmbeddedFieldCorrectly() {
+
+ AggregationOperationContext context = getContext(WrapperAroundWithEmbedded.class);
+
+ Document agg = newAggregation(WrapperAroundWithEmbedded.class,
+ project().and("withEmbedded.prefixedEmbeddableValue.stringValue").as("val")).toDocument("collection", context);
+
+ assertThat(getPipelineElementFromAggregationAt(agg, 0).get("$project"))
+ .isEqualTo(new Document("val", "$withEmbedded.prefix-stringValue"));
+ }
+
+ @Test // DATAMONGO-1902
+ void rendersProjectOnNestedPrefixedEmbeddedFieldWithAtFieldAnnotationCorrectly() {
+
+ AggregationOperationContext context = getContext(WrapperAroundWithEmbedded.class);
+
+ Document agg = newAggregation(WrapperAroundWithEmbedded.class,
+ project().and("withEmbedded.prefixedEmbeddableValue.atFieldAnnotatedValue").as("val")).toDocument("collection",
+ context);
+
+ assertThat(getPipelineElementFromAggregationAt(agg, 0).get("$project"))
+ .isEqualTo(new Document("val", "$withEmbedded.prefix-with-at-field-annotation"));
+ }
+
@org.springframework.data.mongodb.core.mapping.Document(collection = "person")
@AllArgsConstructor
public static class FooPerson {
@@ -433,4 +530,26 @@ static class Nested {
String value1;
@org.springframework.data.mongodb.core.mapping.Field("nestedValue2") String value2;
}
+
+ static class WrapperAroundWithEmbedded {
+
+ String id;
+ WithEmbedded withEmbedded;
+ }
+
+ static class WithEmbedded {
+
+ String id;
+
+ @Embedded.Nullable EmbeddableType embeddableType;
+ @Embedded.Nullable("prefix-") EmbeddableType prefixedEmbeddableValue;
+ }
+
+ static class EmbeddableType {
+
+ String stringValue;
+
+ @org.springframework.data.mongodb.core.mapping.Field("with-at-field-annotation") //
+ String atFieldAnnotatedValue;
+ }
}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/UnionWithOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/UnionWithOperationUnitTests.java
index bb77c86285..cef23de937 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/UnionWithOperationUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/UnionWithOperationUnitTests.java
@@ -83,7 +83,6 @@ void doesNotMapAgainstFieldsFromAPreviousStage() {
UnionWithOperation.unionWith("coll-1").pipeline(Aggregation.project().and("name").as("name")));
List pipeline = agg.toPipeline(contextFor(Supplier.class));
- System.out.println("pipeline: " + pipeline);
assertThat(pipeline).containsExactly(new Document("$project", new Document("supplier", 1)), //
new Document("$unionWith", new Document("coll", "coll-1").append("pipeline",
Arrays.asList(new Document("$project", new Document("name", 1))))));
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java
index fdaf46692d..198a5f8ae5 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java
@@ -51,6 +51,7 @@
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.PersistenceConstructor;
+import org.springframework.data.annotation.Transient;
import org.springframework.data.annotation.TypeAlias;
import org.springframework.data.convert.CustomConversions;
import org.springframework.data.convert.ReadingConverter;
@@ -71,6 +72,7 @@
import org.springframework.data.mongodb.core.convert.MappingMongoConverterUnitTests.ClassWithMapUsingEnumAsKey.FooBarEnum;
import org.springframework.data.mongodb.core.geo.Sphere;
import org.springframework.data.mongodb.core.mapping.Document;
+import org.springframework.data.mongodb.core.mapping.Embedded;
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.mapping.FieldType;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
@@ -2179,6 +2181,197 @@ public void readAndConvertDBRefNestedByMapCorrectly() {
assertThat(((LinkedHashMap) result.get("cluster")).get("_id")).isEqualTo(100L);
}
+ @Test // DATAMONGO-1902
+ void writeFlattensEmbeddedType() {
+
+ WithNullableEmbedded source = new WithNullableEmbedded();
+ source.id = "id-1";
+ source.embeddableValue = new EmbeddableType();
+ source.embeddableValue.listValue = Arrays.asList("list-val-1", "list-val-2");
+ source.embeddableValue.stringValue = "string-val";
+ source.embeddableValue.transientValue = "must-not-be-written";
+ source.embeddableValue.atFieldAnnotatedValue = "@Field";
+
+ org.bson.Document target = new org.bson.Document();
+ converter.write(source, target);
+
+ assertThat(target).containsEntry("_id", "id-1") //
+ .containsEntry("stringValue", "string-val") //
+ .containsEntry("listValue", Arrays.asList("list-val-1", "list-val-2")) //
+ .containsEntry("with-at-field-annotation", "@Field") //
+ .doesNotContainKey("embeddableValue") //
+ .doesNotContainKey("transientValue");
+ }
+
+ @Test // DATAMONGO-1902
+ void writePrefixesEmbeddedType() {
+
+ WithPrefixedNullableEmbedded source = new WithPrefixedNullableEmbedded();
+ source.id = "id-1";
+ source.embeddableValue = new EmbeddableType();
+ source.embeddableValue.listValue = Arrays.asList("list-val-1", "list-val-2");
+ source.embeddableValue.stringValue = "string-val";
+ source.embeddableValue.transientValue = "must-not-be-written";
+ source.embeddableValue.atFieldAnnotatedValue = "@Field";
+
+ org.bson.Document target = new org.bson.Document();
+ converter.write(source, target);
+
+ assertThat(target).containsEntry("_id", "id-1") //
+ .containsEntry("prefix-stringValue", "string-val") //
+ .containsEntry("prefix-listValue", Arrays.asList("list-val-1", "list-val-2")) //
+ .containsEntry("prefix-with-at-field-annotation", "@Field") //
+ .doesNotContainKey("embeddableValue") //
+ .doesNotContainKey("transientValue") //
+ .doesNotContainKey("prefix-transientValue");
+ }
+
+ @Test // DATAMONGO-1902
+ void writeNullEmbeddedType() {
+
+ WithNullableEmbedded source = new WithNullableEmbedded();
+ source.id = "id-1";
+ source.embeddableValue = null;
+
+ org.bson.Document target = new org.bson.Document();
+ converter.write(source, target);
+
+ assertThat(target) //
+ .doesNotContainKey("prefix-stringValue").doesNotContainKey("prefix-listValue")
+ .doesNotContainKey("embeddableValue");
+ }
+
+ @Test // DATAMONGO-1902
+ void writeDeepNestedEmbeddedType() {
+
+ WrapperAroundWithEmbedded source = new WrapperAroundWithEmbedded();
+ source.someValue = "root-level-value";
+ source.nullableEmbedded = new WithNullableEmbedded();
+ source.nullableEmbedded.id = "id-1";
+ source.nullableEmbedded.embeddableValue = new EmbeddableType();
+ source.nullableEmbedded.embeddableValue.listValue = Arrays.asList("list-val-1", "list-val-2");
+ source.nullableEmbedded.embeddableValue.stringValue = "string-val";
+ source.nullableEmbedded.embeddableValue.transientValue = "must-not-be-written";
+ source.nullableEmbedded.embeddableValue.atFieldAnnotatedValue = "@Field";
+
+ org.bson.Document target = new org.bson.Document();
+ converter.write(source, target);
+
+ assertThat(target).containsEntry("someValue", "root-level-value") //
+ .containsEntry("nullableEmbedded", new org.bson.Document("_id", "id-1").append("stringValue", "string-val") //
+ .append("listValue", Arrays.asList("list-val-1", "list-val-2")) //
+ .append("with-at-field-annotation", "@Field")); //
+ }
+
+ @Test // DATAMONGO-1902
+ void readEmbeddedType() {
+
+ org.bson.Document source = new org.bson.Document("_id", "id-1") //
+ .append("stringValue", "string-val") //
+ .append("listValue", Arrays.asList("list-val-1", "list-val-2")) //
+ .append("with-at-field-annotation", "@Field");
+
+ EmbeddableType embeddableValue = new EmbeddableType();
+ embeddableValue.stringValue = "string-val";
+ embeddableValue.listValue = Arrays.asList("list-val-1", "list-val-2");
+ embeddableValue.atFieldAnnotatedValue = "@Field";
+
+ WithNullableEmbedded target = converter.read(WithNullableEmbedded.class, source);
+ assertThat(target.embeddableValue).isEqualTo(embeddableValue);
+ }
+
+ @Test // DATAMONGO-1902
+ void readPrefixedEmbeddedType() {
+
+ org.bson.Document source = new org.bson.Document("_id", "id-1") //
+ .append("prefix-stringValue", "string-val") //
+ .append("prefix-listValue", Arrays.asList("list-val-1", "list-val-2")) //
+ .append("prefix-with-at-field-annotation", "@Field");
+
+ EmbeddableType embeddableValue = new EmbeddableType();
+ embeddableValue.stringValue = "string-val";
+ embeddableValue.listValue = Arrays.asList("list-val-1", "list-val-2");
+ embeddableValue.atFieldAnnotatedValue = "@Field";
+
+ WithPrefixedNullableEmbedded target = converter.read(WithPrefixedNullableEmbedded.class, source);
+ assertThat(target.embeddableValue).isEqualTo(embeddableValue);
+ }
+
+ @Test // DATAMONGO-1902
+ void readNullableEmbeddedTypeWhenSourceDoesNotContainValues() {
+
+ org.bson.Document source = new org.bson.Document("_id", "id-1");
+
+ WithNullableEmbedded target = converter.read(WithNullableEmbedded.class, source);
+ assertThat(target.embeddableValue).isNull();
+ }
+
+ @Test // DATAMONGO-1902
+ void readEmptyEmbeddedTypeWhenSourceDoesNotContainValues() {
+
+ org.bson.Document source = new org.bson.Document("_id", "id-1");
+
+ WithEmptyEmbeddedType target = converter.read(WithEmptyEmbeddedType.class, source);
+ assertThat(target.embeddableValue).isNotNull();
+ }
+
+ @Test // DATAMONGO-1902
+ void readDeepNestedEmbeddedType() {
+
+ org.bson.Document source = new org.bson.Document("someValue", "root-level-value").append("nullableEmbedded",
+ new org.bson.Document("_id", "id-1").append("stringValue", "string-val") //
+ .append("listValue", Arrays.asList("list-val-1", "list-val-2")) //
+ .append("with-at-field-annotation", "@Field"));
+
+ WrapperAroundWithEmbedded target = converter.read(WrapperAroundWithEmbedded.class, source);
+
+ EmbeddableType embeddableValue = new EmbeddableType();
+ embeddableValue.stringValue = "string-val";
+ embeddableValue.listValue = Arrays.asList("list-val-1", "list-val-2");
+ embeddableValue.atFieldAnnotatedValue = "@Field";
+
+ assertThat(target.someValue).isEqualTo("root-level-value");
+ assertThat(target.nullableEmbedded).isNotNull();
+ assertThat(target.nullableEmbedded.embeddableValue).isEqualTo(embeddableValue);
+ }
+
+ @Test // DATAMONGO-1902
+ void readEmbeddedTypeWithComplexValue() {
+
+ org.bson.Document source = new org.bson.Document("_id", "id-1").append("address",
+ new org.bson.Document("street", "1007 Mountain Drive").append("city", "Gotham"));
+
+ WithNullableEmbedded target = converter.read(WithNullableEmbedded.class, source);
+
+ Address expected = new Address();
+ expected.city = "Gotham";
+ expected.street = "1007 Mountain Drive";
+
+ assertThat(target.embeddableValue.address) //
+ .isEqualTo(expected);
+ }
+
+ @Test // DATAMONGO-1902
+ void writeEmbeddedTypeWithComplexValue() {
+
+ WithNullableEmbedded source = new WithNullableEmbedded();
+ source.id = "id-1";
+ source.embeddableValue = new EmbeddableType();
+ source.embeddableValue.address = new Address();
+ source.embeddableValue.address.city = "Gotham";
+ source.embeddableValue.address.street = "1007 Mountain Drive";
+
+ org.bson.Document target = new org.bson.Document();
+ converter.write(source, target);
+
+ assertThat(target) //
+ .containsEntry("address", new org.bson.Document("street", "1007 Mountain Drive").append("city", "Gotham")) //
+ .doesNotContainKey("street") //
+ .doesNotContainKey("address.street") //
+ .doesNotContainKey("city") //
+ .doesNotContainKey("address.city");
+ }
+
static class GenericType {
T content;
}
@@ -2208,6 +2401,7 @@ interface InterfaceType {
}
+ @EqualsAndHashCode
static class Address implements InterfaceType {
String street;
String city;
@@ -2641,6 +2835,50 @@ static class WithExplicitTargetTypes {
Date dateAsObjectId;
}
+ static class WrapperAroundWithEmbedded {
+
+ String someValue;
+ WithNullableEmbedded nullableEmbedded;
+ WithEmptyEmbeddedType emptyEmbedded;
+ WithPrefixedNullableEmbedded prefixedEmbedded;
+ }
+
+ static class WithNullableEmbedded {
+
+ String id;
+
+ @Embedded.Nullable EmbeddableType embeddableValue;
+ }
+
+ static class WithPrefixedNullableEmbedded {
+
+ String id;
+
+ @Embedded.Nullable("prefix-") EmbeddableType embeddableValue;
+ }
+
+ static class WithEmptyEmbeddedType {
+
+ String id;
+
+ @Embedded.Empty EmbeddableType embeddableValue;
+ }
+
+ @EqualsAndHashCode
+ static class EmbeddableType {
+
+ String stringValue;
+ List listValue;
+
+ @Field("with-at-field-annotation") //
+ String atFieldAnnotatedValue;
+
+ @Transient //
+ String transientValue;
+
+ Address address;
+ }
+
static class ReturningAfterConvertCallback implements AfterConvertCallback {
@Override
@@ -2649,4 +2887,5 @@ public Person onAfterConvert(Person entity, org.bson.Document document, String c
return entity;
}
}
+
}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MongoExampleMapperUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MongoExampleMapperUnitTests.java
index d0eb09a40a..5ef5198ae5 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MongoExampleMapperUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MongoExampleMapperUnitTests.java
@@ -32,7 +32,6 @@
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
-
import org.springframework.data.annotation.Id;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.ExampleMatcher;
@@ -41,8 +40,10 @@
import org.springframework.data.mongodb.MongoDatabaseFactory;
import org.springframework.data.mongodb.core.convert.QueryMapperUnitTests.ClassWithGeoTypes;
import org.springframework.data.mongodb.core.convert.QueryMapperUnitTests.WithDBRef;
+import org.springframework.data.mongodb.core.index.Indexed;
import org.springframework.data.mongodb.core.mapping.DBRef;
import org.springframework.data.mongodb.core.mapping.Document;
+import org.springframework.data.mongodb.core.mapping.Embedded;
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.query.UntypedExampleMatcher;
@@ -454,6 +455,60 @@ public void untypedExampleShouldNotInferTypeRestriction() {
assertThat(document).doesNotContainKey("_class");
}
+ @Test // DATAMONGO-1902
+ void mapsEmbeddedType() {
+
+ WithEmbedded probe = new WithEmbedded();
+ probe.embeddableType = new EmbeddableType();
+ probe.embeddableType.atFieldAnnotatedValue = "@Field";
+ probe.embeddableType.stringValue = "string-value";
+
+ org.bson.Document document = mapper.getMappedExample(Example.of(probe, UntypedExampleMatcher.matching()));
+ assertThat(document).containsEntry("stringValue", "string-value").containsEntry("with-at-field-annotation",
+ "@Field");
+ }
+
+ @Test // DATAMONGO-1902
+ void mapsPrefixedEmbeddedType() {
+
+ WithEmbedded probe = new WithEmbedded();
+ probe.prefixedEmbeddableValue = new EmbeddableType();
+ probe.prefixedEmbeddableValue.atFieldAnnotatedValue = "@Field";
+ probe.prefixedEmbeddableValue.stringValue = "string-value";
+
+ org.bson.Document document = mapper.getMappedExample(Example.of(probe, UntypedExampleMatcher.matching()));
+ assertThat(document).containsEntry("prefix-stringValue", "string-value")
+ .containsEntry("prefix-with-at-field-annotation", "@Field");
+ }
+
+ @Test // DATAMONGO-1902
+ void mapsNestedEmbeddedType() {
+
+ WrapperAroundWithEmbedded probe = new WrapperAroundWithEmbedded();
+ probe.withEmbedded = new WithEmbedded();
+ probe.withEmbedded.embeddableType = new EmbeddableType();
+ probe.withEmbedded.embeddableType.atFieldAnnotatedValue = "@Field";
+ probe.withEmbedded.embeddableType.stringValue = "string-value";
+
+ org.bson.Document document = mapper.getMappedExample(Example.of(probe, UntypedExampleMatcher.matching()));
+ assertThat(document).containsEntry("withEmbedded.stringValue", "string-value")
+ .containsEntry("withEmbedded.with-at-field-annotation", "@Field");
+ }
+
+ @Test // DATAMONGO-1902
+ void mapsNestedPrefixedEmbeddedType() {
+
+ WrapperAroundWithEmbedded probe = new WrapperAroundWithEmbedded();
+ probe.withEmbedded = new WithEmbedded();
+ probe.withEmbedded.prefixedEmbeddableValue = new EmbeddableType();
+ probe.withEmbedded.prefixedEmbeddableValue.atFieldAnnotatedValue = "@Field";
+ probe.withEmbedded.prefixedEmbeddableValue.stringValue = "string-value";
+
+ org.bson.Document document = mapper.getMappedExample(Example.of(probe, UntypedExampleMatcher.matching()));
+ assertThat(document).containsEntry("withEmbedded.prefix-stringValue", "string-value")
+ .containsEntry("withEmbedded.prefix-with-at-field-annotation", "@Field");
+ }
+
static class FlatDocument {
@Id String id;
@@ -481,4 +536,29 @@ static class ReferenceDocument {
@Id String id;
String value;
}
+
+ @Document
+ static class WrapperAroundWithEmbedded {
+
+ String id;
+ WithEmbedded withEmbedded;
+ }
+
+ @Document
+ static class WithEmbedded {
+
+ String id;
+
+ @Embedded.Nullable EmbeddableType embeddableType;
+ @Embedded.Nullable("prefix-") EmbeddableType prefixedEmbeddableValue;
+ }
+
+ static class EmbeddableType {
+
+ @Indexed String stringValue;
+
+ @Indexed //
+ @Field("with-at-field-annotation") //
+ String atFieldAnnotatedValue;
+ }
}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java
index 162693cd6b..ce29c2a42b 100755
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java
@@ -37,6 +37,7 @@
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.annotation.Id;
+import org.springframework.data.annotation.Transient;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.geo.Point;
@@ -45,9 +46,9 @@
import org.springframework.data.mongodb.core.Person;
import org.springframework.data.mongodb.core.geo.GeoJsonPoint;
import org.springframework.data.mongodb.core.geo.GeoJsonPolygon;
-import org.springframework.data.mongodb.core.mapping.BasicMongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.DBRef;
import org.springframework.data.mongodb.core.mapping.Document;
+import org.springframework.data.mongodb.core.mapping.Embedded;
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.mapping.FieldType;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
@@ -389,7 +390,7 @@ void convertsDBRefWithExistsQuery() {
Query query = query(where("reference").exists(false));
- BasicMongoPersistentEntity> entity = context.getRequiredPersistentEntity(WithDBRef.class);
+ MongoPersistentEntity> entity = context.getRequiredPersistentEntity(WithDBRef.class);
org.bson.Document mappedObject = mapper.getMappedObject(query.getQueryObject(), entity);
org.bson.Document reference = getAsDocument(mappedObject, "reference");
@@ -405,7 +406,7 @@ void convertsNestedDBRefsCorrectly() {
Query query = query(where("someString").is("foo").andOperator(where("reference").in(reference)));
- BasicMongoPersistentEntity> entity = context.getRequiredPersistentEntity(WithDBRef.class);
+ MongoPersistentEntity> entity = context.getRequiredPersistentEntity(WithDBRef.class);
org.bson.Document mappedObject = mapper.getMappedObject(query.getQueryObject(), entity);
assertThat(mappedObject).containsEntry("someString", "foo");
@@ -446,7 +447,7 @@ void shouldExcludeDBRefAssociation() {
Query query = query(where("someString").is("foo"));
query.fields().exclude("reference");
- BasicMongoPersistentEntity> entity = context.getRequiredPersistentEntity(WithDBRef.class);
+ MongoPersistentEntity> entity = context.getRequiredPersistentEntity(WithDBRef.class);
org.bson.Document queryResult = mapper.getMappedObject(query.getQueryObject(), entity);
org.bson.Document fieldsResult = mapper.getMappedObject(query.getFieldsObject(), entity);
@@ -457,7 +458,7 @@ void shouldExcludeDBRefAssociation() {
@Test // DATAMONGO-686
void queryMapperShouldNotChangeStateInGivenQueryObjectWhenIdConstrainedByInList() {
- BasicMongoPersistentEntity> persistentEntity = context.getRequiredPersistentEntity(Sample.class);
+ MongoPersistentEntity> persistentEntity = context.getRequiredPersistentEntity(Sample.class);
String idPropertyName = persistentEntity.getIdProperty().getName();
org.bson.Document queryObject = query(where(idPropertyName).in("42")).getQueryObject();
@@ -515,7 +516,7 @@ void queryMapperShouldMapDBRefPropertyIfNestedInDocument() {
@Test // DATAMONGO-773
void queryMapperShouldBeAbleToProcessQueriesThatIncludeDbRefFields() {
- BasicMongoPersistentEntity> persistentEntity = context.getRequiredPersistentEntity(WithDBRef.class);
+ MongoPersistentEntity> persistentEntity = context.getRequiredPersistentEntity(WithDBRef.class);
Query qry = query(where("someString").is("abc"));
qry.fields().include("reference");
@@ -781,7 +782,8 @@ void exampleWithCombinedCriteriaShouldBeMappedCorrectly() {
Query query = query(byExample(probe).and("listOfItems").exists(true));
org.bson.Document document = mapper.getMappedObject(query.getQueryObject(), context.getPersistentEntity(Foo.class));
- assertThat(document).containsEntry("embedded\\._id", "conflux").containsEntry("my_items", new org.bson.Document("$exists", true));
+ assertThat(document).containsEntry("embedded\\._id", "conflux").containsEntry("my_items",
+ new org.bson.Document("$exists", true));
}
@Test // DATAMONGO-1988
@@ -1011,6 +1013,184 @@ void shouldParseNestedKeywordWithArgumentMatchingTheSourceEntitiesConstructorCor
assertThat(target).isEqualTo(org.bson.Document.parse("{\"$text\" : { \"$search\" : \"test\" }}"));
}
+ @Test // DATAMONGO-1902
+ void rendersQueryOnEmbeddedObjectCorrectly() {
+
+ EmbeddableType embeddableType = new EmbeddableType();
+ embeddableType.stringValue = "test";
+
+ Query source = query(Criteria.where("embeddableValue").is(embeddableType));
+
+ org.bson.Document target = mapper.getMappedObject(source.getQueryObject(),
+ context.getPersistentEntity(WithEmbedded.class));
+
+ assertThat(target).isEqualTo(new org.bson.Document("stringValue", "test"));
+ }
+
+ @Test // DATAMONGO-1902
+ void rendersQueryOnEmbeddedCorrectly() {
+
+ Query source = query(Criteria.where("embeddableValue.stringValue").is("test"));
+
+ org.bson.Document target = mapper.getMappedObject(source.getQueryObject(),
+ context.getPersistentEntity(WithEmbedded.class));
+
+ assertThat(target).isEqualTo(new org.bson.Document("stringValue", "test"));
+ }
+
+ @Test // DATAMONGO-1902
+ void rendersQueryOnPrefixedEmbeddedCorrectly() {
+
+ Query source = query(Criteria.where("embeddableValue.stringValue").is("test"));
+
+ org.bson.Document target = mapper.getMappedObject(source.getQueryObject(),
+ context.getPersistentEntity(WithPrefixedEmbedded.class));
+
+ assertThat(target).isEqualTo(new org.bson.Document("prefix-stringValue", "test"));
+ }
+
+ @Test // DATAMONGO-1902
+ void rendersQueryOnNestedEmbeddedObjectCorrectly() {
+
+ EmbeddableType embeddableType = new EmbeddableType();
+ embeddableType.stringValue = "test";
+ Query source = query(Criteria.where("withEmbedded.embeddableValue").is(embeddableType));
+
+ org.bson.Document target = mapper.getMappedObject(source.getQueryObject(),
+ context.getPersistentEntity(WrapperAroundWithEmbedded.class));
+
+ assertThat(target).isEqualTo(new org.bson.Document("withEmbedded", new org.bson.Document("stringValue", "test")));
+ }
+
+ @Test // DATAMONGO-1902
+ void rendersQueryOnNestedPrefixedEmbeddedObjectCorrectly() {
+
+ EmbeddableType embeddableType = new EmbeddableType();
+ embeddableType.stringValue = "test";
+ Query source = query(Criteria.where("withPrefixedEmbedded.embeddableValue").is(embeddableType));
+
+ org.bson.Document target = mapper.getMappedObject(source.getQueryObject(),
+ context.getPersistentEntity(WrapperAroundWithEmbedded.class));
+
+ assertThat(target)
+ .isEqualTo(new org.bson.Document("withPrefixedEmbedded", new org.bson.Document("prefix-stringValue", "test")));
+ }
+
+ @Test // DATAMONGO-1902
+ void rendersQueryOnNestedEmbeddedCorrectly() {
+
+ Query source = query(Criteria.where("withEmbedded.embeddableValue.stringValue").is("test"));
+
+ org.bson.Document target = mapper.getMappedObject(source.getQueryObject(),
+ context.getPersistentEntity(WrapperAroundWithEmbedded.class));
+
+ assertThat(target).isEqualTo(new org.bson.Document("withEmbedded.stringValue", "test"));
+ }
+
+ @Test // DATAMONGO-1902
+ void rendersQueryOnNestedPrefixedEmbeddedCorrectly() {
+
+ Query source = query(Criteria.where("withPrefixedEmbedded.embeddableValue.stringValue").is("test"));
+
+ org.bson.Document target = mapper.getMappedObject(source.getQueryObject(),
+ context.getPersistentEntity(WrapperAroundWithEmbedded.class));
+
+ assertThat(target).isEqualTo(new org.bson.Document("withPrefixedEmbedded.prefix-stringValue", "test"));
+ }
+
+ @Test // DATAMONGO-1902
+ void sortByEmbeddableIsEmpty() {
+
+ Query query = new Query().with(Sort.by("embeddableValue"));
+
+ org.bson.Document document = mapper.getMappedSort(query.getSortObject(),
+ context.getPersistentEntity(WithEmbedded.class));
+
+ assertThat(document).isEqualTo(
+ new org.bson.Document("stringValue", 1).append("listValue", 1).append("with-at-field-annotation", 1));
+ }
+
+ @Test // DATAMONGO-1902
+ void sortByEmbeddableValue() {
+
+ // atFieldAnnotatedValue
+ Query query = new Query().with(Sort.by("embeddableValue.stringValue"));
+
+ org.bson.Document document = mapper.getMappedSort(query.getSortObject(),
+ context.getPersistentEntity(WithEmbedded.class));
+
+ assertThat(document).isEqualTo(new org.bson.Document("stringValue", 1));
+ }
+
+ @Test // DATAMONGO-1902
+ void sortByEmbeddableValueWithFieldAnnotation() {
+
+ Query query = new Query().with(Sort.by("embeddableValue.atFieldAnnotatedValue"));
+
+ org.bson.Document document = mapper.getMappedSort(query.getSortObject(),
+ context.getPersistentEntity(WithEmbedded.class));
+
+ assertThat(document).isEqualTo(new org.bson.Document("with-at-field-annotation", 1));
+ }
+
+ @Test // DATAMONGO-1902
+ void sortByPrefixedEmbeddableValueWithFieldAnnotation() {
+
+ Query query = new Query().with(Sort.by("embeddableValue.atFieldAnnotatedValue"));
+
+ org.bson.Document document = mapper.getMappedSort(query.getSortObject(),
+ context.getPersistentEntity(WithPrefixedEmbedded.class));
+
+ assertThat(document).isEqualTo(new org.bson.Document("prefix-with-at-field-annotation", 1));
+ }
+
+ @Test // DATAMONGO-1902
+ void sortByNestedEmbeddableValueWithFieldAnnotation() {
+
+ Query query = new Query().with(Sort.by("withEmbedded.embeddableValue.atFieldAnnotatedValue"));
+
+ org.bson.Document document = mapper.getMappedSort(query.getSortObject(),
+ context.getPersistentEntity(WrapperAroundWithEmbedded.class));
+
+ assertThat(document).isEqualTo(new org.bson.Document("withEmbedded.with-at-field-annotation", 1));
+ }
+
+ @Test // DATAMONGO-1902
+ void sortByNestedPrefixedEmbeddableValueWithFieldAnnotation() {
+
+ Query query = new Query().with(Sort.by("withPrefixedEmbedded.embeddableValue.atFieldAnnotatedValue"));
+
+ org.bson.Document document = mapper.getMappedSort(query.getSortObject(),
+ context.getPersistentEntity(WrapperAroundWithEmbedded.class));
+
+ assertThat(document).isEqualTo(new org.bson.Document("withPrefixedEmbedded.prefix-with-at-field-annotation", 1));
+ }
+
+ @Test // DATAMONGO-1902
+ void projectOnEmbeddableUsesFields() {
+
+ Query query = new Query();
+ query.fields().include("embeddableValue");
+
+ org.bson.Document document = mapper.getMappedFields(query.getFieldsObject(),
+ context.getPersistentEntity(WithEmbedded.class));
+
+ assertThat(document).isEqualTo(
+ new org.bson.Document("stringValue", 1).append("listValue", 1).append("with-at-field-annotation", 1));
+ }
+
+ @Test // DATAMONGO-1902
+ void projectOnEmbeddableValue() {
+
+ Query query = new Query();
+ query.fields().include("embeddableValue.stringValue");
+
+ org.bson.Document document = mapper.getMappedFields(query.getFieldsObject(),
+ context.getPersistentEntity(WithEmbedded.class));
+
+ assertThat(document).isEqualTo(new org.bson.Document("stringValue", 1));
+ }
+
class WithDeepArrayNesting {
List level0;
@@ -1194,4 +1374,38 @@ public WithSingleStringArgConstructor(String value) {
this.value = value;
}
}
+
+ static class WrapperAroundWithEmbedded {
+
+ String someValue;
+ WithEmbedded withEmbedded;
+ WithPrefixedEmbedded withPrefixedEmbedded;
+ }
+
+ static class WithEmbedded {
+
+ String id;
+
+ @Embedded.Nullable EmbeddableType embeddableValue;
+ }
+
+ static class WithPrefixedEmbedded {
+
+ String id;
+
+ @Embedded.Nullable("prefix-") EmbeddableType embeddableValue;
+ }
+
+ static class EmbeddableType {
+
+ String stringValue;
+ List listValue;
+
+ @Field("with-at-field-annotation") //
+ String atFieldAnnotatedValue;
+
+ @Transient //
+ String transientValue;
+ }
+
}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/UpdateMapperUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/UpdateMapperUnitTests.java
index df0b8e68a0..996309dcdf 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/UpdateMapperUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/UpdateMapperUnitTests.java
@@ -37,9 +37,9 @@
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
-
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.annotation.Id;
+import org.springframework.data.annotation.Transient;
import org.springframework.data.convert.CustomConversions;
import org.springframework.data.convert.WritingConverter;
import org.springframework.data.domain.Sort;
@@ -48,6 +48,7 @@
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mongodb.MongoDatabaseFactory;
import org.springframework.data.mongodb.core.DocumentTestUtils;
+import org.springframework.data.mongodb.core.mapping.Embedded;
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.query.Criteria;
@@ -1089,6 +1090,75 @@ void mappingShouldAllowNestedPositionParameterWithIdentifierWhenFieldHasExplicit
assertThat(mappedUpdate).isEqualTo(new Document("$set", new Document("aliased.$[element].value", 10)));
}
+ @Test // DATAMONGO-1902
+ void mappingShouldConsiderValueOfEmbeddedType() {
+
+ Update update = new Update().set("embeddableValue.stringValue", "updated");
+
+ Document mappedUpdate = mapper.getMappedObject(update.getUpdateObject(),
+ context.getPersistentEntity(WithEmbedded.class));
+
+ assertThat(mappedUpdate).isEqualTo(new Document("$set", new Document("stringValue", "updated")));
+ }
+
+ @Test // DATAMONGO-1902
+ void mappingShouldConsiderEmbeddedType() {
+
+ EmbeddableType embeddableType = new EmbeddableType();
+ embeddableType.stringValue = "updated";
+ embeddableType.listValue = Arrays.asList("val-1", "val-2");
+ Update update = new Update().set("embeddableValue", embeddableType);
+
+ Document mappedUpdate = mapper.getMappedObject(update.getUpdateObject(),
+ context.getPersistentEntity(WithEmbedded.class));
+
+ assertThat(mappedUpdate).isEqualTo(new Document("$set",
+ new Document("stringValue", "updated").append("listValue", Arrays.asList("val-1", "val-2"))));
+ }
+
+ @Test // DATAMONGO-1902
+ void mappingShouldConsiderValueOfPrefixedEmbeddedType() {
+
+ Update update = new Update().set("embeddableValue.stringValue", "updated");
+
+ Document mappedUpdate = mapper.getMappedObject(update.getUpdateObject(),
+ context.getPersistentEntity(WithPrefixedEmbedded.class));
+
+ assertThat(mappedUpdate).isEqualTo(new Document("$set", new Document("prefix-stringValue", "updated")));
+ }
+
+ @Test // DATAMONGO-1902
+ void mappingShouldConsiderPrefixedEmbeddedType() {
+
+ EmbeddableType embeddableType = new EmbeddableType();
+ embeddableType.stringValue = "updated";
+ embeddableType.listValue = Arrays.asList("val-1", "val-2");
+
+ Update update = new Update().set("embeddableValue", embeddableType);
+
+ Document mappedUpdate = mapper.getMappedObject(update.getUpdateObject(),
+ context.getPersistentEntity(WithPrefixedEmbedded.class));
+
+ assertThat(mappedUpdate).isEqualTo(new Document("$set",
+ new Document("prefix-stringValue", "updated").append("prefix-listValue", Arrays.asList("val-1", "val-2"))));
+ }
+
+ @Test // DATAMONGO-1902
+ void mappingShouldConsiderNestedPrefixedEmbeddedType() {
+
+ EmbeddableType embeddableType = new EmbeddableType();
+ embeddableType.stringValue = "updated";
+ embeddableType.listValue = Arrays.asList("val-1", "val-2");
+
+ Update update = new Update().set("withPrefixedEmbedded.embeddableValue", embeddableType);
+
+ Document mappedUpdate = mapper.getMappedObject(update.getUpdateObject(),
+ context.getPersistentEntity(WrapperAroundWithEmbedded.class));
+
+ assertThat(mappedUpdate).isEqualTo(new Document("$set", new Document("withPrefixedEmbedded",
+ new Document("prefix-stringValue", "updated").append("prefix-listValue", Arrays.asList("val-1", "val-2")))));
+ }
+
static class DomainTypeWrappingConcreteyTypeHavingListOfInterfaceTypeAttributes {
ListModelWrapper concreteTypeWithListAttributeOfInterfaceType;
}
@@ -1418,4 +1488,37 @@ static class TypeWithFieldNameThatCannotBeDecapitalized {
}
+ static class WrapperAroundWithEmbedded {
+
+ String someValue;
+ WithEmbedded withEmbedded;
+ WithPrefixedEmbedded withPrefixedEmbedded;
+ }
+
+ static class WithEmbedded {
+
+ String id;
+
+ @Embedded.Nullable EmbeddableType embeddableValue;
+ }
+
+ static class WithPrefixedEmbedded {
+
+ String id;
+
+ @Embedded.Nullable("prefix-") EmbeddableType embeddableValue;
+ }
+
+ static class EmbeddableType {
+
+ String stringValue;
+ List listValue;
+
+ @Field("with-at-field-annotation") //
+ String atFieldAnnotatedValue;
+
+ @Transient //
+ String transientValue;
+ }
+
}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolverUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolverUnitTests.java
index 1076e0ac13..7f14080d62 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolverUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolverUnitTests.java
@@ -22,7 +22,8 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
-import java.util.Collections;
+import java.util.Arrays;
+import java.util.LinkedHashSet;
import java.util.List;
import org.junit.Test;
@@ -30,6 +31,7 @@
import org.junit.runners.Suite;
import org.junit.runners.Suite.SuiteClasses;
import org.springframework.core.annotation.AliasFor;
+import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.annotation.Id;
import org.springframework.data.geo.Point;
import org.springframework.data.mongodb.core.DocumentTestUtils;
@@ -42,6 +44,7 @@
import org.springframework.data.mongodb.core.mapping.BasicMongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.DBRef;
import org.springframework.data.mongodb.core.mapping.Document;
+import org.springframework.data.mongodb.core.mapping.Embedded;
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.mapping.Language;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
@@ -1282,6 +1285,44 @@ public void hashedIndexAndIndexViaComposedAnnotation() {
});
}
+ @Test // DATAMONGO-1902
+ public void resolvedIndexOnEmbeddedType() {
+
+ List indexDefinitions = prepareMappingContextAndResolveIndexForType(WithEmbedded.class,
+ EmbeddableType.class);
+
+ assertThat(indexDefinitions).hasSize(2);
+ assertThat(indexDefinitions.get(0)).satisfies(it -> {
+ assertThat(it.getIndexKeys()).containsEntry("stringValue", 1);
+ });
+ assertThat(indexDefinitions.get(1)).satisfies(it -> {
+ assertThat(it.getIndexKeys()).containsEntry("with-at-field-annotation", 1);
+ });
+ }
+
+ @Test // DATAMONGO-1902
+ public void resolvedIndexOnNestedEmbeddedType() {
+
+ List indexDefinitions = prepareMappingContextAndResolveIndexForType(
+ WrapperAroundWithEmbedded.class, WithEmbedded.class, EmbeddableType.class);
+
+ assertThat(indexDefinitions).hasSize(2);
+ assertThat(indexDefinitions.get(0)).satisfies(it -> {
+ assertThat(it.getIndexKeys()).containsEntry("withEmbedded.stringValue", 1);
+ });
+ assertThat(indexDefinitions.get(1)).satisfies(it -> {
+ assertThat(it.getIndexKeys()).containsEntry("withEmbedded.with-at-field-annotation", 1);
+ });
+ }
+
+ @Test // DATAMONGO-1902
+ public void errorsOnIndexOnEmbedded() {
+
+ assertThatExceptionOfType(InvalidDataAccessApiUsageException.class)
+ .isThrownBy(() -> prepareMappingContextAndResolveIndexForType(InvalidIndexOnEmbedded.class));
+
+ }
+
@Document
class MixedIndexRoot {
@@ -1472,6 +1513,41 @@ static class OuterDocumentReferingToIndexedPropertyViaDifferentNonCyclingPaths {
AlternatePathToNoCycleButIndenticallNamedPropertiesDeeplyNestedDocument path2;
}
+ @Document
+ static class WrapperAroundWithEmbedded {
+
+ String id;
+ WithEmbedded withEmbedded;
+ }
+
+ @Document
+ static class WithEmbedded {
+
+ String id;
+
+ @Embedded.Nullable EmbeddableType embeddableType;
+ }
+
+ @Document
+ class InvalidIndexOnEmbedded {
+
+ @Indexed //
+ @Embedded.Nullable //
+ EmbeddableType embeddableType;
+
+ }
+
+ static class EmbeddableType {
+
+ @Indexed String stringValue;
+
+ List listValue;
+
+ @Indexed //
+ @Field("with-at-field-annotation") //
+ String atFieldAnnotatedValue;
+ }
+
static class AlternatePathToNoCycleButIndenticallNamedPropertiesDeeplyNestedDocument {
NoCycleButIndenticallNamedPropertiesDeeplyNested propertyWithIndexedStructure;
}
@@ -1521,17 +1597,17 @@ class WithComposedHashedIndexAndIndex {
}
}
- private static List prepareMappingContextAndResolveIndexForType(Class> type) {
+ private static List prepareMappingContextAndResolveIndexForType(Class>... types) {
- MongoMappingContext mappingContext = prepareMappingContext(type);
+ MongoMappingContext mappingContext = prepareMappingContext(types);
MongoPersistentEntityIndexResolver resolver = new MongoPersistentEntityIndexResolver(mappingContext);
- return resolver.resolveIndexForEntity(mappingContext.getRequiredPersistentEntity(type));
+ return resolver.resolveIndexForEntity(mappingContext.getRequiredPersistentEntity(types[0]));
}
- private static MongoMappingContext prepareMappingContext(Class> type) {
+ private static MongoMappingContext prepareMappingContext(Class>... types) {
MongoMappingContext mappingContext = new MongoMappingContext();
- mappingContext.setInitialEntitySet(Collections.singleton(type));
+ mappingContext.setInitialEntitySet(new LinkedHashSet<>(Arrays.asList(types)));
mappingContext.initialize();
return mappingContext;
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntityUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntityUnitTests.java
index 0fb76ddb0b..28d5123502 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntityUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntityUnitTests.java
@@ -30,10 +30,11 @@
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
-
import org.springframework.context.ApplicationContext;
import org.springframework.core.annotation.AliasFor;
+import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.mapping.MappingException;
+import org.springframework.data.mongodb.core.index.Indexed;
import org.springframework.data.mongodb.core.query.Collation;
import org.springframework.data.spel.ExtensionAwareEvaluationContextProvider;
import org.springframework.data.spel.spi.EvaluationContextExtension;
@@ -63,8 +64,7 @@ void subclassInheritsAtDocumentAnnotation() {
@Test
void evaluatesSpELExpression() {
- MongoPersistentEntity entity = new BasicMongoPersistentEntity<>(
- ClassTypeInformation.from(Company.class));
+ MongoPersistentEntity entity = new BasicMongoPersistentEntity<>(ClassTypeInformation.from(Company.class));
assertThat(entity.getCollection()).isEqualTo("35");
}
@@ -364,16 +364,13 @@ class WithSimpleCollation {}
class WithDocumentCollation {}
@Sharded
- private
- class WithDefaultShardKey {}
+ private class WithDefaultShardKey {}
@Sharded("country")
- private
- class WithSingleShardKey {}
+ private class WithSingleShardKey {}
@Sharded({ "country", "userid" })
- private
- class WithMultiShardKey {}
+ private class WithMultiShardKey {}
static class SampleExtension implements EvaluationContextExtension {
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentPropertyUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentPropertyUnitTests.java
index 40fd6e2147..3fb4f59084 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentPropertyUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentPropertyUnitTests.java
@@ -187,7 +187,7 @@ public void shouldConsiderComposedAnnotationsForFields() {
public void honorsFieldOrderWhenIteratingOverProperties() {
MongoMappingContext context = new MongoMappingContext();
- BasicMongoPersistentEntity> entity = context.getPersistentEntity(Sample.class);
+ MongoPersistentEntity> entity = context.getPersistentEntity(Sample.class);
List properties = new ArrayList<>();
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/MongoMappingContextUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/MongoMappingContextUnitTests.java
index c7d42df28a..a7e454c52a 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/MongoMappingContextUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/MongoMappingContextUnitTests.java
@@ -111,7 +111,7 @@ void doesNotConsiderOverrridenAccessorANewField() {
void mappingContextShouldAcceptClassWithImplicitIdProperty() {
MongoMappingContext context = new MongoMappingContext();
- BasicMongoPersistentEntity> pe = context.getRequiredPersistentEntity(ClassWithImplicitId.class);
+ MongoPersistentEntity> pe = context.getRequiredPersistentEntity(ClassWithImplicitId.class);
assertThat(pe).isNotNull();
assertThat(pe.isIdProperty(pe.getRequiredPersistentProperty("id"))).isTrue();
@@ -121,7 +121,7 @@ void mappingContextShouldAcceptClassWithImplicitIdProperty() {
void mappingContextShouldAcceptClassWithExplicitIdProperty() {
MongoMappingContext context = new MongoMappingContext();
- BasicMongoPersistentEntity> pe = context.getRequiredPersistentEntity(ClassWithExplicitId.class);
+ MongoPersistentEntity> pe = context.getRequiredPersistentEntity(ClassWithExplicitId.class);
assertThat(pe).isNotNull();
assertThat(pe.isIdProperty(pe.getRequiredPersistentProperty("myId"))).isTrue();
@@ -131,7 +131,7 @@ void mappingContextShouldAcceptClassWithExplicitIdProperty() {
void mappingContextShouldAcceptClassWithExplicitAndImplicitIdPropertyByGivingPrecedenceToExplicitIdProperty() {
MongoMappingContext context = new MongoMappingContext();
- BasicMongoPersistentEntity> pe = context.getRequiredPersistentEntity(ClassWithExplicitIdAndImplicitId.class);
+ MongoPersistentEntity> pe = context.getRequiredPersistentEntity(ClassWithExplicitIdAndImplicitId.class);
assertThat(pe).isNotNull();
}
@@ -166,7 +166,7 @@ void shouldNotCreateEntityForEnum() {
MongoMappingContext context = new MongoMappingContext();
- BasicMongoPersistentEntity> entity = context.getRequiredPersistentEntity(ClassWithChronoUnit.class);
+ MongoPersistentEntity> entity = context.getRequiredPersistentEntity(ClassWithChronoUnit.class);
assertThat(entity.getPersistentProperty("unit").isEntity()).isFalse();
assertThat(context.hasPersistentEntityFor(ChronoUnit.class)).isFalse();
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapreduce/MapReduceTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapreduce/MapReduceTests.java
index 32468fde17..5672ee2d65 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapreduce/MapReduceTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapreduce/MapReduceTests.java
@@ -79,19 +79,6 @@ protected void cleanDb() {
template.getMongoDbFactory().getMongoDatabase("jmr1-out-db").drop();
}
- @Test // DATADOC-7
- @Ignore
- public void testForDocs() {
-
- createMapReduceData();
- MapReduceResults results = mongoTemplate.mapReduce("jmr1", MAP_FUNCTION, REDUCE_FUNCTION,
- ValueObject.class);
-
- for (ValueObject valueObject : results) {
- System.out.println(valueObject);
- }
- }
-
@Test // DATAMONGO-260
public void testIssue260() {
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java
index 7055a73e25..849418abfd 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java
@@ -1361,6 +1361,41 @@ void findWithMoreThan10Arguments() {
void spelExpressionArgumentsGetReevaluatedOnEveryInvocation() {
assertThat(repository.findWithSpelByFirstnameForSpELExpressionWithParameterIndexOnly("Dave")).containsExactly(dave);
- assertThat(repository.findWithSpelByFirstnameForSpELExpressionWithParameterIndexOnly("Carter")).containsExactly(carter);
+ assertThat(repository.findWithSpelByFirstnameForSpELExpressionWithParameterIndexOnly("Carter"))
+ .containsExactly(carter);
+ }
+
+ @Test // DATAMONGO-1902
+ void findByValueInsideEmbedded() {
+
+ Person bart = new Person("bart", "simpson");
+ User user = new User();
+ user.setUsername("bartman");
+ user.setId("84r1m4n");
+ bart.setEmbeddedUser(user);
+
+ operations.save(bart);
+
+ List result = repository.findByEmbeddedUserUsername(user.getUsername());
+
+ assertThat(result).hasSize(1);
+ assertThat(result.get(0).getId().equals(bart.getId()));
+ }
+
+ @Test // DATAMONGO-1902
+ void findByEmbedded() {
+
+ Person bart = new Person("bart", "simpson");
+ User user = new User();
+ user.setUsername("bartman");
+ user.setId("84r1m4n");
+ bart.setEmbeddedUser(user);
+
+ operations.save(bart);
+
+ List result = repository.findByEmbeddedUser(user);
+
+ assertThat(result).hasSize(1);
+ assertThat(result.get(0).getId().equals(bart.getId()));
}
}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Person.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Person.java
index 2786505f58..24d42f27a2 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Person.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Person.java
@@ -27,6 +27,7 @@
import org.springframework.data.mongodb.core.index.Indexed;
import org.springframework.data.mongodb.core.mapping.DBRef;
import org.springframework.data.mongodb.core.mapping.Document;
+import org.springframework.data.mongodb.core.mapping.Embedded;
import org.springframework.data.mongodb.core.mapping.Field;
/**
@@ -70,6 +71,9 @@ public enum Sex {
Credentials credentials;
+ @Embedded.Nullable(prefix = "u") //
+ User embeddedUser;
+
public Person() {
this(null, null);
@@ -296,6 +300,14 @@ public List getSkills() {
return skills;
}
+ public User getEmbeddedUser() {
+ return embeddedUser;
+ }
+
+ public void setEmbeddedUser(User embeddedUser) {
+ this.embeddedUser = embeddedUser;
+ }
+
/*
* (non-Javadoc)
*
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java
index 1d340bd196..52041c270f 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java
@@ -401,4 +401,8 @@ Page findByCustomQueryLastnameAndAddressStreetInList(String lastname, Li
Person findPersonByManyArguments(String firstname, String lastname, String email, Integer age, Sex sex,
Date createdAt, List skills, String street, String zipCode, //
String city, UUID uniqueId, String username, String password);
+
+ List findByEmbeddedUserUsername(String username);
+
+ List findByEmbeddedUser(User user);
}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/RepositoryIndexCreationIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/RepositoryIndexCreationIntegrationTests.java
index e98dc8cb2e..2955d1f3d3 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/RepositoryIndexCreationIntegrationTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/RepositoryIndexCreationIntegrationTests.java
@@ -78,6 +78,7 @@ public void testname() {
assertHasIndexForField(indexInfo, "lastname");
assertHasIndexForField(indexInfo, "firstname");
+ assertHasIndexForField(indexInfo, "add");
}
private static void assertHasIndexForField(List indexInfo, String... fields) {
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/IndexEnsuringQueryCreationListenerUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/IndexEnsuringQueryCreationListenerUnitTests.java
index 103da191ff..a8dcf08f81 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/IndexEnsuringQueryCreationListenerUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/IndexEnsuringQueryCreationListenerUnitTests.java
@@ -66,7 +66,7 @@ void setUp() {
partTreeQuery = mock(PartTreeMongoQuery.class, Answers.RETURNS_MOCKS);
when(partTreeQuery.getTree()).thenReturn(partTree);
- when(provider.indexOps(anyString())).thenReturn(indexOperations);
+ when(provider.indexOps(anyString(), any())).thenReturn(indexOperations);
when(queryMethod.getEntityInformation()).thenReturn(entityInformation);
when(entityInformation.getCollectionName()).thenReturn("persons");
}
diff --git a/src/main/asciidoc/new-features.adoc b/src/main/asciidoc/new-features.adoc
index 305e349cdd..9cf28cb30d 100644
--- a/src/main/asciidoc/new-features.adoc
+++ b/src/main/asciidoc/new-features.adoc
@@ -1,6 +1,11 @@
[[new-features]]
= New & Noteworthy
+[[new-features.3.2]]
+== What's New in Spring Data MongoDB 3.2
+
+* Support for <> to unwrap nested objects into the parent `Document`.
+
[[new-features.3.1]]
== What's New in Spring Data MongoDB 3.1
diff --git a/src/main/asciidoc/reference/embedded-documents.adoc b/src/main/asciidoc/reference/embedded-documents.adoc
new file mode 100644
index 0000000000..2d83706a0a
--- /dev/null
+++ b/src/main/asciidoc/reference/embedded-documents.adoc
@@ -0,0 +1,382 @@
+[[embedded-entities]]
+== Embedded Types
+
+Embedded entities are used to design value objects in your Java domain model whose properties are flattened out into the parent's MongoDB Document.
+
+[[embedded-entities.mapping]]
+=== Embedded Types Mapping
+
+Consider the following domain model where `User.name` is annotated with `@Embedded`.
+The `@Embedded` annotation signals that all properties of `UserName` should be unwrapped into the `user` document that owns the `name` property.
+
+.Sample Code of embedding objects
+====
+[source,java]
+----
+class User {
+
+ @Id
+ String userId;
+
+ @Embedded(onEmpty = USE_NULL) <1>
+ UserName name;
+}
+
+class UserName {
+
+ String firstname;
+
+ String lastname;
+
+}
+----
+
+[source,json]
+----
+{
+ "_id" : "1da2ba06-3ba7",
+ "firstname" : "Emma",
+ "lastname" : "Frost"
+}
+----
+<1> When loading the `name` property its value is set to `null` if both `firstname` and `lastname` are either `null` or not present.
+By using `onEmpty=USE_EMPTY` an empty `UserName`, with potential `null` value for its properties, will be created.
+====
+
+For less verbose embeddable type declarations use `@Embedded.Nullable` and `@Embedded.Empty` instead `@Embedded(onEmpty = USE_NULL)` and `@Embedded(onEmpty = USE_EMPTY)`.
+Both annotations are meta-annotated with JSR-305 `@javax.annotation.Nonnull` to aid with nullability inspections.
+
+[WARNING]
+====
+It is possible to use complex types within an embedded object.
+However, those must not be, nor contain embedded fields themselves.
+====
+
+[[embedded-entities.mapping.field-names]]
+=== Embedded Types field names
+
+A value object can be embedded multiple times by using the optional `prefix` attribute of the `@Embedded` annotation.
+By dosing so the chosen prefix is prepended to each property or `@Field("…")` name in the embedded object.
+Please note that values will overwrite each other if multiple properties render to the same field name.
+
+.Sample Code of embedded object with name prefix
+====
+[source,java]
+----
+class User {
+
+ @Id
+ String userId;
+
+ @Embedded.Nullable(prefix = "u_") <1>
+ UserName name;
+
+ @Embedded.Nullable(prefix = "a_") <2>
+ UserName name;
+}
+
+class UserName {
+
+ String firstname;
+
+ String lastname;
+}
+----
+
+[source,json]
+----
+{
+ "_id" : "a6a805bd-f95f",
+ "u_firstname" : "Jean", <1>
+ "u_lastname" : "Grey",
+ "a_firstname" : "Something", <2>
+ "a_lastname" : "Else"
+}
+----
+<1> All properties of `UserName` are prefixed with `u_`.
+<2> All properties of `UserName` are prefixed with `a_`.
+====
+
+While combining the `@Field` annotation with `@Embedded` on the very same property does not make sense and therefore leads to an error.
+It is a totally valid approach to use `@Field` on any of the embedded types properties.
+
+.Sample Code embedded object with `@Field` annotation
+====
+[source,java]
+----
+public class User {
+
+ @Id
+ private String userId;
+
+ @Embedded.Nullable(prefix = "u-") <1>
+ UserName name;
+}
+
+public class UserName {
+
+ @Field("first-name") <2>
+ private String firstname;
+
+ @Field("last-name")
+ private String lastname;
+}
+----
+
+[source,json]
+----
+{
+ "_id" : "2647f7b9-89da",
+ "u-first-name" : "Barbara", <2>
+ "u-last-name" : "Gordon"
+}
+----
+<1> All properties of `UserName` are prefixed with `u-`.
+<2> Final field names are a result of concatenating `@Embedded(prefix)` and `@Field(name)`.
+====
+
+[[embedded-entities.queries]]
+=== Query on Embedded Objects
+
+Defining queries on embedded properties is possible on type- as well as field-level as the provided `Criteria` is matched against the domain type.
+Prefixes and potential custom field names will be considered when rendering the actual query.
+Use the property name of the embedded object to match against all contained fields as shown in the sample below.
+
+.Query on embedded object
+====
+[source,java]
+----
+UserName userName = new UserName("Carol", "Danvers")
+Query findByUserName = query(where("name").is(userName));
+User user = template.findOne(findByUserName, User.class);
+----
+
+[source,json]
+----
+db.collection.find({
+ "firstname" : "Carol",
+ "lastname" : "Danvers"
+})
+----
+====
+
+It is also possible to address any field of the embedded object directly using its property name as shown in the snippet below.
+
+.Query on field of embedded object
+====
+[source,java]
+----
+Query findByUserFirstName = query(where("name.firstname").is("Shuri"));
+List users = template.findAll(findByUserFirstName, User.class);
+----
+
+[source,json]
+----
+db.collection.find({
+ "firstname" : "Shuri"
+})
+----
+====
+
+[[embedded-entities.queries.sort]]
+==== Sort by embedded field.
+
+Fields of embedded objects can be used for sorting via their property path as shown in the sample below.
+
+.Sort on embedded field
+====
+[source,java]
+----
+Query findByUserLastName = query(where("name.lastname").is("Romanoff"));
+List user = template.findAll(findByUserName.withSort(Sort.by("name.firstname")), User.class);
+----
+
+[source,json]
+----
+db.collection.find({
+ "lastname" : "Romanoff"
+}).sort({ "firstname" : 1 })
+----
+====
+
+[NOTE]
+====
+Though possible, using the embedded object itself as sort criteria includes all of its fields in unpredictable order and may result in inaccurate ordering.
+====
+
+[[embedded-entities.queries.project]]
+==== Field projection on embedded objects
+
+Fields of embedded objects can be subject for projection either as a whole or via single fields as shown in the samples below.
+
+.Project on embedded object.
+====
+[source,java]
+----
+Query findByUserLastName = query(where("name.firstname").is("Gamora"));
+findByUserLastName.fields().include("name"); <1>
+List user = template.findAll(findByUserName, User.class);
+----
+
+[source,json]
+----
+db.collection.find({
+ "lastname" : "Gamora"
+},
+{
+ "firstname" : 1,
+ "lastname" : 1
+})
+----
+<1> A field projection on an embedded object includes all of its properties.
+====
+
+.Project on a field of an embedded object.
+====
+[source,java]
+----
+Query findByUserLastName = query(where("name.lastname").is("Smoak"));
+findByUserLastName.fields().include("name.firstname"); <1>
+List user = template.findAll(findByUserName, User.class);
+----
+
+[source,json]
+----
+db.collection.find({
+ "lastname" : "Smoak"
+},
+{
+ "firstname" : 1
+})
+----
+<1> A field projection on an embedded object includes all of its properties.
+====
+
+[[embedded-entities.queries.by-example]]
+==== Query By Example on embedded object.
+
+Embedded objects can be used within an `Example` probe just as any other type.
+Please review the <> section, to learn more about this feature.
+
+[[embedded-entities.queries.repository]]
+==== Repository Queries on embedded objects.
+
+The `Repository` abstraction allows deriving queries on fields of embedded objects as well as the entire object.
+
+.Repository queries on embedded objects.
+====
+[source,java]
+----
+interface UserRepository extends CrudRepository {
+
+ List findByName(UserName username); <1>
+
+ List findByNameFirstname(String firstname); <2>
+}
+----
+<1> Matches against all fields of the embedded object.
+<2> Matches against the `firstname`.
+====
+
+[NOTE]
+====
+Index creation for embedded objects is suspended even if the repository `create-query-indexes` namespace attribute is set to `true`.
+====
+
+[[embedded-entities.update]]
+=== Update on Embedded Objects
+
+Embedded objects can be updated as any other object that is part of the domain model.
+The mapping layer takes care of flattening embedded structures into their surroundings.
+It is possible to update single attributes of the embedded object as well as the entire value as shown in the examples below.
+
+.Update a single field of an embedded object.
+====
+[source,java]
+----
+Update update = new Update().set("name.firstname", "Janet");
+template.update(User.class).matching(where("id").is("Wasp"))
+ .apply(update).first()
+----
+
+[source,json]
+----
+db.collection.update({
+ "_id" : "Wasp"
+},
+{
+ "$set" { "firstname" : "Janet" }
+},
+{ ... }
+)
+----
+====
+
+.Update an embedded object.
+====
+[source,java]
+----
+Update update = new Update().set("name", new Name("Janet", "van Dyne"));
+template.update(User.class).matching(where("id").is("Wasp"))
+ .apply(update).first()
+----
+
+[source,json]
+----
+db.collection.update({
+ "_id" : "Wasp"
+},
+{
+ "$set" {
+ "firstname" : "Janet",
+ "lastname" : "van Dyne",
+ }
+},
+{ ... }
+)
+----
+====
+
+[[embedded-entities.aggregations]]
+=== Aggregations on Embedded Objects
+
+The <> will attempt to map embedded values of typed aggregations.
+Please make sure to work with the property path including the embedded wrapper object when referencing one of its values.
+Other than that no special action is required.
+
+[[embedded-entities.indexes]]
+=== Index on Embedded Objects
+
+It is possible to attach the `@Indexed` annotation to properties of an embedded type just as it is done with regular objects.
+It is not possible to use `@Indexed` along with the `@Embedded` annotation on the owning property.
+
+====
+[source,java]
+----
+public class User {
+
+ @Id
+ private String userId;
+
+ @Embedded(onEmpty = USE_NULL)
+ UserName name; <1>
+
+ // Invalid -> InvalidDataAccessApiUsageException
+ @Indexed <2>
+ @Embedded(onEmpty = USE_Empty)
+ Address address;
+}
+
+public class UserName {
+
+ private String firstname;
+
+ @Indexed
+ private String lastname; <1>
+}
+----
+<1> Index created for `lastname` in `users` collection.
+<2> Invalid `@Indexed` usage along with `@Embedded`
+====
+
+
diff --git a/src/main/asciidoc/reference/mapping.adoc b/src/main/asciidoc/reference/mapping.adoc
index 98ecf074d5..697eba8d20 100644
--- a/src/main/asciidoc/reference/mapping.adoc
+++ b/src/main/asciidoc/reference/mapping.adoc
@@ -833,4 +833,6 @@ Events are fired throughout the lifecycle of the mapping process. This is descri
Declaring these beans in your Spring ApplicationContext causes them to be invoked whenever the event is dispatched.
+include::embedded-documents.adoc[]
+
include::mongo-custom-conversions.adoc[]