问题描述
我正在用吸气剂功能填充猫鼬模式中的虚拟字段.它应该填充该字段而没有任何错误,但是会引发错误MongooseError:如果要填充虚拟机,则必须设置localField和foreignField选项
当我们使用getter填充字段时,则不需要localfield和foriegn字段
const DashboardSchema = new Schema({
userId: {
type: SchemaTypes.ObjectId
}
}, { toJSON: { virtuals: true } });
DashboardSchema.virtual('TopReports').get(function () {
return TopReports.find({ userId: this.userId }).sort('-date').limit(10);
})
如果要使用sort
和limit
进行虚拟填充",那实际上不是您的操作方式.您创建的只是一个虚拟获取器",实际上是在返回异步函数的结果.您可以使用它,但是要解决返回的Promise
则要麻烦得多,并且与populate()
毫无关系,这是您提出错误的地方.
执行此操作还有其他选择.
猫鼬虚拟填充
为此,它最接近您的尝试,相反,您需要这样的东西:
const dashboardSchema = new Schema({
userId: Schema.Types.ObjectId
},
{
toJSON: { virtuals: true },
toObject: { virtuals: true }
});
dashboardSchema.virtual('TopReports', {
ref: 'Report',
localField: 'userId',
foreignField: 'userId',
options: { sort: { date: -1 }, limit: 10 } // optional - could add with populate
});
const reportSchema = new Schema({
userId: Schema.Types.ObjectId,
seq: Number,
date: Date
});
const Dashboard = mongoose.model('Dashboard', dashboardSchema);
const Report = mongoose.model('Report', reportSchema);
这实际上与人口虚拟对象" ,因为给定示例中还包含options
,这与将选项传递给populate方法本身相同.在virtual
中设置options
时,您只需要这样调用:
let result = await Dashboard.findOne().populate('TopReports');
在执行populate()
时,为sort
和limit
设置的默认值会自动应用到此虚拟"字段.如果选择不包括options
,则只需手动添加选项:
let result2 = await Dashboard.findOne().populate({
path: 'TopReports',
options: { sort: '-date', limit: 5 }
});
log(result2);
MongoDB $ lookup
这里的另一种选择是MongoDB基本上具有相同的内置功能,不同的是单个请求,而不是populate()
实际上是多个请求以便从单独的集合中返回数据:
let result = await Dashboard.aggregate([
{ "$lookup": {
"from": Report.collection.name,
"let": { "userId": "$userId" },
"pipeline": [
{ "$match": { "$expr": { "$eq": [ "$userId", "$$userId" ] } } },
{ "$sort": { "date": -1 } },
{ "$limit": 10 }
],
"as": "TopReports"
}}
]);
因此,与populate()
实际发出的许多" find()
请求相比,这是一个具有单个响应的请求.结果相同,只是使用了 ,以应用 $sort
并 $limit
到返回的相关项目数组.
只需做一些工作,您甚至可以从猫鼬模式中检查模式定义(包括已定义的虚拟),并构造相同的 $lookup
语句. 在猫鼬中填充后进行查询,对此模式检查"进行了基本演示.
因此,这取决于最适合您的需求.我建议同时尝试甚至基准测试应用程序性能.
作为完整的演示,这是示例清单.它插入20个东西,并只返回数组中最近"的10个结果:
const { Schema, Types: { ObjectId } } = mongoose = require('mongoose');
const uri = 'mongodb://localhost:27017/test';
const opts = { useNewUrlParser: true };
mongoose.set('useFindAndModify', false);
mongoose.set('useCreateIndex', true);
mongoose.set('debug', true);
const dashboardSchema = new Schema({
userId: Schema.Types.ObjectId
},
{
toJSON: { virtuals: true },
toObject: { virtuals: true }
});
dashboardSchema.virtual('TopReports', {
ref: 'Report',
localField: 'userId',
foreignField: 'userId',
options: { sort: { date: -1 }, limit: 10 } // optional - could add with populate
});
const reportSchema = new Schema({
userId: Schema.Types.ObjectId,
seq: Number,
date: Date
});
const Dashboard = mongoose.model('Dashboard', dashboardSchema);
const Report = mongoose.model('Report', reportSchema);
const log = data => console.log(JSON.stringify(data, undefined, 2));
(async function() {
try {
const conn = await mongoose.connect(uri, opts);
await Promise.all(
Object.entries(conn.models).map(([k,m]) => m.deleteMany())
);
// Insert some things
let { userId } = await Dashboard.create({ userId: new ObjectId() });
const oneDay = ( 1000 * 60 * 60 * 24 );
const baseDate = Date.now() - (oneDay * 30); // 30 days ago
await Report.insertMany(
[ ...Array(20)]
.map((e,i) => ({ userId, seq: i+1, date: baseDate + ( oneDay * i ) }))
);
// Virtual populate
let popresult = await Dashboard.findOne().populate('TopReports');
log(popresult);
// Aggregate $lookup
let result = await Dashboard.aggregate([
{ "$lookup": {
"from": Report.collection.name,
"let": { "userId": "$userId" },
"pipeline": [
{ "$match": { "$expr": { "$eq": [ "$userId", "$$userId" ] } } },
{ "$sort": { "date": -1 } },
{ "$limit": 10 }
],
"as": "TopReports"
}}
]);
log(result);
} catch (e) {
console.error(e)
} finally {
mongoose.disconnect()
}
})()
输出:
Mongoose: dashboards.deleteMany({}, {})
Mongoose: reports.deleteMany({}, {})
Mongoose: dashboards.insertOne({ _id: ObjectId("5cce3d9e16302f32acb5c572"), userId: ObjectId("5cce3d9e16302f32acb5c571"), __v: 0 })
Mongoose: reports.insertMany([ { _id: 5cce3d9e16302f32acb5c573, userId: 5cce3d9e16302f32acb5c571, seq: 1, date: 2019-04-05T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c574, userId: 5cce3d9e16302f32acb5c571, seq: 2, date: 2019-04-06T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c575, userId: 5cce3d9e16302f32acb5c571, seq: 3, date: 2019-04-07T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c576, userId: 5cce3d9e16302f32acb5c571, seq: 4, date: 2019-04-08T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c577, userId: 5cce3d9e16302f32acb5c571, seq: 5, date: 2019-04-09T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c578, userId: 5cce3d9e16302f32acb5c571, seq: 6, date: 2019-04-10T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c579, userId: 5cce3d9e16302f32acb5c571, seq: 7, date: 2019-04-11T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c57a, userId: 5cce3d9e16302f32acb5c571, seq: 8, date: 2019-04-12T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c57b, userId: 5cce3d9e16302f32acb5c571, seq: 9, date: 2019-04-13T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c57c, userId: 5cce3d9e16302f32acb5c571, seq: 10, date: 2019-04-14T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c57d, userId: 5cce3d9e16302f32acb5c571, seq: 11, date: 2019-04-15T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c57e, userId: 5cce3d9e16302f32acb5c571, seq: 12, date: 2019-04-16T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c57f, userId: 5cce3d9e16302f32acb5c571, seq: 13, date: 2019-04-17T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c580, userId: 5cce3d9e16302f32acb5c571, seq: 14, date: 2019-04-18T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c581, userId: 5cce3d9e16302f32acb5c571, seq: 15, date: 2019-04-19T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c582, userId: 5cce3d9e16302f32acb5c571, seq: 16, date: 2019-04-20T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c583, userId: 5cce3d9e16302f32acb5c571, seq: 17, date: 2019-04-21T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c584, userId: 5cce3d9e16302f32acb5c571, seq: 18, date: 2019-04-22T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c585, userId: 5cce3d9e16302f32acb5c571, seq: 19, date: 2019-04-23T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c586, userId: 5cce3d9e16302f32acb5c571, seq: 20, date: 2019-04-24T01:34:22.554Z, __v: 0 } ], {})
Mongoose: dashboards.findOne({}, { projection: {} })
Mongoose: reports.find({ userId: { '$in': [ ObjectId("5cce3d9e16302f32acb5c571") ] } }, { sort: { date: -1 }, limit: 10, projection: {} })
{
"_id": "5cce3d9e16302f32acb5c572",
"userId": "5cce3d9e16302f32acb5c571",
"__v": 0,
"TopReports": [
{
"_id": "5cce3d9e16302f32acb5c586",
"userId": "5cce3d9e16302f32acb5c571",
"seq": 20,
"date": "2019-04-24T01:34:22.554Z",
"__v": 0
},
{
"_id": "5cce3d9e16302f32acb5c585",
"userId": "5cce3d9e16302f32acb5c571",
"seq": 19,
"date": "2019-04-23T01:34:22.554Z",
"__v": 0
},
{
"_id": "5cce3d9e16302f32acb5c584",
"userId": "5cce3d9e16302f32acb5c571",
"seq": 18,
"date": "2019-04-22T01:34:22.554Z",
"__v": 0
},
{
"_id": "5cce3d9e16302f32acb5c583",
"userId": "5cce3d9e16302f32acb5c571",
"seq": 17,
"date": "2019-04-21T01:34:22.554Z",
"__v": 0
},
{
"_id": "5cce3d9e16302f32acb5c582",
"userId": "5cce3d9e16302f32acb5c571",
"seq": 16,
"date": "2019-04-20T01:34:22.554Z",
"__v": 0
},
{
"_id": "5cce3d9e16302f32acb5c581",
"userId": "5cce3d9e16302f32acb5c571",
"seq": 15,
"date": "2019-04-19T01:34:22.554Z",
"__v": 0
},
{
"_id": "5cce3d9e16302f32acb5c580",
"userId": "5cce3d9e16302f32acb5c571",
"seq": 14,
"date": "2019-04-18T01:34:22.554Z",
"__v": 0
},
{
"_id": "5cce3d9e16302f32acb5c57f",
"userId": "5cce3d9e16302f32acb5c571",
"seq": 13,
"date": "2019-04-17T01:34:22.554Z",
"__v": 0
},
{
"_id": "5cce3d9e16302f32acb5c57e",
"userId": "5cce3d9e16302f32acb5c571",
"seq": 12,
"date": "2019-04-16T01:34:22.554Z",
"__v": 0
},
{
"_id": "5cce3d9e16302f32acb5c57d",
"userId": "5cce3d9e16302f32acb5c571",
"seq": 11,
"date": "2019-04-15T01:34:22.554Z",
"__v": 0
}
],
"id": "5cce3d9e16302f32acb5c572"
}
Mongoose: dashboards.aggregate([ { '$lookup': { from: 'reports', let: { userId: '$userId' }, pipeline: [ { '$match': { '$expr': { '$eq': [ '$userId', '$$userId' ] } } }, { '$sort': { date: -1 } }, { '$limit': 10 } ], as: 'TopReports' } } ], {})
[
{
"_id": "5cce3d9e16302f32acb5c572",
"userId": "5cce3d9e16302f32acb5c571",
"__v": 0,
"TopReports": [
{
"_id": "5cce3d9e16302f32acb5c586",
"userId": "5cce3d9e16302f32acb5c571",
"seq": 20,
"date": "2019-04-24T01:34:22.554Z",
"__v": 0
},
{
"_id": "5cce3d9e16302f32acb5c585",
"userId": "5cce3d9e16302f32acb5c571",
"seq": 19,
"date": "2019-04-23T01:34:22.554Z",
"__v": 0
},
{
"_id": "5cce3d9e16302f32acb5c584",
"userId": "5cce3d9e16302f32acb5c571",
"seq": 18,
"date": "2019-04-22T01:34:22.554Z",
"__v": 0
},
{
"_id": "5cce3d9e16302f32acb5c583",
"userId": "5cce3d9e16302f32acb5c571",
"seq": 17,
"date": "2019-04-21T01:34:22.554Z",
"__v": 0
},
{
"_id": "5cce3d9e16302f32acb5c582",
"userId": "5cce3d9e16302f32acb5c571",
"seq": 16,
"date": "2019-04-20T01:34:22.554Z",
"__v": 0
},
{
"_id": "5cce3d9e16302f32acb5c581",
"userId": "5cce3d9e16302f32acb5c571",
"seq": 15,
"date": "2019-04-19T01:34:22.554Z",
"__v": 0
},
{
"_id": "5cce3d9e16302f32acb5c580",
"userId": "5cce3d9e16302f32acb5c571",
"seq": 14,
"date": "2019-04-18T01:34:22.554Z",
"__v": 0
},
{
"_id": "5cce3d9e16302f32acb5c57f",
"userId": "5cce3d9e16302f32acb5c571",
"seq": 13,
"date": "2019-04-17T01:34:22.554Z",
"__v": 0
},
{
"_id": "5cce3d9e16302f32acb5c57e",
"userId": "5cce3d9e16302f32acb5c571",
"seq": 12,
"date": "2019-04-16T01:34:22.554Z",
"__v": 0
},
{
"_id": "5cce3d9e16302f32acb5c57d",
"userId": "5cce3d9e16302f32acb5c571",
"seq": 11,
"date": "2019-04-15T01:34:22.554Z",
"__v": 0
}
]
}
]
错误的方式
只是为了演示"getter"方法有什么问题,下面的示例清单显示了在每个返回的对象上实际解决返回的Promise
的情况:
const { Schema, Types: { ObjectId } } = mongoose = require('mongoose');
const uri = 'mongodb://localhost:27017/test';
const opts = { useNewUrlParser: true };
mongoose.set('useFindAndModify', false);
mongoose.set('useCreateIndex', true);
mongoose.set('debug', true);
const dashboardSchema = new Schema({
userId: Schema.Types.ObjectId
},
{
toJSON: { virtuals: true },
toObject: { virtuals: true }
});
dashboardSchema.virtual('TopReports').get(function() {
return Report.find({ userId: this.userId }).sort("-date").limit(10);
});
const reportSchema = new Schema({
userId: Schema.Types.ObjectId,
seq: Number,
date: Date
});
const Dashboard = mongoose.model('Dashboard', dashboardSchema);
const Report = mongoose.model('Report', reportSchema);
const log = data => console.log(JSON.stringify(data, undefined, 2));
(async function() {
try {
const conn = await mongoose.connect(uri, opts);
await Promise.all(
Object.entries(conn.models).map(([k,m]) => m.deleteMany())
);
// Insert some things
let { userId } = await Dashboard.create({ userId: new ObjectId() });
const oneDay = ( 1000 * 60 * 60 * 24 );
const baseDate = Date.now() - (oneDay * 30); // 30 days ago
await Report.insertMany(
[ ...Array(20)]
.map((e,i) => ({ userId, seq: i+1, date: baseDate + ( oneDay * i ) }))
);
// Mimic the virtual populate with the getter
let results = await Dashboard.find();
for ( let r of results ) {
let obj = { ...r.toObject() }; // copy the plain object data only
obj.TopReports = await r.TopReports; // Resolve the Promise
log(obj);
}
} catch (e) {
console.error(e)
} finally {
mongoose.disconnect()
}
})()
并输出:
Mongoose: dashboards.deleteMany({}, {})
Mongoose: reports.deleteMany({}, {})
Mongoose: dashboards.insertOne({ _id: ObjectId("5cce45193134aa37e88c4114"), userId: ObjectId("5cce45193134aa37e88c4113"), __v: 0 })
Mongoose: reports.insertMany([ { _id: 5cce45193134aa37e88c4115, userId: 5cce45193134aa37e88c4113, seq: 1, date: 2019-04-05T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c4116, userId: 5cce45193134aa37e88c4113, seq: 2, date: 2019-04-06T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c4117, userId: 5cce45193134aa37e88c4113, seq: 3, date: 2019-04-07T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c4118, userId: 5cce45193134aa37e88c4113, seq: 4, date: 2019-04-08T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c4119, userId: 5cce45193134aa37e88c4113, seq: 5, date: 2019-04-09T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c411a, userId: 5cce45193134aa37e88c4113, seq: 6, date: 2019-04-10T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c411b, userId: 5cce45193134aa37e88c4113, seq: 7, date: 2019-04-11T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c411c, userId: 5cce45193134aa37e88c4113, seq: 8, date: 2019-04-12T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c411d, userId: 5cce45193134aa37e88c4113, seq: 9, date: 2019-04-13T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c411e, userId: 5cce45193134aa37e88c4113, seq: 10, date: 2019-04-14T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c411f, userId: 5cce45193134aa37e88c4113, seq: 11, date: 2019-04-15T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c4120, userId: 5cce45193134aa37e88c4113, seq: 12, date: 2019-04-16T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c4121, userId: 5cce45193134aa37e88c4113, seq: 13, date: 2019-04-17T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c4122, userId: 5cce45193134aa37e88c4113, seq: 14, date: 2019-04-18T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c4123, userId: 5cce45193134aa37e88c4113, seq: 15, date: 2019-04-19T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c4124, userId: 5cce45193134aa37e88c4113, seq: 16, date: 2019-04-20T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c4125, userId: 5cce45193134aa37e88c4113, seq: 17, date: 2019-04-21T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c4126, userId: 5cce45193134aa37e88c4113, seq: 18, date: 2019-04-22T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c4127, userId: 5cce45193134aa37e88c4113, seq: 19, date: 2019-04-23T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c4128, userId: 5cce45193134aa37e88c4113, seq: 20, date: 2019-04-24T02:06:17.518Z, __v: 0 } ], {})
Mongoose: dashboards.find({}, { projection: {} })
Mongoose: reports.find({ userId: ObjectId("5cce45193134aa37e88c4113") }, { sort: { date: -1 }, limit: 10, projection: {} })
{
"_id": "5cce45193134aa37e88c4114",
"userId": "5cce45193134aa37e88c4113",
"__v": 0,
"TopReports": [
{
"_id": "5cce45193134aa37e88c4128",
"userId": "5cce45193134aa37e88c4113",
"seq": 20,
"date": "2019-04-24T02:06:17.518Z",
"__v": 0
},
{
"_id": "5cce45193134aa37e88c4127",
"userId": "5cce45193134aa37e88c4113",
"seq": 19,
"date": "2019-04-23T02:06:17.518Z",
"__v": 0
},
{
"_id": "5cce45193134aa37e88c4126",
"userId": "5cce45193134aa37e88c4113",
"seq": 18,
"date": "2019-04-22T02:06:17.518Z",
"__v": 0
},
{
"_id": "5cce45193134aa37e88c4125",
"userId": "5cce45193134aa37e88c4113",
"seq": 17,
"date": "2019-04-21T02:06:17.518Z",
"__v": 0
},
{
"_id": "5cce45193134aa37e88c4124",
"userId": "5cce45193134aa37e88c4113",
"seq": 16,
"date": "2019-04-20T02:06:17.518Z",
"__v": 0
},
{
"_id": "5cce45193134aa37e88c4123",
"userId": "5cce45193134aa37e88c4113",
"seq": 15,
"date": "2019-04-19T02:06:17.518Z",
"__v": 0
},
{
"_id": "5cce45193134aa37e88c4122",
"userId": "5cce45193134aa37e88c4113",
"seq": 14,
"date": "2019-04-18T02:06:17.518Z",
"__v": 0
},
{
"_id": "5cce45193134aa37e88c4121",
"userId": "5cce45193134aa37e88c4113",
"seq": 13,
"date": "2019-04-17T02:06:17.518Z",
"__v": 0
},
{
"_id": "5cce45193134aa37e88c4120",
"userId": "5cce45193134aa37e88c4113",
"seq": 12,
"date": "2019-04-16T02:06:17.518Z",
"__v": 0
},
{
"_id": "5cce45193134aa37e88c411f",
"userId": "5cce45193134aa37e88c4113",
"seq": 11,
"date": "2019-04-15T02:06:17.518Z",
"__v": 0
}
],
"id": "5cce45193134aa37e88c4114"
}
当然,对于每个返回的文档,都需要解析getter返回的Promise
.相比之下,populate()
使用$in
来通过单个请求返回find()
中所有结果的匹配条目,这将为每个Dashboard
文档而不是单个find()
发出一个新的find()
. c24>基于结果中每个Dashboard
文档中找到的所有userId
值.
基本上,与populate()
或$lookup
相对,您实际上是在将"join"逻辑拆分成控制流的一部分,该部分实际上不属于该流,并且变得难以管理以及生成更多内容请求返回服务器.
i am populating a virtual field in mongoose schema with a getter function.It should populate the field without any errors but it is throwing error
MongooseError: If you are populating a virtual, you must set the localFieldand foreignField options
when we are populating a field with getter then the localfield and foriegn field is not required
const DashboardSchema = new Schema({
userId: {
type: SchemaTypes.ObjectId
}
}, { toJSON: { virtuals: true } });
DashboardSchema.virtual('TopReports').get(function () {
return TopReports.find({ userId: this.userId }).sort('-date').limit(10);
})
If you want a "virtual populate" with a sort
and limit
then that's not actually how you do it. What you created is just a "virtual getter" which is actually returning the result of an async function. You can use that, but it's a lot trickier to manage resolving the returned Promise
and it really has nothing to do with populate()
, which is where you are raising an error.
There are also different options to doing this.
Mongoose Virtual Populate
For this which is closest to what you attempted, Instead you want something like this:
const dashboardSchema = new Schema({
userId: Schema.Types.ObjectId
},
{
toJSON: { virtuals: true },
toObject: { virtuals: true }
});
dashboardSchema.virtual('TopReports', {
ref: 'Report',
localField: 'userId',
foreignField: 'userId',
options: { sort: { date: -1 }, limit: 10 } // optional - could add with populate
});
const reportSchema = new Schema({
userId: Schema.Types.ObjectId,
seq: Number,
date: Date
});
const Dashboard = mongoose.model('Dashboard', dashboardSchema);
const Report = mongoose.model('Report', reportSchema);
This is actually almost identical to the documentation example on "Populate Virtuals" in that the given example there also includes the options
which are the same as passing options to the populate method itself. When you set options
in the virtual
you only need invoke like this:
let result = await Dashboard.findOne().populate('TopReports');
The defaults set for sort
and limit
are automatically applied on this "virtual" field as the populate()
is performed. If you chose NOT to include the options
you would just add the options manually:
let result2 = await Dashboard.findOne().populate({
path: 'TopReports',
options: { sort: '-date', limit: 5 }
});
log(result2);
That's all you really need do. The definition of course includes localField
and foreignField
as well as the ref
all so the populate()
call knows where to get the data from and which fields to relate to. There is also an optional justOne
which differentiates between singular and Array
results, as well as a few other options.
MongoDB $lookup
The other option here is that MongoDB basically has the same functionality built in anyway, with the exception that this is single request as opposed to populate()
which is actually multiple requests in order to return the data from separate collections:
let result = await Dashboard.aggregate([
{ "$lookup": {
"from": Report.collection.name,
"let": { "userId": "$userId" },
"pipeline": [
{ "$match": { "$expr": { "$eq": [ "$userId", "$$userId" ] } } },
{ "$sort": { "date": -1 } },
{ "$limit": 10 }
],
"as": "TopReports"
}}
]);
So that's one request with a single response as compared to the "many" find()
requests actually issued by populate()
. It's the same result and just used the "sub-pipeline" form of $lookup
in order to apply the $sort
and $limit
to the returned array of related items.
With a little work, you can even inspect the schema definition ( including a defined virtual ) from the mongoose schema and construct the same $lookup
statement. There is a basic demonstration of this "schema inspection" on Querying after populate in Mongoose.
So it depends on which suits your needs best. I suggest trying both and even bench-marking application performance.
As a full demonstration, here is an example listing. It inserts 20 things and just returns the most "recent" 10 results in the array:
const { Schema, Types: { ObjectId } } = mongoose = require('mongoose');
const uri = 'mongodb://localhost:27017/test';
const opts = { useNewUrlParser: true };
mongoose.set('useFindAndModify', false);
mongoose.set('useCreateIndex', true);
mongoose.set('debug', true);
const dashboardSchema = new Schema({
userId: Schema.Types.ObjectId
},
{
toJSON: { virtuals: true },
toObject: { virtuals: true }
});
dashboardSchema.virtual('TopReports', {
ref: 'Report',
localField: 'userId',
foreignField: 'userId',
options: { sort: { date: -1 }, limit: 10 } // optional - could add with populate
});
const reportSchema = new Schema({
userId: Schema.Types.ObjectId,
seq: Number,
date: Date
});
const Dashboard = mongoose.model('Dashboard', dashboardSchema);
const Report = mongoose.model('Report', reportSchema);
const log = data => console.log(JSON.stringify(data, undefined, 2));
(async function() {
try {
const conn = await mongoose.connect(uri, opts);
await Promise.all(
Object.entries(conn.models).map(([k,m]) => m.deleteMany())
);
// Insert some things
let { userId } = await Dashboard.create({ userId: new ObjectId() });
const oneDay = ( 1000 * 60 * 60 * 24 );
const baseDate = Date.now() - (oneDay * 30); // 30 days ago
await Report.insertMany(
[ ...Array(20)]
.map((e,i) => ({ userId, seq: i+1, date: baseDate + ( oneDay * i ) }))
);
// Virtual populate
let popresult = await Dashboard.findOne().populate('TopReports');
log(popresult);
// Aggregate $lookup
let result = await Dashboard.aggregate([
{ "$lookup": {
"from": Report.collection.name,
"let": { "userId": "$userId" },
"pipeline": [
{ "$match": { "$expr": { "$eq": [ "$userId", "$$userId" ] } } },
{ "$sort": { "date": -1 } },
{ "$limit": 10 }
],
"as": "TopReports"
}}
]);
log(result);
} catch (e) {
console.error(e)
} finally {
mongoose.disconnect()
}
})()
And the output:
Mongoose: dashboards.deleteMany({}, {})
Mongoose: reports.deleteMany({}, {})
Mongoose: dashboards.insertOne({ _id: ObjectId("5cce3d9e16302f32acb5c572"), userId: ObjectId("5cce3d9e16302f32acb5c571"), __v: 0 })
Mongoose: reports.insertMany([ { _id: 5cce3d9e16302f32acb5c573, userId: 5cce3d9e16302f32acb5c571, seq: 1, date: 2019-04-05T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c574, userId: 5cce3d9e16302f32acb5c571, seq: 2, date: 2019-04-06T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c575, userId: 5cce3d9e16302f32acb5c571, seq: 3, date: 2019-04-07T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c576, userId: 5cce3d9e16302f32acb5c571, seq: 4, date: 2019-04-08T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c577, userId: 5cce3d9e16302f32acb5c571, seq: 5, date: 2019-04-09T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c578, userId: 5cce3d9e16302f32acb5c571, seq: 6, date: 2019-04-10T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c579, userId: 5cce3d9e16302f32acb5c571, seq: 7, date: 2019-04-11T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c57a, userId: 5cce3d9e16302f32acb5c571, seq: 8, date: 2019-04-12T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c57b, userId: 5cce3d9e16302f32acb5c571, seq: 9, date: 2019-04-13T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c57c, userId: 5cce3d9e16302f32acb5c571, seq: 10, date: 2019-04-14T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c57d, userId: 5cce3d9e16302f32acb5c571, seq: 11, date: 2019-04-15T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c57e, userId: 5cce3d9e16302f32acb5c571, seq: 12, date: 2019-04-16T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c57f, userId: 5cce3d9e16302f32acb5c571, seq: 13, date: 2019-04-17T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c580, userId: 5cce3d9e16302f32acb5c571, seq: 14, date: 2019-04-18T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c581, userId: 5cce3d9e16302f32acb5c571, seq: 15, date: 2019-04-19T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c582, userId: 5cce3d9e16302f32acb5c571, seq: 16, date: 2019-04-20T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c583, userId: 5cce3d9e16302f32acb5c571, seq: 17, date: 2019-04-21T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c584, userId: 5cce3d9e16302f32acb5c571, seq: 18, date: 2019-04-22T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c585, userId: 5cce3d9e16302f32acb5c571, seq: 19, date: 2019-04-23T01:34:22.554Z, __v: 0 }, { _id: 5cce3d9e16302f32acb5c586, userId: 5cce3d9e16302f32acb5c571, seq: 20, date: 2019-04-24T01:34:22.554Z, __v: 0 } ], {})
Mongoose: dashboards.findOne({}, { projection: {} })
Mongoose: reports.find({ userId: { '$in': [ ObjectId("5cce3d9e16302f32acb5c571") ] } }, { sort: { date: -1 }, limit: 10, projection: {} })
{
"_id": "5cce3d9e16302f32acb5c572",
"userId": "5cce3d9e16302f32acb5c571",
"__v": 0,
"TopReports": [
{
"_id": "5cce3d9e16302f32acb5c586",
"userId": "5cce3d9e16302f32acb5c571",
"seq": 20,
"date": "2019-04-24T01:34:22.554Z",
"__v": 0
},
{
"_id": "5cce3d9e16302f32acb5c585",
"userId": "5cce3d9e16302f32acb5c571",
"seq": 19,
"date": "2019-04-23T01:34:22.554Z",
"__v": 0
},
{
"_id": "5cce3d9e16302f32acb5c584",
"userId": "5cce3d9e16302f32acb5c571",
"seq": 18,
"date": "2019-04-22T01:34:22.554Z",
"__v": 0
},
{
"_id": "5cce3d9e16302f32acb5c583",
"userId": "5cce3d9e16302f32acb5c571",
"seq": 17,
"date": "2019-04-21T01:34:22.554Z",
"__v": 0
},
{
"_id": "5cce3d9e16302f32acb5c582",
"userId": "5cce3d9e16302f32acb5c571",
"seq": 16,
"date": "2019-04-20T01:34:22.554Z",
"__v": 0
},
{
"_id": "5cce3d9e16302f32acb5c581",
"userId": "5cce3d9e16302f32acb5c571",
"seq": 15,
"date": "2019-04-19T01:34:22.554Z",
"__v": 0
},
{
"_id": "5cce3d9e16302f32acb5c580",
"userId": "5cce3d9e16302f32acb5c571",
"seq": 14,
"date": "2019-04-18T01:34:22.554Z",
"__v": 0
},
{
"_id": "5cce3d9e16302f32acb5c57f",
"userId": "5cce3d9e16302f32acb5c571",
"seq": 13,
"date": "2019-04-17T01:34:22.554Z",
"__v": 0
},
{
"_id": "5cce3d9e16302f32acb5c57e",
"userId": "5cce3d9e16302f32acb5c571",
"seq": 12,
"date": "2019-04-16T01:34:22.554Z",
"__v": 0
},
{
"_id": "5cce3d9e16302f32acb5c57d",
"userId": "5cce3d9e16302f32acb5c571",
"seq": 11,
"date": "2019-04-15T01:34:22.554Z",
"__v": 0
}
],
"id": "5cce3d9e16302f32acb5c572"
}
Mongoose: dashboards.aggregate([ { '$lookup': { from: 'reports', let: { userId: '$userId' }, pipeline: [ { '$match': { '$expr': { '$eq': [ '$userId', '$$userId' ] } } }, { '$sort': { date: -1 } }, { '$limit': 10 } ], as: 'TopReports' } } ], {})
[
{
"_id": "5cce3d9e16302f32acb5c572",
"userId": "5cce3d9e16302f32acb5c571",
"__v": 0,
"TopReports": [
{
"_id": "5cce3d9e16302f32acb5c586",
"userId": "5cce3d9e16302f32acb5c571",
"seq": 20,
"date": "2019-04-24T01:34:22.554Z",
"__v": 0
},
{
"_id": "5cce3d9e16302f32acb5c585",
"userId": "5cce3d9e16302f32acb5c571",
"seq": 19,
"date": "2019-04-23T01:34:22.554Z",
"__v": 0
},
{
"_id": "5cce3d9e16302f32acb5c584",
"userId": "5cce3d9e16302f32acb5c571",
"seq": 18,
"date": "2019-04-22T01:34:22.554Z",
"__v": 0
},
{
"_id": "5cce3d9e16302f32acb5c583",
"userId": "5cce3d9e16302f32acb5c571",
"seq": 17,
"date": "2019-04-21T01:34:22.554Z",
"__v": 0
},
{
"_id": "5cce3d9e16302f32acb5c582",
"userId": "5cce3d9e16302f32acb5c571",
"seq": 16,
"date": "2019-04-20T01:34:22.554Z",
"__v": 0
},
{
"_id": "5cce3d9e16302f32acb5c581",
"userId": "5cce3d9e16302f32acb5c571",
"seq": 15,
"date": "2019-04-19T01:34:22.554Z",
"__v": 0
},
{
"_id": "5cce3d9e16302f32acb5c580",
"userId": "5cce3d9e16302f32acb5c571",
"seq": 14,
"date": "2019-04-18T01:34:22.554Z",
"__v": 0
},
{
"_id": "5cce3d9e16302f32acb5c57f",
"userId": "5cce3d9e16302f32acb5c571",
"seq": 13,
"date": "2019-04-17T01:34:22.554Z",
"__v": 0
},
{
"_id": "5cce3d9e16302f32acb5c57e",
"userId": "5cce3d9e16302f32acb5c571",
"seq": 12,
"date": "2019-04-16T01:34:22.554Z",
"__v": 0
},
{
"_id": "5cce3d9e16302f32acb5c57d",
"userId": "5cce3d9e16302f32acb5c571",
"seq": 11,
"date": "2019-04-15T01:34:22.554Z",
"__v": 0
}
]
}
]
The Wrong Way
Just to demonstrate what is wrong with the "getter" approach, here is an example listing showing actually resolving the returned Promise
on each returned object:
const { Schema, Types: { ObjectId } } = mongoose = require('mongoose');
const uri = 'mongodb://localhost:27017/test';
const opts = { useNewUrlParser: true };
mongoose.set('useFindAndModify', false);
mongoose.set('useCreateIndex', true);
mongoose.set('debug', true);
const dashboardSchema = new Schema({
userId: Schema.Types.ObjectId
},
{
toJSON: { virtuals: true },
toObject: { virtuals: true }
});
dashboardSchema.virtual('TopReports').get(function() {
return Report.find({ userId: this.userId }).sort("-date").limit(10);
});
const reportSchema = new Schema({
userId: Schema.Types.ObjectId,
seq: Number,
date: Date
});
const Dashboard = mongoose.model('Dashboard', dashboardSchema);
const Report = mongoose.model('Report', reportSchema);
const log = data => console.log(JSON.stringify(data, undefined, 2));
(async function() {
try {
const conn = await mongoose.connect(uri, opts);
await Promise.all(
Object.entries(conn.models).map(([k,m]) => m.deleteMany())
);
// Insert some things
let { userId } = await Dashboard.create({ userId: new ObjectId() });
const oneDay = ( 1000 * 60 * 60 * 24 );
const baseDate = Date.now() - (oneDay * 30); // 30 days ago
await Report.insertMany(
[ ...Array(20)]
.map((e,i) => ({ userId, seq: i+1, date: baseDate + ( oneDay * i ) }))
);
// Mimic the virtual populate with the getter
let results = await Dashboard.find();
for ( let r of results ) {
let obj = { ...r.toObject() }; // copy the plain object data only
obj.TopReports = await r.TopReports; // Resolve the Promise
log(obj);
}
} catch (e) {
console.error(e)
} finally {
mongoose.disconnect()
}
})()
And output:
Mongoose: dashboards.deleteMany({}, {})
Mongoose: reports.deleteMany({}, {})
Mongoose: dashboards.insertOne({ _id: ObjectId("5cce45193134aa37e88c4114"), userId: ObjectId("5cce45193134aa37e88c4113"), __v: 0 })
Mongoose: reports.insertMany([ { _id: 5cce45193134aa37e88c4115, userId: 5cce45193134aa37e88c4113, seq: 1, date: 2019-04-05T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c4116, userId: 5cce45193134aa37e88c4113, seq: 2, date: 2019-04-06T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c4117, userId: 5cce45193134aa37e88c4113, seq: 3, date: 2019-04-07T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c4118, userId: 5cce45193134aa37e88c4113, seq: 4, date: 2019-04-08T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c4119, userId: 5cce45193134aa37e88c4113, seq: 5, date: 2019-04-09T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c411a, userId: 5cce45193134aa37e88c4113, seq: 6, date: 2019-04-10T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c411b, userId: 5cce45193134aa37e88c4113, seq: 7, date: 2019-04-11T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c411c, userId: 5cce45193134aa37e88c4113, seq: 8, date: 2019-04-12T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c411d, userId: 5cce45193134aa37e88c4113, seq: 9, date: 2019-04-13T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c411e, userId: 5cce45193134aa37e88c4113, seq: 10, date: 2019-04-14T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c411f, userId: 5cce45193134aa37e88c4113, seq: 11, date: 2019-04-15T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c4120, userId: 5cce45193134aa37e88c4113, seq: 12, date: 2019-04-16T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c4121, userId: 5cce45193134aa37e88c4113, seq: 13, date: 2019-04-17T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c4122, userId: 5cce45193134aa37e88c4113, seq: 14, date: 2019-04-18T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c4123, userId: 5cce45193134aa37e88c4113, seq: 15, date: 2019-04-19T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c4124, userId: 5cce45193134aa37e88c4113, seq: 16, date: 2019-04-20T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c4125, userId: 5cce45193134aa37e88c4113, seq: 17, date: 2019-04-21T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c4126, userId: 5cce45193134aa37e88c4113, seq: 18, date: 2019-04-22T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c4127, userId: 5cce45193134aa37e88c4113, seq: 19, date: 2019-04-23T02:06:17.518Z, __v: 0 }, { _id: 5cce45193134aa37e88c4128, userId: 5cce45193134aa37e88c4113, seq: 20, date: 2019-04-24T02:06:17.518Z, __v: 0 } ], {})
Mongoose: dashboards.find({}, { projection: {} })
Mongoose: reports.find({ userId: ObjectId("5cce45193134aa37e88c4113") }, { sort: { date: -1 }, limit: 10, projection: {} })
{
"_id": "5cce45193134aa37e88c4114",
"userId": "5cce45193134aa37e88c4113",
"__v": 0,
"TopReports": [
{
"_id": "5cce45193134aa37e88c4128",
"userId": "5cce45193134aa37e88c4113",
"seq": 20,
"date": "2019-04-24T02:06:17.518Z",
"__v": 0
},
{
"_id": "5cce45193134aa37e88c4127",
"userId": "5cce45193134aa37e88c4113",
"seq": 19,
"date": "2019-04-23T02:06:17.518Z",
"__v": 0
},
{
"_id": "5cce45193134aa37e88c4126",
"userId": "5cce45193134aa37e88c4113",
"seq": 18,
"date": "2019-04-22T02:06:17.518Z",
"__v": 0
},
{
"_id": "5cce45193134aa37e88c4125",
"userId": "5cce45193134aa37e88c4113",
"seq": 17,
"date": "2019-04-21T02:06:17.518Z",
"__v": 0
},
{
"_id": "5cce45193134aa37e88c4124",
"userId": "5cce45193134aa37e88c4113",
"seq": 16,
"date": "2019-04-20T02:06:17.518Z",
"__v": 0
},
{
"_id": "5cce45193134aa37e88c4123",
"userId": "5cce45193134aa37e88c4113",
"seq": 15,
"date": "2019-04-19T02:06:17.518Z",
"__v": 0
},
{
"_id": "5cce45193134aa37e88c4122",
"userId": "5cce45193134aa37e88c4113",
"seq": 14,
"date": "2019-04-18T02:06:17.518Z",
"__v": 0
},
{
"_id": "5cce45193134aa37e88c4121",
"userId": "5cce45193134aa37e88c4113",
"seq": 13,
"date": "2019-04-17T02:06:17.518Z",
"__v": 0
},
{
"_id": "5cce45193134aa37e88c4120",
"userId": "5cce45193134aa37e88c4113",
"seq": 12,
"date": "2019-04-16T02:06:17.518Z",
"__v": 0
},
{
"_id": "5cce45193134aa37e88c411f",
"userId": "5cce45193134aa37e88c4113",
"seq": 11,
"date": "2019-04-15T02:06:17.518Z",
"__v": 0
}
],
"id": "5cce45193134aa37e88c4114"
}
Of course the Promise
returned by the getter needs to be resolved for each returned document. By contrast the populate()
uses $in
in order to return the matching entries for ALL results in the find()
with a single request, and this would issue a new find()
for every Dashboard
document instead of a single find()
based on ALL the userId
values found in every Dashboard
document in the result.
Basically as opposed to either populate()
or $lookup
you are essentially splitting out the "join" logic into parts of the control flow, where it really does not belong, and becomes difficult to manage as well as generating even more requests back to the server.
这篇关于猫鼬用吸气剂功能填充虚拟字段不起作用的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!