An Introduction to Mongoose for MongoDB and Node.js
Mongoose is a JavaScript framework that is commonly used in a Node.js application with a MongoDB database. In this article, I am going to introduce you to Mongoose and MongoDB, and more importantly where these technologies fit in to your application.What Is MongoDB?
Let's start with MongoDB. MongoDB is a database that stores your data as documents. Most commonly these documents resemble a JSON-like structure:
1234{
firstName:
"Jamie"
,
lastName:
"Munro"
}
A document then is placed within a collection. As an example, the above document example defines auser
object. Thisuser
object then would typically be part of a collection calledusers
.One of the key factors with MongoDB is its flexibility when it comes to structure. Even though in the first example, theuser
object contained afirstName
andlastName
property, these properties are not required in everyuser
document that is part of theusers
collection. This is what makes MongoDB very different from a SQL database like MySQL or Microsoft SQL Server that requires a strongly-defined database schema of each object it stores.The ability to create dynamic objects that are stored as documents in the database is where Mongoose comes into play.What Is Mongoose?
Mongoose is an Object Document Mapper (ODM). This means that Mongoose allows you to define objects with a strongly-typed schema that is mapped to a MongoDB document.Mongoose provides an incredible amount of functionality around creating and working with schemas. Mongoose currently contains eight SchemaTypes that a property is saved as when it is persisted to MongoDB. They are:
- String
- Number
- Date
- Buffer
- Boolean
- Mixed
- ObjectId
- Array
Each data type allows you to specify:
- a default value
- a custom validation function
- indicate a field is required
- a get function that allows you to manipulate the data before it is returned as an object
- a set function that allows you to manipulate the data before it is saved to the database
- create indexes to allow data to be fetched faster
Further to these common options, certain data types allow you to further customize how the data is stored and retrieved from the database. For example, aString
data type also allows you to specify the following additional options:
- convert it to lowercase
- convert it to uppercase
- trim data prior to saving
- a regular expression that can limit data allowed to be saved during the validation process
- an enum that can define a list of strings that are valid
TheNumber
andDate
properties both support specifying a minimum and maximum value that is allowed for that field.Most of the eight allowed data types should be quite familiar to you. However, there are several exceptions that may jump out to you, such asBuffer
,Mixed
,ObjectId
, andArray
.TheBuffer
data type allows you to save binary data. A common example of binary data would be an image or an encoded file, such as a PDF document.TheMixed
data type turns the property into an "anything goes" field. This field resembles how many developers may use MongoDB because there is no defined structure. Be wary of using this data type as it loses many of the great features that Mongoose provides, such as data validation and detecting entity changes to automatically know to update the property when saving.TheObjectId
data type commonly specifies a link to another document in your database. For example, if you had a collection of books and authors, the book document might contain anObjectId
property that refers to the specific author of the document.TheArray
data type allows you to store JavaScript-like arrays. With an Array data type, you can perform common JavaScript array operations on them, such as push, pop, shift, slice, etc.
Quick Recap
Before moving on and generating some code, I just wanted to recap what we just learned. MongoDB is a database that allows you to store documents with a dynamic structure. These documents are saved inside a collection.
Mongoose is a JavaScript library that allows you to define schemas with strongly typed data. Once a schema is defined, Mongoose lets you create a Model based on a specific schema. A Mongoose Model is then mapped to a MongoDB Document via the Model's schema definition.
Once you have defined your schemas and models, Mongoose contains many different functions that allow you to validate, save, delete, and query your data using common MongoDB functions. I'll talk about this more with the concrete code examples to follow.
Installing MongoDB
Before we can begin creating our Mongoose schemas and models, MongoDB must be installed and configured. I would suggest visiting MongoDB's Download page. There are several different options available to install. I have linked to the Community Server. This allows you to install a version specific to your operating system. MongoDB also offers an Enterprise Server and a cloud support installation. Since entire books could be written on installing, tuning, and monitoring MongoDB, I am going to stick with the Community Server.
Once you've downloaded and installed MongoDB for your operating system of choice, you will need to start the database. Rather than reinventing the wheel, I would suggest visiting MongoDB's documentation on how to install the MongoDB Community Edition.
I'll wait here while you configure MongoDB. When you're ready, we can move on to setting up Mongoose to connect to your newly installed MongoDB database.
Setting Up Mongoose
Mongoose is a JavaScript framework, and I am going to use it in a Node.js application. If you already have Node.js installed, you can move on to the next step. If you do not have Node.js installed, I suggest you begin by visiting the Node.js Download page and selecting the installer for your operating system.
With Node.js set up and ready to go, I am going to create a new application and then install the Mongoose NPM Package.
With a command prompt that is set to where you wish your application to be installed, you can run the following commands:
1
2
3
| mkdir mongoose_basics cd mongoose_basics npm init |
For the initialization of my application, I left everything as their default values. Now I'm going to install the mongoose package as follows:
1
| npm install mongoose --save |
With all the prerequisites configured, let's connect to a MongoDB database. I've placed the following code inside an index.js file because I chose that as the starting point for my application:
1
2
3
|
The first line of code includes the
mongoose
library. Next, I open a connection to a database that I've called mongoose_basics
using the connect
function.
The
connect
function accepts two other optional parameters. The second parameter is an object of options where you can define things like the username and password, if required. The third parameter, which can also be the second parameter if you have no options, is the callback function after attempting to connect. The callback function can be used in one of two ways:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
| mongoose.connect(uri, options, function (error) { // Check error in initial connection. There is no 2nd param to the callback. }); // Or using promises mongoose.connect(uri, options).then( () => { /** ready to use. The `mongoose.connect()` promise resolves to undefined. */ }, err => { /** handle initial connection error */ } ); |
To avoid a potential introduction to JavaScript Promises, I will use the first way. Below is an updated index.js file:
1
2
3
4
5
6
7
8
9
| var mongoose = require( 'mongoose' ); if (err) throw err; console.log( 'Successfully connected' ); }); |
If an error occurs when connecting to the database, the exception is thrown and all further processing is stopped. When no error occurs, I have logged a success message to the console.
Mongoose is now set up and connected to a database called
mongoose_basics
. My MongoDB connection is using no username, password, or custom port. If you need to set these options or any other option during connection, I suggest reviewing the Mongoose Documentation on connecting. The documentation provides detailed explanations of the many options available as well as how to create multiple connections, connection pooling, replicas, etc.
With a successful connection, let's move on to define a Mongoose Schema
Defining a Mongoose Schema
During the introduction, I showed a
user
object that contained two properties: firstName
and lastName
. In the following example, I've translated that document into a Mongoose Schema:
1
2
3
4
| var userSchema = mongoose.Schema({ firstName: String, lastName: String }); |
This is a very basic Schema that just contains two properties with no attributes associated with it. Let's expand upon this example by converting the first and last name properties to be child objects of a
name
property. The name
property will comprise both the first and last name. I'll also add a created
property that is of type Date
.
1
2
3
4
5
6
7
| var userSchema = mongoose.Schema({ name: { firstName: String, lastName: String }, created: Date }); |
As you can see, Mongoose allows me to create very flexible schemas with many different possible combinations of how I am able to organize my data.
In this next example, I am going to create two new schemas that will demonstrate how to create a relationship to another schema:
author
and book
. The book
schema will contain a reference to the author
schema.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
| var authorSchema = mongoose.Schema({ _id: mongoose.Schema.Types.ObjectId, name: { firstName: String, lastName: String }, biography: String, twitter: String, facebook: String, linkedin: String, profilePicture: Buffer, created: { type: Date, default : Date.now } }); |
Above is the
author
schema that expands upon the concepts of the user
schema that I created in the previous example. To link the Author and Book together, the first property of the author
schema is an _id
property that is an ObjectId
schema type. _id
is the common syntax for creating a primary key in Mongoose and MongoDB. Then, like the user
schema, I've defined a name
property containing the author's first and last name.
Expanding upon the
user
schema, the author
contains several other String
schema types. I've also added a Buffer
schema type that could hold the author's profile picture. The final property holds the created date of the author; however, you may notice it is created slightly differently because it has defined a default value of "now". When an author is persisted to the database, this property will be set to the current date/time.
To complete the schema examples, let's create a
book
schema that contains a reference to the author by using the ObjectId
schema type:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| var bookSchema = mongoose.Schema({ _id: mongoose.Schema.Types.ObjectId, title: String, summary: String, isbn: String, thumbnail: Buffer, author: { type: mongoose.Schema.Types.ObjectId, ref: 'Author' }, ratings: [ { summary: String, detail: String, numberOfStars: Number, created: { type: Date, default : Date.now } } ], created: { type: Date, default : Date.now } }); |
The
book
schema contains several properties of type String
. As mentioned above, it contains a reference to the author
schema. To further demonstrate the powerful schema definitions, the book
schema also contains an Array
of ratings
. Each rating consists of a summary
, detail
, numberOfStars
, and created
date property.
Mongoose allows you the flexibility to create schemas with references to other schemas or, as in the above example with the
ratings
property, it allows you to create an Array
of child properties that could be contained in a related schema (like book to author) or inline as in the above example (with book to a ratings Array
).Creating and Saving Mongoose Models
Since the
author
and book
schemas demonstrate Mongoose's schema flexibility, I am going to continue using those schemas and derive an Author
and Book
model from them.
1
2
3
| var Author = mongoose.model( 'Author' , authorSchema); var Book = mongoose.model( 'Book' , bookSchema); |
A Mongoose Model, when saved, creates a Document in MongoDB with the properties as defined by the schema it is derived from.
To demonstrate creating and saving an object, in this next example, I am going to create several objects: an
Author
Model and several Book
Models. Once created, these objects will be persisted to MongoDB using the save
method of the Model.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
| var jamieAuthor = new Author { _id: new mongoose.Types.ObjectId(), name: { firstName: 'Jamie' , lastName: 'Munro' }, biography: 'Jamie is the author of ASP.NET MVC 5 with Bootstrap and Knockout.js.' , }; jamieAuthor.save( function (err) { if (err) throw err; console.log( 'Author successfully saved.' ); var mvcBook = new Book { _id: new mongoose.Types.ObjectId(), title: 'ASP.NET MVC 5 with Bootstrap and Knockout.js' , author: jamieAuthor._id, ratings:[{ summary: 'Great read' }] }; mvcBook.save( function (err) { if (err) throw err; console.log( 'Book successfully saved.' ); }); var knockoutBook = new Book { _id: new mongoose.Types.ObjectId(), title: 'Knockout.js: Building Dynamic Client-Side Web Applications' , author: jamieAuthor._id }; knockoutBook.save( function (err) { if (err) throw err; console.log( 'Book successfully saved.' ); }); }); |
In the above example, I've shamelessly plugged a reference to my two most recent books. The example starts by creating and saving a
jamieObject
that is created from anAuthor
Model. Inside the save
function of the jamieObject
, if an error occurs, the application will output an exception. When the save is successful, inside the save
function, the two book objects are created and saved. Similar to the jamieObject
, if an error occurs when saving, an error is outputted; otherwise, a success message is outputted in the console.
To create the reference to the Author, the book objects both reference the
author
schema's _id
primary key in the author
property of the book
schema.Validating Data Before Saving
It's quite common for the data that will end up creating a model to be populated by a form on a webpage. Because of this, it's a good idea to validate this data prior to saving the Model to MongoDB.
In this next example, I've updated the previous author schema to add validation on the following properties:
firstName
, twitter
, facebook
, and linkedin
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
| var authorSchema = mongoose.Schema({ _id: mongoose.Schema.Types.ObjectId, name: { firstName: { type: String, required: true }, lastName: String }, biography: String, twitter: { type: String, validate: { validator: function (text) { }, } }, facebook: { type: String, validate: { validator: function (text) { }, } }, linkedin: { type: String, validate: { validator: function (text) { }, } }, profilePicture: Buffer, created: { type: Date, default : Date.now } }); |
The
firstName
property has been attributed with the required
property. Now when I call the save
function, Mongoose will return an error with a message indicating the firstName
property is required. I chose not to make the lastName
property required in case Cher or Madonna were to be authors in my database.
The
twitter
, facebook
, and linkedin
properties all have very similar custom validators applied to them. They each ensure that the values begin with the social networks' respective domain name. These fields are not required, so the validator will only be applied when data is supplied for that property.
Comments
Post a Comment