当foreignField在Array中时查找

时间:2018-05-22 15:14:48

标签: node.js mongodb mongoose aggregation-framework

我想从一个对象查找到一个集合,其中foreignField键嵌入到一个对象数组中。我有:

收集“衬衫”

{ 
      "_id" : ObjectId("5a797ef0768d8418866eb0f6"), 
      "name" : "Supermanshirt", 
      "price" : 9.99,
      "flavours" : [
                     {
                       "flavId" : ObjectId("5a797f8c768d8418866ebad3"), 
                       "size" : "M", 
                       "color": "white",
                     },
                     {
                       "flavId" : ObjectId("3a797f8c768d8418866eb0f7"), 
                       "size" : "XL", 
                       "color": "red",
                     }, 
                   ]
}

收集“篮子”

 { 
      "_id" : ObjectId("5a797ef0333d8418866ebabc"), 
      "basketName" : "Default", 
      "items" : [
                   {
                      "dateAdded" : 1526996879787.0, 
                      "itemFlavId" : ObjectId("5a797f8c768d8418866ebad3")
                   }
                ], 
}

我的查询:

basketSchema.aggregate([
                    {
                       $match: { $and: [{ _id }, { basketName }]},
                    },
                    {
                       $unwind: '$items',
                    },
                    {
                       $lookup:
                       {
                         from: 'shirts',
                         localField: 'items.itemFlavId',
                         foreignField: 'flavours.flavId',
                         as: 'ordered_shirts',
                       },
                    },
                    ]).toArray();

我的预期结果:

[{ 
  "_id" : ObjectId("5a797ef0333d8418866ebabc"), 
  "basketName" : "Default", 
  "items" : [
               {
                  "dateAdded" : 1526996879787.0, 
                  "itemFlavId" : ObjectId("5a797f8c768d8418866ebad3")
               }
            ], 
   "ordered_shirts" : [
                     { 
                        "_id" : ObjectId("5a797ef0768d8418866eb0f6"), 
                        "name" : "Supermanshirt", 
                        "price" : 9.99,
                        "flavours" : [
                                {
                                   "flavId" : ObjectId("5a797f8c768d8418866ebad3"), 
                                   "size" : "M", 
                                   "color": "white",
                                }
                   ]
}
            ], 
}]

但是我的ordered_shirts数组是空的。

如果此foreignField嵌入到其他集合的数组中,如何使用foreignField

我正在使用MongoDB 3.6.4

1 个答案:

答案 0 :(得分:3)

如评论所示,您的代码中只会出现一些指向错误集合的内容。一般情况下,只需查看下面提供的示例列表,看看有什么不同,因为使用您提供的数据和正确的集合名称,实际上会返回您的预期结果。

当然,您需要在"之后进行此类查询。最初的$lookup阶段不是一件简单的事情。从结构的角度来看,你所拥有的东西通常不是一个好主意,因为引用"加入"数组中的项目意味着您始终返回的数据不一定是"相关的"。

有一些方法可以解决这个问题,而且大多数情况都存在"非相关" MongoDB 3.6中引入了$lookup,可以帮助确保您不会返回"不必要的" "加入"。

中的数据

我以"合并"的形式在这里工作在购物篮中使用"items"" sku" 详细信息,因此第一个表单将是:

Optimal MongoDB 3.6

// Store some vars like you have
let _id = ObjectId("5a797ef0333d8418866ebabc"),
    basketName = "Default";

