Search code examples
mongoosecollections

How can I modify multiple Mongoose collections simultaneously?


I have 3 Mongoose collection: Board, Thread and Reply. Each Reply belongs to 1 Thread (as an array's item in "replies" property) and each Thread belongs to 1 Board (as an array's item in "threads" property).

As the code below, whenever I want to add a new Reply to the database by sending a post request, I have to update all 3 collections: Board, Thread and Reply.

Is there any other way to achive this without having to manually modify each collection one by one?

app.route('/api/replies/:board').post(async (req, res) => {
    const { text, delete_password, thread_id } = req.body;
    const board = req.params.board;
    
    if (!thread_id.match(/^[0-9a-fA-F]{24}$/)) {
      res.send('Thread ID is invalid');
    } else {
      const updateBoard = await BoardModel.findOne({name: board}).exec(); //find the Board in Board collection
      const updateThread = updateBoard.threads.id(thread_id); //find the Thread in that Board
      const updateThreadDB = await ThreadModel.findById(thread_id).exec(); //find the Thread in Thread collection
      if (!updateThreadDB || !updateBoard) {
        res.send("Board or thread not found");
      } else {
        //Add new Reply
        const newReply = new ReplyModel({
          text: text,
          delete_password: delete_password,
          reported: false,
        })
        let saveReply = await newReply.save(); //save Reply to Reply collection

        //Update Thread and Board
        updateThreadDB.replies.push(saveReply);
        updateThread.replies.push(saveReply);
        let saveBoard = await updateBoard.save(); //update new Board collection
        let saveThread = await updateThreadDB.save(); //update new Thread collection

        res.json(saveReply);
      }
    }
  })

Here's my models file:

const mongoose = require('mongoose');
mongoose.connect(process.env.DB);
const { Schema } = mongoose;

const replySchema = new Schema ({
  text: {type: String, required: true},
  created_on: {type : Date, required: true, default: () => { return new Date() }},
  delete_password: {type: String, required: true},
  reported: {type: Boolean, required: true},
})
const Reply = mongoose.model('Reply', replySchema);

const threadSchema = new Schema ({
  text: {type: String, required: true},
  created_on: {type : Date, required: true, default: () => { return new Date() }},
  bumped_on: {type : Date, required: true, default: () => { return new Date() }},
  reported: {type: Boolean, required: true},
  delete_password: {type: String, required: true},
  replies: {type: [replySchema], required: true},
});
const Thread = mongoose.model('Thread', threadSchema);

const boardSchema = new Schema ({
  name: {type: String, required: true},
  threads: {type: [threadSchema], required: true},
});
const Board = mongoose.model('Board', boardSchema);

exports.Board = Board;
exports.Thread = Thread;
exports.Reply = Reply;

Solution

  • Looking at the your schemas and the way you are storing the data seems like you are duplicating the data. For example you are creating a new Reply in your replies collection here:

    let saveReply = await newReply.save();
    

    Then you are adding that same data twice. Once to the threds.replies array in the threads collection, and then once to the boards.threads array in the boards collection here:

    updateThreadDB.replies.push(saveReply);
    updateThread.replies.push(saveReply);
    let saveBoard = await updateBoard.save();
    let saveThread = await updateThreadDB.save();
    

    Mogodb documents are limited to 16MB in size so if your application is popular you could end up exceeding the size of each boards document storing all those threads and nested replies in a single doument.

    You need to make use of two things, referenced documents and Model.findByIdAndUpdate().

    You could update your schemas like so:

    const boardSchema = new Schema ({
       name: {
          type: String, 
          required: true
       },
       threads: {
          type: mongoose.Types.ObjectId, //< Store the _id of the thread
          ref: 'Thread', //< This will reference the threads collection
       },
    });
    
    const threadSchema = new Schema ({
       text: {
          type: String, 
          required: true
       },
       //...
       replies: {
          type: mongoose.Types.ObjectId, //< Store the _id of the reply
          ref: 'Reply', //< This will reference the replies collection
       },
    });
    

    Now when you create a Thread you only need to push the _id of the Thread, which is an ObjectId, into the Board.threads once.

    Then when you create a Reply you only need to push the _id of the Reply, which is an ObjectId, into the Thread.replies once.

    Here is a sample of how you could create a new Thread for a Board:

    //Get the board _id
    const boardId = req.params.board;
    
    //Create a new Thread. Use create() instead of new and .save()
    const thread = await Thread.create({
       text: req.body.text,
       //...
    });
    
    // Add the thread._id ObjectId into the board.threads array. 
    // I have used findByIdAndUpdate() for demonstration 
    const board = await Board.findByIdAndUpdate(boardId,
       { $push: { threads: thread._id } },
       { new: true }
    );
    

    Now you don't need to touch the Board document again until you need to delete a Thread.

    Here is an example of how you can create a Reply:

    //Get the thread _id
    const threadId = req.params.thread;
    
    //Create a new Reply. Use create() instead of new and .save()
    const reply = await Reply.create({
       text: req.body.text,
       //...
    });
    
    // Add the reply._id ObjectId into the thread.replies array. 
    // I have used findByIdAndUpdate() for demonstration 
    const thread = await Thread.findByIdAndUpdate(threadId,
       { $push: { replies: reply._id } },
       { new: true }
    );
    

    Notice how I didn't need to go and get the Board parent document. Now you can create as many replies as you want and only need to push the _id of each reply into the parent Thread. This will save so much space as only the ObjectId is being stored in the Thread and only the ObjectId being stored in each Board.

    Lastly, if you want to get each referenced document you can use populate. Please read it to understand but here is a sample of how you could do it:

    //Get the board _id
    const boardId = req.params.board;
    
    const board = await Board.findById(boardId)
    .populate({
        path: 'threads', //< Replace all ObjectIds with the actual Thread document
        model: Thread,
        populate: {
            path: 'replies', //< Within each Thread replace all ObjectIds with the actual Reply document
            model: Reply
        },
    });