【问题标题】:How to project more than the first sub-document when using $elemMatch使用 $elemMatch 时如何投影超过第一个子文档
【发布时间】:2017-10-01 15:27:30
【问题描述】:

我有一个包含以下形式的文档的集合:

{
    "_id" : { "$oid" : "67bg............"},
    "ID"  : "xxxxxxxx",
    "senses" : [
        {
            "word"   : "hello",
            "lang"   : "EN",
            "source" : "EN_DICTIONARY"
        },
        {
            "word"   : "coche",
            "lang"   : "ES",
            "source" : "ES_DICTIONARY"
        },
        {
            "word"   : "bye",
            "lang"   : "EN",
            "source" : "EN_DICTIONARY"
        }
    ]
}

我想找到至少与lang=Xsource=Y 匹配的所有文档,并返回匹配的文档,其中仅包含与lang=Xsource=Y 匹配的senses

我试过这个:

DBObject sensesQuery = new BasicDBObject();
sensesQuery.put("lang", "EN");
sensesQuery.put("source", "EN_DICTIONARY");
DBObject matchQuery = new BasicDBObject("$elemMatch",sensesQuery);

DBObject fields = new BasicDBOject();
fields.put("senses",matchQuery);

DBObject projection = new BasicDBObject();
projection.put("ID",1)
projection.put("senses",matchQuery);
DBCursor cursor = collection.find(fields,projection)

while(cursor.hasNext()) {
    ...
}

我的查询适用于匹配文档,但不适用于投影。以上面的文档为例,如果我运行我的查询,我会得到这个结果:

{
    "_id" : { "$oid" : "67bg............"},
    "ID"  : "xxxxxxxx",
    "senses" : [
        {
            "word"   : "hello",
            "lang"   : "EN",
            "source" : "EN_DICTIONARY"
        }
    ]
}

但我想要这个:

{
    "_id" : { "$oid" : "67bg............"},
    "ID"  : "xxxxxxxx",
    "senses" : [
        {
            "word"   : "hello",
            "lang"   : "EN",
            "source" : "EN_DICTIONARY"
        },
        {
            "word"   : "bye",
            "lang"   : "EN",
            "source" : "EN_DICTIONARY"
        }
    ]
}

我阅读了有关聚合的信息,但我不明白如何在 MongoDB Java 驱动程序中使用它。

谢谢

