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