mongodb – mongoose populate传递找到的对象引用键

我有一个mongoose Group模式,其中包含被邀请者(子文档数组)和currentMove,被邀请者还包含currentMove,我想获得只有具有相同currentMove的子文档的文档.

Group.findById("5a03fa29fafa645c8a399353")
.populate({
    path: 'invitee.user_id',
    select: 'currentMove',
    model:"User",
     match: {
         "currentMove":{
            $eq: "$currentMove"
        }
    }
}) 

这会为匹配查询生成未知的currentMove Object id.我不确定mongoose是否具有此功能.有人可以帮帮我吗?

最佳答案 在现代MongoDB版本中,使用
$lookup而不是.populate()更有效.您想要根据字段比较进行过滤的基本概念是MongoDB与本机运算符完美匹配的东西,但它不是您可以轻松转换为.populate()的东西.

实际上,与.populate()实际使用的唯一方法是首先检索所有结果,然后使用Model.populate()和查询的$where子句,同时按顺序处理结果数组Array.map()将每个文档的本地值应用于“加入”的条件.

它有点混乱,涉及从服务器中提取所有结果并在本地过滤.所以$lookup是我们最好的选择,其中所有“过滤”和“匹配”实际上都发生在服务器上,而不需要通过网络提取不必要的文档只是为了获得结果.

示例模式

您实际上并未在问题中包含“架构”,因此我们只能根据您实际在问题中包含的部分进行近似处理.所以这里我的例子使用:

const userSchema = new Schema({
  name: String,
  currentMove: Number
})

const groupSchema = new Schema({
  name: String,
  topic: String,
  currentMove: Number,
  invitee: [{
    user_id: { type: Schema.Types.ObjectId, ref: 'User' },
    confirmed: { type: Boolean, default: false }
  }]
});

展开$lookup和$group

从这里开始,我们对$lookup查询有不同的方法.第一个基本上涉及在$lookup阶段之前和之后应用$unwind.这部分是因为你的“引用”是数组中的一个嵌入字段,部分原因是因为它实际上是最有效的查询形式,这里使用的可能“连接”结果可能超过BSON限制(文档为16MB)被避免:

  Group.aggregate([
    { "$unwind": "$invitee" },
    { "$lookup": {
      "from": User.collection.name,
      "localField": "invitee.user_id",
      "foreignField": "_id",
      "as": "invitee.user_id"
    }},
    { "$unwind": "$invitee.user_id" },
    { "$redact": {
      "$cond": {
        "if": { "$eq": ["$currentMove", "$invitee.user_id.currentMove"] },
        "then": "$$KEEP",
        "else": "$$PRUNE"
      }
    }},
    { "$group": {
      "_id": "$_id",
      "name": { "$first": "$name" },
      "topic": { "$first": "$topic" },
      "currentMove": { "$first": "$currentMove" },
      "invitee": { "$push": "$invitee" }
    }}
  ]);

这里的关键表达式是$redact,它在返回$lookup结果后处理.这允许逻辑比较父文档中的“currentMove”值和User对象的“已连接”细节.

$unwind我们的数组内容开始,我们使用$group$push来重建数组(如果必须的话)并使用$first选择原始文档的其他字段.

有一些方法可以检查模式并生成这样一个阶段,但这并不是问题的范围.可以在Querying after populate in Mongoose上看到一个示例.请注意,如果您想要返回的字段,那么您将构建此管道阶段,使用这些表达式返回原始形状的文档.

过滤$查找结果

