diff --git a/pom.xml b/pom.xml
index 5225d1e3df..9074164110 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
org.springframework.data
spring-data-mongodb-parent
- 4.2.0-SNAPSHOT
+ 4.2.x-4379-SNAPSHOT
pom
Spring Data MongoDB
diff --git a/spring-data-mongodb-benchmarks/pom.xml b/spring-data-mongodb-benchmarks/pom.xml
index 2de4b6b635..1e0a519d25 100644
--- a/spring-data-mongodb-benchmarks/pom.xml
+++ b/spring-data-mongodb-benchmarks/pom.xml
@@ -7,7 +7,7 @@
org.springframework.data
spring-data-mongodb-parent
- 4.2.0-SNAPSHOT
+ 4.2.x-4379-SNAPSHOT
../pom.xml
diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml
index 060a6d0dd9..fb47df3eb8 100644
--- a/spring-data-mongodb-distribution/pom.xml
+++ b/spring-data-mongodb-distribution/pom.xml
@@ -15,7 +15,7 @@
org.springframework.data
spring-data-mongodb-parent
- 4.2.0-SNAPSHOT
+ 4.2.x-4379-SNAPSHOT
../pom.xml
diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml
index 921254ca44..b99b0b3502 100644
--- a/spring-data-mongodb/pom.xml
+++ b/spring-data-mongodb/pom.xml
@@ -13,7 +13,7 @@
org.springframework.data
spring-data-mongodb-parent
- 4.2.0-SNAPSHOT
+ 4.2.x-4379-SNAPSHOT
../pom.xml
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOperationContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOperationContext.java
index 3fdc1b125c..0ceb1c84d7 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOperationContext.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOperationContext.java
@@ -23,6 +23,7 @@
import org.bson.codecs.configuration.CodecRegistry;
import org.springframework.beans.BeanUtils;
import org.springframework.data.mongodb.CodecRegistryProvider;
+import org.springframework.data.mongodb.MongoCollectionUtils;
import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -79,7 +80,30 @@ default Document getMappedObject(Document document) {
FieldReference getReference(String name);
/**
- * Returns the {@link Fields} exposed by the type. May be a {@literal class} or an {@literal interface}. The default
+ * Obtain the target field name for a given field/type combination.
+ *
+ * @param type The type containing the field.
+ * @param field The property/field name
+ * @return never {@literal null}.
+ * @since 4.2
+ */
+ default String getMappedFieldName(Class> type, String field) {
+ return field;
+ }
+
+ /**
+ * Obtain the collection name for a given {@link Class type} combination.
+ *
+ * @param type
+ * @return never {@literal null}.
+ * @since 4.2
+ */
+ default String getCollection(Class> type) {
+ return MongoCollectionUtils.getPreferredCollectionName(type);
+ }
+
+ /**
+ * Returns the {@link Fields} exposed by the type. Can be a {@literal class} or an {@literal interface}. The default
* implementation uses {@link BeanUtils#getPropertyDescriptors(Class) property descriptors} discover fields from a
* {@link Class}.
*
@@ -109,7 +133,7 @@ default Fields getFields(Class> type) {
/**
* This toggle allows the {@link AggregationOperationContext context} to use any given field name without checking for
- * its existence. Typically the {@link AggregationOperationContext} fails when referencing unknown fields, those that
+ * its existence. Typically, the {@link AggregationOperationContext} fails when referencing unknown fields, those that
* are not present in one of the previous stages or the input source, throughout the pipeline.
*
* @return a more relaxed {@link AggregationOperationContext}.
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GraphLookupOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GraphLookupOperation.java
index a2fe238627..02b3fb4ea8 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GraphLookupOperation.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GraphLookupOperation.java
@@ -46,7 +46,7 @@ public class GraphLookupOperation implements InheritsFieldsAggregationOperation
private static final Set> ALLOWED_START_TYPES = new HashSet>(
Arrays.> asList(AggregationExpression.class, String.class, Field.class, Document.class));
- private final String from;
+ private final Object from;
private final List startWith;
private final Field connectFrom;
private final Field connectTo;
@@ -55,7 +55,7 @@ public class GraphLookupOperation implements InheritsFieldsAggregationOperation
private final @Nullable Field depthField;
private final @Nullable CriteriaDefinition restrictSearchWithMatch;
- private GraphLookupOperation(String from, List startWith, Field connectFrom, Field connectTo, Field as,
+ private GraphLookupOperation(Object from, List startWith, Field connectFrom, Field connectTo, Field as,
@Nullable Long maxDepth, @Nullable Field depthField, @Nullable CriteriaDefinition restrictSearchWithMatch) {
this.from = from;
@@ -82,7 +82,7 @@ public Document toDocument(AggregationOperationContext context) {
Document graphLookup = new Document();
- graphLookup.put("from", from);
+ graphLookup.put("from", getCollectionName(context));
List mappedStartWith = new ArrayList<>(startWith.size());
@@ -99,7 +99,7 @@ public Document toDocument(AggregationOperationContext context) {
graphLookup.put("startWith", mappedStartWith.size() == 1 ? mappedStartWith.iterator().next() : mappedStartWith);
- graphLookup.put("connectFromField", connectFrom.getTarget());
+ graphLookup.put("connectFromField", getForeignFieldName(context));
graphLookup.put("connectToField", connectTo.getTarget());
graphLookup.put("as", as.getName());
@@ -118,6 +118,16 @@ public Document toDocument(AggregationOperationContext context) {
return new Document(getOperator(), graphLookup);
}
+ String getCollectionName(AggregationOperationContext context) {
+ return from instanceof Class> type ? context.getCollection(type) : from.toString();
+ }
+
+ String getForeignFieldName(AggregationOperationContext context) {
+
+ return from instanceof Class> type ? context.getMappedFieldName(type, connectFrom.getTarget())
+ : connectFrom.getTarget();
+ }
+
@Override
public String getOperator() {
return "$graphLookup";
@@ -128,7 +138,7 @@ public ExposedFields getFields() {
List fields = new ArrayList<>(2);
fields.add(new ExposedField(as, true));
- if(depthField != null) {
+ if (depthField != null) {
fields.add(new ExposedField(depthField, true));
}
return ExposedFields.from(fields.toArray(new ExposedField[0]));
@@ -146,6 +156,17 @@ public interface FromBuilder {
* @return never {@literal null}.
*/
StartWithBuilder from(String collectionName);
+
+ /**
+ * Use the given type to determine name of the foreign collection and map
+ * {@link ConnectFromBuilder#connectFrom(String)} against it to consider eventually present
+ * {@link org.springframework.data.mongodb.core.mapping.Field} annotations.
+ *
+ * @param type must not be {@literal null}.
+ * @return never {@literal null}.
+ * @since 4.2
+ */
+ StartWithBuilder from(Class> type);
}
/**
@@ -218,7 +239,7 @@ public interface ConnectToBuilder {
static final class GraphLookupOperationFromBuilder
implements FromBuilder, StartWithBuilder, ConnectFromBuilder, ConnectToBuilder {
- private @Nullable String from;
+ private @Nullable Object from;
private @Nullable List extends Object> startWith;
private @Nullable String connectFrom;
@@ -231,6 +252,14 @@ public StartWithBuilder from(String collectionName) {
return this;
}
+ @Override
+ public StartWithBuilder from(Class> type) {
+
+ Assert.notNull(type, "Type must not be null");
+ this.from = type;
+ return this;
+ }
+
@Override
public ConnectFromBuilder startWith(String... fieldReferences) {
@@ -321,7 +350,7 @@ public GraphLookupOperationBuilder connectTo(String fieldName) {
*/
public static final class GraphLookupOperationBuilder {
- private final String from;
+ private final Object from;
private final List startWith;
private final Field connectFrom;
private final Field connectTo;
@@ -329,7 +358,7 @@ public static final class GraphLookupOperationBuilder {
private @Nullable Field depthField;
private @Nullable CriteriaDefinition restrictSearchWithMatch;
- protected GraphLookupOperationBuilder(String from, List extends Object> startWith, String connectFrom,
+ protected GraphLookupOperationBuilder(Object from, List extends Object> startWith, String connectFrom,
String connectTo) {
this.from = from;
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/LookupOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/LookupOperation.java
index 44d0f1569e..3cfda35cef 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/LookupOperation.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/LookupOperation.java
@@ -39,7 +39,7 @@
*/
public class LookupOperation implements FieldsExposingAggregationOperation, InheritsFieldsAggregationOperation {
- private final String from;
+ private Object from;
@Nullable //
private final Field localField;
@@ -97,6 +97,22 @@ public LookupOperation(String from, @Nullable Let let, AggregationPipeline pipel
*/
public LookupOperation(String from, @Nullable Field localField, @Nullable Field foreignField, @Nullable Let let,
@Nullable AggregationPipeline pipeline, Field as) {
+ this((Object) from, localField, foreignField, let, pipeline, as);
+ }
+
+ /**
+ * Creates a new {@link LookupOperation} for the given combination of {@link Field}s and {@link AggregationPipeline
+ * pipeline}.
+ *
+ * @param from must not be {@literal null}. Can be eiter the target collection name or a {@link Class}.
+ * @param localField can be {@literal null} if {@literal pipeline} is present.
+ * @param foreignField can be {@literal null} if {@literal pipeline} is present.
+ * @param let can be {@literal null} if {@literal localField} and {@literal foreignField} are present.
+ * @param as must not be {@literal null}.
+ * @since 4.2
+ */
+ private LookupOperation(Object from, @Nullable Field localField, @Nullable Field foreignField, @Nullable Let let,
+ @Nullable AggregationPipeline pipeline, Field as) {
Assert.notNull(from, "From must not be null");
if (pipeline == null) {
@@ -125,12 +141,14 @@ public Document toDocument(AggregationOperationContext context) {
Document lookupObject = new Document();
- lookupObject.append("from", from);
+ lookupObject.append("from", getCollectionName(context));
+
if (localField != null) {
lookupObject.append("localField", localField.getTarget());
}
+
if (foreignField != null) {
- lookupObject.append("foreignField", foreignField.getTarget());
+ lookupObject.append("foreignField", getForeignFieldName(context));
}
if (let != null) {
lookupObject.append("let", let.toDocument(context).get("$let", Document.class).get("vars"));
@@ -144,6 +162,16 @@ public Document toDocument(AggregationOperationContext context) {
return new Document(getOperator(), lookupObject);
}
+ String getCollectionName(AggregationOperationContext context) {
+ return from instanceof Class> type ? context.getCollection(type) : from.toString();
+ }
+
+ String getForeignFieldName(AggregationOperationContext context) {
+
+ return from instanceof Class> type ? context.getMappedFieldName(type, foreignField.getTarget())
+ : foreignField.getTarget();
+ }
+
@Override
public String getOperator() {
return "$lookup";
@@ -158,16 +186,28 @@ public static FromBuilder newLookup() {
return new LookupOperationBuilder();
}
- public static interface FromBuilder {
+ public interface FromBuilder {
/**
* @param name the collection in the same database to perform the join with, must not be {@literal null} or empty.
* @return never {@literal null}.
*/
LocalFieldBuilder from(String name);
+
+ /**
+ * Use the given type to determine name of the foreign collection and map
+ * {@link ForeignFieldBuilder#foreignField(String)} against it to consider eventually present
+ * {@link org.springframework.data.mongodb.core.mapping.Field} annotations.
+ *
+ * @param type the type of the target collection in the same database to perform the join with, must not be
+ * {@literal null}.
+ * @return never {@literal null}.
+ * @since 4.2
+ */
+ LocalFieldBuilder from(Class> type);
}
- public static interface LocalFieldBuilder extends PipelineBuilder {
+ public interface LocalFieldBuilder extends PipelineBuilder {
/**
* @param name the field from the documents input to the {@code $lookup} stage, must not be {@literal null} or
@@ -177,7 +217,7 @@ public static interface LocalFieldBuilder extends PipelineBuilder {
ForeignFieldBuilder localField(String name);
}
- public static interface ForeignFieldBuilder {
+ public interface ForeignFieldBuilder {
/**
* @param name the field from the documents in the {@code from} collection, must not be {@literal null} or empty.
@@ -246,7 +286,7 @@ default AsBuilder pipeline(AggregationOperation... stages) {
LookupOperation as(String name);
}
- public static interface AsBuilder extends PipelineBuilder {
+ public interface AsBuilder extends PipelineBuilder {
/**
* @param name the name of the new array field to add to the input documents, must not be {@literal null} or empty.
@@ -264,7 +304,7 @@ public static interface AsBuilder extends PipelineBuilder {
public static final class LookupOperationBuilder
implements FromBuilder, LocalFieldBuilder, ForeignFieldBuilder, AsBuilder {
- private @Nullable String from;
+ private @Nullable Object from;
private @Nullable Field localField;
private @Nullable Field foreignField;
private @Nullable ExposedField as;
@@ -288,6 +328,14 @@ public LocalFieldBuilder from(String name) {
return this;
}
+ @Override
+ public LocalFieldBuilder from(Class> type) {
+
+ Assert.notNull(type, "'From' must not be null");
+ from = type;
+ return this;
+ }
+
@Override
public AsBuilder foreignField(String name) {
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/PrefixingDelegatingAggregationOperationContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/PrefixingDelegatingAggregationOperationContext.java
index 4d0ee5ff6c..27b1ab6737 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/PrefixingDelegatingAggregationOperationContext.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/PrefixingDelegatingAggregationOperationContext.java
@@ -30,9 +30,8 @@
/**
* {@link AggregationOperationContext} implementation prefixing non-command keys on root level with the given prefix.
- * Useful when mapping fields to domain specific types while having to prefix keys for query purpose.
- *
- * Fields to be excluded from prefixing my be added to a {@literal denylist}.
+ * Useful when mapping fields to domain specific types while having to prefix keys for query purpose.
+ * Fields to be excluded from prefixing can be added to a {@literal denylist}.
*
* @author Christoph Strobl
* @author Mark Paluch
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/TypeBasedAggregationOperationContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/TypeBasedAggregationOperationContext.java
index fd54514cbb..efd135d328 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/TypeBasedAggregationOperationContext.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/TypeBasedAggregationOperationContext.java
@@ -92,6 +92,20 @@ public FieldReference getReference(String name) {
return getReferenceFor(field(name));
}
+ @Override
+ public String getCollection(Class> type) {
+
+ MongoPersistentEntity> persistentEntity = mappingContext.getPersistentEntity(type);
+ return persistentEntity != null ? persistentEntity.getCollection() : AggregationOperationContext.super.getCollection(type);
+ }
+
+ @Override
+ public String getMappedFieldName(Class> type, String field) {
+
+ PersistentPropertyPath persistentPropertyPath = mappingContext.getPersistentPropertyPath(field, type);
+ return persistentPropertyPath.getLeafProperty().getFieldName();
+ }
+
@Override
public Fields getFields(Class> type) {
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationTestUtils.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationTestUtils.java
new file mode 100644
index 0000000000..35e45915f5
--- /dev/null
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationTestUtils.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2023. 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.aggregation;
+
+import org.springframework.data.mapping.context.MappingContext;
+import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
+import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver;
+import org.springframework.data.mongodb.core.convert.QueryMapper;
+import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
+import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
+import org.springframework.data.mongodb.test.util.MongoTestMappingContext;
+
+/**
+ * @author Christoph Strobl
+ */
+public final class AggregationTestUtils {
+
+ public static AggregationContextBuilder strict(Class> type) {
+
+ AggregationContextBuilder builder = new AggregationContextBuilder<>();
+ builder.strict = true;
+ return builder.forType(type);
+ }
+
+ public static AggregationContextBuilder relaxed(Class> type) {
+
+ AggregationContextBuilder builder = new AggregationContextBuilder<>();
+ builder.strict = false;
+ return builder.forType(type);
+ }
+
+ public static class AggregationContextBuilder {
+
+ Class> targetType;
+ MappingContext extends MongoPersistentEntity>, MongoPersistentProperty> mappingContext;
+ QueryMapper queryMapper;
+ boolean strict;
+
+ public AggregationContextBuilder forType(Class> type) {
+
+ this.targetType = type;
+ return (AggregationContextBuilder) this;
+ }
+
+ public AggregationContextBuilder using(
+ MappingContext extends MongoPersistentEntity>, MongoPersistentProperty> mappingContext) {
+
+ this.mappingContext = mappingContext;
+ return this;
+ }
+
+ public AggregationContextBuilder using(QueryMapper queryMapper) {
+
+ this.queryMapper = queryMapper;
+ return this;
+ }
+
+ public T ctx() {
+ //
+ if (targetType == null) {
+ return (T) Aggregation.DEFAULT_CONTEXT;
+ }
+
+ MappingContext extends MongoPersistentEntity>, MongoPersistentProperty> ctx = mappingContext != null
+ ? mappingContext
+ : MongoTestMappingContext.newTestContext().init();
+ QueryMapper qm = queryMapper != null ? queryMapper
+ : new QueryMapper(new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, ctx));
+ return (T) (strict ? new TypeBasedAggregationOperationContext(targetType, ctx, qm)
+ : new RelaxedTypeBasedAggregationOperationContext(targetType, ctx, qm));
+ }
+ }
+}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/GraphLookupOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/GraphLookupOperationUnitTests.java
index 5280b603f6..6d85b31ffb 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/GraphLookupOperationUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/GraphLookupOperationUnitTests.java
@@ -22,6 +22,7 @@
import org.bson.Document;
import org.junit.jupiter.api.Test;
import org.springframework.data.mongodb.core.Person;
+import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.query.Criteria;
/**
@@ -34,7 +35,7 @@ public class GraphLookupOperationUnitTests {
@Test // DATAMONGO-1551
public void rejectsNullFromCollection() {
- assertThatIllegalArgumentException().isThrownBy(() -> GraphLookupOperation.builder().from(null));
+ assertThatIllegalArgumentException().isThrownBy(() -> GraphLookupOperation.builder().from((String) null));
}
@Test // DATAMONGO-1551
@@ -158,4 +159,59 @@ public void depthFieldShouldUseTargetFieldInsteadOfAlias() {
assertThat(document).containsEntry("$graphLookup.depthField", "foo.bar");
}
+
+ @Test // GH-4379
+ void unmappedLookupWithFromExtractedFromType() {
+
+ GraphLookupOperation graphLookupOperation = GraphLookupOperation.builder() //
+ .from(Employee.class) //
+ .startWith(LiteralOperators.Literal.asLiteral("hello")) //
+ .connectFrom("manager") //
+ .connectTo("name") //
+ .as("reportingHierarchy");
+
+ assertThat(graphLookupOperation.toDocument(Aggregation.DEFAULT_CONTEXT)).isEqualTo("""
+ { $graphLookup:
+ {
+ from: "employee",
+ startWith : { $literal : "hello" },
+ connectFromField: "manager",
+ connectToField: "name",
+ as: "reportingHierarchy"
+ }
+ }}
+ """);
+ }
+
+ @Test // GH-4379
+ void mappedLookupWithFromExtractedFromType() {
+
+ GraphLookupOperation graphLookupOperation = GraphLookupOperation.builder() //
+ .from(Employee.class) //
+ .startWith(LiteralOperators.Literal.asLiteral("hello")) //
+ .connectFrom("manager") //
+ .connectTo("name") //
+ .as("reportingHierarchy");
+
+ assertThat(graphLookupOperation.toDocument(AggregationTestUtils.strict(Employee.class).ctx())).isEqualTo("""
+ { $graphLookup:
+ {
+ from: "employees",
+ startWith : { $literal : "hello" },
+ connectFromField: "reportsTo",
+ connectToField: "name",
+ as: "reportingHierarchy"
+ }
+ }}
+ """);
+ }
+
+ @org.springframework.data.mongodb.core.mapping.Document("employees")
+ static class Employee {
+
+ String id;
+
+ @Field("reportsTo")
+ String manager;
+ }
}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/LookupOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/LookupOperationUnitTests.java
index 45b63763cf..1127ad6eac 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/LookupOperationUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/LookupOperationUnitTests.java
@@ -25,6 +25,7 @@
import org.bson.Document;
import org.junit.jupiter.api.Test;
import org.springframework.data.mongodb.core.DocumentTestUtils;
+import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.query.Criteria;
/**
@@ -92,7 +93,7 @@ private Document extractDocumentFromLookupOperation(LookupOperation lookupOperat
@Test // DATAMONGO-1326
public void builderRejectsNullFromField() {
- assertThatIllegalArgumentException().isThrownBy(() -> LookupOperation.newLookup().from(null));
+ assertThatIllegalArgumentException().isThrownBy(() -> LookupOperation.newLookup().from((String) null));
}
@Test // DATAMONGO-1326
@@ -195,10 +196,10 @@ void buildsLookupWithJustPipeline() {
void buildsLookupWithLocalAndForeignFieldAsWellAsLetAndPipeline() {
LookupOperation lookupOperation = Aggregation.lookup().from("restaurants") //
- .localField("restaurant_name")
- .foreignField("name")
+ .localField("restaurant_name") //
+ .foreignField("name") //
.let(newVariable("orders_drink").forField("drink")) //
- .pipeline(match(ctx -> new Document("$expr", new Document("$in", List.of("$$orders_drink", "$beverages")))))
+ .pipeline(match(ctx -> new Document("$expr", new Document("$in", List.of("$$orders_drink", "$beverages"))))) //
.as("matches");
assertThat(lookupOperation.toDocument(Aggregation.DEFAULT_CONTEXT)).isEqualTo("""
@@ -216,4 +217,54 @@ void buildsLookupWithLocalAndForeignFieldAsWellAsLetAndPipeline() {
}}
""");
}
+
+ @Test // GH-4379
+ void unmappedLookupWithFromExtractedFromType() {
+
+ LookupOperation lookupOperation = Aggregation.lookup().from(Restaurant.class) //
+ .localField("restaurant_name") //
+ .foreignField("name") //
+ .as("restaurants");
+
+ assertThat(lookupOperation.toDocument(Aggregation.DEFAULT_CONTEXT)).isEqualTo("""
+ { $lookup:
+ {
+ from: "restaurant",
+ localField: "restaurant_name",
+ foreignField: "name",
+ as: "restaurants"
+ }
+ }}
+ """);
+ }
+
+ @Test // GH-4379
+ void mappedLookupWithFromExtractedFromType() {
+
+ LookupOperation lookupOperation = Aggregation.lookup().from(Restaurant.class) //
+ .localField("restaurant_name") //
+ .foreignField("name") //
+ .as("restaurants");
+
+
+ assertThat(lookupOperation.toDocument(AggregationTestUtils.strict(Restaurant.class).ctx())).isEqualTo("""
+ { $lookup:
+ {
+ from: "sites",
+ localField: "restaurant_name",
+ foreignField: "rs_name",
+ as: "restaurants"
+ }
+ }}
+ """);
+ }
+
+ @org.springframework.data.mongodb.core.mapping.Document("sites")
+ static class Restaurant {
+
+ String id;
+
+ @Field("rs_name") //
+ String name;
+ }
}