// Run non-correlated $lookup
let optimal = await Basket.aggregate([
  { "$match": { _id, basketName } },
  { "$lookup": {
    "from": Shirt.collection.name,
    "as": "items",
    "let": { "items": "$items" },
    "pipeline": [
      { "$match": {
        "$expr": {
          "$setIsSubset": ["$$items.itemflavId", "$flavours.flavId"]
        }
      }},
      { "$project": {
        "_id": 0,
        "items": {
          "$map": {
            "input": {
              "$filter": {
                "input": "$flavours",
                "cond": { "$in": [ "$$this.flavId", "$$items.itemFlavId" ]}
              }
            },
            "in": {
              "$mergeObjects": [
                { "$arrayElemAt": [
                  "$$items",
                  { "$indexOfArray": [
                    "$$items.itemFlavId", "$$this.flavId" ] }
                ]},
                { "name": "$name", "price": "$price" },
                "$$this"
              ]
            }
          }
        }
      }},
      { "$unwind": "$items" },
      { "$replaceRoot": {  "newRoot": "$items" } }
    ]
  }}
])

请注意,由于您使用mongoose来保存模型的详细信息,因此我们可以使用Shirt.collection.name从该模型中读取属性,并使用$lookup所需的实际集合名称。这有助于避免代码中的混淆以及“硬编码”#34;类似于集合名称,当它实际存储在其他地方时。这样你应该改变注册"模型的代码"以改变集合名称的方式,这将总是检索正确的名称以便在管道阶段使用。

你使用这种形式的$lookup与MongoDB 3.6的主要原因是你想要使用那个"子管道"操纵外国收集结果"之前"它们被返回并与父文档合并。因为我们正在"合并"将结果放入篮子的现有"items"数组中,我们在"as"的参数中使用相同的字段名称。

$lookup的这种形式中,您通常仍然需要"相关"尽管它可以让你控制你做任何你想做的事情。在这种情况下,我们可以比较父文档中"items"的数组内容,我们将其设置为管道的变量,以与外部集合中"flavours"下的数组一起使用。两个""的逻辑比较。在这里它们的交叉点#34;正在使用$setIsSubset运算符使用$expr,因此我们可以比较"逻辑运算"。

这里的主要工作是在$project中完成的,它只是在外部文档的$map数组的数组上使用"flavours",并使用$filter进行处理与我们传入管道的"items"进行比较并基本上重写,以便"合并"匹配的内容。

$filter缩小列表以仅考虑与"items"中存在的内容相匹配的内容,然后我们可以使用$indexOfArray$arrayElemAt来提取来自"items"的详细信息,并将其与使用$mergeObjects运算符匹配的每个剩余"flavours"条目合并。在这里注意到我们也采取了一些"父母"来自"衬衫的细节"作为"name""price"字段,这些字段与大小和颜色的变化相同。

因为这仍然是一个"数组"在匹配文档中的连接条件,以获得一个"平面列表"适合"合并的对象#34; $lookup生成的"items"中的条目我们只是应用$unwind,这在匹配项目的上下文中只会创建" little"开销和$replaceRoot以便将该密钥下的内容提升到最高级别。

结果只是"合并"内容"items"中列出的内容。

次优MongoDB

替代方法实际上并不那么好,因为所有方法都涉及返回其他"口味"实际上并不匹配篮子里的物品。这基本上涉及"后过滤"从$lookup获得的结果,而不是"预过滤"上面的过程就是这样做的。

所以这里的下一个案例是使用方法来操作返回的数组,以便删除实际上不匹配的项目:

// Using legacy $lookup
let alternate = await Basket.aggregate([
  { "$match": { _id, basketName } },
  { "$lookup": {
    "from": Shirt.collection.name,
    "localField": "items.itemFlavId",
    "foreignField": "flavours.flavId",
    "as": "ordered_items"
  }},
  { "$addFields": {
    "items": {
      "$let": {
        "vars": {
          "ordered_items": {
            "$reduce": {
              "input": {
                "$map": {
                  "input": "$ordered_items",
                  "as": "o",
                  "in": {
                    "$map": {
                      "input": {
                        "$filter": {
                          "input": "$$o.flavours",
                          "cond": {
                            "$in": ["$$this.flavId", "$items.itemFlavId"]
                          }
                        }
                      },
                      "as": "f",
                      "in": {
                        "$mergeObjects": [
                          { "name": "$$o.name", "price": "$$o.price" },
                          "$$f"
                        ]
                      }
                    }
                  }
                }
              },
              "initialValue": [],
              "in": { "$concatArrays": ["$$value", "$$this"] }
            }
          }
        },
        "in": {
          "$map": {
            "input": "$items",
            "in": {
              "$mergeObjects": [
                "$$this",
                { "$arrayElemAt": [
                  "$$ordered_items",
                  { "$indexOfArray": [
                    "$$ordered_items.flavId", "$$this.itemFlavId"
                  ]}
                ]}
              ]
            }
          }
        }
      }
    },
    "ordered_items": "$$REMOVE"
  }}
]);

