Как я справляюсь с вложенными запросами мангуста и асинхронными проблемами в моих отношениях "один ко многим"?


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

Может ли кто-нибудь сказать мне, где я ошибаюсь, или есть ли лучший способ сделать то, что я пытаюсь сделать:

Мой Схемы:

У меня есть три Schemas в Мангусте:

  1. UserSchema (User)
  2. PostSchema (Post)
  3. CommentSchema (PostComment)

Я включил сюда только CommentSchema, чтобы упростить свой вопрос:

var CommentSchema = new mongoose.Schema (
    {
        message: {
            type: String,
            minlength: [2, 'Your comment must be at least 2 characters.'],
            maxlength: [2000, 'Your comment must be less than 2000 characters.'],
            required: [true, 'You cannot submit an empty comment.'],
            trim: true,
        }, // end message field
        userID: {
            type: mongoose.Schema.Types.ObjectId,
            ref: 'User'
        },
        username: {
            type: String,
        },
        postID: {
            type: mongoose.Schema.Types.ObjectId,
            ref: 'Post'
        },
    },
    {
        timestamps: true,
    }
);

При создании нового комментария, _id сообщения записывается в поле .postID комментария.

Моя Псевдокодовая Стратегия:

// query for all posts using a mongoose promise
// run a `for loop` through the array returned posts
    // query my comments collection for any comments pertaining to post[i]
        // attach comments returned to the post[i]
        // push post[i] into a new array (now with comments attached)
        // check if on last run through array
            // res.json the new array back to the front end 
            // On front end just iterate through each post and its contained comments.

Однако, когда я пытаюсь использовать эту стратегию, я получаю некоторые асинхронные проблемы с моим вторым Запрос Мангуста в цикле for.

Мой Фактический Пример Кода:

