Reputation: 3461
When I have a collection schema that has a multi type property. Like it can be number or string
I use mongoose's mixes type for this but all validations are gone.
Question:
var employeeSchema = mongoose.Schema({
_id:false,
title:string,
...
});
EmployeeSchema.methods.(....)//add some methods
var GroupSchema = ({
visitor: {}, // visitor can be an Employee schema or a string of person name
note: string,
time: Date
},{collection:'group'});
How can I define visitor in groupSchema that:
Upvotes: 2
Views: 3447
Reputation: 50396
What you seem to be after here is really a form of "polymorphism" or how we generally implement this in a database as a "discriminator". The basics here is that one form of Object inherits from another, yet is it's own object with it's own unique properties as well as associated methods.
This actually works out better than just distinquishing between a plain "string" and somethig more concrete as an Object, and it is fairly simple to implement. As an example:
var async = require('async'),
util = require('util'),
mongoose = require('mongoose'),
Schema = mongoose.Schema;
mongoose.connect('mongodb://localhost/company');
function BaseSchema() {
Schema.apply(this,arguments);
this.add({
name: String
});
}
util.inherits(BaseSchema,Schema);
var personSchema = new BaseSchema();
personSchema.methods.speak = function() {
console.log( "my name is %s",this.name );
};
var employeeSchema = new BaseSchema({
title: String
});
employeeSchema.methods.speak = function() {
console.log( "my name is %s, and my title is %s",this.name,this.title );
};
var groupSchema = new Schema({
visitor: { "type": Schema.Types.ObjectId, "ref": "Person" },
note: String,
time: { "type": Date, "default": Date.now }
});
var Person = mongoose.model( 'Person', personSchema ),
Employee = Person.discriminator( 'Employee', employeeSchema ),
Group = mongoose.model( 'Group', groupSchema );
async.waterfall(
[
// Clean data
function(callback) {
async.each([Person,Group],function(model,callback) {
model.remove(callback);
},callback);
},
// Add a person
function(callback) {
var person = new Person({ "name": "Bob" });
person.save(function(err,person) {
callback(err,person);
});
},
// Add an employee
function(person,callback) {
var employee = new Employee({ "name": "Sarah", "title": "Manager" });
employee.save(function(err,employee) {
callback(err,person,employee);
});
},
// Add person to group
function(person,employee,callback) {
var group = new Group({
visitor: person
});
group.save(function(err) {
callback(err,employee);
});
},
// Add employee to group
function(employee,callback) {
var group = new Group({
visitor: employee
});
group.save(function(err) {
callback(err);
});
},
// Get groups populated
function(callback) {
Group.find().populate('visitor').exec(function(err,group) {
console.dir(group);
group.forEach(function(member) {
member.visitor.speak();
});
callback(err);
});
}
],
function(err) {
if (err) throw err;
mongoose.disconnect();
}
);
And the very basic output here:
[ { _id: 55d06d984a4690ca1f0d73ed,
visitor: { _id: 55d06d984a4690ca1f0d73eb, name: 'Bob', __v: 0 },
__v: 0,
time: Sun Aug 16 2015 21:01:44 GMT+1000 (AEST) },
{ _id: 55d06d984a4690ca1f0d73ee,
visitor:
{ _id: 55d06d984a4690ca1f0d73ec,
name: 'Sarah',
title: 'Manager',
__v: 0,
__t: 'Employee' },
__v: 0,
time: Sun Aug 16 2015 21:01:44 GMT+1000 (AEST) } ]
my name is Bob
my name is Sarah, and my title is Manager
In short, we have two "types" here as a "Person" and an "Employee". A "Person" is of course the base type that everyone is and therefore can have some base properties, and even methods if wanted. The "Employee" inherits from the base "Person" and therefore shares any properties and methods, as well as being able to define their own.
When setting up the mongoose models here, these are the important lines:
var Person = mongoose.model( 'Person', personSchema ),
Employee = Person.discriminator( 'Employee', employeeSchema ),
Note that "Employee" does not use the standard mongoose.model
contructor, but instead calls Person.discriminator
. This does a special thing, where in fact the "Employee" is actually shared with the "Person" model, but with distinct information to tell mongoose that this is in fact an "Employee".
Note also the contruction on "visitor" in the groupSchema
:
visitor: { "type": Schema.Types.ObjectId, "ref": "Person" },
As mentioned before both "Person" and "Employee" actually share the same base model as "Person", though with some specific traits. So the reference here tells mongoose to resolve the stored ObjectId values from this model.
The beauty of this becomes apparent when you call .populate()
as is done later in the listing. As each of these references is resolved, the correct Object type is replaced for the value that was there. This becomes evident as the .speak()
method is called on each object for "visitor".
my name is Bob
my name is Sarah, and my title is Manager
There are also other great things you get as mongoose can do things such as look at "Employee" as a model and "automatically" filter out any objects that are not an employee in any query. By the same token, using the "Person" model will also show all types in there ( they are distinquished by the "__t" property and it's value ) so you can also exploit that in various query processes.
So if you don't like referencing or generally just prefer to keep all the data in the "groups" collection and not bother with a separate collection for the other "people" data, then this is possible as well.
All you really need here are a few changes to the listing, as notably in the schema definition for "visitor" as a "Mixed" type:
visitor: Schema.Types.Mixed,
And then rather than calling .populate()
since all the data is already there, you just need to do some manual "type casting", which is actually quite simple:
group.forEach(function(member) {
member.visitor = (member.visitor.hasOwnProperty("__t"))
? mongoose.model(member.visitor.__t)(member.visitor)
: Person(member.visitor);
member.visitor.speak();
});
And then everthing works "swimmingly" with embedded data as well.
For a complete listing where using embedded data:
var async = require('async'),
util = require('util'),
mongoose = require('mongoose'),
Schema = mongoose.Schema;
mongoose.connect('mongodb://localhost/company');
function BaseSchema() {
Schema.apply(this,arguments);
this.add({
name: String
});
}
util.inherits(BaseSchema,Schema);
var personSchema = new BaseSchema();
personSchema.methods.speak = function() {
console.log( "my name is %s",this.name );
};
var employeeSchema = new BaseSchema({
title: String
});
employeeSchema.methods.speak = function() {
console.log( "my name is %s and my title is %s",this.name,this.title );
};
var groupSchema = new Schema({
visitor: Schema.Types.Mixed,
note: String,
time: { "type": Date, "default": Date.now }
});
var Person = mongoose.model( 'Person', personSchema, null ),
Employee = Person.discriminator( 'Employee', employeeSchema, null ),
Group = mongoose.model( 'Group', groupSchema );
async.waterfall(
[
// Clean data
function(callback) {
async.each([Person,Group],function(model,callback) {
model.remove(callback);
},callback);
},
// Add a person
function(callback) {
var person = new Person({ "name": "Bob" });
callback(null,person);
},
// Add an employee
function(person,callback) {
var employee = new Employee({ "name": "Sarah", "title": "Manager" });
callback(null,person,employee);
},
// Add person to group
function(person,employee,callback) {
var group = new Group({
visitor: person
});
group.save(function(err) {
callback(err,employee);
});
},
// Add employee to group
function(employee,callback) {
var group = new Group({
visitor: employee
});
group.save(function(err) {
callback(err);
});
},
// Get groups populated
function(callback) {
Group.find().exec(function(err,group) {
console.dir(group);
group.forEach(function(member) {
member.visitor = (member.visitor.hasOwnProperty("__t"))
? mongoose.model(member.visitor.__t)(member.visitor)
: Person(member.visitor);
member.visitor.speak();
});
callback(err);
});
}
],
function(err) {
if (err) throw err;
mongoose.disconnect();
}
);
This has the same output as above and the objects created maintain that handy "discriminator" information. So you can again exploit that much as was mentioned before.
Upvotes: 3