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

时间:2018-11-01 19:36:57

标签: 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',
});

谢谢

1 个答案:

答案 0 :(得分:2)

这里的一大教训应该是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()的实际工作方式,因为没什么说过只返回{{ 1}},例如。

即使是自然的MongoDB查询,“过滤数组”的过程实际上也不是“真正”的,除了“第一和单数匹配”之外,您通常会使用{{ 1}}和$filter运算符。您可以通过位置kind: 'Foo'运算符获得“单数”,但是如果您想要多个“ foo”,则需要$filter

因此,这里的真正核心问题是.aggregate()实际上是错误的位置,并且是“过滤数组”的错误操作。取而代之的是,您真的想“聪明地”只返回您想要之前执行的其他任何操作以“填充”项目的数组条目。

结构问题

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

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

$

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

populate()

这实际上只是从“基本模型”中为“单一”集合调用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(); } })() ,而不是调用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()等都被透明地视为“模型”,但实际上它们在下面做了很酷的事情

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

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

Bar

实际上会打电话

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

填充后过滤

这实际上是一个更长的话题,还有更多的fully answered elsewhere,但是如上面输出所示,这里的基础是__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 } 方法是作为“便利”添加到猫鼬API的。从那时起,MongoDB不断发展,现在将$lookup作为一种“本机”方式,用于通过单个请求在服务器上执行“加入”。

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

populate()

这里使用了两个基本的“优化”,$filter可以从数组中“预丢弃”实际上与我们想要的类型不匹配的项。这可能是完全可选的,稍后会详细介绍,但是在可能的情况下,这可能是一件好事,因为我们甚至不希望在外国集合中寻找匹配populate()的值,除了{{ 1}}的东西。

另一个当然是populate()本身,这意味着我们实际上只是做一个一个,而不是单独往返服务器,而“ join”是在任何响应被完成之前完成的。回到。在这里,我们只寻找外部集合中与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); 数组条目值匹配的_id值。我们已经过滤了'Foo'的内容,因此返回的全部内容:

$lookup

对于“轻微”变化,我们实际上甚至可以使用MongoDB 3.6及更高版本的“子管道”处理来检查_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 } } ] } ,而仅依赖于存储中使用的鉴别符引用固有的“种类”信息:

__t

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

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

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


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

kind