[英]How to find parents based on child fields in mongo using aggregation?
這是我擁有的代碼:
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))
假設我們在“boxes”集合中有以下數據:
[
{ _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' }
]
所以基本上我們在這里做的是獲取所有原始框,如果它們至少有一個狀態為“就緒”的嵌套框,並且所有未嵌套的框狀態為“就緒”。
我認為可以通過使用聚合管道和投影來簡化它。 但是如何?
分解聚合:
一個$group
創建
status
的數組ids
,它將為其添加*original
值box_ready
匹配就緒status
並保持其他字段不變(稍后將使用) 包含整個原始文檔( $$ROOT
)的數組document
{ $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
聚合根據之前創建的數組ids
中$document._id
匹配來保留或刪除記錄(包含匹配的original
和status
)
{ $redact: { "$cond": { "if": { "$setIsSubset": [{ "$map": { "input": { "$literal": ["A"] }, "as": "a", "in": "$document._id" } }, "$ids" ] }, "then": "$$KEEP", "else": "$$PRUNE" } } }
$group
將與前一個$redact
匹配的所有文檔推送到另一個名為filtered
數組(我們現在有 2 個可以合並的數組)
{ $group: { _id: null, box_ready: { $first: "$box_ready" }, filtered: { $push: "$document" } } }
使用帶有setUnion
的$project
來聯合數組box_ready
和filtered
{ $project: { union: { $setUnion: ["$box_ready", "$filtered"] }, _id: 0 } }
$unwind
您獲得的數組以獲得不同的記錄
{ $unwind: "$union" }
$match
僅$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
,您應該能夠這樣做來執行聚合:
Box.aggregate([
//the whole aggregation here
], function(err, result) {
});
您可以嘗試以下操作。 使用 $lookUp 自我加入集合和 $match 階段與 $or 結合 $and 用於第二個條件和 $or 的下一部分用於第一個條件和 $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" }
有幾個答案很接近,但這是最有效的方法。 它累積要拾取的框的“_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
值執行,這比對所有框執行此操作要高效得多。
如果您希望管道更高效,則需要在嵌套框文檔中存儲有關原始框的更多詳細信息(例如其名稱)。
為了實現您的目標,您可以按照以下步驟操作:
首先,選擇狀態記錄已准備好(因為您希望獲得沒有嵌套框但狀態已准備好並且已嵌套框至少有一個帶有統計信息的父級已准備好)
使用
$lookup
查找父框然后
$group
獲得唯一的父框然后
$project
框名稱
所以可以試試這個查詢:
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"}
)
或者
- 獲取狀態記錄已准備就緒
- 獲取所需的記錄 ID
- 根據記錄ID獲取名稱
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}}
)
架構:
const schema = mongoose.Schema({
_id: Number,
....
status: String,
original: { type: Number, ref: 'Box'}
});
const 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
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.