Mongoose Beginner’s Guide. What is Mongoose
By: Onlykiosk Dev Tech
Tags: Mongoose, Mongo, Mongodb

Mongoose Design

Mongoose is designed from the perspective of helping a nodeJS program operate mongoDB. It focuses on solving real-life demands. Most data-related operations take place at the document level. So the design of mongoose emphasizes documents and “downplays” the roles of databases and collections. If you look at the content table of the mongoose manual, you cannot even find the word database or collection.

Database is pre-selected when establishing the connection. So after we are connected, we do not need to worry about selecting databases anymore.

The tricky part is how mongoose views mongoDB collections.

Mongoose regards a collection as a group of documents that meet the same document requirements. These requirements include field name, field value range and type, etc. These requirements are described by the schema. Schema takes effect via model. So here comes the two fundamental mongoose concepts: Schema and Model.

With the mongo shell or the native mongoDB nodeJS driver, we create a database and a collection first. Most document-related operations are executed using methods from the collection object.

But with mongoose, we start with setting requirements on documents. We first design the document schema and then create a model based on that it. Then we create documents based on the model. We access a collection via the model of its documents.

Mongo Shell & nodeJS Driver: db→collection → document

Mongoose: document requirements (schema) →model →documents

Schema and Model

MongoDB is famous for being schema-free. One collection can accept different kinds of documents, thus giving us lots of flexibility. But this does not mean you should just cram any documents into the same collection.

To guarantee data safety, all write operations, especially document insertions and updates, must be carefully examined. A collection should only accept qualified documents. You can also choose to validate documents using the backend program or using the built-in validator of mongoDB.

MongoDB allows us to set perform schema validation during updates and insertions. Validation rules are set a per-collection level. This means a MongoDB collection can decide what kinds of documents it accepts.

With the native mongoDB nodeJS driver, you create a collection using the createCollection method. In its options object, you set the validator property to set the schema rules.

// native mongoDB nodeJS driver let client = MongoClient(uri,{options_obj})

await client.connect()

client.createCollection('coll_name',{ validator:{} })

Mongoose places great emphasis on Schema. Everything in Mongoose must start with a Schema. The first thing you do after getting connected is to create a new schema object.

To create a new schema, you instantiate the Schema constructor and pass a schema object to it. The schema object dictates the acceptable document fields and field values.

// mongoose const schema_example = new Schema({ brand:String, model:String })

The above schema requires the document must have at least one of the two listed properties: brand and model. Both field values must be a string.

Mongoose schema can do many other things, including defining instance methods, static model methods, compound indexes and middleware. For now, we only focus on using schema to define the document structure and casting of properties. Our current priority is to figure out how to insert and read documents using mongoose.

Mongoose Model & MongoDB collection

Unlike what we did with the native NodeJS MongoDB driver, we cannot directly go from the schema object to the new collection. There is an extra link in between: the model. In fact, mongoose does not directly access a collection. It operates a collection via the model of the collection.

To build a model, we use the mongoose.model method. It has two parameters. The first one is the model name and the second the schema. You can directly use the schema object as the second parameter value.

mongoose.model('model_name',schema or {schema_obj})

A model is built on a schema object. You can think of it as a document template. It inherits document requirements from the schema and enforce them on new documents.

The model name will be used to name the new collection. Pay attention here, the model name will NOT be directly used as the collection name. Mongoose appendixes the model name with a letter S and use the “pluralized” model name as the collection name.

For example, in the example below, we name the model car_list, the final collection name will be car_lists.

const Car = mongoose.model('car_list',schema_obj)

The mongoose.model method returns us a constructor. We call it the model constructor. In the following lessons, the term model constructor specifically refer to the constructor function returned by the mongoose.model() method.

The first letter of a constructor’s name should be capitalized. This is why you should capitalize the first letter of the variable that receives the returned value of the model method.

So what can the model constructor do for us?

A document is created by instantiating the model constructor.

After we have created the model constructor, we can start to create new documents. We first prepare the new document using a JS objet literal. Then we need to convert this object literal into a mongoDB document.

This conversion is done by passing this object literal into the model constructor and instantiate that constructor.

The model constructor returns us the final document. Fields that do not meet the schema requirements will be removed from the document.

const Car = mongoose.model('car_list',schema) const car_1 = new Car({ brand:'Toyota', model:'Supra' })

In the above example, we successfully created a new document with two custom custom fields: brand and model. The document ID will be add automatically. The new document will be returned by the model constructor.

Creating the new document is only the first step, the most important step is to save it into the mongoDB collection. This requires the save method. This method comes from the document object returned by the model constructor.

car_1.save() // the save method returns a promise

Now, the new document has been saved into the car_lists collection which is created based on the car_list model.

This is how we create a new document and save it into mongoDB.

Read Documents from a Collection

If we need to look for saved documents, just connect the find method to the corresponding model constructor. The find method is a static method of the model constructor. You do not need to instantiate model constructor this time.

For example, we want to read the car_lists collection. We connect the find method to the Car constructor. The find method returns a promise. The resolved value is an array holding retrieved documents.

const schema = new Schema({ brand:String, model:String }) const Car = mongoose.model('car_list',schema) // new Car({brand:'Toyota', model:'Camry'}).save() // adding new doc Car.find(( err, results )=>{ console.log(results) })

Summary: Inserting and Reading Documents

Here is the full code:

const mongoose = require('mongoose') // mongoose.connect() returns a promise mongoose.connect('mongodb://localhost:33333', { useNewUrlParser:true, useUnifiedTopology:true, dbName:'cars', user:'rootUser', pass:'pwd123' } )

.then( ()=>{ console.log('Connected!') } )

.catch( ()=>{ console.log('Cannot Connect!') } ) let schema_obj = { brand:String, model:String } const Car = mongoose.model('car_list',schema_obj) let car1 = new Car({brand:'Lexus', engine:'Petrol'}) car1.save() // .save() returns a promise as well Car.find((err, results)=>{ console.log(result) }) // Found any problems?

The above program can work perfectly fine. But have you found any problem with it? Look at the position of the save method, is there anything wrong with it?

No Need to “await” mongoose.connect()

The save method comes from the instance of the model constructor. The model constructor is created by the model method from the mongoose object. So in the end, the save method indirectly come from the mongoose object. This means the state of the mongoose object affects the save method.

We can only save a document after we are connected to the mongod process. This means when the save method gets executed, the connection must have already been established.

This means we should run the model and save methods from inside the callback of the mongoose connect method.

If we do not want to use callbacks, we should prefix the connect method with await to hold the execution of the model and save methods until the connection has been established.

So the code we have here should not work because we put the connect, model and save methods in the same scope and did not prefix the connect method with await. So this program should not work because by the time the model and save methods get executed, the connection has not been established yet. The JS engine will not wait for the connection to be established before executing the save method.

But this is not the result we got. The new document has been saved successfully. So how did this happen? Why an incorrect program can produce the right result?

The answer can be found in the manual.