这里我仍然使用一些MongoDB 3.6功能,但这些并不是"要求"涉及的逻辑。这种方法的主要约束实际上是$reduce,它需要MongoDB 3.4或更高版本。

使用相同的"遗产"在您尝试时$lookup的形式,我们仍然会在您显示时获得所需的结果,但这当然包含"flavours"中与广告中的"items"不匹配的信息。与上一个列表中显示的方式大致相同,我们可以在此处应用$filter来删除不匹配的项目。这里的相同过程使用$filter输出作为$map的输入,它再次做同样的事情" merge"过程和以前一样。

$reduce进来的地方是因为得到的处理有一个"数组"来自$lookup的目标文件本身包含"array" "flavours",这些数组需要"合并"进入单个阵列进行进一步处理。 $reduce只使用已处理的输出,并在每个"内部"上执行$concatArrays。返回的数组使这些结果变得奇异。我们已经"合并了#34;内容,所以这成为新的"合并" "items"

旧的仍然是$放松

当然,呈现的最终方式(即使还有其他组合)是在数组上使用$unwind并使用$group将其重新组合在一起:

let old = await Basket.aggregate([
  { "$match": { _id, basketName } },
  { "$unwind": "$items" },
  { "$lookup": {
    "from": Shirt.collection.name,
    "localField": "items.itemFlavId",
    "foreignField": "flavours.flavId",
    "as": "ordered_items"
  }},
  { "$unwind": "$ordered_items" },
  { "$unwind": "$ordered_items.flavours" },
  { "$redact": {
    "$cond": {
      "if": {
        "$eq": [
          "$items.itemFlavId",
          "$ordered_items.flavours.flavId"
        ]
      },
      "then": "$$KEEP",
      "else": "$$PRUNE"
    }
  }},
  { "$group": {
    "_id": "$_id",
    "basketName": { "$first": "$basketName" },
    "items": {
      "$push": {
        "dateAdded": "$items.dateAdded",
        "itemFlavId": "$items.itemFlavId",
        "name": "$ordered_items.name",
        "price": "$ordered_items.price",
        "flavId": "$ordered_items.flavours.flavId",
        "size": "$ordered_items.flavours.size",
        "color": "$ordered_items.flavours.color"
      }
    }
  }}
]);

大多数情况应该是非常自我解释的,因为$unwind只是一个“扁平化”的工具。将数组内容转换为单个文档条目。为了获得我们想要的结果,我们可以使用$redact来比较这两个字段。使用MongoDB 3.6,你可以"在$expr这里使用$match

{ "$match": {
  "$expr": {
    "$eq": [
      "$items.itemFlavId",
      "$ordered_items.flavours.flavId"
    ]
  }
}}

但是当它归结为它时,如果你有MongoDB 3.6及其它功能,那么$unwind是错误的,因为它会实际添加所有开销。

所有真正发生的事情是你$lookup然后"压扁"文档和最后$group所有相关详细信息一起使用$push在篮子中重新创建"items"。它看起来很简单"并且可能是最容易理解的形式,然而"简单"不等于"表现"在现实世界的用例中使用它会非常残酷。

摘要

这应该涵盖在使用"加入"时需要做的事情的解释。这将比较数组中的项目。这可能会引导你走上实现这个并不是一个好主意的道路,并且保持你的" skus"列出"分开"而不是在一个"项目#34;。

