mongodb - Mongoose 填充数组中的单个项目

标签 mongodb mongoose mongoose-populate

我有一个具有动态引用数组的模型。

var postSchema = new Schema({
  name: String,
  targets: [{
    kind: String,
    item: { type: ObjectId, refPath: 'targets.kind' }
  }]
}); 


我正在使用targets属性存储对多个不同模型,用户,线程,附件等的引用。

是否可以仅填充我想要的引用?

Post.find({}).populate({
  // Does not work
  // match: { 'targets.kind': 'Thread' }, // I want to populate only the references that match. ex: Thread, User, Attachment
  path: 'targets.item',
  model: 'targets.kind',
  select: '_id title',
});


谢谢

最佳答案

这里的一大课应该是mongoose.set('debug', true)是您的新“最好的朋友”。这将显示您正在编写的代码向MongoDB发出的实际查询,这一点非常重要,因为当您实际“看到它”时,它就会清除您可能存在的任何误解。

逻辑问题

让我们演示一下为什么您尝试的操作失败了:

const { Schema } = mongoose = require('mongoose');

const uri = 'mongodb://localhost:27017/polypop';

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

const postSchema = new Schema({
  name: String,
  targets: [{
    kind: String,
    item: { type: Schema.Types.ObjectId, refPath: 'targets.kind' }
  }]
});

const fooSchema = new Schema({
 name: String
})

const barSchema = new Schema({
  number: Number
});

const Post = mongoose.model('Post', postSchema);
const Foo = mongoose.model('Foo', fooSchema);
const Bar = mongoose.model('Bar', barSchema);

const log = data => console.log(JSON.stringify(data, undefined, 2));