另一种方法是,您确定“加入”的“未过滤”结果不会导致文档超出BSON限制,而是创建一个单独的目标数组,然后使用$map$filter重新构建“已加入”数组内容和其他数组运算符:

  Group.aggregate([
    { "$lookup": {
      "from": User.collection.name,
      "localField": "invitee.user_id",
      "foreignField": "_id",
      "as": "inviteeT"
    }},
    { "$addFields": {
      "invitee": {
        "$map": {
          "input": {
            "$filter": {
              "input": "$inviteeT",
              "as": "i",
              "cond": { "$eq": ["$$i.currentMove","$currentMove"] }
            }
          },
          "as": "i",
          "in": {
            "_id": {
              "$arrayElemAt": [
                "$invitee._id",
                { "$indexOfArray": ["$invitee.user_id", "$$i._id"] }
              ]
            },
            "user_id": "$$i",
            "confirmed": {
              "$arrayElemAt": [
                "$invitee.confirmed",
                { "$indexOfArray": ["$invitee.user_id","$$i._id"] }
              ]
            }
          }
        }
      }
    }},
    { "$project": { "inviteeT": 0 } },
    { "$match": { "invitee.0": { "$exists": true } } }
  ]);

而不是将过滤“文档”的$redact,我们在这里使用$filter,表达式仅返回共享相同“currentMove”的目标数组“inviteeT”的那些成员.由于这只是“外来”内容,我们使用$map与原始数组“连接”并转置元素.

为了从原始数组中“转置”值,我们使用$arrayElemAt$indexOfArray表达式. $indexOfArray允许我们将目标的“_id”值与原始数组中的“user_id”值进行匹配,并获得它的“索引”位置.我们总是知道这会返回真正的匹配,因为$lookup为我们做了那部分.

然后将“索引”值提供给$arrayElemAt,类似地将值的“映射”应用为类似“$invitee.confirmed”的数组,并返回在相同索引处匹配的值.这基本上是数组之间的“查找”.

与第一个管道示例不同,我们现在仍然拥有“inviteeT”数组以及我们重新编写的“被邀请者”数组,由$addFields提供.因此,摆脱这种情况的一种方法是添加额外的$project并排除不需要的“临时“阵列.当然,因为我们没有$unwind和“过滤器”,所以仍然可能没有匹配的数组条目.因此,$match表达式使用$exists来测试数组结果中存在的0索引,这意味着存在“至少一个”结果,并丢弃具有空数组的任何文档.

MongoDB 3.6“子查询”

MongoDB 3.6使得它更清晰,因为$lookup的新语法允许在参数中给出更具表现力的“管道”以选择返回的结果,而不是简单的“localField”和“foreignField”匹配.

  Group.aggregate([
    { "$lookup": {
      "from": User.collection.name,
      "let": {
        "ids": "$invitee._id",
        "users": "$invitee.user_id",
        "confirmed": "$invitee.confirmed",
        "currentMove": "$currentMove"
      },
      "pipeline": [
        { "$match": {
          "$expr": {
            "$and": [
              { "$in": ["$_id", "$$users"] },
              { "$eq": ["$currentMove", "$$currentMove"] }
            ]
          }
        }},
        { "$project": {
          "_id": {
            "$arrayElemAt": [
              "$$ids",
              { "$indexOfArray": ["$$users", "$_id"] }
            ]
          },
          "user_id": "$$ROOT",
          "confirmed": {
            "$arrayElemAt": [
              "$$confirmed",
              { "$indexOfArray": ["$$users", "$_id"] }
            ]
          }
        }}
      ],
      "as": "invitee"
    }},
    { "$match": { "invitee.0": { "$exists": true } } }
  ])

因此,由于当前如何通过“let”声明将这些数据传递到子管道,因此使用特定输入值的映射数组时,会出现一些稍微“微不足道”的事情.这可能应该更干净,但在当前的候选版本中,这是实际需要表达以便工作的方式.

使用这种新语法,“let”允许我们从当前文档声明“变量”,然后可以在“管道”表达式中引用该表达式,该表达式将被执行以确定返回目标数组的结果.

$expr在这里基本上取代了之前使用的$redact$filter条件,以及将“本地”与“外部”键匹配相结合,这也需要我们声明这样的变量.在这里,我们将源文档中的“$invitee.user_id”值映射到一个变量中,我们在其余表达式中将其称为“$$users”.

这里的$in运算符是聚合框架的变体,它返回一个布尔条件,其中第一个参数“value”在第二个参数“array”中找到.所以这是“外键”过滤器部分.

