MongoDB

The full project code for the example code listed on this webpage can be downloaded from this link.

MongoDB is a noSQL database. It stores data in flexible, JSON-like documents. The data structure can vary between documents (records) within a collection (table). In addition, the data structure can change over time. Mongo fields (attributes) are similar to columns in a SQL table.

SQL defines a schema (structure) on the records that are added to a table. By default, MongoDB does not have a schema. We can use Mongoose (this is a MongoDB modeling tool) to associate a schema with the documents in a collection. A Model uses a schema to ensure that all changes to any documents in a collection abide by the schema.

MongoDB automatically assigns a unique ID to every document in a collection. The ID will have the field name _id.

In order to use MongoDB, we need to install MongoDB (https://www.mongodb.com/download-center/community?jmp=docs). Once it is intalled, we need to run the command mongod.exe, which will be found in the mongo folder that was created as part of the software install. Running mongod.exe will cause a command shell to run in the background. The mongod.exe application is our database server. It handles data requests, manages data access, and performs background management operations.

Install mongoDB and run mongod.exe

Mongoose

MongoDB is the native driver for interacting with a Mongodb data. It does not specify any structure on the data that it holds.

Mongoose is an Object modeling tool that is built on top of MongoDB. Mongoose consists of schema and models:

Mongoose also provides a means to access and manipulate MongoDB data.

NOTE: Whenever we refer to MongoDB in these notes, we are actually referring to MongoDB that is structured, accessed and manipulated using Mongoose.

Install Mongoose

Schema

MongoDB collections are the same as mySQL tables. Each collection must have a schema associated with it, as shown in the code below.

const mongoose = require(`mongoose`)

let productsSchema = new mongoose.Schema(
   {
        name: {type: String},
        price: {type: Number}
   },
   {
       collection: `products`
   })

module.exports = mongoose.model(`products`, productsSchema)

Each field is assigned a schema type. Schema types include:

A complete list of types can be found at this link.

 

We can assign a collection to a schema. This will overwrite the default way that Mongoose sets schemas. We also need to assign the collection when exporting the schema to a model.

Schema Validation

We can add validation to a schema type:

We can combine appropriate validators. For example, the age below is required and has a min value:

age:{type:Number, required:true, min:18}

Custom Schema Validation

We can use the validate property to add a custom validation function to a document. For example, the code below will check that the startDate is earlier than the endDate:

startDate:{type:Date}, 
endDate:{type:Date, validate:function(){return this.startDate < this.endDate}

Schema Error Handling

We can add an error message to any validation property by combining the validation property's value and the error message inside an [] array. For example:

age:{type:Number, min:[18, "Too young"]}

Schema Default Value

A field can have a default value assigned to it. The default will only be applied if no other value is provided for the field when a document is created.

accessLevel:{type:Number, default:1}

startDate:{type:Date, default:Date.now}

Other Schema String Properties

We can apply the lowercase, uppercase and trim properties to fields that are of type String. For example:

model:{type:String, uppercase:true, trim:true}

Connections

To connect to a database called DB_NAME that is stored on the local host and running on the Mongodb default port (27017), we use the code below:

mongoose.connect(`mongodb://localhost/DB_NAME}`, {useNewUrlParser: true, useFindAndModify: false, useCreateIndex: true, useUnifiedTopology: true})

To connect to a database called DB_NAME that is stored on a server called DB_HOST on port DB_PORT, with the username DB_USERNAME and password DB_PASSWORD, we use the code below:

mongoose.connect(`mongodb://DB_USERNAME:DB_PASSWORD@DB_HOST:DB_PORT/DB_NAME}`, {useNewUrlParser: true, useFindAndModify: false, useCreateIndex: true, useUnifiedTopology: true})

In what file should the values DB_NAME, DB_HOST, DB_PORT, DB_USERNAME, and DB_PASSWORD be stored?

documents

MongoDB collections are the same as mySQL tables. MongoDB documents are written as JSON objects.

{model:"BMW", color:"red", year:2020, price:50000}

When a document is added to a mongoDB, it will automatically be assigned a unique id, called _id

Embedded Documents

MongoDB allows us to embed other documents inside any document. These are called embedded documents or subdocuments.

{
    model:"BMW", 
    color:"red", 
    year:2020, 
    price:[{date:2018-09-21, amount:20000},
           {date:2020-04-10, amount:15000}]
}

queries

MongoDB queries are written as JSON objects. The complete list of queries can be found at the MongoDB website. The most commonly used queries are listed below.

Select all

{}   // match all documents

Equality

{model:"BMW"}    // model is ""BMW

And

{model: "BMW", colour:"red"}    // model is "BMW" AND colour is "red"

Or

{$or:[{ color: "red" }, {color:"blue"}]}    // colour is "red" or "blue"

And/Or combination

{model:"BMW", $or:[{colour:"red"}, {colour:""blue}]}    // model is "BMW" AND (colour is "red" OR "blue")

$lt

// less than

{salary:{$lt:50000}}   // salary < 50000

$lte

// less than or equal to
{salary:{$lte:50000}}   // salary <= 50000

$gt

// greater than
{salary:{$gt:50000}}   // salary > 50000

$gte

// greater than or equal to
{salary:{$gt:50000}}   // salary >= 50000

$eq

// equal to
{salary:{$eq:50000}}   // salary === 50000

$ne

// not equal to
{salary:{$ne:50000}}   // salary !== 50000

$in

{colour:$in["red", "green", ""blue]}    // colour is red, green or blue

$nin

{colour:$nin["red", "green", ""blue]}   // colour is NOT red, green or blue

Query Projections

A projection allows us to select which fields will be returned for any document that matches a search query. Projections are written as JSON objects. A projection of 1 means a field will be returned and a projection of 0 means it will not be returned.

{name:1, address:1}    // return the _id, name and address fields

{name:0, address:0}    // return the _id and all other fields except for the name and address fields

{_id:0, name:0, address:0}   // return all fields except for the _id, name and address fields

Projections are optional. In the absense of a Projection, all fields will be returned.

Query Callback functions

MongoDB methods all have the same callback function parameters. These are error and data, where error will be set if there is any error condition and data will contain the documents that match a given query.

methods

MongoDB methods make use of the queries, projections and callback functions described immediately above. The complete list of MongoDB methods is available at this link. Some of the more common MongoDB methods are described below:

insertOne(document, callback)
// insert a document into a collection

insertOne({model:"BMW", colour:"red"}, (error, data) =>
{
})
insert(documents, callback)
// insert a document into a collection

// insert a red BMW, green BMW and blue BMW
insert([{model:"BMW", colour:"red"},
        {model:"BMW", colour:"green"},
        {model:"BMW", colour:"blue"}], (error, data) =>
{
})
findOne(query, projection, callback)
// return the first occurrence  of a document that matches the given query in a collection


// return the first document in the collection that has the name called "Mary"
findOne({name:"Mary"}, (error, data) =>
{
})    
find(query, projection, callback)
// return all documents that match the given query in a collection


// return all documents where the salary is less than 50000
find({salary:{$lt:50000}}, (error, data) =>
{
})   
deleteOne(query, callback)
// delete one document from a collection. The first document that matches the query will be deleted


// delete the document with _id 51e045b39376f4a73c6fd7e0
deleteOne({_id:"51e045b39376f4a73c6fd7e0"}, (error, data) =>
{
})    
delete(query, callback)
// delete one or more documents from a collection. All documents that match the query will be deleted


// delete all documents where the colour is red
delete({colour:"red"}, (error, data) =>
{
})    
distinct(fieldName, callback)
// return the distinct values for a specified field


// list all departments in a company. 
// Note that the query is a fieldName rather than a query
// Note that we only project the department field
distinct("department", {department:1}, (error, data) =>
{
})    

We shall see these methods being used in the code that follows in this section of the notes.

"Cars" Worked Example

The full project code for the example code listed on this webpage can be downloaded from this link.

In the previous example, we stored the cars data in a JSON object on the server-side. In the real-world, it makes much more sense to store the data in a database. In this example, we shall use a database to store the car data.

Client-Side

There are no changes on the client-side, as we are just changing the resource that we access on the server-side. It was a JSON object in the previous example. It is a database in this example.

Server-Side

We set up our MongoDB database in the file below:

server/config/.env

# This file holds global constants that are visible on the Server-side

# Database
DB_NAME = D01234567


# Port
SERVER_PORT = 4000


# Local Host
LOCAL_HOST = http://localhost:3000

Define the DB_NAME

server/config/db.js

const mongoose = require('mongoose')
mongoose.connect(`mongodb://localhost/${process.env.DB_NAME}`, {useNewUrlParser: true, useFindAndModify: false, useCreateIndex: true, useUnifiedTopology: true})

const db = mongoose.connection
db.on('error', console.error.bind(console, 'connection error:'))
db.once('open', () => {console.log("connected to", db.client.s.url)})

Connect to the database DB_NAME that was defined in server/config.env

server/models/cars.js

const mongoose = require(`mongoose`)

let carsSchema = new mongoose.Schema(
   {
        model: {type: String},
        colour: {type: String},
        year: {type: Number},
        price: {type: Number}
   },
   {
       collection: `cars`
   })

module.exports = mongoose.model(`cars`, carsSchema)

We need to create a schema for each collection that we wish to store in a MongoDB.

server/routes/cars.js

const router = require(`express`).Router()

const carsModel = require(`../models/cars`)

// read all records
router.get(`/cars/`, (req, res) => 
{   
    carsModel.find((error, data) => 
    {
        res.json(data)
    })
})


// Read one record
router.get(`/cars/:id`, (req, res) => 
{
    carsModel.findById(req.params.id, (error, data) => 
    {
        res.json(data)
    })
})


// Add new record
router.post(`/cars/`, (req, res) => 
{
    carsModel.create(req.body, (error, data) => 
    {
        res.json(data)
    })
})


// Update one record
router.put(`/cars/:id`, (req, res) => 
{
    carsModel.findByIdAndUpdate(req.params.id, {$set: req.body}, (error, data) => 
    {
        res.json(data)
    })        
})


// Delete one record
router.delete(`/cars/:id`, (req, res) => 
{
    carsModel.findByIdAndRemove(req.params.id, (error, data) => 
    {
        res.json(data)
    })       
})

module.exports = router

Create a router to use in the methods in this file.

const router = require(`express`).Router()

Use the Mongoose model that is defined in the file server/models/cars.js

const carsModel = require(`../models/cars`)

Within each of our router methods, we do an appropriate query on the database. In the case of the router.get() method below, we perform a find() query.
In this example, we are not checking to see if there is an error returned for the query. Instead, the router always returns the data from the query to the client-side Axios method that called the router.get() method.

// read all records
router.get(`/cars/`, (req, res) => 
{   
    carsModel.find((error, data) => 
    {
        res.json(data)
    })
})

The id that is passed into the route is accessible in req.params.id
We use the findById() query with the id that was passed into the route to query the database for one document.

// Read one record
router.get(`/cars/:id`, (req, res) => 
{
    carsModel.findById(req.params.id, (error, data) => 
    {
        res.json(data)
    })
})

The new object that will be added to the database is passed to the router inside its body
The new object is accessible inside req.body
We use the create() query to add a new document to a collection.

// Add new record
router.post(`/cars/`, (req, res) => 
{
    carsModel.create(req.body, (error, data) => 
    {
        res.json(data)
    })
})

The id that is passed into the route is accessible in req.params.id
The new object that will be added to the database is passed to the router inside its body
The new object is accessible inside req.body
We use the findByIdAndUpdate() query to overwrite a document in a collection.

// Update one record
router.put(`/cars/:id`, (req, res) => 
{
    carsModel.findByIdAndUpdate(req.params.id, {$set: req.body}, (error, data) => 
    {
        res.json(data)
    })        
})

The id that is passed into the route is accessible in req.params.id
We use the findByIdAndRemove() query to delete a document from a collection.

// Delete one record
router.delete(`/cars/:id`, (req, res) => 
{
    carsModel.findByIdAndRemove(req.params.id, (error, data) => 
    {
        res.json(data)
    })       
})

Make router available to other files.

module.exports = router

No schema error checking has been done in the file server/models/cars.js, as shown in the code below.

const mongoose = require(`mongoose`)

let carsSchema = new mongoose.Schema(
   {
        model: {type: String},
        colour: {type: String},
        year: {type: Number},
        price: {type: Number}
   },
   {
       collection: `cars`
   })

module.exports = mongoose.model(`cars`, carsSchema)
Change the schema so that the following schema error-checking is done: Test your error-checking code using the server-side console.log()

If a schema error occurs, the router code in the file server/routes/cars.js does not return any database error message to the client-side Axios method that called it. Amend the router code so that it returns any schema error that might occur. You need to also adjust all of the client-side Axios methods and get them to display any returned error message using client-side console.log()

In what folder on your computer are your localhost databases stored?

MongoDBCompass is a GUI tool that can be used to view and manipulate a MongoDB. Install MongooseDBCompass from https://www.mongodb.com/download-center/compass and use it to view your databse data.

 
<div align="center"><a href="../versionC/index.html" title="DKIT Lecture notes homepage for Derek O&#39; Reilly, Dundalk Institute of Technology (DKIT), Dundalk, County Louth, Ireland. Copyright Derek O&#39; Reilly, DKIT." target="_parent" style='font-size:0;color:white;background-color:white'>&nbsp;</a></div>