如何使用聚合基于mongo中的子字段查找父项?

时间:2016-12-22 20:55:31

标签: javascript node.js mongodb mongoose aggregation-framework

这是我的代码:

const _ = require('lodash')
const Box = require('./models/Box')

const boxesToBePicked = await Box.find({ status: 'ready', client: 27 })
const boxesOriginalIds = _(boxesToBePicked).map('original').compact().uniq().value()
const boxesOriginal = boxesOriginalIds.length ? await Box.find({ _id: { $in: boxesOriginalIds } }) : []

const attributes = ['name']

const boxes = [
  ...boxesOriginal,
  ...boxesToBePicked.filter(box => !box.original)
].map(box => _.pick(box, attributes))

让我们说,我们在"框"系列:

[
  { _id: 1, name: 'Original Box #1', status: 'pending' },
  { _id: 2, name: 'Nested box', status: 'ready', original: 1 },
  { _id: 3, name: 'Nested box', status: 'ready', original: 1 },
  { _id: 4, name: 'Nested box', status: 'pending', original: 1 },
  { _id: 5, name: 'Original Box #2', status: 'ready' },
  { _id: 6, name: 'Original Box #3', status: 'pending' },
  { _id: 7, name: 'Nested box', status: 'ready', original: 6 },
  { _id: 8, name: 'Original Box #4', status: 'pending' }
]

工作流

找到所有可以挑选的方框:

const boxesToBePicked = await Box.find({ status: 'ready' })

// Returns:

[
  { _id: 2, name: 'Nested box', status: 'ready', original: 1 },
  { _id: 3, name: 'Nested box', status: 'ready', original: 1 },
  { _id: 5, name: 'Original Box #2', status: 'ready' },
  { _id: 7, name: 'Nested box', status: 'ready', original: 6 }
]

获取原始(父)框的所有ID:

const boxesOriginalIds = _(boxesToBePicked).map('original').compact().uniq().value()

// Returns:

[1, 6]

按照他们的ID获取这些框:

const boxesOriginal = boxesOriginalIds.length ? await Box.find({ _id: { $in: boxesOriginalIds } }) : []

// Returns

[
  { _id: 1, name: 'Original Box #1', status: 'pending' },
  { _id: 6, name: 'Original Box #3', status: 'pending' }
]

加入那些没有嵌套框的框:

const boxes = [
  ...boxesOriginal,
  ...boxesToBePicked.filter(box => !box.original)
].map(box => _.pick(box, attributes))

// Returns

[
  { name: 'Original Box #1' },
  { name: 'Original Box #3' },
  { name: 'Original Box #2' }
]

所以基本上我们在这里做的是获得所有原始盒子,如果他们至少有一个状态为#34;准备好"的嵌套盒子,并且所有没有嵌套的盒子状态为" ready"

我认为可以通过使用聚合管道和投影来简化它。但是如何?

5 个答案:

答案 0 :(得分:2)

您可以尝试以下内容。使用$ lookUp自我加入到集合和$ match阶段用$或与$和第二个条件和$的下一部分或第一个条件和$ group阶段删除重复项和$ project阶段来格式化响应。

db.boxes.aggregate([{
    $lookup: {
        from: "boxes",
        localField: "original",
        foreignField: "_id",
        as: "nested_orders"
    }
}, {
    $unwind: {
        path: "$nested_orders",
        preserveNullAndEmptyArrays: true
    }
}, {
    $match: {
        $or: [{
            $and: [{
                "status": "ready"
            }, {
                "nested_orders": {
                    $exists: false,
                }
            }]
        }, {
            "nested_orders.status": "pending"
        }]
    }
}, {
    $group: {
        "_id": null,
        "names": {
            $addToSet: {
                name: "$name",
                nested_name: "$nested_orders.name"
            }
        }
    }
}, {
    $unwind: "$names"
}, {
    $project: {
        "_id": 0,
        "name": {
            $ifNull: ['$names.nested_name', '$names.name']
        }
    }
}]).pretty();

样本回复

{ "name" : "Original Box #1" }
{ "name" : "Original Box #2" }
{ "name" : "Original Box #3" }

答案 1 :(得分:2)

有几个答案很接近,但这是最有效的方法。它积累了" _id"要拾取的框的值,然后使用$lookup来补充"再水合"每个(顶级)框的完整细节。

db.boxes.aggregate(
    {$group: {
         _id:null, 
         boxes:{$addToSet:{$cond:{
            if:{$eq:["$status","ready"]},
            then:{$ifNull:["$original","$_id"]},
            else:null
         }}}
    }},

    {$lookup: {
          from:"boxes",
          localField:"boxes",
          foreignField:"_id",
          as:"boxes"
    }}
)

您的结果基于样本数据:

{
"_id" : null,
"boxIdsToPickUp" : [
    {
        "_id" : 1,
        "name" : "Original Box #1",
        "status" : "pending"
    },
    {
        "_id" : 5,
        "name" : "Original Box #2",
        "status" : "ready"
    },
    {
        "_id" : 6,
        "name" : "Original Box #3",
        "status" : "pending"
    }
] }

请注意,$lookup仅针对要拾取的框的_id值进行,这比为所有框更有效。

如果您希望管道更多更高效,则需要在嵌套框文档中存储有关原始框的更多详细信息(如名称)。

答案 2 :(得分:1)