由于这是一个“管道”,除了$match之外,我们还可以添加一个$project阶段,该阶段从国外集合中选择了项目.因此,我们再次使用与之前描述的类似的“换位”技术.然后,这使我们能够控制数组中返回的文档的“形状”,因此我们不会像之前那样在$lookup之后“操纵”返回的数组.

同样的情况适用,因为无论你在这里做什么,“子管道”当然可以在过滤条件不匹配时不返回任何结果.因此,使用相同的$exists测试来丢弃这些文档.

所以这一切都非常酷,一旦你习惯了服务器端可用的电源“加入”$lookup的功能,你很可能永远不会回头.虽然语法比引入.populate()的“便利”功能简洁得多,但流量负载减少,更高级的用途和一般表现力基本上弥补了这一点.

作为一个完整的例子,我还包括一个自包含的列表,演示了所有这些.如果您使用附加的MongoDB 3.6兼容服务器运行它,那么您甚至可以进行该演示.

需要最近的Node.js v8.x版本与async / await一起运行(或者在其他支持的情况下启用),但是因为现在是LTS版本,你真的应该正在运行它.至少安装一个测试:)

const mongoose = require('mongoose'),
      Schema = mongoose.Schema;

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

const uri = 'mongodb://localhost/rollgroup',
      options = { useMongoClient: true };

const userSchema = new Schema({
  name: String,
  currentMove: Number
})

const groupSchema = new Schema({
  name: String,
  topic: String,
  currentMove: Number,
  invitee: [{
    user_id: { type: Schema.Types.ObjectId, ref: 'User' },
    confirmed: { type: Boolean, default: false }
  }]
});

const User = mongoose.model('User', userSchema);
const Group = mongoose.model('Group', groupSchema);

function log(data) {
  console.log(JSON.stringify(data, undefined, 2))
}

(async function() {

  try {

    const conn = await mongoose.connect(uri,options);

    let { version } = await conn.db.admin().command({'buildInfo': 1});

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

    // Add some users
    let users = await User.insertMany([
      { name: 'Bill', currentMove: 1 },
      { name: 'Ted', currentMove: 2 },
      { name: 'Fred', currentMove: 3 },
      { name: 'Sally', currentMove: 4 },
      { name: 'Harry', currentMove: 5 }
    ]);

    await Group.create({
      name: 'Group1',
      topic: 'This stuff',
      currentMove: 3,
      invitee: users.map( u =>
        ({ user_id: u._id, confirmed: (u.currentMove === 3) })
      )
    });

    await (async function() {
      console.log('Unwinding example');
      let result = await Group.aggregate([
        { "$unwind": "$invitee" },
        { "$lookup": {
          "from": User.collection.name,
          "localField": "invitee.user_id",
          "foreignField": "_id",
          "as": "invitee.user_id"
        }},
        { "$unwind": "$invitee.user_id" },
        { "$redact": {
          "$cond": {
            "if": { "$eq": ["$currentMove", "$invitee.user_id.currentMove"] },
            "then": "$$KEEP",
            "else": "$$PRUNE"
          }
        }},
        { "$group": {
          "_id": "$_id",
          "name": { "$first": "$name" },
          "topic": { "$first": "$topic" },
          "currentMove": { "$first": "$currentMove" },
          "invitee": { "$push": "$invitee" }
        }}
      ]);

      log(result);

    })();

    await (async function() {
      console.log('Using $filter example');
      let result = await Group.aggregate([
        { "$lookup": {
          "from": User.collection.name,
          "localField": "invitee.user_id",
          "foreignField": "_id",
          "as": "inviteeT"
        }},
        { "$addFields": {
          "invitee": {
            "$map": {
              "input": {
                "$filter": {
                  "input": "$inviteeT",
                  "as": "i",
                  "cond": { "$eq": ["$$i.currentMove","$currentMove"] }
                }
              },
              "as": "i",
              "in": {
                "_id": {
                  "$arrayElemAt": [
                    "$invitee._id",
                    { "$indexOfArray": ["$invitee.user_id", "$$i._id"] }
                  ]
                },
                "user_id": "$$i",
                "confirmed": {
                  "$arrayElemAt": [
                    "$invitee.confirmed",
                    { "$indexOfArray": ["$invitee.user_id","$$i._id"] }
                  ]
                }
              }
            }
          }
        }},
        { "$project": { "inviteeT": 0 } },
        { "$match": { "invitee.0": { "$exists": true } } }
      ]);

      log(result);

    })();

    await (async function() {
      if (parseFloat(version.match(/\d\.\d/)[0]) >= 3.6) {
        console.log('New $lookup example. Yay!');
        let result = await Group.collection.aggregate([
          { "$lookup": {
            "from": User.collection.name,
            "let": {
              "ids": "$invitee._id",
              "users": "$invitee.user_id",
              "confirmed": "$invitee.confirmed",
              "currentMove": "$currentMove"
            },
            "pipeline": [
              { "$match": {
                "$expr": {
                  "$and": [
                    { "$in": ["$_id", "$$users"] },
                    { "$eq": ["$currentMove", "$$currentMove"] }
                  ]
                }
              }},
              { "$project": {
                "_id": {
                  "$arrayElemAt": [
                    "$$ids",
                    { "$indexOfArray": ["$$users", "$_id"] }
                  ]
                },
                "user_id": "$$ROOT",
                "confirmed": {
                  "$arrayElemAt": [
                    "$$confirmed",
                    { "$indexOfArray": ["$$users", "$_id"] }
                  ]
                }
              }}
            ],
            "as": "invitee"
          }},
          { "$match": { "invitee.0": { "$exists": true } } }
        ]).toArray();

        log(result);

      }
    })();

    await (async function() {
      console.log("Horrible populate example :(");

      let results = await Group.find();

       results = await Promise.all(
        results.map( r =>
          User.populate(r,{
            path: 'invitee.user_id',
            match: { "$where": `this.currentMove === ${r.currentMove}` }
          })
        )
      );

      console.log("All members still there");
      log(results);

      // Then we clean it for null values

      results = results.map( r =>
        Object.assign(r,{
          invitee: r.invitee.filter(i => i.user_id !== null)
        })
      );

      console.log("Now they are filtered");
      log(results);

    })();


  } catch(e) {
    console.error(e);
  } finally {
    mongoose.disconnect();
  }

})()

