聚合pipe道中的右外连接

我有两个集合,让我们叫他们CatsParties ,用以下模式:

 { name: String } 

派对

 { date: Date, attendants: [{ cat: { ref: 'Cat' }, role: String }] } 

role象征一些其他的属性,比如说,参加的猫是否是一个VIP成员。

现在我想要列出所有存在的猫(即使那些从未参加任何聚会的穷人小猫),对于每只猫,我都要列出至less一方的所有angular色列表。 此外,我希望整个清单按照(每只猫)最后出席的派对datesorting,与从未参加任何派对的猫是最后一次。

这给我提出了以下问题:

  • Parties聚合不包括从未参加聚会的党派小猫。
  • 聚集Cats过去了»错误的方式«因为我不能$lookup派对猫参加,因为这些信息是在一个子文档数组。

我现在的pipe道给了我至less有一方参加了他们的angular色列表的所有猫,但没有按照最后一个出席的方sorting。 事实上,我可以生活在排除从未参加聚会的猫,但sorting对我来说至关重要:

 Party.aggregate([ { $unwind: '$attendants' }, { $project: { role: '$attendants.role', cat: '$attendants.cat' } }, { $group: { _id: '$cat', roles: { $addToSet: '$role' } } }, { $lookup: { from: 'cats', localField: '_id', foreignField: '_id', as: 'cat' } }, { $unwind: '$cat' }, // (*) { $addFields: { 'cat.roles': '$roles' } }, { $replaceRoot: { newRoot: '$cat' } } ]) 

我目前的想法基本上是一个正确的外部join在(*)添加一个猫参加的派对清单, $project到党的date,然后$group使用$max获取最新的date。 然后我可以$unwind ,现在一个元素的数组和$sort在最后。

问题是在mongo AFAIK中不存在正确的外连接,我不知道如何获得pipe道内每猫的派对列表。

为了澄清,预期的产出应该是类似的

 [ { "_id": "59982d3c7ca25936f8c327c8", "name": "Mr. Kitty", "roles": ["vip", "birthday cat"], "dateOfLastParty": "2017-06-02" }, { "_id": "59982d3c7ca25936f8c327c9", "name": "Snuffles", "roles": ["best looking cat"], "dateOfLastParty": "2017-06-01" }, ... { "_id": "59982d3c7ca25936f8c327c4", "name": "Sad Face McLazytown", "roles": [], "dateOfLastParty": null }, ] 

如上所述,你想要“猫”,所以使用Cat模型,并执行实际上是$lookup固有的“左外连接”,而不是从相反的集合要求“右外连接”,因为“右外join“在MongoDB目前是不可能的。

作为一个“左连接”,它也更加实用,因为你需要“猫”作为你的主要输出源。 链接到“Party”时唯一要考虑的就是每个“Cat”都列在一个数组中,因此您将整个文档重新获得。 因此,所有需要完成的工作都是在$lookup之后的“后期处理”中,只需简单地“过滤”当前cat匹配项的数组内容即可。

幸运的是,我们用$arrayElemAt$indexOfArray获得了很好的特性,可以让我们做到这一点:

 let kitties = await Cat.aggregate([ { '$lookup': { 'from': Party.collection.name, 'localField': '_id', 'foreignField': 'attendants.cat', 'as': 'parties' }}, { '$replaceRoot': { 'newRoot': { '$let': { 'vars': { 'parties': { '$map': { 'input': '$parties', 'as': 'p', 'in': { 'date': '$$p.date', 'role': { '$arrayElemAt': [ '$$p.attendants.role', { '$indexOfArray': [ '$$p.attendants.cat', '$_id' ] } ] } } } } }, 'in': { '_id': '$_id', 'name': '$name', 'roles': '$$parties.role', 'dateOfLastParty': { '$max': '$$parties.date' } } } } }} ]); 

所以我这里“最优”处理的概念实际上使用了$replaceRoot ,因为你可以在$let语句下定义整个文档。 我这样做的原因是,我们可以从前面的$lookup获取"parties"数组输出,并重新devise每个条目,为当前“派对”提取匹配的"role"数据。 这个我们实际上可以自己做一个variables。

“数组variables”的原因是因为我们可以使用$max将“最大/最后”date属性提取为“单数”,并将“angular色”值从该重新构造的内容中提取为“数组”。 这可以很容易地定义你想要的字段。

而且由于这是Cat起初的一个“左连接”,那么那些错过了各方的那些可怜的小猫还在那里,而且还有所需的输出。

两个聚合stream水线阶段。 什么可以更简单!

作为完整的列表:

 const mongoose = require('mongoose'), Schema = mongoose.Schema; mongoose.Promise = global.Promise; mongoose.set('debug',true); const uri = 'mongodb://localhost/catparty', options = { useMongoClient: true }; const catSchema = new Schema({ name: String }); const partySchema = new Schema({ date: Date, attendants: [{ cat: { type: Schema.Types.ObjectId, ref: 'Cat' }, role: String }] }); const Cat = mongoose.model('Cat', catSchema); const Party = mongoose.model('Party', partySchema); function log(data) { console.log(JSON.stringify(data,undefined,2)) } (async function() { try { const conn = await mongoose.connect(uri,options); // Clean collections await Promise.all( Object.keys(conn.models).map( m => conn.models[m].remove({}) ) ); var cats = await Cat.insertMany( ['Fluffy', 'Snuggles', 'Whiskers', 'Socks'].map( name => ({ name }) ) ); cats.shift(); cats = cats.map( (cat,idx) => ({ cat: cat._id, role: (idx === 0) ? 'Host' : 'Guest' }) ); log(cats); let party = await Party.create({ date: new Date(), attendants: cats }); log(party); let kitties = await Cat.aggregate([ { '$lookup': { 'from': Party.collection.name, 'localField': '_id', 'foreignField': 'attendants.cat', 'as': 'parties' }}, { '$replaceRoot': { 'newRoot': { '$let': { 'vars': { 'parties': { '$map': { 'input': '$parties', 'as': 'p', 'in': { 'date': '$$p.date', 'role': { '$arrayElemAt': [ '$$p.attendants.role', { '$indexOfArray': [ '$$p.attendants.cat', '$_id' ] } ] } } } } }, 'in': { '_id': '$_id', 'name': '$name', 'roles': '$$parties.role', 'dateOfLastParty': { '$max': '$$parties.date' } } } } }} ]); log(kitties); } catch(e) { console.error(e); } finally { mongoose.disconnect(); } })(); 

和示例输出:

 [ { "_id": "59a00d9528683e0f59e53460", "name": "Fluffy", "roles": [], "dateOfLastParty": null }, { "_id": "59a00d9528683e0f59e53461", "name": "Snuggles", "roles": [ "Host" ], "dateOfLastParty": "2017-08-25T11:44:21.903Z" }, { "_id": "59a00d9528683e0f59e53462", "name": "Whiskers", "roles": [ "Guest" ], "dateOfLastParty": "2017-08-25T11:44:21.903Z" }, { "_id": "59a00d9528683e0f59e53463", "name": "Socks", "roles": [ "Guest" ], "dateOfLastParty": "2017-08-25T11:44:21.903Z" } ] 

而且你应该能够看到这些“angular色”值如何实际上成为一个包含更多数据的数组。 如果你需要这是一个“独特的列表”,那么简单地用$setDifference包装,如下所示:

 'roles': { '$setDifference': [ '$$parties.role', [] ] }, 

这也包括在内