【问题讨论】:

    标签: java mongodb aggregate projection mongodb-java


    【解决方案1】:

    您在投影以及过滤器上使用$elemMatch 运算符。

    来自the docs

    $elemMatch 运算符将查询结果中的字段内容限制为仅包含与$elemMatch 条件匹配的第一个元素。

    因此,您看到的行为 elemMatch-in-a-projection 的预期行为。

    如果你想在匹配过滤条件的文档中投影senses数组中的所有子文档,那么你可以使用这个:

    projection.put("senses", 1);
    

    但是,如果您只想投影那些与您的过滤条件匹配的子文档,那么$elemMatch 将不适合您,因为它只会返回匹配$elemMatch 条件的第一个元素。您的替代方法是使用聚合框架,例如:

    db.collection.aggregate([
      // matches documents with a senses sub document having the given lang and source values
      {$match: {'senses.lang': 'EN', 'senses.source': 'EN_DICTIONARY'}},
    
      // projects on the senses sub document and filters the output to only return sub 
      // documents having the given lang and source values
      {$project: {
          senses: {
            $filter: {
                input: "$senses",
                as: "sense",
                cond: { $eq: [ "$$sense.lang", 'EN' ], $eq: [ "$$sense.source", 'EN_DICTIONARY' ] }
              }
            }
          }
      }
    ])
    

    这是使用 MongoDB Java 驱动程序的聚合调用:

    Document filter = new Document("senses.lang", "EN").append("senses.source", "EN_DICTIONARY");
    
    DBObject filterExpression = new BasicDBObject();
    filterExpression.put("input", "$senses");
    filterExpression.put("as", "sense");
    filterExpression.put("cond", new BasicDBObject("$and", Arrays.<Object>asList(
            new BasicDBObject("$eq", Arrays.<Object>asList("$$sense.lang", "EN")),
            new BasicDBObject("$eq", Arrays.<Object>asList("$$sense.source", "EN_DICTIONARY")))
    ));
    
    BasicDBObject projectionFilter = new BasicDBObject("$filter", filterExpression);
    
    AggregateIterable<Document> documents = collection.aggregate(Arrays.asList(
            new Document("$match", filter),
            new Document("$project", new Document("senses", projectionFilter))));
    
    for (Document document : documents) {
        logger.info("{}", document.toJson());
    }
    

    结果输出是:

    2017-10-01 17:15:39 [main] INFO  c.s.mongo.MongoClientTest - { "_id" : { "$oid" : "59d10cdfc26584cd8b7a0d3b" }, "senses" : [{ "word" : "hello", "lang" : "EN", "source" : "EN_DICTIONARY" }, { "word" : "bye", "lang" : "EN", "source" : "EN_DICTIONARY" }] }
    

    更新 1:在此评论之后:

    经过长时间的测试,试图理解为什么查询很慢,我注意到“$match”参数不起作用,查询应该只选择至少有一个意义的记录 source = Y AND lang = X 并投影它们,但查询也会返回带有感觉的文档 = []

    此过滤器:new Document("senses.lang", "EN").append("senses.source", "EN_DICTIONARY") 不会匹配没有 senses 属性的文档,也不会匹配具有空 senses 属性的文档。为了验证这一点,我将以下文档添加到我自己的收藏中:

    {
        "_id" : ObjectId("59d72a24c26584cd8b7b70a5"),
        "ID" : "yyyyyyyy"
    }
    
    {
        "_id" : ObjectId("59d72a3ac26584cd8b7b70ae"),
        "ID" : "zzzzzzzzz",
        "senses" : []
    }
    

    然后重新运行上面的代码,我仍然得到了想要的结果。

    我怀疑您关于上述代码不起作用的说法是假阴性,或者您查询的文档与我一直在使用的示例不同。

    为了帮助您自己诊断此问题,您可以...

    • 与其他运营商一起玩,例如$match 阶段在使用和不使用 $exists 运算符时的行为相同:

      new Document("senses", new BasicDBObject("$exists", true))
              .append("senses.lang", new BasicDBObject("$eq", "EN"))
              .append("senses.source", new BasicDBObject("$eq", "EN_DICTIONARY"))
      
    • 移除$project 阶段以查看$match 阶段产生的确切内容。

    【讨论】:

    • AggregationOutput 没有方法 .into();
    • 在 v3.5 中确实如此 :) 但这是次要问题,因为这只是用于输出到列表的语法糖。我已更新答案以返回 AggregateIterable。但是,该答案中的关键点是(1)为什么投影上的 elemMatch 只返回第一个条目以及(2)如何通过聚合框架执行相同的过滤器以及(3)如何通过MongoDB Java 驱动程序。
    • 好的,现在可以了,如何在第一个条件中添加(根据您的答案)“$eq: [ "$$sense.source", 'EN_DICTIONARY' ]" 到 filterExpression 中( "$$sense.lang" , "EN")?
    • @ChristianSordi 我已经更新了答案,您会注意到filterExpression"cond" 属性现在将两个$eq 包装在$and 中。
    • 经过长时间的测试,试图理解为什么查询很慢,我注意到“$match”参数不起作用,查询应该只选择至少有一种意义的记录source = Y AND lang = X 并投影它们,但是查询也返回了带有 senses = [] 的文档,这是不可能的,因为如果文档遵守过滤条件,它应该至少投影了一种感觉。
    猜你喜欢
    • 1970-01-01
    • 2022-01-14
    • 2014-10-21
    • 1970-01-01
    • 2017-12-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-07-04
    相关资源
    最近更新 更多