Post.find({}) // first query
    .then(function(allPosts) {
        for (var i = 0; i < allPosts.length; i++) {
            _post = allPosts[i];
            console.log(_post, i);
            PostComment.find({postID: _post._id}) // **nested query
                .then(function(comments) {
                    console.log({
                        post: _post,  // this value will not match the console log above (outside of the query)
                        index_value: i, // this value too will be out of match with the console log above
                        comments: comments,
                    });
                    // attach new comment to allPosts[i]
                    // push post with comment attached to new array
                    // check if on last iteration, if so res.json new array
                })
                .catch(function(err) {
                    console.log(err);
                })
         }
    .catch(function(err) {
        console.log(err);
    }

Проблема из приведенного выше примера кода:

В приведенном выше примере во втором запросе **nested query, значение i и _post не синхронизированы к моменту возврата данных из обещания мангуста (.then). Цикл for развивается быстрее, чем возвращаются данные. Таким образом, если я попытаюсь прикрепить какие-либо комментарии к родительскому объекту post (_post), переменная уже не синхронизирована с прогрессия цикла for (_post теперь становится следующей позицией в массиве). Я озадачен тем, как исправить это и получить все мои комментарии от каждого поста, и связать это вместе для переднего плана. Я просто запутался в этом вопросе.

Желаемое Поведение:

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

Вывод:

Что я делаю не так? Как я могу перебрать все мои сообщения, и захватить все комментарии для каждого, и сделать его аккуратным для фронтального отображения в Angular? Является ли мой подход неправильным или слишком "дорогостоящим" в моих запросах? Есть ли лучший способ добиться желаемого поведения?

Любое понимание или помощь выше ценится! Я искал вокруг в надежде увидеть другой вопрос, подобный этому, и уже некоторое время бьюсь головой об эту проблему=)

1 3

1 ответ:

Я придумал, как это сделать. Мне пришлось внести некоторые изменения в свои схемы. Поскольку Mongo не допускает объединения, мы можем вместо этого использовать метод populate() Мангуста (http://mongoosejs.com/docs/populate.html).

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

UserSchema:

// Setup dependencies:
var mongoose = require('mongoose'),
    Schema = mongoose.Schema;

// Setup a schema:
var UserSchema = new Schema (
    {
        username: {
            type: String,
            minlength: [2, 'Username must be at least 2 characters.'],
            maxlength: [20, 'Username must be less than 20 characters.'],
            required: [true, 'Your username cannot be blank.'],
            trim: true,
            unique: true, // username must be unique
            dropDups: true,
        }, // end username field
    },
    {
        timestamps: true,
    }
);

// Instantiate our model and export it:
module.exports = mongoose.model('User', UserSchema);

PostSchema:

// Setup dependencies:
var mongoose = require('mongoose'),
    Schema = mongoose.Schema;

// Setup a schema:
var PostSchema = new Schema (
    {
        message: {
            type: String,
            minlength: [2, 'Your post must be at least 2 characters.'],
            maxlength: [2000, 'Your post must be less than 2000 characters.'],
            required: [true, 'You cannot submit an empty post.'],
            trim: true,
        }, // end message field
        user: {
            type: Schema.Types.ObjectId,
            ref: 'User'
        },
        comments: [{
            type: Schema.Types.ObjectId,
            ref: 'Comment'
        }],
    },
    {
        timestamps: true,
    }
);

// updates userID based upon current session login info:
PostSchema.methods.updateUserID = function(id) {
    this.user = id;
    this.save();
    return true;
};

// adds comment to post's comments array:
PostSchema.methods.addComment = function(commentID) {
    this.comments.push(commentID);
    this.save();
    return true;
};

// Instantiate our model and export it:
module.exports = mongoose.model('Post', PostSchema);

CommentSchema:

// Setup dependencies:
var mongoose = require('mongoose'),
    Schema = mongoose.Schema;

// Setup a schema:
var CommentSchema = new Schema (
    {
        message: {
            type: String,
            minlength: [2, 'Your comment must be at least 2 characters.'],
            maxlength: [2000, 'Your comment must be less than 2000 characters.'],
            required: [true, 'You cannot submit an empty comment.'],
            trim: true,
        }, // end message field
        user: {
            type: Schema.Types.ObjectId,
            ref: 'User'
        },
        _post: {
            type: Schema.Types.ObjectId,
            ref: 'Post'
        },
    },
    {
        timestamps: true,
    }
);

// Assigns user ID to comment when called (uses session info):
CommentSchema.methods.updateUserID = function(id) {
    this.user = id;
    this.save();
    return true;
};

// Assigns post ID to comment when called:
CommentSchema.methods.updatePostID = function(id) {
    this._post = id;
    this.save();
    return true;
};

// Instantiate our model and export it:
module.exports = mongoose.model('Comment', CommentSchema);

Решение: используйте метод заполнения:

Используя метод populate() (http://mongoosejs.com/docs/populate.html ) мы можем начать с нашей Постмодели и заполнить поле comments.

Используя методы экземпляра, определенные в PostSchema, мы помещаем идентификаторы комментариев в массив Post.comments, который метод populate() ниже захватывает все объекты комментариев и заменяет идентификаторы с реальными объектами комментариев.

var User = require('mongoose').model('User');
var Post = require('mongoose').model('Post');
var PostComment = require('mongoose').model('Comment');

Post.find({})
    .populate('comments')  // populates comments objects based on ids in `comments`
    .exec()
    .then(function(commentsAndPosts) {
        console.log(commentsAndPosts);
        return res.json(commentsAndPosts);
    })
    .catch(function(err) {
        console.log(err);
        return res.json(err);
    })

Ссылка, предоставленная на документацию Мангуста, имеет хороший чистый пример.

Теперь, на переднем конце, каждый объект post имеет массив комментариев внутри, и все объекты комментариев были заполнены! Мило!

Обзор:

Мне удалось сохранить массив идентификаторов комментариев внутри каждого объекта Post (когда создается новый комментарий, comment._id помещается в массив Post.comments). Используя populate(), мы можем запросить коллекцию комментариев и захватите все объекты комментариев, связанные с соответствующим идентификатором. Это здорово, так как после того, как наш метод заполнения завершен, мы можем отправить назад весь наш массив всех сообщений и комментариев как один объект JSON и повторить их на переднем конце.

Заселение на нескольких уровнях:

Предположим, в приведенных выше сценариях я также хотел получить имя пользователя для авторов сообщений и комментариев (Примечание: каждый объект комментариев и сообщений имеет поле user, в котором хранится User._id.

Мы можем использовать заполните, вложив некоторые параметры, чтобы достичь нескольких уровней следующим образом. Это будет предоставлять те же данные, что и в примере выше (все сообщения и все комментарии), но включает имена пользователей для комментариев и сообщений на основе сохраненного идентификатора пользователя:

Post.find({}) // finds all posts
    .populate('user') // populates the user (based on ID) and returns user object (entire user object)
    .populate({ 
        path: 'comments', // populates all comments based on comment ID's
        populate: {  path: 'user' } // populates all 'user" fields for all comments (again, based on user ID)
    })
    .exec()
    .then(function(commentsAndPostsWithUsers) {
        console.log(commentsAndPostsWithUsers);
        return res.json(commentsAndPostsWithUsers);
    })
    .catch(function(err) {
        console.log(err);
    })

В приведенном выше примере мы начинаем с захвата всех сообщений, а затем захватываем все объекты пользователя для каждого сообщения,а затем все комментарии вместе с каждым объектом пользователя для каждого комментария и связываем все это!

Перебираем дальше Передний конец с угловым:

Мы можем перебирать наши возвращенные сообщения, используя ng-repeat**:

<!-- Repeating Posts -->
<div ng-repeat="post in commentsAndPostsWithUsers >
    <h3 ng-bind="post.user.username"></h3>
    <h2 ng-bind="post.createdAt"></h2>
    <p ng-bind="post.message"></p>
    <!-- Repeating Comments -->
    <div ng-repeat="comment in post.comments">
        <h4 ng-bind="comment.user.username"></h4>
        <h5 ng-bind="comment.createdAt"></h5>
        <p ng-bind="comment.message"></p>
    </div>
</div>

** угловой контроллер не показан в приведенном выше примере кода.

Мы можем получить доступ к имени пользователя для сообщений или комментариев через, post.user.username или post.user.username, соответственно, поскольку весь объект пользователя был прикреплен (таким образом, почему мы должны перейти вниз, чтобы получить имя пользователя). Затем мы можем использовать другой ng-repeat для перебора post.comments, показывая все комментарии. Желаю всего наилучшего и надеюсь, что это поможет кто-нибудь, избегайте путаницы, которую я сделал!