MongoDB is one of the most popular NoSQL databases in the world. While it is easy to use and understand compared to SQL databases, it is a schemaless database.
What is a schema?
A "Schema" for a database can be compared with the "Class" in object-oriented programming. Just like a class that provides a blueprint for creating objects in a program, a schema provides a blueprint for creating objects (called documents in MongoDB) in a database.
In a Node.js application, we can use the Mongoose object data modeling (ODM) tool to define a schema for a MongoDB collection. A Mongoose schema defines the document structure, document properties, default values, validators, static methods, virtuals, etc.
Installing Mongoose
Before we get started with Mongoose schemas, make sure that you have already installed and configured both Node.js and MongoDB (instructions for Mac and Ubuntu).
You can then install Mongoose in your Node.js project with the following command:
$ npm install mongoose --save
Now you can require Mongoose in your application:
const mongoose = require('mongoose')
Take a look at this article to learn more about getting started with Mongoose in Node.js.
Defining a schema
Everything in Mongoose starts with defining a schema. It maps directly to a MongoDB collection and defines the document structure within that collection.
Let us look at the below example that defines a schema for the blog post:
const mongoose = require('mongoose')
const { Schema } = mongoose
const postSchema = new Schema({
title: String,
author: String,
body: String,
comments: [{ body: String, createdAt: Date }],
published: {
type: Boolean,
default: false
},
createdAt: {
type: Date,
default: Date.now
},
meta: {
upvotes: Number,
bookmarks: Number
}
})
Each property defined in the above blogSchema
is cast to its associated schema type before storing it in the document. For example, the property author
will be cast to the String
data type, and the property createdAt
will be cast to a Date
schema type.
Notice above that only the schema type is required for each property. It can be defined using a shorthand notation (without the type
attribute) or an object notation. If you choose to use the object notation, you must define the type
attribute.
Mongoose allows the following schema types:
- String
- Number
- Date
- Buffer
- Boolean
- Mixed
- ObjectId
- Array
- Decimal128
- Map
A key in a Mongoose schema can also be assigned a nested schema containing its own keys and types, like the meta
property above. It can even be an array of a nested schema like the comments
property.
Creating a model
A Mongoose model is a compiled version of the schema definition that maps directly to a single document in the collection.
To create a model in Mongoose, you use the mongoose.model()
method with the model name as the first parameter and the schema definition as the second parameter:
const Post = mongoose.model('Post', postSchema)
Now the Post
model is ready for querying, creating, updating, and removing documents from the posts
collection in MongoDB:
const post = new Post({
title: 'Mongoose Introduction',
author: 'Atta'
})
post.title // Mongoose Introduction
post.author // Atta
post.published // false
Overriding the _id
property
By default, Mongoose automatically adds the _id
property to every schema definition:
const user = new Schema({ name: String })
user.path('_id') // ObjectId { path: '_id', instance: 'ObjectID', ...}
When you create a new document, a unique value is auto-generated by Mongoose and saved in the _id
property.
However, you can override Mongoose's default _id
with your own _id
property:
const birdSchema = new Schema({ _id: Number, name: String })
const Bird = mongoose.model('Bird', birdSchema)
Make sure you assign a value to _id
when saving the document. Otherwise, Mongoose will refuse to save the document and throw an error:
const bird = new Bird({ name: 'Sparrow' })
await bird.save() // Throws "document must have an _id before saving"
// Set `_id` field before calling `save()`
bird._id = 45
await bird.save()
Validation
Mongoose allows you to define validation constraints in your schema. For example, let us say you want to make sure that every employee must have a name
property and a unique ssn
property in the employees
collection. You can make the name
field required
, and the ssn
filed unique
as shown below:
const empSchema = new Schema({
name: {
type: String,
required: true
},
ssn: {
type: String,
unique: true
},
age: Number
})
const Employee = mongoose.model('Employee', empSchema)
const emp = new Employee({ age: 45 })
await emp.save() // Throws "Path `name` is required."
Instance methods
Each instance of a Mongoose model is called a document. Documents have their own built-in instance methods. You can also define your own custom instance methods:
const foodSchema = new Schema({
name: String,
type: String
})
// Define custom method
foodSchema.methods.similarFoods = function (cb) {
return mongoose.model('Food').find({ type: this.type }, cb)
}
const Food = mongoose.model('Food', foodSchema)
Now each instance of Food
will have access to the similarFoods()
method:
const food = new Food({ name: 'Pizza', type: 'Fast Food' })
food.similarFoods((err, foods) => {
console.log(foods)
})
Static methods
You can also add static methods to your model by using the statics
field:
// Define static method
foodSchema.statics.findByName = function (name) {
return this.find({ name: new RegExp(name, 'i') })
}
const Food = mongoose.model('Food', foodSchema)
const foods = await Food.findByName('Pizza')
Query helpers
Query helpers are like instance methods but only for Mongoose queries. You can define query helper functions to extend the Mongoose query building API:
// Define query helper
foodSchema.query.byName = function (name) {
return this.where({ name: new RegExp(name, 'i') })
}
const Food = mongoose.model('Food', foodSchema)
Food.find()
.byName('burger')
.exec((err, foods) => {
console.log(foods)
})
Indexes
With Mongoose, you can define indexes within your schema at the path level or the schema level. Schema-level indexes are usually required when you create compound indexes:
const postSchema = new Schema({
title: String,
author: String,
body: String,
tags: {
type: [String],
index: true // Path level index
}
})
// Schema level compound index
postSchema.index({ name: 1, author: -1 })
Virtuals
Virtuals are document properties that you can get and set, but they do not get persisted to MongoDB. These properties are commonly used for formatting or combining fields as well as for splitting a single value into multiple values for storage.
Let us look at the following user's schema:
const userSchema = new Schema({
name: {
first: String,
last: String
},
age: Number
})
const User = mongoose.model('User', userSchema)
const john = new User({
name: { first: 'John', last: 'Doe' },
age: 32
})
console.log(`${john.name.first} ${john.name.last}`) // John Doe
Instead of concatenating first
and last
names every time, we can want to create a virtual property called fullName
that returns the full name:
userSchema.virtual('fullName').get(function () {
return `${this.name.first} ${this.name.last}`
})
Now whenever you access the fullName
property, Mongoose will call your virtual method:
console.log(john.fullName) // John Doe
Take a look at this article to learn more about Mongoose virtuals.
Aliases
Mongoose uses aliases to get and set another property value while saving the network bandwidth. It allows you to convert a short name of the property stored in MongoDB into a longer name for code readability:
const userSchema = new Schema({
n: {
type: String,
alias: 'name'
}
})
const User = mongoose.model('User', userSchema)
const user = new User({ name: 'Alex' })
console.log(user) // { n: 'Alex' }
console.log(user.n) // Alex
user.name = 'John'
console.log(user.n) // John
Options
The Schema
constructor takes a second options
parameter that you can use to configure the schema. For example, we can disable the _id
attribute completely using the { _id: false }
schema option:
const userSchema = new Schema(
{
name: String
},
{ _id: false }
)
const User = mongoose.model('User', userSchema)
const user = new User({ name: 'Atta' })
console.log(user) // { name: 'Atta' }
You can find a complete list of schema options on the Mongoose documentation.
✌️ Like this article? Follow me on Twitter and LinkedIn. You can also subscribe to RSS Feed.