【问题标题】:Spring Data MongoDB Lookup with Pipeline Aggregation使用管道聚合的 Spring Data MongoDB 查找
【发布时间】:2018-06-29 18:36:56
【问题描述】:

如何将以下 MongoDB 查询转换为 Java Spring 应用程序使用的查询?我找不到通过提供的lookup 方法使用pipeline 的方法。

这是我尝试转换的查询。我还想指出,我没有使用 $unwind,因为我希望 deliveryZipCodeTimings 作为返回对象中的一个分组集合。

db.getCollection('fulfillmentChannel').aggregate([
    {
        $match: {
            "dayOfWeek": "SOME_VARIABLE_STRING_1"
        }
    },
    {
        $lookup: {
            from: "deliveryZipCodeTiming",
            let: { location_id: "$fulfillmentLocationId" },
            pipeline: [{
                $match: {
                    $expr: {
                        $and: [
                            {$eq: ["$fulfillmentLocationId", "$$location_id"]},
                            {$eq: ["$zipCode", "SOME_VARIABLE_STRING_2"]}
                        ]
                    }
                }
            },
            { 
                $project: { _id: 0, zipCode: 1, cutoffTime: 1 } 
            }],
            as: "deliveryZipCodeTimings"
        }
    },
    {
        $match: {
            "deliveryZipCodeTimings": {$ne: []}
        }
    }
])

