Now that you understand the native driver, it won’t be hard to make the jump to using Mongoose. Mongoose is an Object Document Model (ODM) library that provides additional functionality to the MongoDB Node.js native driver. For the most part, it is used as a way to apply a structured schema to a MongoDB collection. This provides the benefits of validation and type casting.
Mongoose also attempts to simplify some of the complexities of making database calls by implementing builder objects that allow you to pipe additional commands into find, update, save, remove, aggregate, and other database operations. This can make it easier to implement your code.
This chapter discusses the Mongoose module and how to use it to implement a structured schema and validation on your collections. You are introduced to new objects and a new way of implementing MongoDB in your Node.js applications. Mongoose doesn’t replace the MongoDB Node.js native driver; instead, it enhances it with additional functionality.
Mongoose is an Object Document Model (ODM) library that wraps around the MongoDB Node.js driver. The main purpose is to provide a schema-based solution to model data stored in the MongoDB database.
The chief benefits of using Mongoose are:
You can create a schema structure for your documents.
Objects/documents in the model can be validated.
Application data can be typecast into the object model.
Business logic hooks can be applied using middleware.
Mongoose is in some ways easier to use than the MongoDB Node.js native driver.
However, there are some downsides to using Mongoose as well. Those drawbacks are:
You are required to provide a schema, which isn’t always the best option when MongoDB doesn’t require it.
It doesn’t perform certain operations, such as storing data, as well as the native driver does.
Mongoose sits on top of the MongoDB Node.js native driver and extends the functionality in a couple of different ways. It adds some new objects—Schema
, Model
, and Document
—that provide the functionality necessary to implement the ODM and validation.
The Schema
object defines the structured schema for documents in a collection. It allows you to define the fields and types to include, uniqueness, indexes, and validation. The Model
object acts as a representation of all documents in the collection. The Document
object acts as a representation of the individual document in a collection.
Mongoose also wraps the standard functionality used for implementing query and aggregation parameters into new objects, Query
and Aggregate
, which allow you to apply the parameters of database operations in a series of method calls before finally executing them. This can make it simpler to implement code as well as reuse instances of those object to perform multiple database operations.
Connecting to the MongoDB database using Mongoose is similar to using the connection string method discussed in Chapter 13, “Getting Started with MongoDB and Node.js.” It uses the same connection string format and options syntax shown below:
connect(uri, options, [callback])
The connect()
method is exported at the root level of the mongoose
module. For example, the following code connects to the words
database on the localhost
:
var mongoose = require('mongoose'); mongoose.connect('mongodb://localhost/words');
The connection can be closed using the disconnect()
method of the mongoose
module, for example:
mongoose.disconnect();
Once created, the underlying Connection
object can be accessed in the connection
attribute of the mongoose
module. The Connection
object provides access to the connection, underlying Db
object, and Model
object that represents the collection. This gives you access to all the Db
object functionality described in Chapter 13. For example, to list the collections on the database you could use the following code:
mongoose.connection.db.collectionNames(function(err, names){ console.log(names); });
The Connection
object emits the open
event that can be used to wait for the connection to open before trying to access the database. To illustrate the basic life cycle of a MongoDB connection via Mongoose, Listing 16.1 imports the mongoose
module, connects to the MongoDB database, waits for the open
event, and then displays the collections in the database and disconnects.
Listing 16.1 mongoose_connect.js
: Connecting to a MongoDB database by using Mongoose
1 var mongoose = require('mongoose'); 2 mongoose.connect('mongodb://localhost/words'); 3 mongoose.connection.on('open', function(){ 4 console.log(mongoose.connection.collection); 5 mongoose.connection.db.collectionNames(function(err, names){ 6 console.log(names); 7 mongoose.disconnect(); 8 }); 9 });
Listing 16.1 Output mongoose_connect.js
: Connecting to a MongoDB database by using Mongoose
[Function] [ Collection { s: { pkFactory: [Object], db: [Object], topology: [Object], dbName: 'words', options: [Object], namespace: 'words.word_stats', readPreference: null, slaveOk: false, serializeFunctions: undefined, raw: undefined, promoteLongs: undefined, promoteValues: undefined, promoteBuffers: undefined, internalHint: null, collectionHint: null, name: 'word_stats', promiseLibrary: [Function: Promise], readConcern: undefined } } ]
A fundamental requirement of using Mongoose is to implement a schema. The schema defines the fields and field types for documents in a collection. This can be useful if your data is structured in a way that supports a schema because you can validate and typecast objects to match the requirements of the schema.
For each field in the schema, you need to define a specific value type. The value types supported are:
String
Number
Boolean
or Bool
Array
Buffer
Date
ObjectId
or Oid
Mixed
A schema must be defined for each different document type that you plan to use. Also, you should only store one document type in each collection.
Mongoose uses the term path
to define access paths to fields in the main document as well as subdocuments. For example, if a document has a field named name
, which is a subdocument with title
, first
, and last
properties, the following are all paths:
name name.title name.first name.last
To define a schema for a model, you need to create a new instance of a Schema
object. The Schema
object definition
accepts an object describing the schema as the first parameter and an options
object as the second parameter:
new Schema(definition, options)
The options
object defines the interaction with the collection on the MongoDB server. The most commonly used options that can be specified are shown in Table 16.1.
Table 16.1 Options that can be specified when defining a Schema
object
Option |
Description |
|
A Boolean that, when |
|
A Boolean that, when |
|
Specifies the maximum number of documents supported in a capped collection. |
|
Specifies the collection name to use for this |
|
A Boolean that, when |
|
A Boolean that, when |
|
Specifies the replica read preferences. Value can be |
|
A Boolean; when |
|
A Boolean that, when |
For example, to create a schema for a collection called students
, with a name
field that is a String
type, an average
field that is a Number
type, and a scores
field that is an Array
of Number
types, you use:
var schema = new Schema({ name: String, average: Number, scores: [Number] }, {collection:'students'});
You might want to assign indexes to specific field that you frequently use to find documents. You can apply indexes to a schema object when defining the schema or using the index(fields)
command. For example, both of the following commands add an index
to the name
field in ascending order:
var schema = new Schema({ name: {type: String, index: 1} }); //or var schema = new Schema({name: String}); schema.index({name:1});
You can get a list of indexed fields on a schema object using the indexes()
method. For example:
schema.indexes()
You can also specify that the value of a field must be unique in the collection, meaning no other documents can have the same value for that field. This is done by adding the unique
property to the Schema
object definition. For example, to add an index
and make the name
field unique
in the collection, you use:
var schema = new Schema({ name: {type: String, index: 1, unique: true} });
You can also specify that a field must be included when creating a new instance of a Document
object for the model. By default, if you do not specify a field when creating a Document
instance, the object is created without one. For fields that must exist in your model, add the required property when defining the Schema
. For example, to add an index, ensure uniqueness, and force including the name
field in the collection, you use:
var schema = new Schema({ name: {type: String, index: 1, unique: true, required: true} });
You can get a list of required fields on a schema object using the requiredPaths()
method. For example:
schema.requiredPaths()
Mongoose schemas enables you to add methods to the Schema
object that are automatically available on document objects in the model. This allows you to call the methods using the Document
object.
Methods are added to the Schema
object by assigning a function to the Schema.methods
property. The function is just a standard JavaScript function assigned to the Document
object. The Document
object can be accessed using the this
keyword. For example, the following assigns a function named fullName
to a model that returns a combination of the first and last name.
var schema = new Schema({ first: String, last: String }); schema.methods.fullName = function(){ return this.first + " " + this.last; };
Listing 16.2 implements a schema on the word_stats
collection defined in Chapter 15, “Accessing MongoDB from Node.js.” This schema is used in other examples in this chapter, so it is exported in the final line of code. Notice that the word
and first
fields have an index
assigned to them and that the word
field is both unique
and required
.
For the stats
subdocument the document is defined as normal but with types specified in lines 9–11. Also notice that for the charsets
field, which is an array of subdocuments, the syntax defines an array and defines the single subdocument type for the model. In lines 13–15 a startsWith()
method is implemented that is available on Document
objects in the model. Listing 16.2 Output shows the required paths and indexes.
Listing 16.2 word_schema.js
: Defining the schema for the word_stats
collection
01 var mongoose = require('mongoose'); 02 var Schema = mongoose.Schema; 03 var wordSchema = new Schema({ 04 word: {type: String, index: 1, required:true, unique: true}, 05 first: {type: String, index: 1}, 06 last: String, 07 size: Number, 08 letters: [String], 09 stats: { 10 vowels:Number, consonants:Number}, 11 charsets: [Schema.Types.Mixed] 12 }, {collection: 'word_stats'}); 13 wordSchema.methods.startsWith = function(letter){ 14 return this.first === letter; 15 }; 16 exports.wordSchema = wordSchema; 17 console.log("Required Paths: "); 18 console.log(wordSchema.requiredPaths()); 19 console.log("Indexes: "); 20 console.log(wordSchema.indexes());
Listing 16.2 Output word_schema.js
: Defining the schema for the word_stats
collection
Required Paths: [ ‘word’ ] Indexes: [ [ { word: 1 }, { background: true } ], [ { first: 1 }, {background: true } ] ]
Once you have defined the Schema
object for your model, you need to compile it into a Model
object. When Mongoose compiles the model, it uses the connection to the MongoDB established by mongoose.connect()
and ensures that the collection is created and has the appropriate indexes, as well as required and unique settings when applying changes.
The compiled Model
object acts in much the same way as the Collection
object defined in Chapter 13. It provides the functionality to access, update, and remove objects in the model and subsequently in the MongoDB collection.
To compile the model, you use the model()
method in the mongoose
module. The model()
method uses the following syntax:
model(name, [schema], [collection], [skipInit])
The name
parameter is a string that can be used to find the model later using model(name)
. The schema
parameter is the Schema
object discussed in the previous section. The collection
parameter is the name of the collection to connect to if one is not specified in the Schema
object. The skipInit
option is a Boolean that defaults to false
. When true
, the initialization process is skipped and a simple Model
object with no connection to the database is created.
The following shows an example of compiling the model for the Schema
object defined in Listing 16.2:
var Words = mongoose.model('Words', wordSchema);
You can then access the compiled Model
object at any time using the following:
mongoose.model('Words')
Once you have the Schema
object compiled into a Model
object, you are completely ready to begin accessing, adding, updating, and deleting documents in the model, which makes the changes to the underlying MongoDB database. However, before you jump in, you need to understand the nature of the Query
object provided with Mongoose.
Many of the methods in the Model
object match those in the Collection
object defined in Chapter 13. For example, there are find()
, remove()
, update()
, count()
, distinct()
, and aggregate()
methods. The parameters for these methods are for the most part exactly the same as for the Collection
object with a major difference: the callback
parameter.
Using the Mongoose Model
object, you can either pass in the callback
function or omit it from the parameters of the method. If the callback
function is passed in, the methods behave as you would expect them to. The request is made to MongoDB, and the results returned in the callback
function.
However, if you do not pass in a callback
function, the actual MongoDB request is not sent. Instead, a Query
object is returned that allows you to add additional functionality to the request before executing it. Then, when you are ready to execute the database call, you use the exec(callback)
method on the Query
object.
The simplest way to explain this is to look at an example of a find()
request; using the same syntax as in the native driver is perfectly acceptable in Mongoose:
model.find({value:{$gt:5}},{sort:{[['value',-1]]}, fields:{name:1, title:1, value:1}}, function(err, results){});
However, using Mongoose, all the query
options can also be defined separately using the following code:
var query = model.find({}); query.where('value').lt(5); query.sort('-value'); query.select('name title value'); query.exec(function(err, results){});
The model.find()
call returns a Query
object instead of performing the find()
because no callback is specified. Notice that the query
properties and options
properties are broken out in subsequent method calls on the query
object. Then, once the query
object is fully built, the exec()
method is called and the callback
function is passed into that.
You can also string the query
object methods together, for example:
model.find({}).where('value').lt(5).sort('-value').select('name title value') .exec(function(err, results){});
When exec()
is called, the Mongoose library builds the necessary query
and options
parameters and then makes the native call to MongoDB. The results are returned in the callback
function.
Each Query
object must have a database operation associated with it. The database operation determines what action to take when connecting to the database, from finding documents to storing them. There are two ways to assign a database operation to a query object. One way is to call the operation from the Model
object and not specify a callback. The query
object returned has that operation assigned to it. For example:
var query = model.find();
Once you already have a Query
object, you can change the operation that is applied by calling the method on the Query
object. For example, the following code creates a Query
object with the count()
operation and then switches to a find()
operation:
var query = model.count(); query.where('value').lt(5); query.exec(function(){}); query.find(); query.exec(function(){});
This allows you to dynamically reuse the same Query
object to perform multiple database operations. Table 16.2 lists the operation methods that you can call on the Query
object. This is also the list of methods on a compiled Model
object that can return a Query
object by omitting the callback
function. Keep in mind that if you pass in a callback
function to any of these methods, the operation is executed and the callback called when finished.
Table 16.2 Methods available on the Query
and Model
objects to set the database operation
Method |
Description |
|
Inserts the objects specified in the function(err, doc1, doc2, doc3, …) |
|
Sets the operation to |
|
Sets the operation to |
|
Sets the operation to a |
|
Sets the operation to a |
|
Sets the operation to a |
Sets the operation to a |
|
|
Sets the operation to a |
|
Sets the operation to |
|
Applies one or more aggregate |
The Query
object also has methods that allow you to set the options such as limit
, skip
, and select
that define how the request is processed on the server. These can be set in the options
parameter of the methods listed in Table 16.2 or by calling the methods on the Query
object listed in Table 16.3.
Table 16.3 Methods available on the Query
and Model
objects to set the database operation options
Method |
Description |
|
Sets the options used to interact with MongoDB when performing the database request. See Table 15.2 for a description of the options that can be set. |
|
Sets the maximum |
|
Specifies the fields that should be included in each document of the result set. The select('name +title -value'); select({name:1, title:1, value:0); |
Specifies the sort('name -value'); sort({name:1, value:-1}) |
|
|
Specifies the |
|
Enables you to set the read |
|
Sets the query to a snapshot query when |
|
When set to |
|
Specifies the indexes to use or exclude when finding documents. Use a value of hint(name:1, title:-1); |
|
Adds the |
The Query
object also allows you to set the operators and values used to find the document that you want to apply the database operations to. These operators define this like “field values greater than a certain amount.” The operators all work off a path to the field. That path can be specified by the where()
method, or included in the operator method. If no operator method is specified, the last path passed to a where()
method is used.
For example, the gt()
operator below compares against the value field:
query.where('value').gt(5)
However, in the following statement, the lt()
operator compares against the score field:
query.where('value').gt(5).lt('score', 10);
Table 16.4 lists the most common methods that can be applied to the Query
object.
Table 16.4 Methods available on Query
objects to define the query operators
Method |
Description |
|
Sets the current field where(' |
Matches values that are greater than the value specified in the query. For example: gt('value', 5)gt(5) |
|
|
Matches fields that are equal to or greater than the |
|
Matches values that are less than the |
|
Matches fields that are less than or equal to the |
|
Matches all fields that are not equal to the |
|
Matches any of the values that exist in an in(' |
|
Matches values that do not exist in an |
|
Joins query clauses with a logical OR and returns all documents that match the or([{size:{$lt:5}},{size:{$gt:10}}]) |
|
Joins query clauses with a logical AND and returns all documents that match the and([{size:{$lt:10}},{size:{$gt:5}}]) |
|
Joins query clauses with a logical NOR and returns all documents that fail to match both nor([{size:{$lt:5}},{name:"myName"}]) |
|
Matches documents that have the specified field. For example: exists( |
|
Performs a modulo operation on the value of a field and selects documents that have the matching remainder. For example: mod('size', 2,0) |
|
Selects documents where values match a specified regular regex('myField', 'some.*exp') |
Matches array fields that contain all elements specified in the array all('myArr', ['one','two','three']) |
|
|
Selects documents if an element in the array of subdocuments has fields that match all the specified elemMatch('item', {{value:5},size:{$lt:3}}) elemMatch('item', function(elem){ elem.where('value', 5); elem.where('size').gt(3); }) |
|
Selects documents if the array field is a specified size. For example: size('myArr', 5) |
When you use the Model
object to retrieve documents from the database, the documents are presented in the callback function as Mongoose Document
objects. Document
objects inherit from the Model
class and represent the actual document in the collection. The Document
object allows you to interact with the document from the perspective of your schema model by providing a number of methods and extra properties that support validation and modifications.
Table 16.5 lists the most useful methods and properties on the Document
object.
Table 16.5 Methods and properties available on document
objects
Method/Property |
Description |
|
Returns |
|
Contains the |
|
Returns the value of the specified |
|
Sets the |
|
Updates the document in the MongoDB database. The |
Saves changes that have been made to the |
|
|
Removes the |
|
A Boolean that, if |
|
Returns |
|
Returns |
|
Returns |
|
Marks the |
|
Returns an array of paths in the object that have been modified. |
|
Returns a JSON string representation of the |
|
Returns a normal JavaScript object without the extra properties and methods of the |
|
Returns a string representation of the |
|
Performs a validation on the |
|
Marks the |
|
Contains a list of errors in the document. |
|
Links to the |
Finding documents using the mongoose
module is similar in some ways to using the MongoDB Node.js native driver and yet different in other ways. The concepts of logic operators, limit, skip, and distinct are all the same. However, there are two big differences.
The first major difference is that when using Mongoose, the statements used to build the request can be piped together and reused because of the Query
object discussed earlier in this chapter. This allows Mongoose code to be much more dynamic and flexible when defining what documents to return and how to return them.
For example, all three of the following queries are identical, just built in different ways:
var query1 = model.find({name:'test'}, {limit:10, skip:5, fields:{name:1,value:1}}); var query2 = model.find().where('name','test').limit(10).skip(5). select({name:1,value:1}); var query3 = model.find(). query3.where('name','test') query3.limit(10).skip(5); query3.select({name:1,value:1});
A good rule to follow when building your query object using Mongoose is to only add things as you need them in your code.
The second major difference is that MongoDB operations such as find()
and findOne()
return Document
objects instead of JavaScript objects. Specifically, find()
returns an array of Document
objects instead of a Cursor
object, and findOne()
returns a single Document
object. The Document
objects allow you to perform the operations listed in Table 16.5.
Listing 16.3 illustrates several examples of implementing the Mongoose way of retrieving objects from the database. Lines 9–14 count the number of words that begin and end with a vowel. Then in line 15 the same query object is changed to a find()
operation, and a limit()
and sort()
are added before executing it in line 16.
Lines 22–32 use mod()
to find words with an even length and greater than six characters. Also the output is limited to ten documents, and each document returns only the word
and size
fields.
Listing 16.3 mongoose_find.js
: Finding documents in a collection by using Mongoose
01 var mongoose = require('mongoose'); 02 var db = mongoose.connect('mongodb://localhost/words'); 03 var wordSchema = require('./word_schema.js').wordSchema; 04 var Words = mongoose.model('Words', wordSchema); 05 setTimeout(function(){ 06 mongoose.disconnect(); 07 }, 3000); 08 mongoose.connection.once('open', function(){ 09 var query = Words.count().where('first').in(['a', 'e', 'i', 'o', 'u']); 10 query.where('last').in(['a', 'e', 'i', 'o', 'u']); 11 query.exec(function(err, count){ 12 console.log(" There are " + count + 13 " words that start and end with a vowel"); 14 }); 15 query.find().limit(5).sort({size:-1}); 16 query.exec(function(err, docs){ 17 console.log(" Longest 5 words that start and end with a vowel: "); 18 for (var i in docs){ 19 console.log(docs[i].word); 20 } 21 }); 22 query = Words.find(); 23 query.mod('size',2,0); 24 query.where('size').gt(6); 25 query.limit(10); 26 query.select({word:1, size:1}); 27 query.exec(function(err, docs){ 28 console.log(" Words with even lengths and longer than 5 letters: "); 29 for (var i in docs){ 30 console.log(JSON.stringify(docs[i])); 31 } 32 }); 33 });
Listing 16.3 Output mongoose_find.js
: Finding documents in a collection by using Mongoose
There are 5 words that start and end with a vowel Words with even lengths and longer than 5 letters: {"_id":"598e0ebd0850b51290642f8e","word":"american","size":8} {"_id":"598e0ebd0850b51290642f9e","word":"question","size":8} {"_id":"598e0ebd0850b51290642fa1","word":"government","size":10} {"_id":"598e0ebd0850b51290642fbe","word":"national","size":8} {"_id":"598e0ebd0850b51290642fcc","word":"business","size":8} {"_id":"598e0ebd0850b51290642ff9","word":"continue","size":8} {"_id":"598e0ebd0850b51290643012","word":"understand","size":10} {"_id":"598e0ebd0850b51290643015","word":"together","size":8} {"_id":"598e0ebd0850b5129064301a","word":"anything","size":8} {"_id":"598e0ebd0850b51290643037","word":"research","size":8} Longest 5 words that start and end with a vowel: administrative infrastructure intelligence independence architecture
Documents can be added to the MongoDB library using either the create()
method on the Model
object or the save()
method on a newly created Document
object. The create()
method accepts an array of JavaScript objects and creates a Document
instance for each JavaScript object, which applies validation and a middleware framework to them. Then the Document
objects are saved to the database.
The syntax of the create()
method is shown below:
create(objects, [callback])
The callback
function of the create
method receives an error
for the first parameter if it occurs and then additional parameters, one for each document. Lines 27–32 of Listing 16.4 illustrate using the create()
method and handling the saved documents coming back. Notice that the create()
method is called on the Model
object Words
and that the arguments are iterated on to display the created documents, as shown in Listing 16.4 Output.
The save()
method is called on a Document
object that has already been created. It can be called even if the document has not yet been created in the MongoDB database, in which case the new document is inserted. The syntax for the save()
method is
save([callback])
Listing 16.4 also illustrates the save()
method of adding documents to a collection using Mongoose. Notice that a new Document
instance is created in lines 6–11 and that the save()
method is called on that document instance.
Listing 16.4 mongoose_create.js:
Creating new documents in a collection by using Mongoose
01 var mongoose = require('mongoose'); 02 var db = mongoose.connect('mongodb://localhost/words'); 03 var wordSchema = require('./word_schema.js').wordSchema; 04 var Words = mongoose.model('Words', wordSchema); 05 mongoose.connection.once('open', function(){ 06 var newWord1 = new Words({ 07 word:'gratifaction', 08 first:'g', last:'n', size:12, 09 letters: ['g','r','a','t','i','f','c','o','n'], 10 stats: {vowels:5, consonants:7} 11 }); 12 console.log("Is Document New? " + newWord1.isNew); 13 newWord1.save(function(err, doc){ 14 console.log(" Saved document: " + doc); 15 }); 16 var newWord2 = { word:'googled', 17 first:'g', last:'d', size:7, 18 letters: ['g','o','l','e','d'], 19 stats: {vowels:3, consonants:4} 20 }; 21 var newWord3 = { 22 word:'selfie', 23 first:'s', last:'e', size:6, 24 letters: ['s','e','l','f','i'], 25 stats: {vowels:3, consonants:3} 26 }; 27 Words.create([newWord2, newWord3], function(err){ 28 for(var i=1; i<arguments.length; i++){ 29 console.log(" Created document: " + arguments[i]); 30 } 31 mongoose.disconnect(); 32 }); 33 });
Listing 16.4 Output mongoose_create.js:
Creating new documents in a collection by using Mongoose
Is Document New? True Saved document: { __v: 0, word: 'gratifaction', first: 'g', last: 'n', size: 12, _id: 598e10192e335a163443ec13, charsets: [], stats: { vowels: 5, consonants: 7 }, letters: [ 'g', 'r', 'a', 't', 'i', 'f', 'c', 'o', 'n' ] } Created document: { __v: 0, word: 'googled', first: 'g', last: 'd', size: 7, _id: 598e10192e335a163443ec14, charsets: [], stats: { vowels: 3, consonants: 4 }, letters: [ 'g', 'o', 'l', 'e', 'd' ] },{ __v: 0, word: 'selfie', first: 's', last: 'e', size: 6, _id: 598e10192e335a163443ec15, charsets: [], stats: { vowels: 3, consonants: 3 }, letters: [ 's', 'e', 'l', 'f', 'i' ] }
There are several methods for updating documents when using Mongoose. Which one you use depends on the nature of your application. One method is simply to call the save()
function described in the previous section. The save()
method can be called on objects already created in the database.
The other way is to use the update()
method on either the Document
object for a single update or on the Model
object to update multiple documents in the model. The advantages of the update()
method are that it can be applied to multiple objects and provides better performance. The following sections describe these methods.
You have already seen how to use the save()
method to add a new document to the database. You can also use it to update an existing object. Often the save()
method is the most convenient to use when working with MongoDB because you already have an instance of the Document
object.
The save()
method detects whether the object is new, determines which fields have changed, and then builds a database request that updates those fields in the database. Listing 16.5 illustrates implementing a save()
request. The word book
is retrieved from the database, and the first letter is capitalized, changing the word and first fields.
Notice that doc.isNew
in line 8 reports that the document is not new. Also, in line 14 the modified fields are reported to the console using doc.modifiedFields()
. These are the fields that are updated.
Listing 16.5 mongoose_save.js
: Saving documents in a collection by using Mongoose
01 var mongoose = require('mongoose'); 02 var db = mongoose.connect('mongodb://localhost/words'); 03 var wordSchema = require('./word_schema.js').wordSchema; 04 var Words = mongoose.model('Words', wordSchema); 05 mongoose.connection.once('open', function(){ 06 var query = Words.findOne().where('word', 'book'); 07 query.exec(function(err, doc){ 08 console.log("Is Document New? " + doc.isNew); 09 console.log(" Before Save: "); 10 console.log(doc.toJSON()); 11 doc.set('word','Book'); 12 doc.set('first','B'); 13 console.log(" Modified Fields: "); 14 console.log(doc.modifiedPaths()); 15 doc.save(function(err){ 16 Words.findOne({word:'Book'}, function(err, doc){ 17 console.log(" After Save: "); 18 console.log(doc.toJSON()); 19 mongoose.disconnect(); 20 }); 21 }); 22 }); 23 });
Listing 16.5 Output mongoose_save.js
: Saving documents in a collection by using Mongoose
Is Document New? false Before Save: { _id: 598e0ebd0850b51290642fc7, word: 'book', first: 'b', last: 'k', size: 4, charsets: [ { chars: [Object], type: 'consonants' }, { chars: [Object], type: 'vowels' } ], stats: { vowels: 2, consonants: 2 }, letters: [ 'b', 'o', 'k' ] } Modified Fields: [ 'word', 'first' ] After Save: { _id: 598e0ebd0850b51290642fc7, word: 'Book', first: 'B', last: 'k', size: 4, charsets: [ { chars: [Object], type: 'consonants' }, { chars: [Object], type: 'vowels' } ], stats: { vowels: 2, consonants: 2 }, letters: [ 'b', 'o', 'k' ] }
The Document
object also provides the Update()
method that enables you to update a single document using the update operators described in Table 14.2. The syntax for the update()
method on Document
objects is shown below:
update(update, [options], [callback])
The update
parameter is an object that defines the update operation to perform on the document. The options
parameter specifies the write preferences, and the callback function accepts an error
as the first argument and the number
of documents updated as the second.
Listing 16.6 shows an example of using the update()
method to update the word gratifaction
to gratifactions
by setting the word
, size
, and last
fields using a $set
operator as well as pushing the letter s
on the end of letters
using the $push
operator.
Listing 16.6 mongoose_update_one.js
: Updating a single document in a collection by using Mongoose
01 var mongoose = require('mongoose'); 02 var db = mongoose.connect('mongodb://localhost/words'); 03 var wordSchema = require('./word_schema.js').wordSchema; 04 var Words = mongoose.model('Words', wordSchema); 05 mongoose.connection.once('open', function(){ 06 var query = Words.findOne().where('word', 'gratifaction'); 07 query.exec(function(err, doc){ 08 console.log("Before Update: "); 09 console.log(doc.toString()); 10 var query = doc.update({$set:{word:'gratifactions', 11 size:13, last:'s'}, 12 $push:{letters:'s'}}); 13 query.exec(function(err, results){ 14 console.log(" %d Documents updated", results); 15 Words.findOne({word:'gratifactions'}, function(err, doc){ 16 console.log(" After Update: "); 17 console.log(doc.toString()); 18 mongoose.disconnect(); 19 }); 20 }); 21 }); 22 });
Listing 16.6 Output mongoose_update_one.js
: Updating a single document in a collection by using Mongoose
Before Update: { _id: 598e10192e335a163443ec13, word: 'gratifaction', first: 'g', last: 'n', size: 12, __v: 0, charsets: [], stats: { vowels: 5, consonants: 7 }, letters: [ 'g', 'r', 'a', 't', 'i', 'f', 'c', 'o', 'n' ] } NaN Documents updated After Update: { _id: 598e10192e335a163443ec13, word: 'gratifactions', first: 'g', last: 's', size: 13, __v: 0, charsets: [], stats: { vowels: 5, consonants: 7 }, letters: [ 'g', 'r', 'a', 't', 'i', 'f', 'c', 'o', 'n', 's' ] }
The Model
object also provides an Update()
method that allows you to update multiple documents in a collection using the update
operators described in Table 14.2. The syntax for the update()
method on Model
objects is slightly different, as shown below:
update(query, update, [options], [callback])
The query
parameter defines the query used to identify which objects to update. The update
parameter is an object that defines the update operation to perform on the document. The options
parameter specifies the write preferences, and the callback
function accepts an error
as the first argument and the number
of documents updated as the second.
A nice thing about updating at the Model
level is that you can use the Query
object to define which objects should be updated. Listing 16.7 shows an example of using the update()
method to update the size
field of words that match the regex /grati.*/
to 0
. Notice that an update
object is defined in line 11; however, multiple query options are piped onto the Query
object before executing it in line 14. Then another find()
request is made, this time using the regex /grat.*/
to show that only those matching the update query actually change.
Listing 16.7 mongoose_update_many.js
: Updating multiple documents in a collection by using Mongoose
01 var mongoose = require('mongoose'); 02 var db = mongoose.connect('mongodb://localhost/words'); 03 var wordSchema = require('./word_schema.js').wordSchema; 04 var Words = mongoose.model('Words', wordSchema); 05 mongoose.connection.once('open', function(){ 06 Words.find({word:/grati.*/}, function(err, docs){ 07 console.log("Before update: "); 08 for (var i in docs){ 09 console.log(docs[i].word + " : " + docs[i].size); 10 } 11 var query = Words.update({}, {$set: {size: 0}}); 12 query.setOptions({multi: true}); 13 query.where('word').regex(/grati.*/); 14 query.exec(function(err, results){ 15 Words.find({word:/grat.*/}, function(err, docs){ 16 console.log(" After update: "); 17 for (var i in docs){ 18 console.log(docs[i].word + " : " + docs[i].size); 19 } 20 mongoose.disconnect(); 21 }); 22 }); 23 }); 24 });
Listing 16.7 Output mongoose_update_many.js
: Updating multiple documents in a collection by using Mongoose
Before update: gratifactions : 13 immigration : 11 integration : 11 migration : 9 After update: grateful : 8 gratifactions : 0 immigration : 0 integrate : 9 integrated : 10 integration : 0 migration : 0
There are two main ways to remove objects from a collection using Mongoose. You can use the remove()
method on either the Document
object for a single deletion or on the Model
object to delete multiple documents in the model. Deleting a single object is often convenient if you already have a Document
instance. However, it is often much more efficient to delete multiple documents at the same time at the Model
level. The following sections describe these methods.
The Document
object provides the remove()
method that allows you to delete a single document from the model. The syntax for the remove()
method on Document
objects is shown below. The callback
function accepts an error
as the only argument if an error occurs or the deleted document
as the second if the delete is successful:
remove([callback])
Listing 16.8 shows an example of using the remove()
method to remove the word unhappy
.
Listing 16.8 mongoose_remove_one.js
: Deleting a document from a collection by using Mongoose
01 var mongoose = require('mongoose'); 02 var db = mongoose.connect('mongodb://localhost/words'); 03 var wordSchema = require('./word_schema.js').wordSchema; 04 var Words = mongoose.model('Words', wordSchema); 05 mongoose.connection.once('open', function(){ 06 var query = Words.findOne().where('word', 'unhappy'); 07 query.exec(function(err, doc){ 08 console.log("Before Delete: "); 09 console.log(doc); 10 doc.remove(function(err, deletedDoc){ 11 Words.findOne({word:'unhappy'}, function(err, doc){ 12 console.log(" After Delete: "); 13 console.log(doc); 14 mongoose.disconnect(); 15 }); 16 }); 17 }); 18 });
Listing 16.8 Output mongoose_remove_one.js
: Deleting a document from a collection by using Mongoose
Before Delete: { _id: 598e0ebd0850b51290643f21, word: 'unhappy', first: 'u', last: 'y', size: 7, charsets: [ { chars: [Object], type: 'consonants' }, { chars: [Object], type: 'vowels' } ], stats: { vowels: 2, consonants: 5 }, letters: [ 'u', 'n', 'h', 'a', 'p', 'y' ] } After Delete: null
The Model
object also provides a remove()
method that allows you to delete multiple documents in a collection using a single call to the database. The syntax for the remove()
method on Model
objects is slightly different, as shown below:
update(query, [options], [callback])
The query
parameter defines the query used to identify which objects to delete. The options
parameter specifies the write preferences, and the callback
function accepts an error
as the first argument and the number
of documents deleted as the second.
A nice thing about deleting at the Model
level is that you delete multiple documents in the same operation, saving the overhead of multiple requests. Also, you can use the Query
object to define which objects should be updated.
Listing 16.9 shows an example of using the remove()
method to delete words that match the regex /grati.*/
expression. Notice that multiple query options are piped onto the Query
object before executing it in line 13. The number of documents removed is displayed, and then another find()
request is made, this time using the regex /grat.*/
to show that only those matching the remove
query actually are deleted.
Listing 16.9 mongoose_remove_many.js
: Deleting multiple documents in a collection by using Mongoose
01 var mongoose = require('mongoose'); 02 var db = mongoose.connect('mongodb://localhost/words'); 03 var wordSchema = require('./word_schema.js').wordSchema; 04 var Words = mongoose.model('Words', wordSchema); 05 mongoose.connection.once('open', function(){ 06 Words.find({word:/grat.*/}, function(err, docs){ 07 console.log("Before delete: "); 08 for (var i in docs){ 09 console.log(docs[i].word); 10 } 11 var query = Words.remove(); 12 query.where('word').regex(/grati.*/); 13 query.exec(function(err, results){ 14 console.log(" %d Documents Deleted.", results); 15 Words.find({word:/grat.*/}, function(err, docs){ 16 console.log(" After delete: "); 17 for (var i in docs){ 18 console.log(docs[i].word); 19 } 20 mongoose.disconnect(); 21 }); 22 }); 23 }); 24 });
Listing 16.9 Output mongoose_remove_many.js
: Deleting multiple documents in a collection by using Mongoose
Before delete: grateful gratifactions immigration integrate integrated integration migration NaN Documents Deleted. After delete: grateful integrate integrated
The Model
object provides an aggregate()
method that allows you to implement the MongoDB aggregation pipeline discussed in Chapter 15. If you haven’t read the aggregation section in Chapter 15 yet, you should do so before reading this section. Aggregation in Mongoose works similarly to the way it works in the MongoDB Node.js native driver. In fact, you can use the exact same syntax if you want. You also have the option of using the Mongoose Aggregate
object to build and then execute the aggregation pipeline.
The Aggregate
object works similarly to the Query
object in that if you pass in a callback
function, aggregate()
is executed immediately. If not, an Aggregate
object is returned, and you can apply a pipeline method.
For example, the following calls the aggregate()
method immediately:
model.aggregate([{$match:{value:15}}, {$group:{_id:"$name"}}], function(err, results) {});
You can also pipeline aggregation operations using an instance of the Aggregate
object. For example:
var aggregate = model.aggregate(); aggregate.match({value:15}); aggregate.group({_id:"$name"}); aggregate.exec();
Table 16.6 describes the methods that can be called on the Aggregate
object.
Table 16.6 Pipeline methods for the Aggregate object in Mongoose
Method |
Description |
|
Executes the |
|
Appends additional append({match:{size:1}}, {$group{_id:"$title"}}, {$limit:2}) |
|
Appends a group({_id:"$title", largest:{$max:"$size"}}) |
|
Appends a |
|
Appends a match({value:{$gt:7, $lt:14}, title:"new"}) |
|
Appends a project operation defined by the project({_id:"$name", value:"$score", largest:{$max:"$size"}}) |
|
Specifies the replica |
|
Appends a |
|
Appends a sort({name:1, value:-1}) |
|
Appends an unwind("arrField1", "arrField2", "arrField3") |
Listing 16.10 illustrates implementing aggregation in Mongoose using three examples. The first example, in lines 9–19, implements aggregation in the native driver way, but by using the Model
object. The aggregated result set is the largest and smallest word sizes for words beginning with a vowel.
The next example, in lines 20–27, implements aggregation by creating an Aggregate
object and appending operations to it using the match()
, append()
, and limit()
methods. The results are stats for the five four-letter words.
The final example, in lines 28–35, uses the group()
, sort()
, and limit()
methods to build the aggregation pipeline that results in the top five letters with the largest average word size.
Listing 16.10 mongoose_aggregate.js
: Aggregating data from documents in a collection by using Mongoose
01 var mongoose = require('mongoose'); 02 var db = mongoose.connect('mongodb://localhost/words'); 03 var wordSchema = require('./word_schema.js').wordSchema; 04 var Words = mongoose.model('Words', wordSchema); 05 setTimeout(function(){ 06 mongoose.disconnect(); 07 }, 3000); 08 mongoose.connection.once('open', function(){ 09 Words.aggregate([{$match: {first:{$in:['a','e','i','o','u']}}}, 10 {$group: {_id:"$first", 11 largest:{$max:"$size"}, 12 smallest:{$min:"$size"}, 13 total:{$sum:1}}}, 14 {$sort: {_id:1}}], 15 function(err, results){ 16 console.log(" Largest and smallest word sizes for " + 17 "words beginning with a vowel: "); 18 console.log(results); 19 }); 20 var aggregate = Words.aggregate(); 21 aggregate.match({size:4}); 22 aggregate.limit(5); 23 aggregate.append({$project: {_id:"$word", stats:1}}); 24 aggregate.exec(function(err, results){ 25 console.log(" Stats for 5 four letter words: "); 26 console.log(results); 27 }); 28 var aggregate = Words.aggregate(); 29 aggregate.group({_id:"$first", average:{$avg:"$size"}}); 30 aggregate.sort('-average'); 31 aggregate.limit(5); 32 aggregate.exec( function(err, results){ 33 console.log(" Letters with largest average word size: "); 34 console.log(results); 35 }); 36 });
Listing 16.10 Output mongoose_aggregate.js
: Aggregating data from documents in a collection by using Mongoose
Stats for 5 four letter words: [ { stats: { vowels: 2, consonants: 2 }, _id: 'have' }, { stats: { vowels: 1, consonants: 3 }, _id: 'that' }, { stats: { vowels: 1, consonants: 3 }, _id: 'with' }, { stats: { vowels: 1, consonants: 3 }, _id: 'this' }, { stats: { vowels: 1, consonants: 3 }, _id: 'they' } ] Largest and smallest word sizes for words beginning with a vowel: [ { _id: 'a', largest: 14, smallest: 1, total: 295 }, { _id: 'e', largest: 13, smallest: 3, total: 239 }, { _id: 'i', largest: 14, smallest: 1, total: 187 }, { _id: 'o', largest: 14, smallest: 2, total: 118 }, { _id: 'u', largest: 13, smallest: 2, total: 57 } ] Letters with largest average word size: [ { _id: 'i', average: 8.20855614973262 }, { _id: 'e', average: 7.523012552301255 }, { _id: 'c', average: 7.419068736141907 }, { _id: 'a', average: 7.145762711864407 }, { _id: 'p', average: 7.01699716713881 } ]
One of the most important aspects of the mongoose
module is that of validation against a defined model. Mongoose provides a built-in validation framework that only requires you to define validation functions to perform on specific fields that need to be validated. When you try to create a new instance of a Document
, read a Document
from the database, or save a Document
, the validation framework calls your custom validation methods and returns an error if the validation fails.
The validation framework is actually simple to implement. You call the validate()
method on the specific path in the Model
object that you want to apply validation to and pass in a validation
function. The validation
function should accept the value of the field and then use that value to return true
or false
depending on whether the value is valid. The second parameter to the validate()
method is an error
string that is applied to the error object if validation fails. For example:
Words.schema.path('word').validate(function(value){ return value.length < 20; }, "Word is Too Big");
The error object thrown by validation has the following fields:
error.errors.<field>.message
: String defined when adding the validate function
error.errors.<field>.type
: Type of validation error
error.errors.<field>.path
: Path in the object that failed validation
error.errors.<field>.value
: Value that failed validation
error.name
: Error type name
err.message
: Error Message
Listing 16.11 shows a simple example of adding validation to the word model, where a word of length 0 or greater than 20 is invalid. Notice that when the newWord
is saved in line 18, an error is passed to the save()
function. The output in lines 12–26 shows the various values of different parts of the error, as shown in Listing 16.11 Output. You can use these values to determine how to handle validation failures in the code.
Listing 16.11 mongoose_validation.js
: Implementing validation of documents in the model by using Mongoose
01 var mongoose = require('mongoose'); 02 var db = mongoose.connect('mongodb://localhost/words'); 03 var wordSchema = require('./word_schema.js').wordSchema; 04 var Words = mongoose.model('Words', wordSchema); 05 Words.schema.path('word').validate(function(value){ 06 return value.length < 0; 07 }, "Word is Too Small"); 08 Words.schema.path('word').validate(function(value){ 09 return value.length > 20; 10 }, "Word is Too Big"); 11 mongoose.connection.once('open', function(){ 12 var newWord = new Words({ 13 word:'supercalifragilisticexpialidocious', 14 first:'s', 15 last:'s', 16 size:'supercalifragilisticexpialidocious'.length, 17 }); 18 newWord.save(function (err) { 19 console.log(err.errors.word.message); 20 console.log(String(err.errors.word)); 21 console.log(err.errors.word.type); 22 console.log(err.errors.word.path); 23 console.log(err.errors.word.value); 24 console.log(err.name); 25 console.log(err.message); 26 mongoose.disconnect(); 27 }); 28 });
Listing 16.11 Output mongoose_validation.js
: Implementing validation of documents in the model by using Mongoose
Word is Too Small Word is Too Small undefined word supercalifragilisticexpialidocious ValidationError Words validation failed
Mongoose provides a middleware framework where pre
and post
functions are called before and after the init()
, validate()
, save()
, and remove()
methods on a Document
object. A middleware framework allows you to implement functionality that should be applied before or after a specific step in the process. For example, when creating word documents using the model defined earlier in this chapter, you may want to automatically set the size to the length of the word field as shown below in the following pre
save()
middleware function:
Words.schema.pre('save', function (next) { console.log('%s is about to be saved', this.word); console.log('Setting size to %d', this.word.length); this.size = this.word.length; next(); });
There are two types of middleware functions—the pre
and the post
functions—and they are handled a bit differently. The pre
functions receive a next
parameter, which is the next middleware function to execute. The pre
functions can be called asynchronously or synchronously. In the case of the asynchronous method, an additional done
parameter is passed to the pre
function allowing you to notify the asynchronous framework that you are finished. If you are applying operations that should be done in order in the middleware, you use the synchronous method.
To apply the middleware synchronously, you simply call next()
in the middleware function. For example:
schema.pre('save', function(next){ next(); });
To apply the middleware asynchronously, add a true
parameter to the pre()
method to denote asynchronous behavior and then call doAsync(done)
inside the middleware function. For example:
schema.pre('save', true, function(next, done){ next(); doAsync(done); });
The post
middleware functions are called after the init
, validate
, save
, or remove
operation has been processed. This allows you to do any cleanup work necessary when applying the operation. For example, the following implements a simple post
save
method that logs that the object has been saved:
schema.post('save', function(doc){ console.log("Document Saved: " + doc.toString()); });
Listing 16.12 illustrates the process of implementing middleware for each stage of the Document
life cycle. Notice that the validate
and save
middleware functions are executed when saving the document. The init
middleware functions are executed when retrieving the document from MongoDB using findOne()
. The remove
middleware functions are executed when using remove()
to delete the document from MongoDB.
Also notice that the this
keyword can be used in all the middleware functions except pre
init
to access the Document
object. In the case of pre
init
, we do not have a document from the database yet to use.
Listing 16.12 mongoose_middleware.js
: Applying a middleware framework to a model by using Mongoose
01 var mongoose = require('mongoose'); 02 var db = mongoose.connect('mongodb://localhost/words'); 03 var wordSchema = require('./word_schema.js').wordSchema; 04 var Words = mongoose.model('Words', wordSchema); 05 Words.schema.pre('init', function (next) { 06 console.log('a new word is about to be initialized from the db'); 07 next(); 08 }); 09 Words.schema.pre('validate', function (next) { 10 console.log('%s is about to be validated', this.word); 11 next(); 12 }); 13 Words.schema.pre('save', function (next) { 14 console.log('%s is about to be saved', this.word); 15 console.log('Setting size to %d', this.word.length); 16 this.size = this.word.length; 17 next(); 18 }); 19 Words.schema.pre('remove', function (next) { 20 console.log('%s is about to be removed', this.word); 21 next(); 22 }); 23 Words.schema.post('init', function (doc) { 24 console.log('%s has been initialized from the db', doc.word); 25 }); 26 Words.schema.post('validate', function (doc) { 27 console.log('%s has been validated', doc.word); 28 }); 29 Words.schema.post('save', function (doc) { 30 console.log('%s has been saved', doc.word); 31 }); 32 Words.schema.post('remove', function (doc) { 33 console.log('%s has been removed', doc.word); 34 }); 35 mongoose.connection.once('open', function(){ 36 var newWord = new Words({ 37 word:'newword', 38 first:'t', 39 last:'d', 40 size:'newword'.length, 41 }); 42 console.log(" Saving: "); 43 newWord.save(function (err){ 44 console.log(" Finding: "); 45 Words.findOne({word:'newword'}, function(err, doc){ 46 console.log(" Removing: "); 47 newWord.remove(function(err){ 48 mongoose.disconnect(); 49 }); 50 }); 51 }); 52 });
Listing 16.12 Output mongoose_middleware.js
: Applying a middleware framework to a model by using Mongoose
Saving: newword is about to be validated newword has been validated newword is about to be saved Setting size to 7 newword has been saved Finding: a new word is about to be initialized from the db newword has been initialized from the db Removing: newword is about to be removed newword has been removed
This chapter introduced you to Mongoose, which provides a structured schema to a MongoDB collection that provides the benefits of validation and typecasting. You learned about the new Schema
, Model
, Query
, and Aggregation
objects and how to use them to implement an ODM. You also got a chance to use the sometimes more friendly Mongoose methods to build a Query
object before executing database commands.
You were also introduced to the validation and middleware frameworks. The validation framework allows you to validate specific fields in the model before trying to save them to the database. The middleware framework allows you to implement functionality that happens before and/or after each init
, validate
, save
, or remove
operation.
3.12.162.37