16
Using Mongoose for Structured Schema and Validation

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.

Understanding Mongoose

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:

Images You can create a schema structure for your documents.

Images Objects/documents in the model can be validated.

Images Application data can be typecast into the object model.

Images Business logic hooks can be applied using middleware.

Images 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:

Images You are required to provide a schema, which isn’t always the best option when MongoDB doesn’t require it.

Images It doesn’t perform certain operations, such as storing data, as well as the native driver does.

Additional Objects

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 a MongoDB Database Using Mongoose

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 } } ]

Defining a Schema

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:

Images String

Images Number

Images Boolean or Bool

Images Array

Images Buffer

Images Date

Images ObjectId or Oid

Images 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.

Understanding Paths

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

Creating a Schema Definition

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

autoIndex

A Boolean that, when true, indicates that the autoindex feature for the collection is turned off. The default is true.

bufferCommands

A Boolean that, when true, indicates that commands that cannot be completed due to connection issues are buffered. The default is true.

capped

Specifies the maximum number of documents supported in a capped collection.

collection

Specifies the collection name to use for this Schema model. Mongoose automatically connects to this collection when compiling the schema model.

id

A Boolean that, when true, indicates that the documents in the model will have an id getter that corresponds to the _id value of the object. The default is true.

_id

A Boolean that, when true, indicates that Mongoose automatically assigns an _id field to your documents. The default is true.

read

Specifies the replica read preferences. Value can be primary, primaryPreferred, secondary, secondaryPreferred, or nearest.

safe

A Boolean; when true, Mongoose applies a write concern to requests that update the database. The default is true.

strict

A Boolean that, when true, indicates that attributes passed in the object that do not appear in the defined schema are not saved to the database. The default is true.

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'});

Adding Indexes to a Schema

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()

Implementing Unique Fields

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}
});

Forcing Required Fields

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()

Adding Methods to the Schema Model

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;
};

Implementing the Schema on the Words Database

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 } ] ]

Compiling a Model

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')

Understanding the Query Object

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.

Setting the Query Database Operation

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

create(objects, [callback])

Inserts the objects specified in the objects parameter to the MongoDB database. The objects parameter can be a single JavaScript object or an array of JavaScript objects. A Document object instance for the model is created for each object. The callback function receives an error object as the first parameter and the saved documents as additional objects. For example:

function(err, doc1, doc2, doc3, …)

count([query], [callback])

Sets the operation to count. When the callback is executed, the results returned are the number of items matching the Query.

distinct([query], [field], [callback])

Sets the operation to distinct, which limits the results to an array of the distinct values of the field specified when the callback is executed.

find([query], [options], [callback])

Sets the operation to a find, which returns an array of the Document objects that match the Query.

findOne([query], [options], [callback])

Sets the operation to a findOne, which returns the first Document object that matches the Query.

findOneAndRemove([query], [options], [callback])

Sets the operation to a findAndRemove, which deletes the first document in the collection that matches the Query.

findOneAndUpdate([query], [update], [options], [callback])

Sets the operation to a findAndUpdate, which updates the first document in the collection that matches the Query. The update operation is specified in the update parameter. See Table 14.2 for the update operators that can be used.

remove([query], [options], [callback])

Sets the operation to a remove, which deletes all the documents in the collection that match the Query.

update([query], [update], [options], [callback])

Sets the operation to update, which updates all documents in the collection that match the Query. The update operation is specified in the update parameter. See Table 14.2 for the update operators that can be used.

aggregate(operators, [callback])

Applies one or more aggregate operators to the collection. The callback function accepts an error as the first parameter and an array of JavaScript objects representing the aggregated results as the second parameter.

Setting the Query Database Operation Options

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

setOptions(options)    

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.

limit(number)

Sets the maximum number of documents to include in the results.

select(fields)

Specifies the fields that should be included in each document of the result set. The fields parameter can be either a space-separated string or an object. When using the string method, adding a + to the beginning of the field name forces inclusion even if the field doesn’t exist in the document, and adding a - excludes the field. For example:

