有引用时不调用 Mongoose 模型填充回调
Posted
技术标签:
【中文标题】有引用时不调用 Mongoose 模型填充回调【英文标题】:Mongoose Model populate callback is not called when there are references 【发布时间】:2017-12-13 04:26:59 【问题描述】:我正在尝试使用 Mongoose 的 Model.populate() 在通过聚合和展开获取的文档的子结构中将用户 ID 转换为用户。我猜我的架构有问题,或者放松可能会破坏与子架构的连接。
问题是:当存在有效引用时,根本不会调用填充回调。当没有子结构时调用回调,原始文档不变。
结构:
我有一篇文章,每个用户都可以没有或有多个 ArticleRatings。
我在 ArticleRating 中使用了两个引用来关联文章和进行评级的用户。
流程:
该过程实际上是将文章导出为(旧版)CSV 格式,并将结构扁平化为具有用户特定评级的重复文章行。展开非常适合此操作,保留空值会保留没有评分的文章。
调试:
我尝试深入了解 Model.populate 代码。所有的 Promise 和回调包装器都变得相当复杂,但我可以看到底层的填充调用也没有调用内部回调。我没有使用 promise 变体 - 但不是 100% 确定我是否应该使用? (Mongoose 文档对回调和承诺之间的用例有点模糊)。
我已经仔细检查了我的架构,并尝试将模型显式添加到 populate 调用中(不应该需要它,因为它在架构中)。没有错误或异常,不会崩溃。
在 Chrome 调试器中单步执行代码会显示模型,正如我所期望的那样:前几篇文章有一个 rating.userId 和一个有效的 ObjectId,但在这些情况下,填充回调根本不会被调用。接下来的一百篇文章没有设置“评级”,并且所有文章都可靠地调用了回调。
所以我猜我做错了什么是导致 Model.populate 走在一条没有正确出错的路径上?
注意:我知道我可以重写代码以使用聚合 $lookup 或其他嵌入结构而不是外部引用,但我正处于功能拼图的最后一块,并希望让它按原样工作。
这是简化的架构:
const ArticleRatingSchema = new Schema(
articleId: type: Schema.Types.ObjectId, ref:'Article',
userId: type: Schema.Types.ObjectId, ref:'User',
rating: String,
comment: String,
);
const ArticleSchema = new Schema(
title: String,
rating: ArticleRatingSchema,
);
这是查找
// Find all articles relating to this project, and their ratings.
// Unwind does the duplicate per-user, and preserve keeps un-rated articles.
articleModel.aggregate([
$match: projectId: projectId,
$lookup:from:'articleratings', localField:'_id', foreignField:'articleId', as:'rating' ,
$unwind: path:'$rating', preserveNullAndEmptyArrays:true
], (err, models) =>
if (!err)
models.map((article) =>
articleModel.populate(article, path:'rating.userId', model:'User', (err, article)=>
// Process the article...
// this callback only gets called where there is NO rating in the article.
);
);
【问题讨论】:
【参考方案1】:我意识到这是因为我在同步 map() 循环中处理集合,这两种情况必须不同,因为不匹配的填充是同步回调,而匹配的替换回调稍后。
如果我在回调中使用console.log(),我发现这四个匹配的案例是在CSV已经被格式化和下载后最后处理的。
所以答案是:populate IS 被调用,但是是异步的。
我需要重新设计 map() 循环以适应通常的异步模式。
【讨论】:
【参考方案2】:我个人对您认为在聚合管道中使用 $lookup
并希望得到 .populate()
的结果感到困惑。因为要求使用.populate()
本质上意味着正在向服务器发出额外的查询以“模拟连接”。
因此,既然$lookup
实际上是“在服务器上加入”,那么您真的应该为此使用$lookup
。
您可以使用.populate()
,我将展示一些代码来证明它可以完成。但这在这里确实是多余的,因为您还不如只在服务器上完成所有工作。
所以我对你似乎拥有的结构的最佳“近似”是:
文章
"_id" : ObjectId("5962104312246235cdcceb16"),
"title" : "New News",
"ratings" : [ ],
"__v" : 0
文章评分
"_id" : ObjectId("5962104312246235cdcceb17"),
"articleId" : ObjectId("5962104312246235cdcceb16"),
"userId" : ObjectId("5962104312246235cdcceb13"),
"rating" : "5",
"comment" : "Great!",
"__v" : 0
"_id" : ObjectId("5962104312246235cdcceb18"),
"articleId" : ObjectId("5962104312246235cdcceb16"),
"userId" : ObjectId("5962104312246235cdcceb14"),
"rating" : "3",
"comment" : "Okay I guess ;)",
"__v" : 0
"_id" : ObjectId("5962104312246235cdcceb19"),
"articleId" : ObjectId("5962104312246235cdcceb16"),
"userId" : ObjectId("5962104312246235cdcceb15"),
"rating" : "1",
"comment" : "Hated it :<",
"__v" : 0
用户
"_id" : ObjectId("5962104312246235cdcceb13"),
"name" : "Bill",
"email" : "bill@example.com",
"__v" : 0
"_id" : ObjectId("5962104312246235cdcceb14"),
"name" : "Fred",
"email" : "fred@example.com",
"__v" : 0
"_id" : ObjectId("5962104312246235cdcceb15"),
"name" : "Ted",
"email" : "ted@example.com",
"__v" : 0
然后是聚合语句:
Article.aggregate(
[
"$lookup":
"from": ArticleRating.collection.name,
"localField": "_id",
"foreignField": "articleId",
"as": "ratings"
,
"$unwind": "$ratings" ,
"$lookup":
"from": User.collection.name,
"localField": "ratings.userId",
"foreignField": "_id",
"as": "ratings.userId",
,
"$unwind": "$ratings.userId" ,
"$group":
"_id": "$_id",
"title": "$first": "$title" ,
"ratings": "$push": "$ratings"
],
(err,articles) =>
if (err) callback(err);
log(articles);
callback();
)
结果:
"_id": "5962126f3ef2fb35efeefd94",
"title": "New News",
"ratings": [
"_id": "5962126f3ef2fb35efeefd95",
"articleId": "5962126f3ef2fb35efeefd94",
"userId":
"_id": "5962126f3ef2fb35efeefd91",
"name": "Bill",
"email": "bill@example.com",
"__v": 0
,
"rating": "5",
"comment": "Great!",
"__v": 0
,
"_id": "5962126f3ef2fb35efeefd96",
"articleId": "5962126f3ef2fb35efeefd94",
"userId":
"_id": "5962126f3ef2fb35efeefd92",
"name": "Fred",
"email": "fred@example.com",
"__v": 0
,
"rating": "3",
"comment": "Okay I guess ;)",
"__v": 0
,
"_id": "5962126f3ef2fb35efeefd97",
"articleId": "5962126f3ef2fb35efeefd94",
"userId":
"_id": "5962126f3ef2fb35efeefd93",
"name": "Ted",
"email": "ted@example.com",
"__v": 0
,
"rating": "1",
"comment": "Hated it :<",
"__v": 0
]
在“评分”本身上“填充”对“articleId”的引用没有意义。但我们确实“填充”了文章的“评分”,以及每个评分的“用户”。
示例清单
以两种方式显示它,使用.populate()
(在$lookup
之后)就像你正在尝试的那样,也只是使用普通的$lookup
。
方法使用“plain promises”和async.map
交替:
const async = require('async'),
mongoose = require('mongoose'),
Schema = mongoose.Schema;
mongoose.Promise = global.Promise;
mongoose.set('debug',true);
mongoose.connect('mongodb://localhost/publication');
const userSchema = new Schema(
name: String,
email: String
);
const User = mongoose.model('User', userSchema);
const articleRatingSchema = new Schema(
articleId: type: Schema.Types.ObjectId, ref:'Article',
userId: type: Schema.Types.ObjectId, ref:'User',
rating: String,
comment: String,
);
const articleSchema = new Schema(
title: String,
ratings: [articleRatingSchema]
)
const Article = mongoose.model('Article', articleSchema);
const ArticleRating = mongoose.model('ArticleRating', articleRatingSchema);
function log(data)
console.log(JSON.stringify(data,undefined,2))
const userData = [
name: 'Bill', rating: 5, comment: 'Great!' ,
name: 'Fred', rating: 3, comment: 'Okay I guess ;)' ,
name: 'Ted', rating: 1, comment: 'Hated it :<'
];
async.series(
[
// Clean data
(callback) =>
async.each(mongoose.models,(model,callback) =>
model.remove(,callback),callback),
// Insert data
(callback) =>
async.waterfall(
[
// User and article
(callback) =>
async.parallel(
"users": (callback) =>
User.create(
["Bill", "Fred", "Ted"].map( name =>
( name, email: `$name.toLowerCase()@example.com` )
),
callback
),
"article": (callback) =>
Article.create( title: "New News" ,callback)
,
callback
),
// Article Ratings
(data,callback) =>
ArticleRating.create(
data.users.map( u => (
articleId: data.article._id,
userId: u._id,
rating: userData.find( ud => ud.name === u.name ).rating,
comment: userData.find( ud => ud.name === u.name ).comment
)),
callback
)
],
callback
),
// $lookup and populate async.map
(callback) =>
Article.aggregate(
[
"$lookup":
"from": ArticleRating.collection.name,
"localField": "_id",
"foreignField": "articleId",
"as": "ratings"
],
(err,articles) =>
if (err) callback(err);
async.map(
articles.map( a => new Article(a) ),
(article,callback) =>
async.map(
article.ratings,
(rating,callback) =>
ArticleRating.populate(rating, path: 'userId' ,callback),
(err,ratings) =>
if (err) callback(err);
article.ratings = ratings
callback(null,article)
),
(err,articles) =>
if (err) callback(err);
log(articles);
callback();
)
),
// $look and populate Promise
(callback) =>
Article.aggregate(
[
"$lookup":
"from": ArticleRating.collection.name,
"localField": "_id",
"foreignField": "articleId",
"as": "ratings"
]
)
.then(articles =>
Promise.all(
articles.map( a => new Article(a) ).map(article =>
new Promise((resolve,reject) =>
Promise.all(
article.ratings.map( rating =>
ArticleRating.populate(rating, path: 'userId' )
)
).then(ratings =>
article.ratings = ratings;
resolve(article);
).catch(reject)
)
)
)
)
.then(articles =>
log(articles);
callback();
)
.catch(err => callback(err)),
// Plain $lookup
(callback) =>
Article.aggregate(
[
"$lookup":
"from": ArticleRating.collection.name,
"localField": "_id",
"foreignField": "articleId",
"as": "ratings"
,
"$unwind": "$ratings" ,
"$lookup":
"from": User.collection.name,
"localField": "ratings.userId",
"foreignField": "_id",
"as": "ratings.userId",
,
"$unwind": "$ratings.userId" ,
"$group":
"_id": "$_id",
"title": "$first": "$title" ,
"ratings": "$push": "$ratings"
],
(err,articles) =>
if (err) callback(err);
log(articles);
callback();
)
],
(err) =>
if (err) throw err;
mongoose.disconnect();
);
完整输出
Mongoose: users.remove(, )
Mongoose: articles.remove(, )
Mongoose: articleratings.remove(, )
Mongoose: users.insert( name: 'Bill', email: 'bill@example.com', _id: ObjectId("596219ff6f73ed36d868ed40"), __v: 0 )
Mongoose: users.insert( name: 'Fred', email: 'fred@example.com', _id: ObjectId("596219ff6f73ed36d868ed41"), __v: 0 )
Mongoose: users.insert( name: 'Ted', email: 'ted@example.com', _id: ObjectId("596219ff6f73ed36d868ed42"), __v: 0 )
Mongoose: articles.insert( title: 'New News', _id: ObjectId("596219ff6f73ed36d868ed43"), ratings: [], __v: 0 )
Mongoose: articleratings.insert( articleId: ObjectId("596219ff6f73ed36d868ed43"), userId: ObjectId("596219ff6f73ed36d868ed40"), rating: '5', comment: 'Great!', _id: ObjectId("596219ff6f73ed36d868ed44"), __v: 0 )
Mongoose: articleratings.insert( articleId: ObjectId("596219ff6f73ed36d868ed43"), userId: ObjectId("596219ff6f73ed36d868ed41"), rating: '3', comment: 'Okay I guess ;)', _id: ObjectId("596219ff6f73ed36d868ed45"), __v: 0 )
Mongoose: articleratings.insert( articleId: ObjectId("596219ff6f73ed36d868ed43"), userId: ObjectId("596219ff6f73ed36d868ed42"), rating: '1', comment: 'Hated it :<', _id: ObjectId("596219ff6f73ed36d868ed46"), __v: 0 )
Mongoose: articles.aggregate([ '$lookup': from: 'articleratings', localField: '_id', foreignField: 'articleId', as: 'ratings' ], )
Mongoose: users.find( _id: '$in': [ ObjectId("596219ff6f73ed36d868ed40") ] , fields: )
Mongoose: users.find( _id: '$in': [ ObjectId("596219ff6f73ed36d868ed41") ] , fields: )
Mongoose: users.find( _id: '$in': [ ObjectId("596219ff6f73ed36d868ed42") ] , fields: )
[
"_id": "596219ff6f73ed36d868ed43",
"title": "New News",
"__v": 0,
"ratings": [
"_id": "596219ff6f73ed36d868ed44",
"articleId": "596219ff6f73ed36d868ed43",
"userId":
"_id": "596219ff6f73ed36d868ed40",
"name": "Bill",
"email": "bill@example.com",
"__v": 0
,
"rating": "5",
"comment": "Great!",
"__v": 0
,
"_id": "596219ff6f73ed36d868ed45",
"articleId": "596219ff6f73ed36d868ed43",
"userId":
"_id": "596219ff6f73ed36d868ed41",
"name": "Fred",
"email": "fred@example.com",
"__v": 0
,
"rating": "3",
"comment": "Okay I guess ;)",
"__v": 0
,
"_id": "596219ff6f73ed36d868ed46",
"articleId": "596219ff6f73ed36d868ed43",
"userId":
"_id": "596219ff6f73ed36d868ed42",
"name": "Ted",
"email": "ted@example.com",
"__v": 0
,
"rating": "1",
"comment": "Hated it :<",
"__v": 0
]
]
Mongoose: articles.aggregate([ '$lookup': from: 'articleratings', localField: '_id', foreignField: 'articleId', as: 'ratings' ], )
Mongoose: users.find( _id: '$in': [ ObjectId("596219ff6f73ed36d868ed40") ] , fields: )
Mongoose: users.find( _id: '$in': [ ObjectId("596219ff6f73ed36d868ed41") ] , fields: )
Mongoose: users.find( _id: '$in': [ ObjectId("596219ff6f73ed36d868ed42") ] , fields: )
[
"_id": "596219ff6f73ed36d868ed43",
"title": "New News",
"__v": 0,
"ratings": [
"_id": "596219ff6f73ed36d868ed44",
"articleId": "596219ff6f73ed36d868ed43",
"userId":
"_id": "596219ff6f73ed36d868ed40",
"name": "Bill",
"email": "bill@example.com",
"__v": 0
,
"rating": "5",
"comment": "Great!",
"__v": 0
,
"_id": "596219ff6f73ed36d868ed45",
"articleId": "596219ff6f73ed36d868ed43",
"userId":
"_id": "596219ff6f73ed36d868ed41",
"name": "Fred",
"email": "fred@example.com",
"__v": 0
,
"rating": "3",
"comment": "Okay I guess ;)",
"__v": 0
,
"_id": "596219ff6f73ed36d868ed46",
"articleId": "596219ff6f73ed36d868ed43",
"userId":
"_id": "596219ff6f73ed36d868ed42",
"name": "Ted",
"email": "ted@example.com",
"__v": 0
,
"rating": "1",
"comment": "Hated it :<",
"__v": 0
]
]
Mongoose: articles.aggregate([ '$lookup': from: 'articleratings', localField: '_id', foreignField: 'articleId', as: 'ratings' , '$unwind': '$ratings' , '$lookup': from: 'users', localField: 'ratings.userId', foreignField: '_id', as: 'ratings.userId' , '$unwind': '$ratings.userId' , '$group': _id: '$_id', title: '$first': '$title' , ratings: '$push': '$ratings' ], )
[
"_id": "596219ff6f73ed36d868ed43",
"title": "New News",
"ratings": [
"_id": "596219ff6f73ed36d868ed44",
"articleId": "596219ff6f73ed36d868ed43",
"userId":
"_id": "596219ff6f73ed36d868ed40",
"name": "Bill",
"email": "bill@example.com",
"__v": 0
,
"rating": "5",
"comment": "Great!",
"__v": 0
,
"_id": "596219ff6f73ed36d868ed45",
"articleId": "596219ff6f73ed36d868ed43",
"userId":
"_id": "596219ff6f73ed36d868ed41",
"name": "Fred",
"email": "fred@example.com",
"__v": 0
,
"rating": "3",
"comment": "Okay I guess ;)",
"__v": 0
,
"_id": "596219ff6f73ed36d868ed46",
"articleId": "596219ff6f73ed36d868ed43",
"userId":
"_id": "596219ff6f73ed36d868ed42",
"name": "Ted",
"email": "ted@example.com",
"__v": 0
,
"rating": "1",
"comment": "Hated it :<",
"__v": 0
]
]
【讨论】:
以上是关于有引用时不调用 Mongoose 模型填充回调的主要内容,如果未能解决你的问题,请参考以下文章