(async function() {

  try {

    const conn = await mongoose.connect(uri, { useNewUrlParser: true });

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

    // Create some things
    let [foo, bar] = await Promise.all(
      [{ _t: 'Foo', name: 'Bill' }, { _t: 'Bar', number: 1 }]
        .map(({ _t, ...d }) => mongoose.model(_t).create(d))
    );

    log([foo, bar]);

    // Add a Post

    let post = await Post.create({
      name: 'My Post',
      targets: [{ kind: 'Foo', item: foo }, { kind: 'Bar', item: bar }]
    });

    log(post);

    let found = await Post.findOne();
    log(found);

    let result = await Post.findOne()
      .populate({
        match: { 'targets.kind': 'Foo' },    // here is the problem!
        path: 'targets.item',
      });

    log(result);

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

})()


因此,那里的注释显示match是逻辑问题,因此让我们看一下调试输出并查看原因:

Mongoose: posts.deleteMany({}, {})
Mongoose: foos.deleteMany({}, {})
Mongoose: bars.deleteMany({}, {})
Mongoose: foos.insertOne({ _id: ObjectId("5bdbc70996ed8e3295b384a0"), name: 'Bill', __v: 0 })
Mongoose: bars.insertOne({ _id: ObjectId("5bdbc70996ed8e3295b384a1"), number: 1, __v: 0 })
[
  {
    "_id": "5bdbc70996ed8e3295b384a0",
    "name": "Bill",
    "__v": 0
  },
  {
    "_id": "5bdbc70996ed8e3295b384a1",
    "number": 1,
    "__v": 0
  }
]
Mongoose: posts.insertOne({ _id: ObjectId("5bdbc70996ed8e3295b384a2"), name: 'My Post', targets: [ { _id: ObjectId("5bdbc70996ed8e3295b384a4"), kind: 'Foo', item: ObjectId("5bdbc70996ed8e3295b384a0") }, { _id: ObjectId("5bdbc70996ed8e3295b384a3"), kind: 'Bar', item: ObjectId("5bdbc70996ed8e3295b384a1") } ], __v: 0 })
{
  "_id": "5bdbc70996ed8e3295b384a2",
  "name": "My Post",
  "targets": [
    {
      "_id": "5bdbc70996ed8e3295b384a4",
      "kind": "Foo",
      "item": {
        "_id": "5bdbc70996ed8e3295b384a0",
        "name": "Bill",
        "__v": 0
      }
    },
    {
      "_id": "5bdbc70996ed8e3295b384a3",
      "kind": "Bar",
      "item": {
        "_id": "5bdbc70996ed8e3295b384a1",
        "number": 1,
        "__v": 0
      }
    }
  ],
  "__v": 0
}
Mongoose: posts.findOne({}, { projection: {} })
{
  "_id": "5bdbc70996ed8e3295b384a2",
  "name": "My Post",
  "targets": [
    {
      "_id": "5bdbc70996ed8e3295b384a4",
      "kind": "Foo",
      "item": "5bdbc70996ed8e3295b384a0"
    },
    {
      "_id": "5bdbc70996ed8e3295b384a3",
      "kind": "Bar",
      "item": "5bdbc70996ed8e3295b384a1"
    }
  ],
  "__v": 0
}
Mongoose: posts.findOne({}, { projection: {} })
Mongoose: bars.find({ 'targets.kind': 'Foo', _id: { '$in': [ ObjectId("5bdbc70996ed8e3295b384a1") ] } }, { projection: {} })
Mongoose: foos.find({ 'targets.kind': 'Foo', _id: { '$in': [ ObjectId("5bdbc70996ed8e3295b384a0") ] } }, { projection: {} })
{
  "_id": "5bdbc70996ed8e3295b384a2",
  "name": "My Post",
  "targets": [
    {
      "_id": "5bdbc70996ed8e3295b384a4",
      "kind": "Foo",
      "item": null
    },
    {
      "_id": "5bdbc70996ed8e3295b384a3",
      "kind": "Bar",
      "item": null
    }
  ],
  "__v": 0
}


这是完整的输出,表明其他所有内容都在正常运行,实际上,如果没有match,您将获得填充后的数据。但是,请仔细查看发布给foobar集合的两个查询:

Mongoose: bars.find({ 'targets.kind': 'Foo', _id: { '$in': [ ObjectId("5bdbc70996ed8e3295b384a1") ] } }, { projection: {} })
Mongoose: foos.find({ 'targets.kind': 'Foo', _id: { '$in': [ ObjectId("5bdbc70996ed8e3295b384a0") ] } }, { projection: {} })


因此,实际上是在'targets.kind'match集合中搜索包含在foo下的bar,而不是在您期望的posts集合中搜索。随着输出的其余部分,这应该使您了解populate()的实际工作原理,因为如示例所示,什么也没有说要专门返回kind: 'Foo'的“数组项”。

即使是自然的MongoDB查询,“过滤数组”的过程实际上也不是“真正”的,并且除了“第一和单数匹配”之外,您实际上通常会使用.aggregate()$filter运算符。您可以通过位置$运算符获得“单数”,但是如果您希望“所有foos”存在多个,则需要$filter代替。

因此,真正的核心问题是populate()实际上是错误的位置和“过滤数组”的错误操作。相反,您真的想“聪明地”仅返回想要的数组条目,然后再执行其他“填充”项的操作。

结构性问题

从上面的清单(该清单暗示了问题中所暗示的内容)中注意到,为了“加入”并获得总体结果,引用了“多个模型”。尽管这在“ RDBMS领域”中似乎是合乎逻辑的,但对于MongoDB和“文档数据库”的常规“ ilk”而言,这样做当然既不可行也不实用或有效。

这里要记住的关键是,“集合”中的“文档”不需要都具有与RDBMS相同的“表结构”。结构可能会有所不同,虽然建议不要“百变”,但将“多态对象”存储在单个集合中肯定是非常有效的。毕竟,您实际上想将所有这些东西都引用回同一父对象,所以为什么它们需要位于不同的集合中?简而言之,它们根本不需要:

const { Schema } = mongoose = require('mongoose');

const uri = 'mongodb://localhost:27017/polypop';

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

const postSchema = new Schema({
  name: String,
  targets: [{
    kind: String,
    item: { type: Schema.Types.ObjectId, ref: 'Target' }
  }]
});

const targetSchema = new Schema({});

const fooSchema = new Schema({
  name: String
});

const barSchema = new Schema({
  number: Number
});

const bazSchema = new Schema({
  title: String
});

const log = data => console.log(JSON.stringify(data, undefined, 2));

const Post = mongoose.model('Post', postSchema);
const Target = mongoose.model('Target', targetSchema);
const Foo = Target.discriminator('Foo', fooSchema);
const Bar = Target.discriminator('Bar', barSchema);
const Baz = Target.discriminator('Baz', bazSchema);

(async function() {

  try {

    const conn = await mongoose.connect(uri,{ useNewUrlParser: true });

    // Clean data - bit hacky but just a demo
    await Promise.all(
      Object.entries(conn.models).map(([k, m]) => m.deleteMany() )
    );

    // Insert some things
    let [foo1, bar, baz, foo2] = await Promise.all(
      [
        { _t: 'Foo', name: 'Bill' },
        { _t: 'Bar', number: 1 },
        { _t: 'Baz', title: 'Title' },
        { _t: 'Foo', name: 'Ted' }
      ].map(({ _t, ...d }) => mongoose.model(_t).create(d))
    );

    log([foo1, bar, baz, foo2]);

    // Add a Post
    let post = await Post.create({
      name: 'My Post',
      targets: [
        { kind: 'Foo', item: foo1 },
        { kind: 'Bar', item: bar },
        { kind: 'Baz', item: baz },
        { kind: 'Foo', item: foo2 }
      ]
    });

    log(post);

    let found = await Post.findOne();
    log(found);

    let result1 = await Post.findOne()
      .populate({
        path: 'targets.item',
        match: { __t: 'Foo' }
      });
    log(result1);

    let result2 = await Post.aggregate([
      // Only get documents with a matching entry
      { "$match": {
        "targets.kind": "Foo"
      }},
      // Optionally filter the array
      { "$addFields": {
        "targets": {
          "$filter": {
            "input": "$targets",
            "cond": {
              "$eq": [ "$$this.kind", "Foo" ]
             }
          }
        }
      }},
      // Lookup from single source
      { "$lookup": {
        "from": Target.collection.name,
        "localField": "targets.item",
        "foreignField": "_id",
        "as": "matches"
      }},
      // Marry up arrays
      { "$project": {
        "name": 1,
        "targets": {
          "$map": {
            "input": "$targets",
            "in": {
              "kind": "$$this.kind",
              "item": {
                "$arrayElemAt": [
                  "$matches",
                  { "$indexOfArray": [ "$matches._id", "$$this.item" ] }
                ]
              }
            }
          }
        }
      }}
    ]);
    log(result2);

    let result3 = await Post.aggregate([
      // Only get documents with a matching entry
      { "$match": {
        "targets.kind": "Foo"
      }},
      // Optionally filter the array
      { "$addFields": {
        "targets": {
          "$filter": {
            "input": "$targets",
            "cond": {
              "$eq": [ "$$this.kind", "Foo" ]
             }
          }
        }
      }},
      // Lookup from single source with overkill of type check
      { "$lookup": {
        "from": Target.collection.name,
        "let": { "targets": "$targets" },
        "pipeline": [
          { "$match": {
            "$expr": {
              "$in": [ "$_id", "$$targets.item" ]
            },
            "__t": "Foo"
          }}
        ],
        "as": "matches"
      }},
      // Marry up arrays
      { "$project": {
        "name": 1,
        "targets": {
          "$map": {
            "input": "$targets",
            "in": {
              "kind": "$$this.kind",
              "item": {
                "$arrayElemAt": [
                  "$matches",
                  { "$indexOfArray": [ "$matches._id", "$$this.item" ] }
                ]
              }
            }
          }
        }
      }}
    ]);
    console.log(result3);    

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

})()


这需要更长的时间,并且还有更多的概念可以解决,但是基本原理是,对于不同的类型,我们只使用一个,而不是使用“多个集合”。为此,“猫鼬”方法在模型设置中使用"discriminators",这与代码的这一部分都相关:

const Post = mongoose.model('Post', postSchema);
const Target = mongoose.model('Target', targetSchema);
const Foo = Target.discriminator('Foo', fooSchema);
const Bar = Target.discriminator('Bar', barSchema);
const Baz = Target.discriminator('Baz', bazSchema);


实际上,这只是从“基本模型”中为“单一”集合调用.discriminator()而不是调用mongoose.model()。就此而言,真正的好处是,就其余代码而言,BazBar等都被透明地视为“模型”,但是它们实际上在下面做了很酷的事情。

因此,所有这些“相关的事物”(即使您还不这么认为也确实存在)实际上都保存在同一个集合中,但是使用各个模型进行的操作要考虑到“自动” kind键。默认情况下为__t,但实际上您可以在选项中指定所需的内容。

尽管所有这些实际上都在同一个集合中这一事实非常重要,因为您基本上可以轻松地查询同一个集合以获取不同类型的数据。简单地说:

Foo.find({})


实际会打电话

targets.find({ __t: 'Foo' })


并自动执行此操作。但更重要的是

Target.find({ __t: { "$in": [ 'Foo', 'Baz' ] } })


将通过“单个请求”返回“单个集合”中的所有预期结果。

因此,请查看此结构下的修订版populate()

let result1 = await Post.findOne()
  .populate({
    path: 'targets.item',
    match: { __t: 'Foo' }
  });
log(result1);


而是显示在日志中:

Mongoose: posts.findOne({}, { projection: {} })
Mongoose: targets.find({ __t: 'Foo', _id: { '$in': [ ObjectId("5bdbe2895b1b843fba050569"), ObjectId("5bdbe2895b1b843fba05056a"), ObjectId("5bdbe2895b1b843fba05056b"), ObjectId("5bdbe2895b1b843fba05056c") ] } }, { projection: {} })


请注意,即使所有“四个”相关的ObjectId值都随请求一起发送,__t: 'Foo'的附加约束也绑定了实际返回并结婚的文档。然后,仅填充'Foo'项,结果变得不言而喻。还要注意“捕获”:

{
  "_id": "5bdbe2895b1b843fba05056d",
  "name": "My Post",
  "targets": [
    {
      "_id": "5bdbe2895b1b843fba050571",
      "kind": "Foo",
      "item": {
        "__t": "Foo",
        "_id": "5bdbe2895b1b843fba050569",
        "name": "Bill",
        "__v": 0
      }
    },
    {
      "_id": "5bdbe2895b1b843fba050570",
      "kind": "Bar",
      "item": null
    },
    {
      "_id": "5bdbe2895b1b843fba05056f",
      "kind": "Baz",
      "item": null
    },
    {
      "_id": "5bdbe2895b1b843fba05056e",
      "kind": "Foo",
      "item": {
        "__t": "Foo",
        "_id": "5bdbe2895b1b843fba05056c",
        "name": "Ted",
        "__v": 0
      }
    }
  ],
  "__v": 0
}


填充后过滤

这实际上是一个更长的话题,更多的是fully answered elsewhere,但是如上面输出所示,这里的基本知识是populate()仍然对将数组中的结果仅“过滤”为所需的匹配项毫无作用。

另一件事是,从“性能”的角度来看,populate()并不是一个好主意,因为真正发生的是“另一个查询”(在第二种形式中,我们优化为仅一个)或可能是“许多查询”。 ”,具体取决于您的结构将被发布到数据库中,并且结果将在客户端上一起重建。

总的来说,您最终返回的数据要比实际需要的多得多,并且充其量您要依靠手动客户端筛选来丢弃那些不需要的结果。因此,“理想”位置是让“服务器”执行此类操作,并且仅返回您实际需要的数据。

populate()方法是很早以前作为“方便”添加到猫鼬API的。从那时起,MongoDB不断发展,现在将$lookup作为“本机”方式用于通过单个请求在服务器上执行“联接”。

有多种方法可以执行此操作,但只需触摸与现有populate()功能紧密相关但有改进的“两个”即可:

let result2 = await Post.aggregate([
  // Only get documents with a matching entry
  { "$match": {
    "targets.kind": "Foo"
  }},
  // Optionally filter the array
  { "$addFields": {
    "targets": {
      "$filter": {
        "input": "$targets",
        "cond": {
          "$eq": [ "$$this.kind", "Foo" ]
         }
      }
    }
  }},
  // Lookup from single source
  { "$lookup": {
    "from": Target.collection.name,
    "localField": "targets.item",
    "foreignField": "_id",
    "as": "matches"
  }},
  // Marry up arrays
  { "$project": {
    "name": 1,
    "targets": {
      "$map": {
        "input": "$targets",
        "in": {
          "kind": "$$this.kind",
          "item": {
            "$arrayElemAt": [
              "$matches",
              { "$indexOfArray": [ "$matches._id", "$$this.item" ] }
            ]
          }
        }
      }
    }
  }}
]);
log(result2);


此处使用了两个基本的“优化” $filter,以便从数组中“预丢弃”实际上与所需类型不匹配的项。这可能是完全可选的,稍后再进行详细说明,但是在可能的情况下,这可能是一件好事,因为除了_id以外,我们甚至不会在外部集合中寻找匹配的'Foo'值。 。

当然,另一个是$lookup本身,这意味着我们实际上只是做一个而不是单独往返服务器,而在返回任何响应之前完成了“ join”。在这里,我们只寻找外部集合中与_id数组条目值匹配的target.items值。我们已经过滤了'Foo'的那些,因此返回了所有内容:

  {
    "_id": "5bdbe6aa2c4a2240c16802e2",
    "name": "My Post",
    "targets": [
      {
        "kind": "Foo",
        "item": {
          "_id": "5bdbe6aa2c4a2240c16802de",
          "__t": "Foo",
          "name": "Bill",
          "__v": 0
        }
      },
      {
        "kind": "Foo",
        "item": {
          "_id": "5bdbe6aa2c4a2240c16802e1",
          "__t": "Foo",
          "name": "Ted",
          "__v": 0
        }
      }
    ]
  }


对于“轻微”变化,我们实际上甚至可以使用MongoDB 3.6及更高版本的“子管道”处理来检查__t表达式中的$lookup值。这里的主要用例是,如果您选择完全从父kind中删除​​Post,而仅依赖于存储中使用的鉴别符引用固有的“种类”信息:

let result3 = await Post.aggregate([
  // Only get documnents with a matching entry
  { "$match": {
    "targets.kind": "Foo"
  }},
  // Optionally filter the array
  { "$addFields": {
    "targets": {
      "$filter": {
        "input": "$targets",
        "cond": {
          "$eq": [ "$$this.kind", "Foo" ]
         }
      }
    }
  }},
  // Lookup from single source with overkill of type check
  { "$lookup": {
    "from": Target.collection.name,
    "let": { "targets": "$targets" },
    "pipeline": [
      { "$match": {
        "$expr": {
          "$in": [ "$_id", "$$targets.item" ]
        },
        "__t": "Foo"
      }}
    ],
    "as": "matches"
  }},
  // Marry up arrays
  { "$project": {
    "name": 1,
    "targets": {
      "$map": {
        "input": "$targets",
        "in": {
          "kind": "$$this.kind",
          "item": {
            "$arrayElemAt": [
              "$matches",
              { "$indexOfArray": [ "$matches._id", "$$this.item" ] }
            ]
          }
        }
      }
    }
  }}
]);
log(result3);


这具有相同的“过滤”结果,并且类似地是“单个请求”和“单个响应”。

整个主题范围扩大了一点,尽管聚合管道可能比简单的populate()调用显得笨拙得多,但是编写一个可以从您的模型中抽象出来并可以生成所需的大多数数据结构代码的包装器相当简单。 。您可以在"Querying after populate in Mongoose"上看到有关此操作的概述,从本质上讲,这是您在解决了“多个集合联接”的最初问题以及您为什么真正不需要它们之后在这里基本要问的相同问题。

此处需要注意的是,$lookup实际上没有办法“动态”确定要“加入”的集合。您需要像在此一样静态地包含该信息,因此这是另一个实际上偏爱“区分符”而不是使用多个集合的原因。这不仅是“更好的性能”,而且实际上它是性能最高的选项将实际支持您尝试做的事情的唯一方法。



作为参考,第二个列表的“完成”(由于最大帖子长度而被截断)输出为:

Mongoose: posts.deleteMany({}, {})
Mongoose: targets.deleteMany({}, {})
Mongoose: targets.deleteMany({}, {})
Mongoose: targets.deleteMany({}, {})
Mongoose: targets.deleteMany({}, {})
Mongoose: targets.insertOne({ _id: ObjectId("5bdbe2895b1b843fba050569"), __t: 'Foo', name: 'Bill', __v: 0 })
Mongoose: targets.insertOne({ _id: ObjectId("5bdbe2895b1b843fba05056a"), __t: 'Bar', number: 1, __v: 0 })
Mongoose: targets.insertOne({ _id: ObjectId("5bdbe2895b1b843fba05056b"), __t: 'Baz', title: 'Title', __v: 0 })
Mongoose: targets.insertOne({ _id: ObjectId("5bdbe2895b1b843fba05056c"), __t: 'Foo', name: 'Ted', __v: 0 })
[
  {
    "_id": "5bdbe2895b1b843fba050569",
    "__t": "Foo",
    "name": "Bill",
    "__v": 0
  },
  {
    "_id": "5bdbe2895b1b843fba05056a",
    "__t": "Bar",
    "number": 1,
    "__v": 0
  },
  {
    "_id": "5bdbe2895b1b843fba05056b",
    "__t": "Baz",
    "title": "Title",
    "__v": 0
  },
  {
    "_id": "5bdbe2895b1b843fba05056c",
    "__t": "Foo",
    "name": "Ted",
    "__v": 0
  }
]
Mongoose: posts.insertOne({ _id: ObjectId("5bdbe2895b1b843fba05056d"), name: 'My Post', targets: [ { _id: ObjectId("5bdbe2895b1b843fba050571"), kind: 'Foo', item: ObjectId("5bdbe2895b1b843fba050569") }, { _id: ObjectId("5bdbe2895b1b843fba050570"), kind: 'Bar', item: ObjectId("5bdbe2895b1b843fba05056a") }, { _id: ObjectId("5bdbe2895b1b843fba05056f"), kind: 'Baz', item: ObjectId("5bdbe2895b1b843fba05056b") }, { _id: ObjectId("5bdbe2895b1b843fba05056e"), kind: 'Foo', item: ObjectId("5bdbe2895b1b843fba05056c") } ], __v: 0 })
{
  "_id": "5bdbe2895b1b843fba05056d",
  "name": "My Post",
  "targets": [
    {
      "_id": "5bdbe2895b1b843fba050571",
      "kind": "Foo",
      "item": {
        "_id": "5bdbe2895b1b843fba050569",
        "__t": "Foo",
        "name": "Bill",
        "__v": 0
      }
    },
    {
      "_id": "5bdbe2895b1b843fba050570",
      "kind": "Bar",
      "item": {
        "_id": "5bdbe2895b1b843fba05056a",
        "__t": "Bar",
        "number": 1,
        "__v": 0
      }
    },
    {
      "_id": "5bdbe2895b1b843fba05056f",
      "kind": "Baz",
      "item": {
        "_id": "5bdbe2895b1b843fba05056b",
        "__t": "Baz",
        "title": "Title",
        "__v": 0
      }
    },
    {
      "_id": "5bdbe2895b1b843fba05056e",
      "kind": "Foo",
      "item": {
        "_id": "5bdbe2895b1b843fba05056c",
        "__t": "Foo",
        "name": "Ted",
        "__v": 0
      }
    }
  ],
  "__v": 0
}
Mongoose: posts.findOne({}, { projection: {} })
{
  "_id": "5bdbe2895b1b843fba05056d",
  "name": "My Post",
  "targets": [
    {
      "_id": "5bdbe2895b1b843fba050571",
      "kind": "Foo",
      "item": "5bdbe2895b1b843fba050569"
    },
    {
      "_id": "5bdbe2895b1b843fba050570",
      "kind": "Bar",
      "item": "5bdbe2895b1b843fba05056a"
    },
    {
      "_id": "5bdbe2895b1b843fba05056f",
      "kind": "Baz",
      "item": "5bdbe2895b1b843fba05056b"
    },
    {
      "_id": "5bdbe2895b1b843fba05056e",
      "kind": "Foo",
      "item": "5bdbe2895b1b843fba05056c"
    }
  ],
  "__v": 0
}
Mongoose: posts.findOne({}, { projection: {} })
Mongoose: targets.find({ __t: 'Foo', _id: { '$in': [ ObjectId("5bdbe2895b1b843fba050569"), ObjectId("5bdbe2895b1b843fba05056a"), ObjectId("5bdbe2895b1b843fba05056b"), ObjectId("5bdbe2895b1b843fba05056c") ] } }, { projection: {} })
{
  "_id": "5bdbe2895b1b843fba05056d",
  "name": "My Post",
  "targets": [
    {
      "_id": "5bdbe2895b1b843fba050571",
      "kind": "Foo",
      "item": {
        "__t": "Foo",
        "_id": "5bdbe2895b1b843fba050569",
        "name": "Bill",
        "__v": 0
      }
    },
    {
      "_id": "5bdbe2895b1b843fba050570",
      "kind": "Bar",
      "item": null
    },
    {
      "_id": "5bdbe2895b1b843fba05056f",
      "kind": "Baz",
      "item": null
    },
    {
      "_id": "5bdbe2895b1b843fba05056e",
      "kind": "Foo",
      "item": {
        "__t": "Foo",
        "_id": "5bdbe2895b1b843fba05056c",
        "name": "Ted",
        "__v": 0
      }
    }
  ],
  "__v": 0
}
Mongoose: posts.aggregate([ { '$match': { 'targets.kind': 'Foo' } }, { '$addFields': { targets: { '$filter': { input: '$targets', cond: { '$eq': [ '$$this.kind', 'Foo' ] } } } } }, { '$lookup': { from: 'targets', localField: 'targets.item', foreignField: '_id', as: 'matches' } }, { '$project': { name: 1, targets: { '$map': { input: '$targets', in: { kind: '$$this.kind', item: { '$arrayElemAt': [ '$matches', { '$indexOfArray': [ '$matches._id', '$$this.item' ] } ] } } } } } } ], {})
[
  {
    "_id": "5bdbe2895b1b843fba05056d",
    "name": "My Post",
    "targets": [
      {
        "kind": "Foo",
        "item": {
          "_id": "5bdbe2895b1b843fba050569",
          "__t": "Foo",
          "name": "Bill",
          "__v": 0
        }
      },
      {
        "kind": "Foo",
        "item": {
          "_id": "5bdbe2895b1b843fba05056c",
          "__t": "Foo",
          "name": "Ted",
          "__v": 0
        }
      }
    ]
  }
]
Mongoose: posts.aggregate([ { '$match': { 'targets.kind': 'Foo' } }, { '$addFields': { targets: { '$filter': { input: '$targets', cond: { '$eq': [ '$$this.kind', 'Foo' ] } } } } }, { '$lookup': { from: 'targets', let: { targets: '$targets' }, pipeline: [ { '$match': { '$expr': { '$in': [ '$_id', '$$targets.item' ] }, __t: 'Foo' } } ], as: 'matches' } }, { '$project': { name: 1, targets: { '$map': { input: '$targets', in: { kind: '$$this.kind', item: { '$arrayElemAt': [ '$matches', { '$indexOfArray': [ '$matches._id', '$$this.item' ] } ] } } } } } } ], {})

关于mongodb - Mongoose 填充数组中的单个项目,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/53108254/

相关文章:

mongodb - 在 Grails 3.x 中安装和使用 MongoDB

linux - 为什么在 Linux 上出现错误 mongod dead 但 subsys 被锁定和日志文件可用空间不足?

node.js - Mongoose 转换为 ObjectId 值失败

node.js - 尝试将 mongoose 与 webpack 结合使用时出现大量神秘错误和警告

node.js - 精益()里面填充 Mongoose

mongodb - MongoDB 中文档引用关系的 Mongoose 实现

javascript - 使用 ES6 Proxy 懒加载资源

javascript - 为什么我的 Node Express 应用程序无法从 Mongoose 获取数据?

node.js - mock 鹅 : how to simulate a error in mongoose?

mongodb - Mongoose- 使用特定项目将数组中的数据搜索/过滤到另一个数组中