select('name +title -value');
select({name:1, title:1, value:0);

sort(fields)

Specifies the fields to sort on in string form or object form. For example:

sort('name -value');
sort({name:1, value:-1})

skip(number)

Specifies the number of documents to skip at the beginning of the result set.

read(preference)

Enables you to set the read preference to primary, primaryPreferred, secondary, secondaryPreferred, or nearest.

snapshot(Boolean)

Sets the query to a snapshot query when true.

safe(Boolean)

When set to true, the database request uses a write concern for update operations.

hint(hints)

Specifies the indexes to use or exclude when finding documents. Use a value of 1 for include and -1 for exclude. For example:

hint(name:1, title:-1);

comment(string)

Adds the string to the MongoDB log with the query. This is useful to identify queries in the log files.

Setting the Query Operators

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

where(path,[value])

Sets the current field path for the operators. If a value is also included, only documents where that field equals the value are included. For example:

where('name', "myName")

gt([path], value)

Matches values that are greater than the value specified in the query. For example:

gt('value', 5)gt(5)

gte([path], value)

Matches fields that are equal to or greater than the value specified in the query.

lt([path], value)

Matches values that are less than the value specified in the query.

lte([path], value)

Matches fields that are less than or equal to the value specified in the query.

ne([path], value)

Matches all fields that are not equal to the value specified in the query.

in([path], array)

Matches any of the values that exist in an array specified in the query. For example:

in('name', ['item1', 'item2'])
in(['item1', 'item2'])

nin([path], array)

Matches values that do not exist in an array specified in the query.

or(conditions)

Joins query clauses with a logical OR and returns all documents that match the conditions of either clause. For example:

or([{size:{$lt:5}},{size:{$gt:10}}])

and(conditions)

Joins query clauses with a logical AND and returns all documents that match the conditions of both clauses. For example:

and([{size:{$lt:10}},{size:{$gt:5}}])

nor(conditions)

Joins query clauses with a logical NOR and returns all documents that fail to match both conditions. For example:

nor([{size:{$lt:5}},{name:"myName"}])

exists([path], Boolean)

Matches documents that have the specified field. For example:

exists('name', true}
exists('title', false}

mod([path], value, remainder)

Performs a modulo operation on the value of a field and selects documents that have the matching remainder. For example:

mod('size', 2,0)

regex([path], expression)

Selects documents where values match a specified regular expression. For example:

regex('myField', 'some.*exp')

all([path], array)

Matches array fields that contain all elements specified in the array parameter. For example:

all('myArr', ['one','two','three'])

elemMatch([path],  criteria)    

Selects documents if an element in the array of subdocuments has fields that match all the specified $elemMatch criteria. The criteria can be an object or a function. For example:

elemMatch('item', {{value:5},size:{$lt:3}})
elemMatch('item', function(elem){
 elem.where('value', 5);
 elem.where('size').gt(3);
})

size([path], value)

Selects documents if the array field is a specified size. For example:

size('myArr', 5)

Understanding the Document Object

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

equals(doc)

Returns true if this Document object matches the document specified by the doc parameter.

id

Contains the _id value of the document.

get(path, [type])

Returns the value of the specified path. The type parameter allows you to typecast the type of value to return.

set(path, value, [type])

Sets the value of the field at the specified path. The type parameter allows you to typecast the type of value to set.

update(update, [options], [callback])

Updates the document in the MongoDB database. The update parameter specifies the update operators to apply to the document. See Table 14.2 for the update operators that can be used.

save([callback])

Saves changes that have been made to the Document object to the MongoDB database. The callback function accepts an error object as the only parameter.

remove([callback])

Removes the Document object from the MongoDB database. The callback function accepts an error object as the only parameter.

isNew

A Boolean that, if true, indicates this is a new object to the model that has not been stored in MongoDB.

isInit(path)

Returns true if the field at this path has been initialized.

isSelected(path)

Returns true if the field at this path was selected in the result set returned from MongoDB.

isModified(path)

Returns true if the field at this path has been modified but not yet saved to MongoDB.

markModified(path)

Marks the path as being modified so that it will be saved/updated to MongoDB.

modifiedPaths()

Returns an array of paths in the object that have been modified.

toJSON()

Returns a JSON string representation of the Document object.

toObject()

Returns a normal JavaScript object without the extra properties and methods of the Document object.

toString()

Returns a string representation of the Document object.

validate(callback)

Performs a validation on the Document. The callback function accepts only an error parameter.

invalidate(path, msg, value)

Marks the path as invalid causing the validation to fail. The msg and value parameters specify the error message and value.

errors

Contains a list of errors in the document.

schema

Links to the Schema object that defines the Document object’s model.

Finding Documents Using Mongoose

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

Adding Documents Using Mongoose

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' ] }

Updating Documents Using Mongoose

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.

Saving Document Changes

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' ] }

Updating a Single Document

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' ] }

Updating Multiple Documents

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

Removing Documents Using Mongoose

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.

Removing a Single Document

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

Removing Multiple Documents

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

Aggregating Documents Using Mongoose

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

exec(callback)

Executes the Aggregate object pipeline in the order they are added. The callback function receives an error object as the first parameter and an array of JavaScript objects as the second, representing the aggregated results.

append(operations)

Appends additional operations to the Aggregation object pipeline. You can apply multiple operations, for example:

append({match:{size:1}}, {$group{_id:"$title"}}, {$limit:2})

group(operators)

Appends a group operation defined by the group operators. For example:

group({_id:"$title", largest:{$max:"$size"}})

limit(number)

Appends a limit operation that limits the aggregated results to a specific number.

match(operators)

Appends a match operation defined by the operators parameter. For example:

match({value:{$gt:7, $lt:14},  title:"new"})

project(operators)

Appends a project operation defined by the operators parameter. For example:

project({_id:"$name", value:"$score", largest:{$max:"$size"}})

read(preference)

Specifies the replica read preference use for aggregation. Value can be primary, primaryPreferred, secondary, secondaryPreferred, or nearest.

skip(number)

Appends a skip operation that skips the first number documents when applying the next operation in the aggregation pipeline.

sort(fields)

Appends a sort operation to the aggregation pipeline. The fields are specified in an object where a value of 1 is include, and a value of -1 is exclude. For example:

sort({name:1, value:-1})

unwind(arrFields)

Appends an unwind operation to the aggregation pipeline, which unwinds the arrFields by creating a new document in the aggregation set for each value in the array. For example:

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 } ]

Using the Validation Framework

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:

Images error.errors.<field>.message: String defined when adding the validate function

Images error.errors.<field>.type: Type of validation error

Images error.errors.<field>.path: Path in the object that failed validation

Images error.errors.<field>.value: Value that failed validation

Images error.name: Error type name

Images 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

Implementing Middleware Functions

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

Summary

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.

Next

In the next chapter, you delve into some more advanced MongoDB topics, such as indexes, replication, and sharding.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.12.150.203