Sails.js заполняет вложенные ассоциации

У меня есть вопрос об ассоциациях в Sails.js версии 0.10-rc5. Я создавал приложение, в котором несколько моделей связаны друг с другом, и я пришел к тому моменту, когда мне нужно каким-то образом найти связи.

Есть три части:

Во-первых, это что-то вроде поста в блоге, написанного пользователем. В блоге я хочу показать информацию о связанном пользователе, например, его имя. Теперь все отлично работает здесь. До следующего шага: я пытаюсь показать комментарии, которые связаны с постом.

Комментарии представляют собой отдельную модель, называемую комментарием. Каждый из которых также имеет автора (пользователя), связанного с ним. Я могу легко показать список комментариев, хотя, когда я хочу отобразить информацию о пользователе, связанную с комментарием, я не могу понять, как заполнить комментарий информацией о пользователе.

В моем контроллере я пытаюсь сделать что-то вроде этого:

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 будет неопределенным. Если я попытаюсь получить доступ к свойству user, например, comment.user, оно покажет его идентификатор. Что говорит мне, что это не автоматически заполняет информацию пользователя в комментарии, когда я связываю комментарий с другой моделью.

У кого-нибудь есть идеалы, чтобы решить это правильно:)?

Заранее спасибо!

PS

Для пояснения, вот как я в основном настроил ассоциации в разных моделях:

// 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 ответов

Или вы можете использовать встроенную функцию 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]" внутри массива также являются объектами "обещания". Это означает, что они не содержали данные о значении, когда они были впервые выполнены, пока не получили значение. Так что эта функция "распространения" будет ждать, пока придет значение статьи, и продолжит делать все остальное.

На данный момент нет встроенного способа заполнения вложенных ассоциаций. Лучше всего использовать async для сопоставления:

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), но, с другой стороны, на самом деле это довольно эффективно.

Я создал модуль NPM для этого называется nested-pop. Вы можете найти его по ссылке ниже.

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;
);

Начиная с sailsjs 1.0, запрос на "глубокое заполнение" по-прежнему открыт, но следующее решение асинхронной функции выглядит достаточно элегантным IMO:

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');
}

Стоит сказать, что есть запрос на добавление для добавления вложенного населения: https://github.com/balderdashy/waterline/pull/1052

Запрос на извлечение в данный момент не объединен, но вы можете использовать его, установив его непосредственно с

npm i Atlantis-Software/waterline#deepPopulate

С этим вы можете сделать что-то вроде .populate('user.comments ...)',

 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);
   }
);

Вы можете использовать асинхронную библиотеку, которая очень проста и понятна. Для каждого комментария, относящегося к сообщению, вы можете заполнить много полей, как вы хотите, выделенными задачами, выполнить их параллельно и получить результаты, когда все задачи будут выполнены. Наконец, вам нужно только вернуть конечный результат.

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);
            });
        });

Конечно, это старый вопрос, но гораздо более простым решением было бы перебрать комментарии, заменив каждое свойство комментария "user" (которое является идентификатором) на полную информацию о пользователе с помощью async await.

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;
}

Надеюсь это поможет!

Если кто-то хочет сделать то же самое, но для нескольких сообщений, вот один из способов сделать это:

  • найти все идентификаторы пользователей в сообщениях
  • запросить всех пользователей за 1 раз из БД
  • обновить сообщения с этими пользователями

Учитывая, что один и тот же пользователь может писать несколько комментариев, мы гарантируем повторное использование этих объектов. Также мы делаем только 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;
  });
Другие вопросы по тегам