分解聚合:

  • 创建

    $group
    • ids匹配的数组status,为其添加*original
    • box_ready匹配的数组status,并保持其他字段不变(稍后将使用)
    • 数组document,其中包含整个原始文档($$ROOT

      {
          $group: {
              _id: null,
              ids: {
                  $addToSet: {
                      $cond: [
                          { $eq: ["$status", "ready"] },
                          "$original", null
                      ]
                  }
              },
              box_ready: {
                  $addToSet: {
                      $cond: [
                          { $eq: ["$status", "ready"] },
                          { _id: "$_id", name: "$name", original: "$original", status: "$status" },
                          null
                      ]
                  }
              },
              document: { $push: "$$ROOT" }
          }
      }
      
  • $unwind文档字段,用于删除数组

    {
        $unwind: "$document"
    }
    
  • 使用$redact聚合根据先前创建的数组$document._id中的ids的匹配来保留或删除记录(包含匹配的original和{ {1}})

    status
  • { $redact: { "$cond": { "if": { "$setIsSubset": [{ "$map": { "input": { "$literal": ["A"] }, "as": "a", "in": "$document._id" } }, "$ids" ] }, "then": "$$KEEP", "else": "$$PRUNE" } } } 将与之前的$group匹配的所有文档推送到另一个名为$redact的数组(我们现在有2个可以联合的数组)

    filtered
  • 使用{ $group: { _id: null, box_ready: { $first: "$box_ready" }, filtered: { $push: "$document" } } } $project合并数组setUnionbox_ready

    filtered
  • { $project: { union: { $setUnion: ["$box_ready", "$filtered"] }, _id: 0 } } 您获取的数组以获取不同的记录

    $unwind
  • { $unwind: "$union" } 只有$match缺少且不为空的那些(最初是状态:就绪状态必须在第一个original上获得空值

    $group

整个聚合查询是:

{
    $match: {
        "union.original": {
            "$exists": false
        },
        "union": { $nin: [null] }
    }
}

它给你:

db.collection.aggregate(
    [{
        $group: {
            _id: null,
            ids: {
                $addToSet: {
                    $cond: [
                        { $eq: ["$status", "ready"] },
                        "$original", null
                    ]
                }
            },
            box_ready: {
                $addToSet: {
                    $cond: [
                        { $eq: ["$status", "ready"] },
                        { _id: "$_id", name: "$name", original: "$original", status: "$status" },
                        null
                    ]
                }
            },
            document: { $push: "$$ROOT" }
        }
    }, {
        $unwind: "$document"
    }, {
        $redact: {
            "$cond": {
                "if": {
                    "$setIsSubset": [{
                            "$map": {
                                "input": { "$literal": ["A"] },
                                "as": "a",
                                "in": "$document._id"
                            }
                        },
                        "$ids"
                    ]
                },
                "then": "$$KEEP",
                "else": "$$PRUNE"
            }
        }
    }, {

        $group: {
            _id: null,
            box_ready: { $first: "$box_ready" },
            filtered: { $push: "$document" }
        }

    }, {
        $project: {
            union: {
                $setUnion: ["$box_ready", "$filtered"]
            },
            _id: 0
        }
    }, {
        $unwind: "$union"
    }, {
        $match: {
            "union.original": {
                "$exists": false
            },
            "union": { $nin: [null] }
        }
    }]
)

如果要选择特定字段,请使用其他{ "union" : { "_id" : 1, "name" : "Original Box #1", "status" : "pending" } } { "union" : { "_id" : 5, "name" : "Original Box #2", "status" : "ready" } } { "union" : { "_id" : 6, "name" : "Original Box #3", "status" : "pending" } }

对于$project,你应该可以这样做来执行聚合:

mongoose

答案 3 :(得分:1)

为了实现您的目标,您可以按照以下步骤进行操作:

  
      
  1. 首先选择记录状态已准备就绪(因为您希望获得没有嵌套框但状态已准备好的的父母和谁   嵌套框至少有一个 stats准备就绪

  2.   
  3. 使用$lookup

  4. 查找父框   
  5. 然后$group获取唯一的父框

  6.   
  7. 然后$project框名称

  8.   

所以可以尝试这个查询:

db.getCollection('boxes').aggregate(
        {$match:{"status":'ready'}},
        {$lookup: {from: "boxes", localField: "original", foreignField: "_id", as: "parent"}},
        {$unwind: {path: "$parent",preserveNullAndEmptyArrays: true}},
        {$group:{
                _id:null,
                list:{$addToSet:{"$cond": [ { "$ifNull": ["$parent.name", false] }, {name:"$parent.name"}, {name:"$name"} ]}}
                }
        },
        {$project:{name:"$list.name", _id:0}},
        {$unwind: "$name"}
 )

  
      
  1. 获取状态记录
  2.   
  3. 获得所需的recordID
  4.   
  5. 根据recordID获取名称
  6.   
db.getCollection('boxes').aggregate(
        {$match:{"status":'ready'}},
        {$group:{
                _id:null,
                parent:{$addToSet:{"$cond": [ { "$ifNull": ["$original", false] }, "$original", "$_id" ]}}
                }
        },
        {$unwind:"$parent"},
        {$lookup: {from: "boxes", localField: "parent", foreignField: "_id", as: "parent"}},
        {$project: {"name" : { $arrayElemAt: [ "$parent.name", 0 ] }, _id:0}}
 )

答案 4 :(得分:1)

使用mongoose(4.x)

架构:

var schema = mongoose.Schema({
    _id: Number,
    ....
    status: String,
    original: { type: Number, ref: 'Box'}
});
var Box = mongoose.model('Box', schema);

实际查询:

Box
    .find({ status: 'ready' })
    .populate('original')
    .exec((err, boxes) => {
        if (err) return;
        boxes = boxes.map((b) => b.original ? b.original : b);
        boxes = _.uniqBy(boxes, '_id');
        console.log(boxes);
    });

关于Mongoose的文档#populate:http://mongoosejs.com/docs/populate.html