【问题讨论】:

    标签: spring mongodb spring-data aggregation-framework spring-data-mongodb


    【解决方案1】:

    根据@dnickless 提供的信息,我能够解决这个问题。我将发布完整的解决方案,希望它在未来对其他人有所帮助。

    我正在使用mongodb-driver:3.6.4

    首先,我必须创建一个自定义聚合操作类,以便我可以传入自定义 JSON mongodb 查询以用于聚合操作。这将允许我在 $lookup 中使用 pipeline,而我正在使用的驱动程序版本不支持。

    public class CustomProjectAggregationOperation implements AggregationOperation {
        private String jsonOperation;
    
        public CustomProjectAggregationOperation(String jsonOperation) {
            this.jsonOperation = jsonOperation;
        }
    
        @Override
        public Document toDocument(AggregationOperationContext aggregationOperationContext) {
            return aggregationOperationContext.getMappedObject(Document.parse(jsonOperation));
        }
    }
    

    现在我们能够将自定义 JSON 查询传递到我们的 mongodb spring 实现中,剩下的就是将这些值插入到 TypedAggregation 查询中。

    public List<FulfillmentChannel> getFulfillmentChannels(
        String SOME_VARIABLE_STRING_1, 
        String SOME_VARIABLE_STRING_2) {
    
        AggregationOperation match = Aggregation.match(
                Criteria.where("dayOfWeek").is(SOME_VARIABLE_STRING_1));
        AggregationOperation match2 = Aggregation.match(
                Criteria.where("deliveryZipCodeTimings").ne(Collections.EMPTY_LIST));
        String query =
                "{ $lookup: { " +
                        "from: 'deliveryZipCodeTiming'," +
                        "let: { location_id: '$fulfillmentLocationId' }," +
                        "pipeline: [{" +
                        "$match: {$expr: {$and: [" +
                        "{ $eq: ['$fulfillmentLocationId', '$$location_id']}," +
                        "{ $eq: ['$zipCode', '" + SOME_VARIABLE_STRING_2 + "']}]}}}," +
                        "{ $project: { _id: 0, zipCode: 1, cutoffTime: 1 } }]," +
                        "as: 'deliveryZipCodeTimings'}}";
    
        TypedAggregation<FulfillmentChannel> aggregation = Aggregation.newAggregation(
                FulfillmentChannel.class,
                match,
                new CustomProjectAggregationOperation(query),
                match2
        );
    
        AggregationResults<FulfillmentChannel> results = 
            mongoTemplate.aggregate(aggregation, FulfillmentChannel.class);
        return results.getMappedResults();
    }
    

    【讨论】:

    • 很好的答案!这对我正在处理的查询很有帮助。在比赛中使用日期时,我确实发现了一个小问题。当直接在 let 或 match 中使用日期时,CustomProjectAggregationOperation 的构造函数应该只接受 Document 而不是 String ,它需要稍后解析。字符串的解析似乎弄乱了日期,导致结果不好或根本没有结果。
    • @AlwaysLearning:我尝试使用 TypedAggregation,但是,我的 AggregationResult 与我的实体不同。我收到一个 PropertyReferenceException,说实体上不存在查找 ID。
    • 我不确定为什么没有人指向“ExposedFields”,当 mongo 的 Spring 数据解析管道中的每个步骤时,它会查找上一步中的暴露字段,如果是自定义聚合,它是我因错误 java.lang.IllegalArgumentException: Invalid reference 而失败。我必须实现 FieldsExposingAggregationOperation 和 FieldsExposingAggregationOperation.InheritsFieldsAggregationOperation 并提供 getFields 方法的实现。 @Override public ExposedFields getFields() { return ExposedFields.synthetic(Fields.fields(this.exposedField)); }
    【解决方案2】:

    驱动程序几乎总是落后于 MongoDB 提供的当前语言功能 - 因此一些最新和最强大的功能还不能通过 API 很好地访问。恐怕这是其中一种情况,您需要求助于使用字符串。有点像这样(未经测试):

    AggregationOperation match = Aggregation.match(Criteria.where("dayOfWeek").is("SOME_VARIABLE_STRING_1"));
    AggregationOperation match2 = Aggregation.match(Criteria.where("deliveryZipCodeTimings").ne([]));
    String query = "{ $lookup: { from: 'deliveryZipCodeTiming', let: { location_id: '$fulfillmentLocationId' }, pipeline: [{ $match: { $expr: { $and: [ { $eq: ['$fulfillmentLocationId', '$$location_id']}, { $eq: ['$zipCode', 'SOME_VARIABLE_STRING_2']} ]} } }, { $project: { _id: 0, zipCode: 1, cutoffTime: 1 } }], as: 'deliveryZipCodeTimings' } }";
    Aggregation.newAggregation(match, (DBObject) JSON.parse(query), match2);
    

    【讨论】:

    • 感谢@dnickless,但是我没有看到任何将DBObject 作为参数的Aggregation.newAggregation。你知道我怎样才能将DBObject 传递给Aggregation.newAggregation 吗?
    • @AlwaysLearning:你是对的......让我谷歌一下......如果没有 Spring,它可能是这样的:stackoverflow.com/questions/36352110/…
    • 我认为你可以这样做(通过实现你自己的基于 AggregationOperation 类的 JSON 字符串):stackoverflow.com/questions/39393672/…
    • 以上链接中提供的解决方案为您提供了最大程度的灵活性,同时也为您提供了前进的动力。但是,在此处从此类中获取代码会更简洁:github.com/spring-projects/spring-data-mongodb/blob/master/…,然后对其进行扩展以使其支持您需要的内容,然后创建拉取请求。
    • 感谢@dnickless,您的意见帮助我找到了最终解决方案。赞成。
    【解决方案3】:

    我想添加这个我的解决方案,在某些方面重复之前发布的解决方案。

    Mongo 驱动程序 v3.x

    对于 Mongo 驱动程序 v3.x,我找到了以下解决方案:

    import java.util.Collections;
    import java.util.List;
    import java.util.Map;
    import java.util.stream.Collectors;
    
    import com.mongodb.BasicDBList;
    import com.mongodb.BasicDBObject;
    import com.mongodb.util.JSON;
    
    import org.bson.Document;
    import org.springframework.data.mongodb.core.aggregation.AggregationOperation;
    import org.springframework.data.mongodb.core.aggregation.AggregationOperationContext;
    
    public class JsonOperation implements AggregationOperation {
    
        private List<Document> documents;
    
        public JsonOperation(String json) {
            Object root = JSON.parse(json);
    
            documents = root instanceof BasicDBObject
                        ? Collections.singletonList(new Document(((BasicDBObject) root).toMap()))
                        : ((BasicDBList) root).stream().map(item -> new Document((Map<String, Object>) ((BasicDBObject) item).toMap())).collect(Collectors.toList());
        }
    
        @Override
        public Document toDocument(AggregationOperationContext context) {
            // Not necessary to return anything as we override toPipelineStages():
            return null;
        }
    
        @Override
        public List<Document> toPipelineStages(AggregationOperationContext context) {
            return documents;
        }
    }
    

    然后假设在某些资源aggregations.json 中给出了聚合步骤:

    [
      {
        $match: {
          "userId": "..."
        }
      },
      {
        $lookup: {
          let: {
            ...
          },
          from: "another_collection",
          pipeline: [
            ...
          ],
          as: "things"
        }
      },
      {
        $sort: {
          "date": 1
        }
      }
    ]
    

    可以按如下方式使用上述类:

    import static org.springframework.data.mongodb.core.aggregation.Aggregation.newAggregation;
    
    Collection<ResultDao> results = mongoTemplate.aggregate(newAggregation(new JsonOperation(resourceToString("aggregations.json", StandardCharsets.UTF_8))), "some_collection", ResultDao.class).getMappedResults();
    

    Mongo 驱动程序 v4.x

    由于 JSON 类已从 Mongo v4 中删除,我已将该类重写如下:

    import java.util.Collections;
    import java.util.List;
    
    import org.bson.Document;
    import org.springframework.data.mongodb.core.aggregation.AggregationOperation;
    import org.springframework.data.mongodb.core.aggregation.AggregationOperationContext;
    
    public class JsonOperation implements AggregationOperation {
    
        private List<Document> documents;
    
        private static final String DUMMY_KEY = "dummy";
    
        public JsonOperation(String json) {
            documents = parseJson(json);
        }
    
        static final List<Document> parseJson(String json) {
            return (json.startsWith("["))
                        ? Document.parse("{\"" + DUMMY_KEY + "\": " + json + "}").getList(DUMMY_KEY, Document.class)
                        : Collections.singletonList(Document.parse(json));
        }
    
        @Override
        public Document toDocument(AggregationOperationContext context) {
            // Not necessary to return anything as we override toPipelineStages():
            return null;
        }
    
        @Override
        public List<Document> toPipelineStages(AggregationOperationContext context) {
            return documents;
        }
    
        @Override
        public String getOperator() {
            return documents.iterator().next().keySet().iterator().next();
        }
    }
    

    但是由于字符串操作,实现现在有点难看。如果有人对如何以更优雅的方式解析对象数组有更好的了解,请编辑这篇文章或发表评论。理想情况下,Mongo 核心中应该有一些方法可以解析 JSON 对象或列表(返回 BasicDBObject/BasicDBListDocument/List&lt;Document&gt;)。

    另外请注意,我已经跳过了在 toPipelineStages() 方法中转换 Document 实例的步骤,因为在我的情况下没有必要:

    @Override
    public List<Document> toPipelineStages(AggregationOperationContext context) {
        return documents.stream().map(document -> context.getMappedObject(document)).collect(Collectors.toList());
    }
    
    

    【讨论】:

      【解决方案4】:

      当我使用接受的答案中解释的方式时,我遇到了一些 JSON 解析异常,所以我深入挖掘了默认的 MongoDB java 驱动程序(版本 3)文档类来构建聚合查询,发现可以构建任何聚合查询如下,

      如下替换 mongo 控制台查询中的每个元素

      1. 花括号({) -> 新文档()
      2. 参数名称相同
      3. 冒号(:) -> 昏迷(,)
      4. Coma(,) -> .append()
      5. 方括号([) -> Arrays.asList()
        AggregationOperation customLookupOperation = new AggregationOperation() {
                      @Override
                      public Document toDocument(AggregationOperationContext context) {
                          return new Document(
                                  "$lookup",
                                  new Document("from", "deliveryZipCodeTiming")
                                          .append("let",new Document("location_id", "$fulfillmentLocationId"))
                                          .append("pipeline", Arrays.<Object> asList(
                                                  new Document("$match", new Document("$expr", new Document("$and",
                                                          Arrays.<Object>asList(
                                                                  new Document("$eq", Arrays.<Object>asList("$fulfillmentLocationId", "$$location_id")),
                                                                  new Document("$eq", Arrays.<Object>asList("$zipCode", "SOME_VARIABLE_STRING_2"))
                                                          )))),
                                                  new Document("$project", new Document("_id",0).append("zipCode", 1)
                                                          .append("cutoffTime", 1)
      )
                                          ))
                                          .append("as", "deliveryZipCodeTimings")
                          );
                      }
                  };
      

      终于可以在聚合管道中使用聚合操作了,

                  Aggregation aggregation = Aggregation.newAggregation(matchOperation,customLookupOperation,matchOperation2);
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2023-02-25
        • 2018-07-07
        • 1970-01-01
        • 2020-04-13
        • 2020-07-27
        • 2020-06-13
        相关资源
        最近更新 更多