Sails.js填充嵌套关联

51

我在 Sails.js version 0.10-rc5 中的关联模型方面有一个问题。我正在构建一个应用程序,其中多个模型彼此相关联,我已经到达了需要嵌套关联的地步。

这里有三个部分:

首先有类似博客文章的内容,由用户编写。在博客文章中,我想显示与用户相关的信息,例如他们的用户名。现在,这里一切都运行良好。直到下一步:我正在尝试显示与帖子关联的评论。

评论是一个名为 Comment 的单独模型。每个评论也与作者(用户)相关联。我可以轻松显示评论列表,但是当我想显示与评论相关联的用户信息时,我无法弄清楚如何填充评论和用户信息。

在我的控制器中,我正在尝试做这样的事情:

Post
  .findOne(req.param('id'))
  .populate('user')
  .populate('comments') // I want to populate this comment with .populate('user') or something
  .exec(function(err, post) {
    // Handle errors & render view etc.
  });

在我的文章“展示”操作中,我试图像这样检索信息(简化):

<ul> 
  <%- _.each(post.comments, function(comment) { %>
    <li>
      <%= comment.user.name %>
      <%= comment.description %>
    </li>
  <% }); %>
</ul>

但是comment.user.name将会是未定义的。如果我尝试访问'用户'属性,例如comment.user,它将显示它的ID。这告诉我当我将评论与另一个模型关联时,它不会自动填充用户信息到评论中。

有没有什么好的解决方法呢?

提前感谢!

P.S.

为了澄清,这是我在不同模型中基本设置关联的方式:

// User.js
posts: {
  collection: 'post'
},   
hours: {
  collection: 'hour'
},
comments: {
  collection: 'comment'
}

// Post.js
user: {
  model: 'user'
},
comments: {
  collection: 'comment',
  via: 'post'
}

// Comment.js
user: {
  model: 'user'
},
post: {
  model: 'post'
}
9个回答

45

或者你可以使用内置的Blue Bird Promise功能来实现它。(适用于Sails@v0.10.5)

请参考以下代码:

var _ = require('lodash');

...

Post
  .findOne(req.param('id'))
  .populate('user')
  .populate('comments')
  .then(function(post) {
    var commentUsers = User.find({
        id: _.pluck(post.comments, 'user')
          //_.pluck: Retrieves the value of a 'user' property from all elements in the post.comments collection.
      })
      .then(function(commentUsers) {
        return commentUsers;
      });
    return [post, commentUsers];
  })
  .spread(function(post, commentUsers) {
    commentUsers = _.indexBy(commentUsers, 'id');
    //_.indexBy: Creates an object composed of keys generated from the results of running each element of the collection through the given callback. The corresponding value of each key is the last element responsible for generating the key
    post.comments = _.map(post.comments, function(comment) {
      comment.user = commentUsers[comment.user];
      return comment;
    });
    res.json(post);
  })
  .catch(function(err) {
    return res.serverError(err);
  });

一些解释:

  1. 我使用Lo-Dash来处理数组。更多细节请参考官方文档
  2. 注意第一个"then"函数里的返回值,那个包含在数组中的"[post, commentUsers]"对象也是"promise"对象。这意味着它们在第一次执行时并不包含实际数据值,直到获得了值才会如此。因此,"spread"函数将等待实际值的到来并继续执行其余操作。

1
你能解释一下这行代码的含义吗:_.pluck(results.post.comments, 'user')?我没有看到 results 在哪里被声明过。 - OneHoopyFrood
我刚刚看了一下你的其他答案。我可以看出你很有知识,但我希望你能再详细解释一些。不要只提供代码,还要提供解释性的答案。欢迎加入社区。 - OneHoopyFrood
1
感谢@colepanike的热心建议,我已经修复了我的代码片段,使其更加规范,并添加了一些说明。 - Fermin Yang

26

目前还没有内置的方法来填充嵌套关联。您最好使用异步进行映射:

async.auto({

    // First get the post  
    post: function(cb) {
        Post
           .findOne(req.param('id'))
           .populate('user')
           .populate('comments')
           .exec(cb);
    },

    // Then all of the comment users, using an "in" query by
    // setting "id" criteria to an array of user IDs
    commentUsers: ['post', function(cb, results) {
        User.find({id: _.pluck(results.post.comments, 'user')}).exec(cb);
    }],

    // Map the comment users to their comments
    map: ['commentUsers', function(cb, results) {
        // Index comment users by ID
        var commentUsers = _.indexBy(results.commentUsers, 'id');
        // Get a plain object version of post & comments
        var post = results.post.toObject();
        // Map users onto comments
        post.comments = post.comments.map(function(comment) {
            comment.user = commentUsers[comment.user];
            return comment;
        });
        return cb(null, post);
    }]

}, 
   // After all the async magic is finished, return the mapped result
   // (or an error if any occurred during the async block)
   function finish(err, results) {
       if (err) {return res.serverError(err);}
       return res.json(results.map);
   }
);

虽然它没有嵌套人口普查(正在开发中,但可能不适用于v0.10)那么漂亮,但好的一面是它实际上相当高效。


1
我按照您的逻辑进行了更复杂的深度关联,一切似乎都没问题,但在将JSON对象转换为字符串时出现了问题,它省略了一些字段。您能否请看一下:https://dev59.com/doLba4cB1Zd3GeqPbDKK? - user2867106
1
不错的 async.auto() 使用。 - Isilmë O.
2
这里使用 async.waterfall 不是更合适吗? - Cobby
1
有人可以帮我解决这个问题吗:http://stackoverflow.com/questions/29588582/sails-js-waterline-nested-populate-query - Kadosh
尝试使用Angular端点消耗这些映射结果是否有问题?我的意思是,这在服务器上运行良好,但是在Angular中我得到了不同的JSON数据,只有帖子和用户数据,而没有评论。 - Andrés Da Viá
显示剩余3条评论

