'Mongoose Virtual - Count references in another model that are in local array of references
I have 2 models Comment and Report.
const mongoose = require('mongoose');
const CommentSchema = new mongoose.Schema(
{
content: {
type: String,
trim: true,
maxLength: 2048,
},
createdAt: {
type: Date,
default: Date.now,
},
parent: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Comment',
required: false,
},
replies: [
{
type: mongoose.Schema.Types.ObjectId,
ref: 'Comment',
},
],
isReply: {
type: Boolean,
default: false,
},
},
{ toJSON: { virtuals: true }, toObject: { virtuals: true } }
);
CommentSchema.virtual('reportCount', {
ref: 'Report',
localField: '_id',
foreignField: 'comment',
justOne: false,
count: true,
});
CommentSchema.virtual('reportReplyCount', {
ref: 'Report',
localField: 'replies',
foreignField: 'comment',
justOne: false,
count: true,
});
module.exports = mongoose.model('Comment', CommentSchema);
Comment has field replies which is array of references pointing to the Comment model. A User can report a comment, and when that happens a new Report document is stored in Report collection, and it contains a reference to that comment and a reference to a User. I have 2 virtual properties in the Comment Schema, reportCount (show number of reports for that comment) and reportReplyCount (shows number of reports on comment replies). Now the reportCount works flawlessly, but the reportReplyCount does not. When I create a comment, and the replies to that comment, it shows number of replies instead of number of reports. I googled but could not find anything similar.
const mongoose = require('mongoose');
const ReportSchema = new mongoose.Schema({
description: {
type: String,
trim: true,
required: true,
maxLength: 100,
},
reporter: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true,
},
createdAt: {
type: Date,
default: Date.now,
},
comment: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Comment',
required: true,
},
});
module.exports = mongoose.model('Report', ReportSchema);
Solution 1:[1]
I don't know what you're trying to do exactly, but I've looked around and there doesn't seem to be any existing solution for this. Virtuals are one way of solving the problem, but I haven't seen an answer that uses it in this scenario.
You could try to create a new virtual called reportReplyCount that shows the number of reports on replies. Then use aggregate on Comment and replace reportCount with the new virtual. You can use something like the following:
CommentSchema.virtual('reportReplyCount', {
ref: 'Report', // reference to Report model
localField: 'replies', // matches field Comment Schema has named 'replies'
foreignField: 'comment', // matches field Report Schema has named 'comment' (foreign key in Report model)
justOne: false, // this is going to return all related documents, not just one (just like reportCount)
count: true, // set it to true so it returns a number instead of an array of documents
});
CommentSchema.methods = { ... }
CommentSchema.statics = { ... }
module.exports = mongoose.model('Comment', CommentSchema);
I would avoid using virtuals in your case if you can find another solution for your problem.
As a side note, I have seen developers create a new model to act as the virtual, like this:
const mongoose = require('mongoose');
const ReportSchema = new mongoose.Schema({
description: {
type: String,
trim: true,
required: true,
maxLength: 100,
},
reporter: { // reference to User model (foreign key)
type: mongoose.Schema.Types.ObjectId,
ref: 'User', // reference to User model (foreign key)
});
module.exports = mongoose.model('Report', ReportSchema);
// Now you need an instance of that new Schema called VirtualReport. The schema must follow the same format as the "real" Report's schema did above but with a few extra parameters that refer to the virtual and it's definition (as in how it will behave).
const VirtualReportSchema = new mongoose.Schema({ ... }, { _id : false });
module.exports = mongoose.model('VirtualReport', VirtualReportSchema);
Then all you need to do is, in your schema that has the virtual:
// Now you can use VirtualReport like any other model. It will work just like Report but it won't get stored in the database.
CommentSchema.virtual('reportReplyCount', {
ref: 'VirtualReport', // reference to VirtualReport model
localField: 'replies', // matches field Comment Schema has named 'replies'
foreignField: 'comment', // matches field VirtualReport Schema has named 'comment' (foreign key in VirtualReport model)
justOne: false, // this is going to return all related documents, not just one (just like reportCount)
count: true, // set it to true so it returns a number instead of an array of documents
});
CommentSchema.methods = { ... }
CommentSchema.statics = { ... }
module.exports = mongoose.model('Comment', CommentSchema);
But please note that the virtual's definition ("how it will behave") must contain _id property set to false (otherwise an error will be thrown). This is because when virtuals are used in subdocuments and a user references them via dot notation (e.g., commentToBeDeleted[parent].reportReplyCount), dot notation tries to access _id property of the virtual. If it's set to false, dot notation won't be able to find that virtual and you'll get an error. So don't forget to set _id property to false!
BTW, this question was asked here. It's rather unfortunate that the question was asked on Stack Overflow instead of MongoDB's own docs where a link is provided to the explanation for virtuals (well there is also a comment about "justOne" but at least it refers directly to documentation).
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | PJMan0300 |