下列出所有相关内容

它也应该部分是一个教训,#34;加入"一般来说,使用MongoDB并不是一个好主意。你真的应该定义这样的关系,他们是绝对必要的"。在这种"篮子中的项目细节的情况下,那么与传统的RDBMS模式相反,它在性能方面实际上会更好地简化"嵌入"那个细节从一开始。通过这种方式,您不需要复杂的连接条件来获得结果,这可能已经在存储中保存了"几个字节" 但是花费的时间比什么本来应该是一个简单的篮子请求与所有细节已经"嵌入"。这真的应该是你首先使用像MongoDB这样的东西的主要原因。

因此,如果您必须这样做,那么您应该坚持使用第一个表单,因为您可以使用可用的功能,然后最好地使用它们。虽然其他方法似乎更容易,但它不会对应用程序性能有所帮助,当然最好的性能也会嵌入到开始。

以下完整列表用于演示上述方法,并进行基本比较,以证明所提供的数据实际上是"加入"只要应用程序设置的其他部分正常工作。所以关于"应该如何完成的模型"除了展示完整的概念外。

const { Schema, Types: { ObjectId } } = mongoose = require('mongoose');

const uri = 'mongodb://localhost/basket';

mongoose.Promise = global.Promise;
mongoose.set('debug', true);


const basketItemSchema = new Schema({
  dateAdded: { type: Number, default: Date.now() },
  itemFlavId: { type: Schema.Types.ObjectId }
},{ _id: false });

const basketSchema = new Schema({
  basketName: String,
  items: [basketItemSchema]
});

const flavourSchema = new Schema({
  flavId: { type: Schema.Types.ObjectId },
  size: String,
  color: String
},{ _id: false });

const shirtSchema = new Schema({
  name: String,
  price: Number,
  flavours: [flavourSchema]
});

const Basket = mongoose.model('Basket', basketSchema);
const Shirt = mongoose.model('Shirt', shirtSchema);

const log = data => console.log(JSON.stringify(data, undefined, 2));

