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

时间:2017-11-11 09:22:43

标签: mongodb mongoose mongodb-query

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

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

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

1 个答案:

答案 0 :(得分:1)

在现代MongoDB版本中,使用$lookup而不是.populate()效率更高。此外,您希望根据字段比较进行过滤的基本概念是MongoDB与本机操作符完美匹配的内容,但它不是您可以轻松转换为.populate()的内容。

事实上,与.populate()实际使用的唯一方法是首先检索所有结果,然后在处理结果数组时使用Model.populate()$where子句进行查询使用Array.map(),以便将每个文档的本地值应用于" join"上。

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

示例架构

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

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查询采用了不同的方法。第一个基本上涉及在$unwind阶段之前和之后应用$lookup。这部分是因为您的参考"是数组中的嵌入字段,也部分是因为它实际上是最有效的查询形式,可以在这里使用" join"结果可能会超出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来重建数组(如果必须)并使用{{3}选择原始文档的其他字段}。

有很多方法可以检查架构并生成这样一个阶段,但这并不是问题的范围。在$first上可以看到一个例子。指出如果你想要返回的字段,那么你将构建这个管道阶段,使用这些表达式返回原始形状的文档。

过滤$ lookup结果

另一种方法,您可以确定"未经过滤的" "加入"的结果不会导致文档超过BSON限制,而是创建一个单独的目标数组,然后重建你的"加入"使用Querying after populate in Mongoose$map以及其他数组运算符的数组内容:

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

我们在这里使用$filter而不是$redact来过滤"文档"而不是返回目标数组"inviteeT"的那些成员分享相同的"currentMove"。因为这只是"外国"内容,我们"加入"使用$filter使用原始数组并转置元素。

要做到这一点"换位"对于原始数组中的值,我们使用$map$arrayElemAt表达式。 $indexOfArray允许我们将目标的"_id"值与原始数组中的"user_id"值进行匹配,并获得" s" index"位置。我们总是知道这会返回真正的匹配,因为$indexOfArray为我们做了那部分。

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

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

MongoDB 3.6"子查询"

MongoDB 3.6使得它更加清晰,因为$exists的新语法允许更具表现力的管道"在论证中给出选择返回的结果,而不是简单的"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"允许我们声明"变量"从当前文档中可以在"pipeline"表达式中引用,该表达式将被执行以确定返回目标数组的结果。

此处$lookup基本上取代之前使用的$expr$redact条件,以及合并" local"到"外国"密钥匹配也要求我们声明这样的变量。在这里,我们将源文档中的"$invitee.user_id"值映射到我们在其余表达式中称为"$$users"的变量。

此处的$filter运算符是聚合框架的变体,它返回一个布尔条件,其中第一个参数" value"在第二个参数" array"中找到。所以这是"外键"过滤部分。

由于这是"管道",除了$in之外,我们还可以添加$project阶段,该阶段从外部集合中选择项目。所以我们再次使用类似的#34;换位"技术到之前描述的。然后,这使我们能够控制"形状"数组中返回的文档,因此我们不会操作返回的数组""我们之前做的$lookup

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

所以它非常酷,一旦你习惯了服务器端可用的电源,就可以加入" $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()以及#34;每个"返回的文档,最终可以删除不需要的数组项,并为其他聚合查询提供相同的结果。

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

这不是你编写快速有效代码的方式。所以这里的例子有时是"看起来很简单"实际上造成的伤害要大于好处。