Copyright Derek O'Reilly, Dundalk Institute of Technology (DkIT), Dundalk, Co. Louth, Ireland.
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.
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.
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:
...
require(`./config/db`)
...
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:
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)
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.
We can add validation to a schema type:
grade: {type:Number, required:true}
grade: {type:Number, min:0, max:100} validFrom: {type:Date, min:2020-01-01, max:2020-12-31}
model: {type:String, minlength:4, maxlength:10} email: {type:String, match:/^([\w-\.]+@([\w-]+\.)+[\w-]{2,4})?$/} // field must match the regular expression department: {type:String, enum:["IT", "HR", "Sales"]} // department must be IT, HR or SalesWe can place an enum in an variable. For example:
const departments = "IT,HR,Sales".split(",") department: {type:String, enum:departments}
We can combine appropriate validators. For example, the age below is required and has a min value:
age:{type:Number, required:true, min:18}
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}}
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"]}
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}
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}
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
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.
{} // match all documents
{model:"BMW"} // model is "BMW"
{model:"BMW", colour:"red"} // model is "BMW" and colour is "red"
{$or:[{color:"red"}, {color:"blue"}]} // colour is "red" or "blue"
{model:"BMW", $or:[{colour:"red"}, {colour:"blue"}]} // model is "BMW" AND (colour is "red" OR "blue")
// less than {salary:{$lt:50000}} // salary < 50000
// less than or equal to {salary:{$lte:50000}} // salary <= 50000
// greater than {salary:{$gt:50000}} // salary > 50000
// greater than or equal to {salary:{$gte:50000}} // salary >= 50000
// equal to {salary:{$eq:50000}} // salary === 50000
// not equal to {salary:{$ne:50000}} // salary !== 50000
{colour:{$in:["red", "green", "blue"]}} // colour is red, green or blue
{colour:{$nin:["red", "green", "blue"]}} // colour is NOT red, green or blue
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 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:
... const carsModel = require(`../models/cars`) ...
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.
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:
// insert a document into a collection carsModel.create({model:"BMW", colour:"red"}, (error, data) => { })
// 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) => { })
// 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) => { })
// 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) => { })
// 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 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) => { })
// 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.
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.
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.
We set up our MongoDB database in the file below:
# 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
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-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.
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.
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:
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.
Copyright Derek O' Reilly, Dundalk Institute of Technology (DkIT), Dundalk, Co. Louth, Ireland.