thread = new mongoose.Schema({
post: {type: Schema.Types.ObjectId, ref: 'Post'},
comment: {type: Schema.Types.ObjectId, ref: 'Comment'},
})
But in fact, thread can only have post or comment, it cannot have both at same time.
so ideal definition should be something like enum
thread = new mongoose.Schema({
data: {type: Schema.Types.ObjectId, ref: 'Post'} OR?? {type: Schema.Types.ObjectId, ref: 'Comment'},
})
How can I define this kind of thing in Mongoose?
Or what is the right way to do this kind of thing?
What you want is discriminators. This of course actually refers to the same "core" model, but it's handled in a special way so that different types of objects are distinquished and even considered to have their own model.
As an example listing for usage:
var async = require('async'),
util = require('util'),
mongoose = require('mongoose'),
Schema = mongoose.Schema;
mongoose.connect('mongodb://localhost/content');
mongoose.set("debug",true);
function BaseSchema() {
Schema.apply(this,arguments);
this.add({
"author": String,
"body": String,
"created": { "type": Date, "default": Date.now() }
});
}
util.inherits(BaseSchema,Schema);
var itemSchema = new BaseSchema();
var postSchema = new BaseSchema({
"title": String,
"score": Number
});
var commentSchema = new BaseSchema({
"post": { "type": Schema.Types.ObjectId, "ref": "Post" }
});
var Item = mongoose.model('Item',itemSchema),
Post = Item.discriminator('Post',postSchema),
Comment = Item.discriminator('Comment',commentSchema);
async.series(
[
// Clean data
function(callback) {
Item.remove({},callback);
},
// Insert Post
function(callback) {
async.waterfall(
[
function(callback) {
Post.create({
"title": "This post",
"author": "bill",
"body": "Whoa!"
},callback);
},
function(post,callback) {
Comment.create({
"author": "ted",
"body": "Excellent!",
"post": post
},callback);
}
],
callback
);
},
function(callback) {
console.log("All Items");
Item.find().exec(function(err,docs) {
console.log(docs);
callback(err);
})
},
function(callback) {
console.log("Just populated Comments");
Comment.find().populate('post').exec(function(err,docs) {
console.log(docs)
callback(err);
});
}
],
function(err) {
if (err) throw err;
mongoose.disconnect();
}
);
And the output:
All Items
Mongoose: items.find({}) { fields: undefined }
[ { created: Wed Mar 16 2016 14:15:56 GMT+1100 (AEDT),
__t: 'Post',
__v: 0,
body: 'Whoa!',
author: 'bill',
title: 'This post',
_id: 56e8cfed833e67750b678d9c },
{ created: Wed Mar 16 2016 14:15:56 GMT+1100 (AEDT),
__t: 'Comment',
__v: 0,
post: 56e8cfed833e67750b678d9c,
body: 'Excellent!',
author: 'ted',
_id: 56e8cfed833e67750b678d9d } ]
Just populated Comments
Mongoose: items.find({ __t: 'Comment' }) { fields: undefined }
Mongoose: items.find({ __t: 'Post', _id: { '$in': [ ObjectId("56e8cfed833e67750b678d9c") ] } }) { fields: undefined }
[ { created: Wed Mar 16 2016 14:15:56 GMT+1100 (AEDT),
__t: 'Comment',
__v: 0,
post:
{ created: Wed Mar 16 2016 14:15:56 GMT+1100 (AEDT),
__t: 'Post',
__v: 0,
body: 'Whoa!',
author: 'bill',
title: 'This post',
_id: 56e8cfed833e67750b678d9c },
body: 'Excellent!',
author: 'ted',
_id: 56e8cfed833e67750b678d9d } ]
If you look through the listing what you see is that we do indeed create a single model for Item
which is going to hold all objects, but the most interesting thing happens on the following lines:
var Item = mongoose.model('Item',itemSchema),
Post = Item.discriminator('Post',postSchema),
Comment = Item.discriminator('Comment',commentSchema);
So instead of using mongoose.model
for the next two model definitions we call Item.discriminator()
instead. This creates "special models" that will be contained within Item
and of course each type has it's own attached schema, and any logic that would be associated with that schema.
Since all data is actually in the same collection, even though we use each model seperately, you see that there is a special key addded to each object as __t
. This contains the registered name of the model that the data is actually associated with.
The the "debug"
option set, you can see how mongoose actually issues the queries. So when you just want to work on Comment
, the __t
value is automatically added to the query conditions ( and object creation of course ). In the same case where we asked "comments" to contain a reference to Post
, the same sort of filtering is applied when looking for things that should be referenced in the Post
model.
This is a pretty powerful pattern, and can either also employ the same sort of inheritance as demonstrated here or alternately just base of a completely blank or seperate schema definition.
If it were just a field for "reference" to data in this collection. Then using the base model of Item
in a foreign collection has the same result.
var otherSchema = new Schema({
"item": { "type": Schema.Types.ObjectId, "ref": "Item" }
});
mongoose.model("Other",otherSchema);
mongoose.model("Other").find().populate("item").exec(function(err,docs( {
// populates references with either Comment or Post
// as per their assigned __t value
});
If the referenced item is a Comment
, then it gets all the attached benefits of the coments schema. Or if it is a Post
, then you see the same first class object treatment.
So whatever way you want to use it, once defined using a discriminator then mongoose will work it out for you.