5
我创建了一个名为 nested-pop 的 NPM 模块,您可以在下面的链接中找到它。

https://www.npmjs.com/package/nested-pop

以下是使用方法。

var nestedPop = require('nested-pop');

User.find()
.populate('dogs')
.then(function(users) {

    return nestedPop(users, {
        dogs: [
            'breed'
        ]
    }).then(function(users) {
        return users
    }).catch(function(err) {
        throw err;
    });

}).catch(function(err) {
    throw err;
);

我可以请你审阅一下如何提供个人开源库?吗? - Martijn Pieters
@Jam Risser,我该如何使用您的NPM模块获取5个级别的关联数据? - Mayur Shah

3
值得一提的是,有一个拉取请求正在添加嵌套填充:https://github.com/balderdashy/waterline/pull/1052 拉取请求目前尚未合并,但您可以直接使用它进行安装。
npm i Atlantis-Software/waterline#deepPopulate

使用它你可以像这样做 .populate('user.comments ...')

它支持 MSSQL 适配器吗? - Raj Adroit
@RajAdroit 不知道,我还没有使用 Waterline 和 MSSQL。 - Boris Zagoruiko

3
 sails v0.11 doesn't support _.pluck and _.indexBy use sails.util.pluck and sails.util.indexBy instead.

async.auto({

     // First get the post  
    post: function(cb) {
        Post
           .findOne(req.param('id'))
           .populate('user')
           .populate('comments')
           .exec(cb);
    },

    // Then all of the comment users, using an "in" query by
    // setting "id" criteria to an array of user IDs
    commentUsers: ['post', function(cb, results) {
        User.find({id:sails.util.pluck(results.post.comments, 'user')}).exec(cb);
    }],

    // Map the comment users to their comments
    map: ['commentUsers', function(cb, results) {
        // Index comment users by ID
        var commentUsers = sails.util.indexBy(results.commentUsers, 'id');
        // Get a plain object version of post & comments
        var post = results.post.toObject();
        // Map users onto comments
        post.comments = post.comments.map(function(comment) {
            comment.user = commentUsers[comment.user];
            return comment;
        });
        return cb(null, post);
    }]

}, 
   // After all the async magic is finished, return the mapped result
   // (or an error if any occurred during the async block)
   function finish(err, results) {
       if (err) {return res.serverError(err);}
       return res.json(results.map);
   }
);

2

截至sailsjs 1.0版本,“深度填充”请求仍未解决,但以下异步函数解决方案在我看来已经足够优雅:

const post = await Post
    .findOne({ id: req.param('id') })
    .populate('user')
    .populate('comments');
if (post && post.comments.length > 0) {
   const ids = post.comments.map(comment => comment.id);
   post.comments = await Comment
      .find({ id: commentId })
      .populate('user');
}

2
你可以使用非常简洁易懂的async库。对于与帖子相关的每个评论,您可以使用专门的任务填充许多字段,以并行方式执行它们,并在所有任务完成后检索结果。最后,你只需要返回最终结果。
Post
        .findOne(req.param('id'))
        .populate('user')
        .populate('comments') // I want to populate this comment with .populate('user') or something
        .exec(function (err, post) {

            // populate each post in parallel
            async.each(post.comments, function (comment, callback) {

                // you can populate many elements or only one...
                var populateTasks = {
                    user: function (cb) {
                        User.findOne({ id: comment.user })
                            .exec(function (err, result) {
                                cb(err, result);
                            });
                    }
                }

                async.parallel(populateTasks, function (err, resultSet) {
                    if (err) { return next(err); }

                    post.comments = resultSet.user;
                    // finish
                    callback();
                });

            }, function (err) {// final callback
                if (err) { return next(err); }

                return res.json(post);
            });
        });

0

如果有人想要对多篇文章进行相同操作,以下是一种方法:

  • 查找所有帖子中的用户ID
  • 从数据库中一次性查询所有用户
  • 使用这些用户更新帖子

考虑到同一用户可能会写多个评论,我们确保重复使用这些对象。此外,我们只进行了1次额外的查询(如果我们为每个帖子单独执行此操作,则需要进行多次查询)。

await Post.find()
  .populate('comments')
  .then(async (posts) => {
    // Collect all comment user IDs
    const userIDs = posts.reduce((acc, curr) => {
      for (const comment of post.comments) {
        acc.add(comment.user);
      }
      return acc;
    }, new Set());

    // Get users
    const users = await User.find({ id: Array.from(userIDs) });
    const usersMap = users.reduce((acc, curr) => {
      acc[curr.id] = curr;
      return acc;
    }, {});

    // Assign users to comments
    for (const post of posts) {
      for (const comment of post.comments) {
        if (comment.user) {
          const userID = comment.user;
          comment.user = usersMap[userID];
        }
      }
    }

    return posts;
  });

0

虽然这是一个老问题,但更简单的解决方案是循环遍历评论,使用async await替换每个评论的“user”属性(即id),并将其替换为用户的完整详细信息。

async function getPost(postId){
   let post = await Post.findOne(postId).populate('user').populate('comments');
   for(let comment of post.comments){
       comment.user = await User.findOne({id:comment.user});
   }
   return post;
}

希望这能帮到你!


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接