Mongodb Mongoose在数组中填充单个项
我有一个模型,它有一个动态引用数组Mongodb Mongoose在数组中填充单个项,mongodb,mongoose,mongoose-populate,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
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();
}
})()
因此,这里的注释显示了匹配
是逻辑的问题,所以让我们看看调试输出,看看原因:
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
}
这是一个完整的输出,显示其他一切都在实际工作,事实上,如果没有匹配
,您将获得项目的填充数据。但请仔细查看向foo
和bar
集合发出的两个查询:
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”
实际上是在foo
和bar
集合中搜索的,而不是posts
集合中的。与输出的其余部分一起,这应该可以让您了解populate()
实际上是如何工作的,因为没有任何内容明确要求只返回类型的“数组项”:“Foo”
这种“过滤数组”的过程实际上甚至不是“真正的”自然的MongoDB查询,除了“第一个和单数匹配”之外,您通常会使用.aggregate()
和操作符。您可以通过positional$
操作符获得“singular”,但是如果您想要在有多个foo的地方使用“all foo”,那么它需要使用
所以这里真正的核心问题是populate()
实际上是“过滤数组”的错误位置和错误操作。相反,您确实希望在执行任何其他操作来“填充”项目之前,只“智能地”返回所需的数组项
结构问题
注意到上面的列表是问题中暗示的寓言,为了“加入”并获得整体结果,提到了“多个模型”。虽然这在“RDBMS领域”中似乎是合乎逻辑的,但在MongoDB和“文档数据库”的一般“同类”中这样做肯定是不实际、不高效的
这里需要记住的关键是,“集合”中的“文档”不必都具有与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();
}
})()
这有点长,还有一些概念需要解决,但基本原则是,我们不需要对不同的类型使用“多个集合”,而是只使用一个。用于此目的的“猫鼬”方法用于模型设置,该模型设置与本部分代码相关:
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()
。这一点的真正好处在于,就代码的其余部分而言,Baz
和Bar
等都被视为透明的“模型”,但它们实际上在做一些真正酷的事情
因此,所有这些“相关的东西”(即使你不这么认为,它们确实是)实际上都保存在同一个集合中,但是使用单个模型的操作考虑了一个“自动”kind
键。默认情况下,这是\uu 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
值的所有“四个”都随请求一起发送,\uu t:'Foo'
的附加约束也会绑定实际返回和合并的文档。当只填充'Foo'
条目时,结果变得不言而喻。但也要注意“陷阱”:
填充后过滤
这实际上是一个更长、更多的主题,但如上面的输出所示,这里的基本内容是populate()
对于实际“过滤”数组中的结果以仅获得所需的匹配项,仍然完全不做任何事情
另一件事是,从“性能”的角度来看,populate()
真的不是一个好主意,因为真正发生的是“另一个查询”(在第二种形式中,我们只优化了一个或多个查询)根据您的结构,数据实际上被发送到数据库,并且结果在客户机上一起重建
总的来说,您最终返回的数据比实际需要的要多得多,并且最多只能依靠手动客户端筛选来丢弃这些不需要的结果。因此,“理想”的立场是
{
"_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
}
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": "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
}
}
]
}
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);
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' ] } ] } } } } } } ], {})