优雅地返回在MongoDb聚合结果

时间:2016-04-07 12:29:13

标签: mongodb mongodb-query aggregation-framework

我尝试了几种创建聚合管道的方法,该管道只返回文档嵌入式数组中的匹配条目,但没有找到任何实用的方法。

是否有一些MongoDB功能可以避免我非常笨拙和容易出错的方法?

'workshop'系列中的文档看起来像这样......

{
  "_id": ObjectId("57064a294a54b66c1f961aca"),
  "type": "normal",
  "version": "v1.4.5",
  "invitations": [],
  "groups": [
    {
      "_id": ObjectId("57064a294a54b66c1f961acb"),
      "role": "facilitator"
    },
    {
      "_id": ObjectId("57064a294a54b66c1f961acc"),
      "role": "contributor"
    },
    {
      "_id": ObjectId("57064a294a54b66c1f961acd"),
      "role": "broadcaster"
    },
    {
      "_id": ObjectId("57064a294a54b66c1f961acf"),
      "role": "facilitator"
    }
  ]
}

groups数组中的每个条目都提供一个唯一的ID,以便当团队成员点击具有该salted ID的URL时,会在研讨会中为其分配给定角色。

如果_id匹配像ObjectId("57064a294a54b66c1f961acb")这样的组数组中的条目,我需要从聚合管道返回这样的单个记录 - 基本上只返回嵌入的组数组中的匹配条目。

    {
      "_id": ObjectId("57064a294a54b66c1f961acb"),
      "role": "facilitator",
      "workshopId": ObjectId("57064a294a54b66c1f961aca")
    },

在此示例中,workshopId已添加为额外字段以标识父文档,但其余字段应 ALL 来自原始组条目中具有匹配_id的字段。

我采用的方法可以实现这个目标,但是存在很多问题并且可能效率低下(重复过滤子句)。

return workshopCollection.aggregate([
    {$match:{groups:{$elemMatch:{_id:groupId}}}},
    {$unwind:"$groups"},
    {$match:{"groups._id":groupId}},
    {$project:{
        _id:"$groups._id",
        role:"$groups.role",
        workshopId:"$_id",
    }},
]).toArray();

更糟糕的是,由于它明确包含条目中的命名字段,因此将省略添加到记录中的任何未来字段。我也无法将此查找操作概括为“邀请”或其他嵌入式命名数组的情况,除非我事先知道数组条目的字段是什么。

我想知道在管道的$ project阶段使用$或$ elemMatch运算符是否是正确的方法,但到目前为止,它们要么被忽略,要么在运行管道时触发运算符有效性错误。

问题

是否有另一种聚合运算符或替代方法可以帮助我解决这个相当主流的问题 - 只返回文档数组中的匹配条目?

1 个答案:

答案 0 :(得分:0)

以下实施可以处理任意查询,将结果作为“顶级文档”提供。并避免在管道中重复过滤。

function retrieveArrayEntry(collection, arrayName, itemMatch){
    var match = {};
    match[arrayName]={$elemMatch:itemMatch};
    var project = {};
    project[arrayName+".$"] = true;
    return collection.findOne(
        match,
        project
    ).then(function(doc){
        if(doc !== null){
            var result = doc[arrayName][0];
            result._docId = doc._id;
            return result;
        }
        else{
            return null;
        }
    });
}

可以这样调用......

retrieveArrayEntry(workshopCollection, "groups", {_id:ObjectId("57064a294a54b66c1f961acb")})

但是,它依赖于集合findOne(...)方法而不是聚合(...),因此将限制为从第一个匹配文档提供第一个匹配的数组条目。引用数组匹配子句的预测显然不可能通过聚合(...)以与通过findXXX()方法相同的方式实现。

更通用(但令人困惑且效率低下)的实现允许检索多个匹配的文档和子文档。它通过unpackMatch方法解决了MongoDb与Document和Subdocument匹配的语法一致性所带来的困难,从而导致了不正确的“等同”。标准例如...

{greetings:{_id:ObjectId("437908743")}}

...转换为匹配所需的语法'标准(如Within a mongodb $match, how to test for field MATCHING , rather than field EQUALLING所述)......

{"greetings._id":ObjectId("437908743")}

导致以下实施......

function unpackMatch(pathPrefix, match){
    var unpacked = {};
    Object.keys(match).map(function(key){
        unpacked[pathPrefix + "." + key] = match[key];
    })
    return unpacked;
}

function retrieveArrayEntries(collection, arrayName, itemMatch){

    var matchDocs = {},
        projectItems = {},
        unwindItems = {},
        matchUnwoundByMap = {};

    matchDocs.$match={};
    matchDocs.$match[arrayName]={$elemMatch:itemMatch};

    projectItems.$project = {};
    projectItems.$project[arrayName]=true;

    unwindItems.$unwind = "$" + arrayName;

    matchUnwoundByMap.$match = unpackMatch(arrayName, itemMatch);

    return collection.aggregate([matchDocs, projectItems, unwindItems, matchUnwoundByMap]).toArray().then(function(docs){
        return docs.map(function(doc){
            var result = doc[arrayName];
            result._docId = doc._id;
            return result;
        });
    });
}