猫鼬填充数组中的单个项目

Mongoose populate single item in array

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

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

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

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

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) 是你的新 "best friend"。这将显示从您正在编写的代码向 MongoDB 发出的实际查询,这非常重要,因为当您实际 "see it" 时,它会消除您可能有的任何误解。

逻辑问题

让我们来演示一下您尝试失败的确切原因:

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: {} })

因此,您在 match 下包含的 'targets.kind' 实际上是在 foobar 集合中搜索的,而 没有posts 集合中,正如您所期望的那样。与输出的其余部分一起,这应该让您了解 populate() 实际上是如何工作的,因为 nothing 曾经专门说过 return "array entries",如示例所示,属于 kind: 'Foo'

这个 "filtering the array" 的过程实际上不是 "really" 甚至是一个自然的 MongoDB 查询,除了 "first and singular match" 你实际上通常会使用 .aggregate()$filter operator. You can get "singular" via the positional $ operator but if you wanted "all foos" where there was more than one, then it needs the $filter 代替。

所以这里真正的核心问题是populate()实际上是对"filter the array"的错误地方和错误操作。相反,你真的想 "smartly" return 只有你想要的数组条目 你对 "populate" 项目做任何其他事情之前。

结构问题

从上面的列表中可以看出,这是对问题中暗示内容的寓言,为了 "join" 并获得总体结果,"multiple models" 被引用。虽然这在 "RDBMS land" 中看起来合乎逻辑,但对于 MongoDB 和 "document databases" 的一般 "ilk" 来说肯定不是这种情况,也不实用或有效。

这里要记住的关键是,"collection" 中的 "documents" 不必像 RDBMS 那样具有相同的 "table structure"。结构可能会有所不同,虽然不 "vary wildly" 可能是可取的,但在单个集合中存储 "polymorphic objects" 当然是非常有效的。毕竟,您实际上想要将所有这些东西引用回同一个父对象,那么为什么它们需要在不同的集合中呢?简单地说,他们根本不需要:

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

})()

这有点长,还有一些概念需要解决,但基本原则是,我们不会对不同的类型使用 "multiple collections",我们只会使用 一个。 "mongoose" 方法在模型设置中使用 "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);

这实际上只是从 "base model" 为 "singular" 集合调用 .discriminator() 而不是调用 mongoose.model()。就你的代码的其余部分而言,这样做的真正好处是,BazBar 等都被透明地视为 "model",但它们实际上确实在做一些事情下面很酷。

所以所有这些 "related things"(它们确实是,即使你还不这么认为)实际上都保存在同一个集合中,但是使用各个模型的操作考虑了 "automatic" kind 键。默认情况下是 __t,但实际上您可以在选项中指定任何内容。

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

Foo.find({})

实际上会打电话给

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

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

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

将 return 从 "single collection" 和 "single request" 获得所有预期结果。

所以看看修改后的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: {} })

请注意,即使所有 "four" 的相关 ObjectId 值都随请求一起发送,__t: 'Foo' 的附加约束也绑定了实际 returned 的文档并结婚了。然后结果变得不言自明,因为只填充了 'Foo' 个条目。但还要注意 "catch":

{
  "_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() 实际上仍然对 "filtering" 结果完全没有任何作用在数组中仅包含所需的匹配项。

另一件事是,从 "performance" 的角度来看,populate() 确实不是一个好主意,因为真正发生的是 "another query"(在我们的第二种形式中,我们优化了只是一个)或可能 "many queries" 取决于你的结构实际上被发布到数据库并且结果正在客户端上一起重建。

总的来说,您最终 return 获取的数据比您实际需要的要多得多,充其量您只能依靠手动客户端过滤来丢弃那些不需要的结果。所以 "ideal" 的立场是让 "server" 做那种事,而 只有 return 你真正需要的数据。

很久以前,populate() 方法作为 "convenience" 添加到猫鼬 API "very" 中。从那时起,MongoDB 继续前进,现在将 $lookup 作为一种 "native" 方式,通过单个请求在服务器上执行 "join"。

有多种方法可以做到这一点,但仅涉及 "two" 与现有 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);

两个基本的 "optimizations" 使用 $filter 以便 "pre-discard" 数组中的项目实际上与我们想要的类型不匹配。这可以是完全可选的,稍后会详细介绍,但在可能的情况下,这可能是一件好事,因为我们甚至不会在外部集合中寻找匹配的 _id 值,除了 'Foo' 东西.

另一个当然是 $lookup 本身,这意味着我们实际上只是 一个 和 [=172= 而不是单独往返服务器] 在任何响应 returned 之前完成。这里我们只是在外部集合中查找 _id 值与 target.items 数组条目值的匹配。我们已经为 'Foo' 筛选了那些,所以这就是得到 returned:

的全部内容
  {
    "_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
        }
      }
    ]
  }

对于 "slight" 的变体,我们实际上什至可以使用 "sub-pipeline" 处理 MongoDB 3.6 来检查 $lookup 表达式中的 __t 值,并且更大。这里的主要用例是,如果您选择从父 Post 中完全删除 kind 并仅依赖存储中使用的鉴别器引用固有的 "kind" 信息:

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

这具有相同的 "filtered" 结果并且类似地是 "single request" 和 "single response"。

整个主题变得有点宽泛,尽管聚合管道可能看起来比简单的 populate() 调用笨拙得多,但编写一个可以从您的模型中抽象出来并几乎生成的包装器是相当微不足道的所需的大部分数据结构代码。您可以在 "Querying after populate in Mongoose" 上看到有关此操作的概述,在我们解决 "multiple collection joins" 的初始问题以及为什么您真的不需要它们后,这本质上与您在这里问的问题基本相同.

这里要注意的是 $lookup 实际上无法 "dynamically" 确定要 "join" 到哪个集合。您需要像此处那样静态地包含该信息,因此这是实际支持 "discriminators" 而不是使用多个集合的另一个原因。它不仅 "better performance",而且实际上是性能最高的选项真正支持您尝试做的事情的唯一方式。


作为参考,第二个列表的 "complete"(由于最大 post 长度而被截断)输出为:

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