(async function() {

  try {

    const conn = await mongoose.connect(uri);

    // clean data
    await Promise.all(Object.entries(conn.models).map(([k,m]) => m.remove()));

    // set up data for test
    await Basket.create({
      _id: ObjectId("5a797ef0333d8418866ebabc"),
      basketName: "Default",
      items: [
        {
          dateAdded: 1526996879787.0,
          itemFlavId: ObjectId("5a797f8c768d8418866ebad3")
        }
      ]
    });

    await Shirt.create({
      _id: ObjectId("5a797ef0768d8418866eb0f6"),
      name: "Supermanshirt",
      price: 9.99,
      flavours: [
        {
          flavId: ObjectId("5a797f8c768d8418866ebad3"),
          size: "M",
          color: "white"
        },
        {
          flavId: ObjectId("3a797f8c768d8418866eb0f7"),
          size: "XL",
          color: "red"
        }
      ]
    });

    // Store some vars like you have
    let _id = ObjectId("5a797ef0333d8418866ebabc"),
        basketName = "Default";

    // Run non-correlated $lookup
    let optimal = await Basket.aggregate([
      { "$match": { _id, basketName } },
      { "$lookup": {
        "from": Shirt.collection.name,
        "as": "items",
        "let": { "items": "$items" },
        "pipeline": [
          { "$match": {
            "$expr": {
              "$setIsSubset": ["$$items.itemflavId", "$flavours.flavId"]
            }
          }},
          { "$project": {
            "_id": 0,
            "items": {
              "$map": {
                "input": {
                  "$filter": {
                    "input": "$flavours",
                    "cond": { "$in": [ "$$this.flavId", "$$items.itemFlavId" ]}
                  }
                },
                "in": {
                  "$mergeObjects": [
                    { "$arrayElemAt": [
                      "$$items",
                      { "$indexOfArray": [
                        "$$items.itemFlavId", "$$this.flavId" ] }
                    ]},
                    { "name": "$name", "price": "$price" },
                    "$$this"
                  ]
                }
              }
            }
          }},
          { "$unwind": "$items" },
          { "$replaceRoot": {  "newRoot": "$items" } }
        ]
      }}
    ])

    log(optimal);

    // Using legacy $lookup
    let alternate = await Basket.aggregate([
      { "$match": { _id, basketName } },
      { "$lookup": {
        "from": Shirt.collection.name,
        "localField": "items.itemFlavId",
        "foreignField": "flavours.flavId",
        "as": "ordered_items"
      }},
      { "$addFields": {
        "items": {
          "$let": {
            "vars": {
              "ordered_items": {
                "$reduce": {
                  "input": {
                    "$map": {
                      "input": "$ordered_items",
                      "as": "o",
                      "in": {
                        "$map": {
                          "input": {
                            "$filter": {
                              "input": "$$o.flavours",
                              "cond": {
                                "$in": ["$$this.flavId", "$items.itemFlavId"]
                              }
                            }
                          },
                          "as": "f",
                          "in": {
                            "$mergeObjects": [
                              { "name": "$$o.name", "price": "$$o.price" },
                              "$$f"
                            ]
                          }
                        }
                      }
                    }
                  },
                  "initialValue": [],
                  "in": { "$concatArrays": ["$$value", "$$this"] }
                }
              }
            },
            "in": {
              "$map": {
                "input": "$items",
                "in": {
                  "$mergeObjects": [
                    "$$this",
                    { "$arrayElemAt": [
                      "$$ordered_items",
                      { "$indexOfArray": [
                        "$$ordered_items.flavId", "$$this.itemFlavId"
                      ]}
                    ]}
                  ]
                }
              }
            }
          }
        },
        "ordered_items": "$$REMOVE"
      }}
    ]);
    log(alternate);

    // Or really old style

    let old = await Basket.aggregate([
      { "$match": { _id, basketName } },
      { "$unwind": "$items" },
      { "$lookup": {
        "from": Shirt.collection.name,
        "localField": "items.itemFlavId",
        "foreignField": "flavours.flavId",
        "as": "ordered_items"
      }},
      { "$unwind": "$ordered_items" },
      { "$unwind": "$ordered_items.flavours" },
      { "$redact": {
        "$cond": {
          "if": {
            "$eq": [
              "$items.itemFlavId",
              "$ordered_items.flavours.flavId"
            ]
          },
          "then": "$$KEEP",
          "else": "$$PRUNE"
        }
      }},
      { "$group": {
        "_id": "$_id",
        "basketName": { "$first": "$basketName" },
        "items": {
          "$push": {
            "dateAdded": "$items.dateAdded",
            "itemFlavId": "$items.itemFlavId",
            "name": "$ordered_items.name",
            "price": "$ordered_items.price",
            "flavId": "$ordered_items.flavours.flavId",
            "size": "$ordered_items.flavours.size",
            "color": "$ordered_items.flavours.color"
          }
        }
      }}
    ]);

    log(old);


  } catch(e) {
    console.error(e)
  } finally {
    process.exit()
  }

})()

示例输出为:

Mongoose: baskets.remove({}, {})
Mongoose: shirts.remove({}, {})
Mongoose: baskets.insertOne({ _id: ObjectId("5a797ef0333d8418866ebabc"), basketName: 'Default', items: [ { dateAdded: 1526996879787, itemFlavId: ObjectId("5a797f8c768d8418866ebad3") } ], __v: 0 })
Mongoose: shirts.insertOne({ _id: ObjectId("5a797ef0768d8418866eb0f6"), name: 'Supermanshirt', price: 9.99, flavours: [ { flavId: ObjectId("5a797f8c768d8418866ebad3"), size: 'M', color: 'white' }, { flavId: ObjectId("3a797f8c768d8418866eb0f7"), size: 'XL', color: 'red' } ], __v: 0 })
Mongoose: baskets.aggregate([ { '$match': { _id: 5a797ef0333d8418866ebabc, basketName: 'Default' } }, { '$lookup': { from: 'shirts', as: 'items', let: { items: '$items' }, pipeline: [ { '$match': { '$expr': { '$setIsSubset': [ '$$items.itemflavId', '$flavours.flavId' ] } } }, { '$project': { _id: 0, items: { '$map': { input: { '$filter': { input: '$flavours', cond: { '$in': [Array] } } }, in: { '$mergeObjects': [ { '$arrayElemAt': [Array] }, { name: '$name', price: '$price' }, '$$this' ] } } } } }, { '$unwind': '$items' }, { '$replaceRoot': { newRoot: '$items' } } ] } } ], {})
[
  {
    "_id": "5a797ef0333d8418866ebabc",
    "basketName": "Default",
    "items": [
      {
        "dateAdded": 1526996879787,
        "itemFlavId": "5a797f8c768d8418866ebad3",
        "name": "Supermanshirt",
        "price": 9.99,
        "flavId": "5a797f8c768d8418866ebad3",
        "size": "M",
        "color": "white"
      }
    ],
    "__v": 0
  }
]
Mongoose: baskets.aggregate([ { '$match': { _id: 5a797ef0333d8418866ebabc, basketName: 'Default' } }, { '$lookup': { from: 'shirts', localField: 'items.itemFlavId', foreignField: 'flavours.flavId', as: 'ordered_items' } }, { '$addFields': { items: { '$let': { vars: { ordered_items: { '$reduce': { input: { '$map': { input: '$ordered_items', as: 'o', in: { '$map': [Object] } } }, initialValue: [], in: { '$concatArrays': [ '$$value', '$$this' ] } } } }, in: { '$map': { input: '$items', in: { '$mergeObjects': [ '$$this', { '$arrayElemAt': [ '$$ordered_items', [Object] ] } ] } } } } }, ordered_items: '$$REMOVE' } } ], {})
[
  {
    "_id": "5a797ef0333d8418866ebabc",
    "basketName": "Default",
    "items": [
      {
        "dateAdded": 1526996879787,
        "itemFlavId": "5a797f8c768d8418866ebad3",
        "name": "Supermanshirt",
        "price": 9.99,
        "flavId": "5a797f8c768d8418866ebad3",
        "size": "M",
        "color": "white"
      }
    ],
    "__v": 0
  }
]
Mongoose: baskets.aggregate([ { '$match': { _id: 5a797ef0333d8418866ebabc, basketName: 'Default' } }, { '$unwind': '$items' }, { '$lookup': { from: 'shirts', localField: 'items.itemFlavId', foreignField: 'flavours.flavId', as: 'ordered_items' } }, { '$unwind': '$ordered_items' }, { '$unwind': '$ordered_items.flavours' }, { '$redact': { '$cond': { if: { '$eq': [ '$items.itemFlavId', '$ordered_items.flavours.flavId' ] }, then: '$$KEEP', else: '$$PRUNE' } } }, { '$group': { _id: '$_id', basketName: { '$first': '$basketName' }, items: { '$push': { dateAdded: '$items.dateAdded', itemFlavId: '$items.itemFlavId', name: '$ordered_items.name', price: '$ordered_items.price', flavId: '$ordered_items.flavours.flavId', size: '$ordered_items.flavours.size', color: '$ordered_items.flavours.color' } } } } ], {})
[
  {
    "_id": "5a797ef0333d8418866ebabc",
    "basketName": "Default",
    "items": [
      {
        "dateAdded": 1526996879787,
        "itemFlavId": "5a797f8c768d8418866ebad3",
        "name": "Supermanshirt",
        "price": 9.99,
        "flavId": "5a797f8c768d8418866ebad3",
        "size": "M",
        "color": "white"
      }
    ]
  }
]