7 Simple MongoDB/Mongoose Tips Make Your Code Faster

Learn How To Power Up Your MongoDB Or Mongoose With Few Tricks.

Kush Savani
JavaScript in Plain English

--

Last year, somewhere around Christmas, I started my journey as a NodeJS developer with MongoDB. I know in starting. I made many rookie mistakes and spend plenty of hours in StackOverflow and GitHub to find an appropriate solution. After several exhausting experiences. I find small but very useful tricks in MongoDB or mongoose.

1. lean()

When you execute any query in mongoose before the result, mongoose performs hydrate() a model function, which is used to create a new document from existing raw data, pre-saved in the DB. The returned document Is an instance of Mongoose Document class which is much heavy because they have a lot of internal state for change tracking. lean() create a shortcut from thehydrate()function and make queries faster and less memory-intensive but the return documents are plain old JavaScript objects (POJOs) not mongoose documents.

// Module that estimates the size of an object in memory
const sizeof = require('object-sizeof');

const normalDoc = await MyModel.findOne();
// To enable the `lean` option for a query, use the `lean()` function.
const leanDoc = await MyModel.findOne().lean();

sizeof(normalDoc); // >= 1000
sizeof(leanDoc); // 86, 10x smaller!

2. Virtuals Property

Some of us are already familiar with mongoose virtual property. But here some hidden behavior of virtual. Basically, virtual is just the computed property of MongoDB documents that are not stored in MongoDB.

Suppose you have two string properties: firstName and lastName. You can create a virtual property fullName that lets you set both of these properties at once.

const userSchema = mongoose.Schema({
firstName: String,
lastName: String
});
// Create a virtual property `fullName`.
userSchema.virtual('fullName').get(function() {
return `${this.firstName} $(this.lastName}`;
});
const User = mongoose.model('User', userSchema);

let doc = await User.create({ firstName: 'Tim', lastName: 'Burton' });
// `fullName` is now a property on documents.
doc.fullName; // 'Tim Burton'

Mongoose does not include virtuals when you convert a document to JSON or POJO. That means no virtual if you use .lean(). For example, if you pass a document to the Express res.json() function, virtuals will not be included by default. If you want to get virtual in res.json() just set toJSON schema option to {virtuals: true} .

const userSchema = mongoose.Schema({
firstName: String,
lastName: String
}, { toJSON: {virtuals: true} });

3. Indexes()

I was surprised when I read about MongoDB index functionality. You can compare the MongoDB index with the SQL indexes both are almost the same. We can define these indexes within our schema at the path level or the schema level. Defining indexes at the schema level is necessary when creating compound indexes.

Indexes support the efficient execution of queries in MongoDB. Without indexes, MongoDB must perform a collection scan, i.e. scan every document in a collection, to select those documents that match the query statement. If an appropriate index exists for a query, MongoDB can use the index to limit the number of documents it must inspect.

const userSchema= new Schema({
name: String,
email: { type: [String], index: true }
// field level
});

userSchema.index({ email: 1, name: -1 });
// schema level

Note: indexes use a combination of memory and temporary files on disk to complete index builds, default limit on memory usage is 200 megabytes (for versions 4.2.3 and later) and 500 (for versions 4.2.2 and earlier). You can override the memory limit by setting the maxIndexBuildMemoryUsageMegabytes server parameter.

4. sort()

I know the sort() function is pretty common in MongoDB, but here I’m talking about give some extra power to the sort() function. If you know that default sort() gives you results by sorting case-sensitively. I guess very few readers were known about collation the options in MongoDB. collation allows users to specify language-specific rules for string comparison, such as rules for letter case and accent marks.

User.find().collation({locale:'en',strength: 1}).sort({username:1})
.then( (users) =>{
//do your stuff
}); // sort by username without case sensitivity.
UserSchema.index(
{username:1},
{collation: { locale: 'en', strength: 1}}
);
// index on username without case sensitivity.

Here, locale: 'en' show that the collection is in English, and strength: 1 show that collation performs comparisons of the base characters only, ignoring other differences such as diacritics and case. Also, collation have a few more options that you can explore.

5. Instance methods

In MongoDB, documents are basically small instances of a real model. MongoDB is rich in build-in instance methods. Also, MongoDB provides custom document instance methods. These methods will have access to the model object and they can be used quite creatively.

// Define `getFullName` instance method.
userSchema.methods.getFullName = function() {
return 'Mr.' + this.firstName+ ' ' + this.lastName
}
// This method accessible via a model instance.
let model = new UserModel({
firstName: 'Thomas',
lastName: 'Anderson'
})
let initials = model.getFullName();
console.log(initials)
// This will output: Mr. Thomas Anderson

Note: Do not declare methods using ES6 arrow functions (=>). Arrow functions explicitly prevent binding this. So, your method will not have access to the document and the above examples will not work.

6. Static Methods

Static methods are similar to instance methods, but the difference is just statics are the methods defined on the model, On the other hand, methods are defined on the document (instance). statics keyword defines the static method. Let’s create a getAllUser static method to get all user data from the database.

userSchema.statics.getAllUsers = function() {
return new Promise((resolve, reject) => {
this.find((err, docs) => {
if(err) {
console.error(err)
return reject(err)
}
resolve(docs)
})
})
}

With getAllUsers statics, the model class returns all the user data from the database by calling these statics.

UserModel.getAllUsers()
.then(docs => {
console.log(docs)
})
.catch(err => {
console.error(err)
})

In my suggestion, you should usestatics methods instead of repeating the same query multiple times. Adding instance and static methods is a nice approach to implement an interface to database interactions on collections and records.

7. Middleware

Middleware is used to manipulate a specific stage of a pipeline. Mongoose has 4 types of middleware: document middleware, model middleware, aggregate middleware, and query middleware.

For instance, models have pre and post functions that take two parameters:

  1. Type of event (‘init’, ‘validate’, ‘save’, ‘remove’)
  2. A callback that is executed with this, by referencing the model instance.

Let’s create userSchemawith email and password.

const userSchema= new Schema({
email: {
type: String,
unique: true
// `email` must be unique
},
password: String
});

Pre Hook:

What if you want to save the password always in encrypt format. For that, one of the solution is you have to manually encrypt the password before saving, and another solution is mongoose helps you to encrypt your password field before going to store in a database. pre the hook will handle your middleware logic.

userSchema.pre('save', function (next) {
this.password =
hashPassword(this.password); // Replace with encrypted password
// Call the next function in the pre-save chain
next();
})

If any pre-hook errors out, mongoose will not execute subsequent middleware or the hooked function. Mongoose will instead pass an error to the callback and/or reject the returned promise.

Post Hook

Post hook middleware run between database and query response. It will help you to manipulate your query result before sending it to the endpoint. Let’s take one more example. Our userSchema says that the email field is unique. But when you try to save that email that Is already stored in the database, MongoDB sends an error, and instantly your node server crash. You have to restart the node application again.

For saving your node server from crashing, the post hook takes the responsibility of the error handling and tries to keep the running server.

userSchema.post('save', function(error, doc, next) {
if (error.name === 'MongoError' && error.code === 11000) {
next(new Error('There was a duplicate key error'));
} else {
next();
}
});
Photo by Nick Fewings on Unsplash

Conclusion

Now, I’m sure that now no one can underestimate your raw skill in MongoDB, and never try to call you a rookie in this field. I just try to cover those functionalities which will handy to you and make your code faster and more optimized. We have barely scratched the surface exploring some of the capabilities of Mongoose. It is a rich library full of useful and powerful features. Have fun coding.

--

--