有引用时不调用 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 模型填充回调的主要内容,如果未能解决你的问题,请参考以下文章

Mongoose - 填充删除的空引用?

使用外部文件中的模型填充 Mongoose 模式属性

Mongoose 填充返回 null

与 mongoDb 和 mongoose 聚合后填充单个 ObjectId 引用

Mongoose:深人口(填充人口密集的领域)

Mongoose:深人口(填充人口密集的领域)