MongoDB

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

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 database server (https://www.mongodb.com/download-center/community?jmp=docs), as shown below:

Click on the "Select Package" button.

This will cause the "Download" link to display. Click on the "Download" link to do the installation. If installing on Windows, you should select msi as the package option.

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.

WARNING: Mongod.exe MUST be running in the background BEFORE you run the nodemon command line for any project that uses MongoDB.

Install MongoDB. As part of the mongoDB installation, you will be given the option to install MongoDB Compass. Install this, too.

Run MongoDB.

Run MongoDB Compass.

The first time that you run MongoDB Compass, you can select the "Fill in connection fields individually", as shown in the image below.

The screen below will open. Do not change the default options that are offered. Press the "Connect" button. This will connect to your mongoDB server. Once you have made the connection once, it will then be available in the "Recents" section of MongoDB Compass.

Mongoose

MongoDB is the native driver for interacting with a Mongodb database server. MongoDB does not specify any structure on the data that it holds. Mongoose provides a means to structure the data that is stored in a MongoDB database server. Mongoose also provides a means to interact with the structured data.

 

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

 

Use the command line below to install mongoose.

npm install mongoose -g

 

In order to use Mongoose to connect to a MongoDB database server, we need to create a new file called server/config/db.js and place the code below into it.

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

 

The value process.env.DB_NAME needs to be set in the server/config/.env file, as shown below.

server/config/.env

...


# Database
DB_NAME = D01234567


...

The database does not have to be called D01234567. You can name it to another value if you wish.

 

The database needs to be included in the server/server.js file, as shown below:

server/server.js

...
	

require(`./config/db`)

	
...

Schema

Mongoose collections are the same as mySQL tables and Mongoose documents are the same as mySQL records. By default, the documents contained in Mongoose collections have no structure. We use schema to impose structure on the documents contained in a Mongoose collection. The schema allow us to assign a type (such as String or Number), default values, and validators to each property (collection properties are the same as a column in an SQL table) in a collection.

Each schema must have a model associated with it. A model defines the programming interface for interacting with a collection (read, insert, delete, et cetera). Associating a model with a schema will ensure that all of the model's programming interactions with any documents in a collection abide to the schema's structure.

 

In our project, we need to create a schema and model for the cars collection. This is saved in the file server/models/cars.js, as shown in the code below:

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)

Schema Types

Each property (same as a mySQL field in a record) in a schema must have a schema type associated with it. Schema types include:

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

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} 

documents

Mongoose documents are the same as mySQL records. Mongoose 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

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:{$gte: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 properties (fields) will be returned for any document that matches a search query. Projections are written as JSON objects. A projection of 1 means a property 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.

Server-Side Routing

Server-side routers are used to interact with database collection models. Each router must include the code to require any database collection models that it interacts with. In our project, the code below allows the cars router to interact with the cars model:

system/routes/cars.js

...


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


...

Query Callback functions

MongoDB methods all have the same callback function parameters. The callback function parameters 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:

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

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

// insert a red BMW, green BMW and blue BMW
carsModel.create([{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 _id 51e045b39376f4a73c6fd7e0
carsModel.findOne({_id:"51e045b39376f4a73c6fd7e0"}, (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
carsModel.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
carsModel.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
carsModel.delete({colour:"red"}, (error, data) =>
{
})    
distinct(fieldName, projection, 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
// In this example, we only project the department field
carsModel.distinct("department", {department:1}, (error, data) =>
{
})    

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

Open the server_side_routing project from the previous section in these notes. Change the code so that the cars data is held in a MongoDB database.

"Cars" Worked Example

The full project code for the "Cars" Worked Example that is described below 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/server.js

// Server-side global variables
require(`dotenv`).config({path:`./config/.env`})


// Database
require(`./config/db`)


// Express
const express = require(`express`)
const app = express()

app.use(require(`body-parser`).json())
app.use(require(`cors`)({credentials: true, origin: process.env.LOCAL_HOST}))


// Routers
app.use(require(`./routes/cars`))


// Port
app.listen(process.env.SERVER_PORT, () => 
{
    console.log(`Connected to port ` + process.env.SERVER_PORT)
})


// Error 404
app.use((req, res, next) => {next(createError(404))})

// Other errors
app.use(function (err, req, res, next)
{
    console.error(err.message)
    if (!err.statusCode) 
    {
        err.statusCode = 500
    }
    res.status(err.statusCode).send(err.message)
})

Initialise the database in the app.

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>