In this blog post we will see advance usage of mongoose library.
In the previous blog post we saw how to perform various operations on mongodb in fast way. We didn’t define any schema , validation and other feature. In this post we will see how to to use all mongoose features.
Collection Schema
Each collection on your mongo database is represented using a schema. Schema are basically column definitions of your collection, which you provide to mongoose to help out in validations, type checking and other operations.
Lets first define a very simple Schema and use it
var userSchema = mongoose.Schema({
firstname: String,
lastname: String,
created_at: Date,
is_active: Boolean
});
var User = conn.model('User', userSchema);
var u1 = new User({
firstname: 'Manish',
lastname: 'Prakash',
created_at: new Date(),
is_active: true
});
u1.save(function (err) {
if (err) {
console.log(err);
} else {
console.log('Done');
}
});
This is how it gets saved in mongodb, using all proper data types.
{
"_id": ObjectId("548d0b64fbcb585c0b8292c4"),
"firstname": "Manish",
"lastname": "Prakash",
"created_at": ISODate("2014-12-14T04:00:36.811Z"),
"is_active": true,
"__v": NumberInt(0)
}
Schema Validations
We can also extend schema for validation of our objects. I will expand our user schema to include more options
var allowedTypes = ['Level1′,'Level2′,'Level3'];
var userSchema = mongoose.Schema({
firstname: String,
lastname: String,
created_at: Date,
is_active: Boolean,
meta: {
friends: Number,
likes: Number,
votes: Number,
dislikes: Number
},
comments: [{body: String, date: Date}],
updated_at: {type: Date, default: Date.now},
age: {type: Number, min: 5, max: 40},
type: {type: String, enum: allowedTypes},
username: {type: String, lowercase: true, required: true, trim: true},
internal\_name: {type: String, match: /int\_/},
last\_payment\_date: {type: Date, default: Date.now, expires: 60 \* 60 \* 31}
});
var User = conn.model('User', userSchema);
var u1 = new User({
firstname: 'Manish',
lastname: 'Prakash',
created_at: new Date(),
is_active: true,
username : 'manish_iitg'
});
u1.save(function (err) {
if (err) {
console.log(err);
} else {
console.log('Done');
process.exit();
}
});
This is what we see in our database
{
"_id": ObjectId("548d0f141b41baf82259d815"),
"firstname": "Manish",
"lastname": "Prakash",
"created_at": ISODate("2014-12-14T04:16:20.542Z"),
"is_active": true,
"username": "manish_iitg",
"last\_payment\_date": ISODate("2014-12-14T04:16:20.544Z"),
"updated_at": ISODate("2014-12-14T04:16:20.543Z"),
"comments": [
],
"__v": NumberInt(0)
}
As you can see ‘last_payment_date’ and ‘updated_at’ have been filled up with default values.
If we try to save this model
var User = conn.model('User', userSchema);
var u1 = new User({
firstname: 'Manish',
lastname: 'Prakash',
created_at: new Date(),
is_active: true,
type : 'Invalid',
age : 2,
internal_name : 'xyz'
});
u1.save(function (err) {
if (err) {
console.log(err);
} else {
console.log('Done');
process.exit();
}
});
We get a bunch of error message due to the validation applied.
{ [ValidationError: User validation failed]
message: 'User validation failed',
name: 'ValidationError',
errors:
{ username:
{ [ValidatorError: Path \`username\` is required.]
properties: [Object],
message: 'Path \`username\` is required.',
name: 'ValidatorError',
kind: 'required',
path: 'username',
value: undefined },
internal_name:
{ [ValidatorError: Path \`internal_name\` is invalid (xyz).]
properties: [Object],
message: 'Path \`internal_name\` is invalid (xyz).',
name: 'ValidatorError',
kind: 'regexp',
path: 'internal_name',
value: 'xyz' },
age:
{ [ValidatorError: Path \`age\` (2) is less than minimum allowed value (5).]
properties: [Object],
message: 'Path \`age\` (2) is less than minimum allowed value (5).',
name: 'ValidatorError',
kind: 'min',
path: 'age',
value: 2 },
type:
{ [ValidatorError: \`Invalid\` is not a valid enum value for path \`type\`.]
properties: [Object],
message: '\`Invalid\` is not a valid enum value for path \`type\`.',
name: 'ValidatorError',
kind: 'enum',
path: 'type',
value: 'Invalid' } } }
We have also used another schema option ‘expires’ in ‘last_payment_date’, this basically sets the seconds after which a document should be deleted automatically by mongo. It works on version v2.4 and later. More details here http://docs.mongodb.org/manual/tutorial/expire-data/
There are also custom mongoose schema type which you can include in your collection like Email, URL more details here https://www.npmjs.com/package/openifyit-mongoose-types
Schema Custom Validation
Lets add a custom validation for 10 digit mobile number.
var allowedTypes = ['Level1′,'Level2′,'Level3'];
var userSchema = mongoose.Schema({
firstname: String,
lastname: String,
created_at: Date,
is_active: Boolean,
meta: {
friends: Number,
likes: Number,
votes: Number,
dislikes: Number
},
comments: [{body: String, date: Date}],
updated_at: {type: Date, default: Date.now},
age: {type: Number, min: 5, max: 40},
type: {type: String, enum: allowedTypes},
username: {type: String, lowercase: true, required: true, trim: true},
internal\_name: {type: String, match: /int\_/},
last\_payment\_date: {type: Date, default: Date.now, expires: 60 \* 60 \* 31},
phone: {
type: String,
required: true,
validate: {
validator: function(v) {
return /^[0-9]{10}$/.test(v);
},
message: '{VALUE} is not a valid phone number!'
}
},
});
var User = conn.model('User', userSchema);
var u1 = new User({
firstname: 'Manish',
lastname: 'Prakash',
created_at: new Date(),
is_active: true,
username : 'manish_iitg',
phone : '987654'
});
u1.save(function (err) {
if (err) {
console.log(err);
} else {
console.log('Done');
process.exit();
}
});
The above will throw a validation error. Since 10 digit phone number is required.
Below code with 10 digit phone number will work.
var u1 = new User({
firstname: 'Manish',
lastname: 'Prakash',
created_at: new Date(),
is_active: true,
username : 'manish_iitg',
phone : '9876543210'
});
Schema Instance Methods and Static Methods
We can set different functions to our Schema as static and instance methods. Static method apply to model directly and instance methods apply to model documents. Let see how it works
Instance Methods
userSchema.methods.isLevelAllowed = function (cb) {
var age = this.age;
var type = this.type;
var model = this.model('User');
//can perform any database operation on the model as well
if (age == 10 && (type == 'Level2' || type == 'Level3')) {
cb(false, false);
} else {
cb(false, true);
}
}
var User = conn.model('User', userSchema);
//make sure model is created after instance method is defined
var u1 = new User({
firstname: 'Manish',
lastname: 'Prakash',
created_at: new Date(),
is_active: true,
username: 'manish_iitg',
type: 'Level3',
age: 5,
internal\_name: 'int\_xetro',
meta: {
likes: 1
}
});
//isLevelAllowed is an instance method, hence used on a document.
u1.isLevelAllowed(function (err, allowed) {
if (allowed) {
console.log('Allowed');
} else {
console.log('Not Allowed');
}
process.exit();
});
Static Method
userSchema.statics.findAllByLevel = function (level, cb) {
this.find({
level: level
}, function (err, result) {
cb(err, result);
})
}
var User = conn.model('User', userSchema);
User.findAllByLevel('Level1', function (err, results) {
})
//static methods get directly called on model itself.
Schema Indexes
Mongodb also supports many indexes which we can apply using schemas
var userSchema = mongoose.Schema({
firstname: String,
lastname: String,
created_at: Date,
is_active: Boolean,
email : {type:String,required:true,index:{unique:true}}, //field level
meta: {
friends: Number,
likes: {type: Number, default: -1},
votes: Number,
dislikes: Number
},
comments: [{body: String, date: Date}],
updated_at: {type: Date, default: Date.now},
age: {type: Number, min: 5, max: 40},
type: {type: String, enum: allowedTypes},
username: {type: String, lowercase: true, required: true, trim: true},
internal\_name: {type: String, match: /int\_/},
last\_payment\_date: {type: Date, default: Date.now, expires: 60 \* 60 \* 31}
});
userSchema.index({firstname:-1,lastname:-1}); //schema level
If for some reason indexes are not getting created, restart mongodb server and check if index constraints are not failing already. Which means for index unique on email field, if you db already has duplicate emails indexes won’t work.
You can view indexes in rockmongo from DB -> Collection > More -> Indexes
Mongoose also tries to create indexes on each type application start, which causes overheads. To stop this do
userSchema.set('autoIndex',false);
There are few other schema options which can be seen here http://mongoosejs.com/docs/guide.html#autoIndex
Schema Middleware
Middleware are basically functions which are called during the flow of execution of a model. There are two types of middleware ‘pre’ and ‘post’ and 4 different execution flow methods init, validate, save and remove.
userSchema.pre('save', function (next) {
// do stuff
next();
});
if some async operation is required
userSchema.pre('save',true, function (next,done) {
// do stuff
next();
asyncOperation(done);
});
so model ‘save’ will only be called when all asyncOperations are done. We can also pass error objects to middleware
userSchema.pre('save', function (next) {
var err = new Error('something went wrong');
next(err);
});
More details can be seen here http://mongoosejs.com/docs/middleware.html
Schema Population
Mongodb doesn’t have a concept of joins, but when we need to store relationships we use multiple collection and ObjectIds for the same. Mongoose provides an easy way to load collections with relationships, if we define it in scheme.
I will create two schema to demonstrate the same
var userSchema = mongoose.Schema({
firstname: String,
lastname: String,
created_at: {type: Date, default: Date.now},
is_active: {type: Boolean, default: true},
email: {type: String, required: true, index: {unique: true}},
comments: [{type: Schema.Types.ObjectId, ref: 'Comment'}]
});
var User = conn.model('User', userSchema);
var commentSchema = mongoose.Schema({
text: String,
created_at: {type: Date, default: Date.now},
by: {type: Schema.Types.ObjectId, ref: 'User'}
});
var Comment = conn.model('Comment', commentSchema);
We use the ‘ref’ parameter to define relationships. With the code below i will insert data into database as per the relationship.
Till now everything is normal, we have inserted 2 users and 2 comments. Now we can use the ‘populate’ method to load relationship automatically e.g
User.find({
email: '[email protected]'
}).populate('comments').lean().exec(function (err, result) {
console.log(result);
});
This will load replace comment_ids with comment objects automatically. We can also add extra where condition in the populate function as below
User.find({
email: '[email protected]'
}).populate({
path: 'comments',
match: {text: /Comment/i}
}).lean().exec(function (err, result) {
console.log(result);
});
As we can easily populate comment_ids with comment objects, we can easily do the reverse.
var u1 = new User({
firstname: 'Manish',
lastname: 'Prakash',
email: '[email protected]',
comments: []
});
u1.comments.push(new Comment({
text: 'Text1'
}));
u1.comments.push(new Comment({
text: 'Text2'
}));
u1.save();
This will automatically replace ‘comments’ with ObjectIds.