Compare commits

..

2 Commits

Author SHA1 Message Date
Christoph Strobl
cd44432f81 Include & Exclude paths in project aggregation stage. 2023-07-06 14:19:11 +02:00
Christoph Strobl
f42e63d613 Prepare issue branch. 2023-07-06 10:27:35 +02:00
14 changed files with 140 additions and 343 deletions

View File

@@ -5,7 +5,7 @@
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>4.2.x-4379-SNAPSHOT</version>
<version>4.2.x-4428-SNAPSHOT</version>
<packaging>pom</packaging>
<name>Spring Data MongoDB</name>

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>4.2.x-4379-SNAPSHOT</version>
<version>4.2.x-4428-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@@ -15,7 +15,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>4.2.x-4379-SNAPSHOT</version>
<version>4.2.x-4428-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@@ -13,7 +13,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>4.2.x-4379-SNAPSHOT</version>
<version>4.2.x-4428-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@@ -23,7 +23,6 @@ import org.bson.Document;
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;
@@ -80,30 +79,7 @@ public interface AggregationOperationContext extends CodecRegistryProvider {
FieldReference getReference(String name);
/**
* 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
* Returns the {@link Fields} exposed by the type. May be a {@literal class} or an {@literal interface}. The default
* implementation uses {@link BeanUtils#getPropertyDescriptors(Class) property descriptors} discover fields from a
* {@link Class}.
*
@@ -133,7 +109,7 @@ public interface AggregationOperationContext extends CodecRegistryProvider {
/**
* 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}.

View File

@@ -46,7 +46,7 @@ public class GraphLookupOperation implements InheritsFieldsAggregationOperation
private static final Set<Class<?>> ALLOWED_START_TYPES = new HashSet<Class<?>>(
Arrays.<Class<?>> asList(AggregationExpression.class, String.class, Field.class, Document.class));
private final Object from;
private final String from;
private final List<Object> 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(Object from, List<Object> startWith, Field connectFrom, Field connectTo, Field as,
private GraphLookupOperation(String from, List<Object> startWith, Field connectFrom, Field connectTo, Field as,
@Nullable Long maxDepth, @Nullable Field depthField, @Nullable CriteriaDefinition restrictSearchWithMatch) {
this.from = from;
@@ -82,7 +82,7 @@ public class GraphLookupOperation implements InheritsFieldsAggregationOperation
Document graphLookup = new Document();
graphLookup.put("from", getCollectionName(context));
graphLookup.put("from", from);
List<Object> mappedStartWith = new ArrayList<>(startWith.size());
@@ -99,7 +99,7 @@ public class GraphLookupOperation implements InheritsFieldsAggregationOperation
graphLookup.put("startWith", mappedStartWith.size() == 1 ? mappedStartWith.iterator().next() : mappedStartWith);
graphLookup.put("connectFromField", getForeignFieldName(context));
graphLookup.put("connectFromField", connectFrom.getTarget());
graphLookup.put("connectToField", connectTo.getTarget());
graphLookup.put("as", as.getName());
@@ -118,16 +118,6 @@ public class GraphLookupOperation implements InheritsFieldsAggregationOperation
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";
@@ -138,7 +128,7 @@ public class GraphLookupOperation implements InheritsFieldsAggregationOperation
List<ExposedField> 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]));
@@ -156,17 +146,6 @@ public class GraphLookupOperation implements InheritsFieldsAggregationOperation
* @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);
}
/**
@@ -239,7 +218,7 @@ public class GraphLookupOperation implements InheritsFieldsAggregationOperation
static final class GraphLookupOperationFromBuilder
implements FromBuilder, StartWithBuilder, ConnectFromBuilder, ConnectToBuilder {
private @Nullable Object from;
private @Nullable String from;
private @Nullable List<? extends Object> startWith;
private @Nullable String connectFrom;
@@ -252,14 +231,6 @@ public class GraphLookupOperation implements InheritsFieldsAggregationOperation
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) {
@@ -350,7 +321,7 @@ public class GraphLookupOperation implements InheritsFieldsAggregationOperation
*/
public static final class GraphLookupOperationBuilder {
private final Object from;
private final String from;
private final List<Object> startWith;
private final Field connectFrom;
private final Field connectTo;
@@ -358,7 +329,7 @@ public class GraphLookupOperation implements InheritsFieldsAggregationOperation
private @Nullable Field depthField;
private @Nullable CriteriaDefinition restrictSearchWithMatch;
protected GraphLookupOperationBuilder(Object from, List<? extends Object> startWith, String connectFrom,
protected GraphLookupOperationBuilder(String from, List<? extends Object> startWith, String connectFrom,
String connectTo) {
this.from = from;

View File

@@ -39,7 +39,7 @@ import org.springframework.util.Assert;
*/
public class LookupOperation implements FieldsExposingAggregationOperation, InheritsFieldsAggregationOperation {
private Object from;
private final String from;
@Nullable //
private final Field localField;
@@ -97,22 +97,6 @@ public class LookupOperation implements FieldsExposingAggregationOperation, Inhe
*/
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) {
@@ -141,14 +125,12 @@ public class LookupOperation implements FieldsExposingAggregationOperation, Inhe
Document lookupObject = new Document();
lookupObject.append("from", getCollectionName(context));
lookupObject.append("from", from);
if (localField != null) {
lookupObject.append("localField", localField.getTarget());
}
if (foreignField != null) {
lookupObject.append("foreignField", getForeignFieldName(context));
lookupObject.append("foreignField", foreignField.getTarget());
}
if (let != null) {
lookupObject.append("let", let.toDocument(context).get("$let", Document.class).get("vars"));
@@ -162,16 +144,6 @@ public class LookupOperation implements FieldsExposingAggregationOperation, Inhe
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";
@@ -186,28 +158,16 @@ public class LookupOperation implements FieldsExposingAggregationOperation, Inhe
return new LookupOperationBuilder();
}
public interface FromBuilder {
public static 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 interface LocalFieldBuilder extends PipelineBuilder {
public static interface LocalFieldBuilder extends PipelineBuilder {
/**
* @param name the field from the documents input to the {@code $lookup} stage, must not be {@literal null} or
@@ -217,7 +177,7 @@ public class LookupOperation implements FieldsExposingAggregationOperation, Inhe
ForeignFieldBuilder localField(String name);
}
public interface ForeignFieldBuilder {
public static interface ForeignFieldBuilder {
/**
* @param name the field from the documents in the {@code from} collection, must not be {@literal null} or empty.
@@ -286,7 +246,7 @@ public class LookupOperation implements FieldsExposingAggregationOperation, Inhe
LookupOperation as(String name);
}
public interface AsBuilder extends PipelineBuilder {
public static 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.
@@ -304,7 +264,7 @@ public class LookupOperation implements FieldsExposingAggregationOperation, Inhe
public static final class LookupOperationBuilder
implements FromBuilder, LocalFieldBuilder, ForeignFieldBuilder, AsBuilder {
private @Nullable Object from;
private @Nullable String from;
private @Nullable Field localField;
private @Nullable Field foreignField;
private @Nullable ExposedField as;
@@ -328,14 +288,6 @@ public class LookupOperation implements FieldsExposingAggregationOperation, Inhe
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) {

View File

@@ -30,8 +30,9 @@ import org.springframework.lang.Nullable;
/**
* {@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. <br />
* Fields to be excluded from prefixing can be added to a {@literal denylist}.
* Useful when mapping fields to domain specific types while having to prefix keys for query purpose.
* <br />
* Fields to be excluded from prefixing my be added to a {@literal denylist}.
*
* @author Christoph Strobl
* @author Mark Paluch

View File

@@ -167,6 +167,46 @@ public class ProjectionOperation implements FieldsExposingAggregationOperation {
return new ProjectionOperation(this.projections, projections);
}
/**
* @param path
* @return
* @since 4.2
*/
public ProjectionOperation andIncludePath(String path) {
return andIncludePaths(new String[] { path });
}
/**
* @param paths
* @return
* @since 4.2
*/
public ProjectionOperation andIncludePaths(String... paths) {
List<FieldProjection> projections = FieldProjection.from(Fields.fields(paths), 1);
return new ProjectionOperation(this.projections, projections);
}
/**
* @param path
* @return
* @since 4.2
*/
public ProjectionOperation andExcludePath(String path) {
return andExcludePaths(new String[] { path });
}
/**
* @param paths
* @return
* @since 4.2
*/
public ProjectionOperation andExcludePaths(String... paths) {
List<FieldProjection> projections = FieldProjection.from(Fields.fields(paths), 0);
return new ProjectionOperation(this.projections, projections);
}
/**
* Includes the given fields into the projection.
*
@@ -495,8 +535,8 @@ public class ProjectionOperation implements FieldsExposingAggregationOperation {
if (value instanceof AggregationExpression) {
return this.operation.and(new ExpressionProjection(Fields.field(alias, alias), (AggregationExpression) value));
}
return this.operation.and(new FieldProjection(Fields.field(alias, getRequiredName()), null));
Field field = Fields.field(alias, getRequiredName());
return this.operation.and(new FieldProjection(field, null));
}
@Override
@@ -1329,6 +1369,11 @@ public class ProjectionOperation implements FieldsExposingAggregationOperation {
private final Field field;
private final @Nullable Object value;
private final ProjectOn projectOn;
enum ProjectOn {
PATH, NAME
}
/**
* Creates a new {@link FieldProjection} for the field of the given name, assigning the given value.
@@ -1341,11 +1386,16 @@ public class ProjectionOperation implements FieldsExposingAggregationOperation {
}
private FieldProjection(Field field, @Nullable Object value) {
this(field, value, value instanceof Integer ? ProjectOn.PATH : ProjectOn.NAME);
}
private FieldProjection(Field field, @Nullable Object value, ProjectOn project) {
super(new ExposedField(field.getName(), true));
this.field = field;
this.value = value;
this.projectOn = project;
}
/**
@@ -1372,7 +1422,8 @@ public class ProjectionOperation implements FieldsExposingAggregationOperation {
List<FieldProjection> projections = new ArrayList<FieldProjection>();
for (Field field : fields) {
projections.add(new FieldProjection(field, value));
projections
.add(new FieldProjection(field, value, value instanceof Integer ? ProjectOn.PATH : ProjectOn.NAME));
}
return projections;
@@ -1382,12 +1433,13 @@ public class ProjectionOperation implements FieldsExposingAggregationOperation {
* @return {@literal true} if this field is excluded.
*/
public boolean isExcluded() {
return Boolean.FALSE.equals(value);
return Boolean.FALSE.equals(value) || value instanceof Number number && number.intValue() == 0;
}
@Override
public Document toDocument(AggregationOperationContext context) {
return new Document(field.getName(), renderFieldValue(context));
return new Document(ProjectOn.NAME.equals(projectOn) ? field.getName() : context.getReference(field.getTarget()).getRaw(),
renderFieldValue(context));
}
private Object renderFieldValue(AggregationOperationContext context) {

View File

@@ -92,20 +92,6 @@ public class TypeBasedAggregationOperationContext implements AggregationOperatio
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<MongoPersistentProperty> persistentPropertyPath = mappingContext.getPersistentPropertyPath(field, type);
return persistentPropertyPath.getLeafProperty().getFieldName();
}
@Override
public Fields getFields(Class<?> type) {

View File

@@ -1,87 +0,0 @@
/*
* 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<TypeBasedAggregationOperationContext> strict(Class<?> type) {
AggregationContextBuilder<AggregationOperationContext> builder = new AggregationContextBuilder<>();
builder.strict = true;
return builder.forType(type);
}
public static AggregationContextBuilder<TypeBasedAggregationOperationContext> relaxed(Class<?> type) {
AggregationContextBuilder<AggregationOperationContext> builder = new AggregationContextBuilder<>();
builder.strict = false;
return builder.forType(type);
}
public static class AggregationContextBuilder<T extends AggregationOperationContext> {
Class<?> targetType;
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext;
QueryMapper queryMapper;
boolean strict;
public AggregationContextBuilder<TypeBasedAggregationOperationContext> forType(Class<?> type) {
this.targetType = type;
return (AggregationContextBuilder<TypeBasedAggregationOperationContext>) this;
}
public AggregationContextBuilder<T> using(
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext) {
this.mappingContext = mappingContext;
return this;
}
public AggregationContextBuilder<T> 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));
}
}
}

View File

@@ -36,6 +36,7 @@ import org.springframework.data.mongodb.core.aggregation.ProjectionOperationUnit
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.Field;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.query.Criteria;
@@ -653,6 +654,58 @@ public class AggregationUnitTests {
assertThat(documents.get(2)).isEqualTo("{ $sort : { 'serial_number' : -1, 'label_name' : -1 } }");
}
@Test // GH-4428
void projectIncludePath() {
MongoMappingContext mappingContext = new MongoMappingContext();
RelaxedTypeBasedAggregationOperationContext context = new RelaxedTypeBasedAggregationOperationContext(
Root.class, mappingContext,
new QueryMapper(new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, mappingContext)));
assertThat(project("flat").andIncludePath("list.element").toDocument(context)).isEqualTo(
Document.parse("""
{
"$project": {
"flat": 1,
"list.elE_m_enT": 1
}
}""")
);
}
@Test // GH-4428
void projectExcludePath() {
MongoMappingContext mappingContext = new MongoMappingContext();
RelaxedTypeBasedAggregationOperationContext context = new RelaxedTypeBasedAggregationOperationContext(
Root.class, mappingContext,
new QueryMapper(new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, mappingContext)));
assertThat(project("flat").andExcludePath("list.element").toDocument(context)).isEqualTo(
Document.parse("""
{
"$project": {
"flat": 1,
"list.elE_m_enT": 0
}
}""")
);
}
static class Root {
String flat;
List<Nested> list;
}
static class Nested {
@Field("elE_m_enT")
int element;
String description;
}
private Document extractPipelineElement(Document agg, int index, String operation) {
List<Document> pipeline = (List<Document>) agg.get("pipeline");

View File

@@ -22,7 +22,6 @@ import java.util.Arrays;
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;
/**
@@ -35,7 +34,7 @@ public class GraphLookupOperationUnitTests {
@Test // DATAMONGO-1551
public void rejectsNullFromCollection() {
assertThatIllegalArgumentException().isThrownBy(() -> GraphLookupOperation.builder().from((String) null));
assertThatIllegalArgumentException().isThrownBy(() -> GraphLookupOperation.builder().from(null));
}
@Test // DATAMONGO-1551
@@ -159,59 +158,4 @@ public class GraphLookupOperationUnitTests {
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;
}
}

View File

@@ -25,7 +25,6 @@ import java.util.List;
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;
/**
@@ -93,7 +92,7 @@ public class LookupOperationUnitTests {
@Test // DATAMONGO-1326
public void builderRejectsNullFromField() {
assertThatIllegalArgumentException().isThrownBy(() -> LookupOperation.newLookup().from((String) null));
assertThatIllegalArgumentException().isThrownBy(() -> LookupOperation.newLookup().from(null));
}
@Test // DATAMONGO-1326
@@ -196,10 +195,10 @@ public class LookupOperationUnitTests {
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("""
@@ -217,54 +216,4 @@ public class LookupOperationUnitTests {
}}
""");
}
@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;
}
}