聚合管道中的右外连接

时间:2017-08-25 10:24:23

标签: node.js mongodb mongoose aggregation-framework

我有两个集合,让我们称之为CatsParties,使用以下模式:

{ name: String }

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

其中role表示某些其他属性,例如,参与猫是否为VIP会员。

现在我想得到一个所有猫的清单(即使那些从未参加任何派对的可怜猫咪),每只猫,我想要一份有关它所有角色的列表至少有一方。此外,我希望这个整个列表按照(每只猫)最后一次参加的派对date与从未参加任何派对的猫进行排序。

这给我带来了以下问题:

  • Parties上进行整合,排除了从未参加过派对的派对小猫。
  • 聚合Cats类似的错误方式«因为我不能$lookup参加猫派对,因为该信息位于子文档数组中。

我现在拥有的管道给了我所有参加过至少一个派对的猫,他们的角色列表,但没有按最后参加的派对排序。事实上,我可以将排除从未参加过聚会的猫排除在外,但排序对我来说至关重要:

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' } }
])

我目前的想法基本上是(*)的正确的外部联接,以添加猫参加过的聚会列表,$project该聚会的日期,然后$group使用{{ 1}}获取最新日期。然后我可以$max现在是单元素数组,最后是$unwind

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

澄清一下,预期的输出应该是

$sort

1 个答案:

答案 0 :(得分:1)

如上所述,您希望“猫”使用Cat模型并执行$lookup实际固有的“左外连接”,而不是要求“右外连接”来自对方的集合,因为此时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"数组输出并重新整形每个条目,为当前的“kitty”提取匹配的"role"数据派对。我们实际上可以自己创建一个变量。

“数组变量”的原因是因为我们可以使用$max将“最大/最后”日期属性提取为“单数”,并仍将“角色”值提取为“数组”重塑了内容。这样可以轻松定义所需的字段。

因为它是从Cat开始的“左连接”,所以那些错过了所有各方的可怜的小猫仍然存在,并且仍然有所需的输出。

两个聚合管道阶段。什么可以更简单!

作为完整列表:

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"
  }
]

您应该能够看到这些“角色”值实际上如何变成具有更多数据的数组。如果您需要将其作为“唯一列表”,则只需使用$setDifference换行,如下所示:

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

这也包括在内