Mongodb mongoose填充传递找到的对象引用键

Mongodb mongoose填充传递找到的对象引用键,mongodb,mongoose,mongodb-query,Mongodb,Mongoose,Mongodb Query,我有一个mongoose组模式,其中包含invitee(子文档数组)和currentMove,invitee也包含currentMove,我希望只获取具有相同currentMove的子文档的文档 Group.findById("5a03fa29fafa645c8a399353") .populate({ path: 'invitee.user_id', select: 'currentMove', model:"User", match: {

我有一个mongoose组模式,其中包含invitee(子文档数组)和currentMove,invitee也包含currentMove,我希望只获取具有相同currentMove的子文档的文档

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

这将为匹配查询生成未知的currentMove对象id。我不确定mongoose是否有这个功能。有人能帮我吗?

在现代MongoDB版本中,在这里使用它比在这里使用
.populate()
更有效。另外,您希望基于字段比较进行筛选的基本概念是MongoDB在本机运算符方面做得很好的,但您不能轻松地将其转换为
.populate()

事实上,与
.populate()
一起实际使用的唯一方法是首先检索所有结果,然后使用
Model.populate()
上的查询all子句,同时使用
数组.map()
处理结果数组,以便将每个文档的本地值应用于要“连接”的条件

这一切都很混乱,涉及到从服务器中提取所有结果并在本地进行过滤。因此,这是我们最好的选择,所有的“过滤”和“匹配”实际上都在服务器上进行,而不需要通过网络拉取不必要的文档来获得结果

示例模式 你的问题中实际上没有包含“模式”,因此我们只能根据你在问题中实际包含的部分进行近似处理。因此,我这里的示例使用:

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 在这里,我们有不同的查询方法。第一个基本上包括在阶段之前和之后应用。这部分是因为“引用”是数组中的一个嵌入字段,另一部分是因为它实际上是这里使用的最有效的查询表单,可以避免可能超过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" }
    }}
  ]);
这里的键表达式是在返回结果后处理的。这允许对父文档中的
“currentMove”
值和
用户
对象的“连接”详细信息进行逻辑比较

由于我们使用数组内容,因此我们使用来重建数组(如果必须),并使用来选择原始文档的其他字段

有很多方法可以检查模式并生成这样一个阶段,但这并不在问题的范围之内。可以在上看到一个例子。要点是,如果您希望返回字段,那么您可以使用这些表达式来构建这个管道阶段,以返回原始形状的文档

筛选$lookup结果 如果您确信“联接”的“未过滤”结果不会导致文档超过BSON限制,则另一种方法是创建单独的目标数组,然后使用and和其他数组运算符重建“联接”数组内容:

  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 } } }
  ]);
我们在这里使用表达式,而不是过滤“文档”,只返回目标数组
“inviteeT”
中共享相同
“currentMove”
的那些成员。因为这只是“外来”内容,所以我们使用并转置元素与原始数组“连接”

为了从原始数组中“转换”值,我们使用and表达式。允许我们将目标的
“\u id”
值与原始数组中的
“user\u id”
值相匹配,并获得其“索引”位置。我们一直都知道这是一场真正的比赛,因为这部分是他们为我们做的

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

与第一个管道示例不同,我们现在仍然有
“inviteeT”
数组以及我们的重新编写的
“invitee”
数组。因此,消除这种情况的一种方法是添加一个额外的数组,并排除不需要的“临时”数组。当然,由于我们没有使用“筛选”和“筛选”,因此仍然存在根本没有匹配数组项的可能结果。因此,表达式用于测试数组结果中是否存在
0
索引,这意味着存在“至少一个”结果,并丢弃任何具有空数组的文档

MongoDB 3.6“子查询” MongoDB 3.6使这一点更加清晰,因为新的语法允许在参数中提供更具表现力的“管道”来选择返回的结果,而不是简单的
“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”
允许我们从当前文档中声明“变量”,然后可以在
“管道”
表达式中引用这些变量,该表达式将被执行,以确定返回目标数组的结果

此处基本上替换了以前使用的or条件,并将“本地”键匹配与“外部”键匹配相结合,这也要求我们声明这样的变量。在这里,我们将源文档中的
“$invitee.user_id”
值映射到一个变量中,我们在其余表达式中将该变量称为
“$$users”

这里的运算符是聚合框架的一个变体,它返回一个布尔条件,其中第一个参数“value”位于第二个参数“array”中。这就是“外国佬”
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
      }
    ]
  }
]
   // 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);
[
  {
    "_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
      }
    ]
  }
]