为每个示例提供输出:

Mongoose: users.remove({}, {})
Mongoose: groups.remove({}, {})
Mongoose: users.insertMany([ { __v: 0, name: 'Bill', currentMove: 1, _id: 5a0afda01643cf41789e500a }, { __v: 0, name: 'Ted', currentMove: 2, _id: 5a0afda01643cf41789e500b }, { __v: 0, name: 'Fred', currentMove: 3, _id: 5a0afda01643cf41789e500c }, { __v: 0, name: 'Sally', currentMove: 4, _id: 5a0afda01643cf41789e500d }, { __v: 0, name: 'Harry', currentMove: 5, _id: 5a0afda01643cf41789e500e } ], {})
Mongoose: groups.insert({ name: 'Group1', topic: 'This stuff', currentMove: 3, _id: ObjectId("5a0afda01643cf41789e500f"), invitee: [ { user_id: ObjectId("5a0afda01643cf41789e500a"), _id: ObjectId("5a0afda01643cf41789e5014"), confirmed: false }, { user_id: ObjectId("5a0afda01643cf41789e500b"), _id: ObjectId("5a0afda01643cf41789e5013"), confirmed: false }, { user_id: ObjectId("5a0afda01643cf41789e500c"), _id: ObjectId("5a0afda01643cf41789e5012"), confirmed: true }, { user_id: ObjectId("5a0afda01643cf41789e500d"), _id: ObjectId("5a0afda01643cf41789e5011"), confirmed: false }, { user_id: ObjectId("5a0afda01643cf41789e500e"), _id: ObjectId("5a0afda01643cf41789e5010"), confirmed: false } ], __v: 0 })
Unwinding example
Mongoose: groups.aggregate([ { '$unwind': '$invitee' }, { '$lookup': { from: 'users', localField: 'invitee.user_id', foreignField: '_id', as: 'invitee.user_id' } }, { '$unwind': '$invitee.user_id' }, { '$redact': { '$cond': { if: { '$eq': [ '$currentMove', '$invitee.user_id.currentMove' ] }, then: '$$KEEP', else: '$$PRUNE' } } }, { '$group': { _id: '$_id', name: { '$first': '$name' }, topic: { '$first': '$topic' }, currentMove: { '$first': '$currentMove' }, invitee: { '$push': '$invitee' } } } ], {})
[
  {
    "_id": "5a0afda01643cf41789e500f",
    "name": "Group1",
    "topic": "This stuff",
    "currentMove": 3,
    "invitee": [
      {
        "user_id": {
          "_id": "5a0afda01643cf41789e500c",
          "__v": 0,
          "name": "Fred",
          "currentMove": 3
        },
        "_id": "5a0afda01643cf41789e5012",
        "confirmed": true
      }
    ]
  }
]
Using $filter example
Mongoose: groups.aggregate([ { '$lookup': { from: 'users', localField: 'invitee.user_id', foreignField: '_id', as: 'inviteeT' } }, { '$addFields': { invitee: { '$map': { input: { '$filter': { input: '$inviteeT', as: 'i', cond: { '$eq': [ '$$i.currentMove', '$currentMove' ] } } }, as: 'i', in: { _id: { '$arrayElemAt': [ '$invitee._id', { '$indexOfArray': [ '$invitee.user_id', '$$i._id' ] } ] }, user_id: '$$i', confirmed: { '$arrayElemAt': [ '$invitee.confirmed', { '$indexOfArray': [ '$invitee.user_id', '$$i._id' ] } ] } } } } } }, { '$project': { inviteeT: 0 } }, { '$match': { 'invitee.0': { '$exists': true } } } ], {})
[
  {
    "_id": "5a0afda01643cf41789e500f",
    "name": "Group1",
    "topic": "This stuff",
    "currentMove": 3,
    "invitee": [
      {
        "_id": "5a0afda01643cf41789e5012",
        "user_id": {
          "_id": "5a0afda01643cf41789e500c",
          "__v": 0,
          "name": "Fred",
          "currentMove": 3
        },
        "confirmed": true
      }
    ],
    "__v": 0
  }
]
New $lookup example. Yay!
Mongoose: groups.aggregate([ { '$lookup': { from: 'users', let: { ids: '$invitee._id', users: '$invitee.user_id', confirmed: '$invitee.confirmed', currentMove: '$currentMove' }, pipeline: [ { '$match': { '$expr': { '$and': [ { '$in': [ '$_id', '$$users' ] }, { '$eq': [ '$currentMove', '$$currentMove' ] } ] } } }, { '$project': { _id: { '$arrayElemAt': [ '$$ids', { '$indexOfArray': [ '$$users', '$_id' ] } ] }, user_id: '$$ROOT', confirmed: { '$arrayElemAt': [ '$$confirmed', { '$indexOfArray': [ '$$users', '$_id' ] } ] } } } ], as: 'invitee' } }, { '$match': { 'invitee.0': { '$exists': true } } } ])
[
  {
    "_id": "5a0afda01643cf41789e500f",
    "name": "Group1",
    "topic": "This stuff",
    "currentMove": 3,
    "invitee": [
      {
        "_id": "5a0afda01643cf41789e5012",
        "user_id": {
          "_id": "5a0afda01643cf41789e500c",
          "__v": 0,
          "name": "Fred",
          "currentMove": 3
        },
        "confirmed": true
      }
    ],
    "__v": 0
  }
]
Horrible populate example :(
Mongoose: groups.find({}, { fields: {} })
Mongoose: users.find({ _id: { '$in': [ ObjectId("5a0afda01643cf41789e500a"), ObjectId("5a0afda01643cf41789e500b"), ObjectId("5a0afda01643cf41789e500c"), ObjectId("5a0afda01643cf41789e500d"), ObjectId("5a0afda01643cf41789e500e") ] }, '$where': 'this.currentMove === 3' }, { fields: {} })
All members still there
[
  {
    "_id": "5a0afda01643cf41789e500f",
    "name": "Group1",
    "topic": "This stuff",
    "currentMove": 3,
    "__v": 0,
    "invitee": [
      {
        "user_id": null,
        "_id": "5a0afda01643cf41789e5014",
        "confirmed": false
      },
      {
        "user_id": null,
        "_id": "5a0afda01643cf41789e5013",
        "confirmed": false
      },
      {
        "user_id": {
          "_id": "5a0afda01643cf41789e500c",
          "__v": 0,
          "name": "Fred",
          "currentMove": 3
        },
        "_id": "5a0afda01643cf41789e5012",
        "confirmed": true
      },
      {
        "user_id": null,
        "_id": "5a0afda01643cf41789e5011",
        "confirmed": false
      },
      {
        "user_id": null,
        "_id": "5a0afda01643cf41789e5010",
        "confirmed": false
      }
    ]
  }
]
Now they are filtered
[
  {
    "_id": "5a0afda01643cf41789e500f",
    "name": "Group1",
    "topic": "This stuff",
    "currentMove": 3,
    "__v": 0,
    "invitee": [
      {
        "user_id": {
          "_id": "5a0afda01643cf41789e500c",
          "__v": 0,
          "name": "Fred",
          "currentMove": 3
        },
        "_id": "5a0afda01643cf41789e5012",
        "confirmed": true
      }
    ]
  }
]

使用populate()

所以在这里使用.populate()实际上非常可怕.当然它看起来更少,但它实际上做了许多根本不需要的事情,而且都是因为“加入”不会发生在服务器上:

   // Note that we cannot populate "here" since we need the returned value
   let results = await Group.find();

   // The value is only in context as we use `Array.map()` to process each result
   results = await Promise.all(
    results.map( r =>
      User.populate(r,{
        path: 'invitee.user_id',
        match: { "$where": `this.currentMove === ${r.currentMove}` }
      })
    )
  );

  console.log("All members still there");
  log(results);

  // Then we clean it for null values

  results = results.map( r =>
    Object.assign(r,{
      invitee: r.invitee.filter(i => i.user_id !== null)
    })
  );

  console.log("Now they are filtered");
  log(results);

所以我还在上面的输出中包括了,以及整个代码清单.

问题变得明显,因为您无法将填充“直接”链接到第一个查询.实际上,您需要返回文档(可能是所有文档),以便在后续填充中使用当前文档值.并且必须为返回的每个文档处理.

不仅如此,但populate()不会将数组“过滤”为仅匹配的数组,即使查询条件也是如此.它所做的就是将不匹配的元素设置为null:

[
  {
    "_id": "5a0afa889f9f7e4064d8794d",
    "name": "Group1",
    "topic": "This stuff",
    "currentMove": 3,
    "__v": 0,
    "invitee": [
      {
        "user_id": null,
        "_id": "5a0afa889f9f7e4064d87952",
        "confirmed": false
      },
      {
        "user_id": null,
        "_id": "5a0afa889f9f7e4064d87951",
        "confirmed": false
      },
      {
        "user_id": {
          "_id": "5a0afa889f9f7e4064d8794a",
          "__v": 0,
          "name": "Fred",
          "currentMove": 3
        },
        "_id": "5a0afa889f9f7e4064d87950",
        "confirmed": true
      },
      {
        "user_id": null,
        "_id": "5a0afa889f9f7e4064d8794f",
        "confirmed": false
      },
      {
        "user_id": null,
        "_id": "5a0afa889f9f7e4064d8794e",
        "confirmed": false
      }
    ]
  }
]

然后,需要再次处理Array.filter()以返回“每个”文档,最终可以删除不需要的数组项,并为其他聚合查询提供相同的结果.

所以它“真的很浪费”而且不是一个很好的做事方式.当你实际在服务器上进行大部分处理时,拥有数据库的意义不大.实际上,我们可能只是简单地返回填充的结果,然后运行Array.filter()以删除不需要的条目.

这不是你如何编写快速有效的代码.所以这里的例子有时候“看起来很简单”实际上造成了更多的伤害而不是好处.

点赞