Section 10: MongoDB and Promises (Task App)

70. Databases and Advanced Asynchronous Development

71. MongoDB and NoSQL Databases

We can find MongoDB at mongodb.com.

    • A “collection” is similar to an SQL “table”.
    • A “document” is similar to an SQL “row”.
    • A “field” is similar to an SQL “column”.

72. Installing MongoDB on MacOS and Linux

Navigate to https://www.mongodb.com/try/download/community and download the MongoDB Community Server.

In the downloads directory, extract the .tgz file by double clicking on it.  I renamed the directory that was created to mongodb and moved the entire directory to my home directory.

Mongodb needs a place to store the databases, so we create a directory in our home directory named mongodb-data.

From the terminal, start the mongodb server (mongod) using the following command.  Note the dbpath flag specifies the location where we want to store the mongodb database files.

$ /Users/eric/mongodb/bin/mongod --dbpath=/Users/eric/mongodb-data

73. Installing MongoDB on Windows

74. Installing Database GUI Viewer

Navigate to robomongo.org and download the Robo 3T application.

Now start the Robo 3T application.

Press Create in the upper left corner to create a connection to our mongodb database.  Change the Name of the connection to Local MongoDB Database.  Note the port number, 27017.  Press Test, to confirm the connection settings are correct. and if successful, press save.

Next press Connect to connect Robo 3T to the mongodb database.

In the Robo 3T application, right click on the Name of the database connection in the upper left corner and select Open Shell.

Type the following JavaScript command in the shell and press the green execute arrow in the toolbar to see the version of the mongodb server that is Robo 3T is connected to.

db.version()

75. Connecting and Inserting Document

Navigate to mongodb.com.  Navigate to the documentation page and find the Mongodb Drivers (Develop Applications -> Drivers).  Click on Node.js to find the documentation for the Node.js library.  Find the API documentation. (we’ll refer to this documentation later).

In another browser tab google npm mongodb to find the npm mongodb package page.

Open your IDE.  We currently have the mongodb database running in the terminal and want to keep it running, so hit + to open another terminal.  Run the following commands to create a new npm project for our task application.

$ mkdir task-manager
$ cd task-manager
$ npm init -y
$ npm i mongodb@4.2.2

In the task-manager folder, create a file named mongodb.js file.  We’ll use this file to perform all of the CRUD operations (create, read, update, delete).  In the file, let’s get the mongodb object from the mongodb library and then get the MongoClient object from the mongodb object.  The MongoClient object will allow us to perform the CRUD operations.

const mongodb = require('mongodb')
const MongoClient = mongodb.MongoClient

Define the URL of the mongodb server on our local machine and a name for the database.

const connectionURL = 'mongodb://127.0.0.1:27017'
const databaseName = 'task-manager'

Now, our app can connect to the database using the MongoClient.

MongoClient.connect(connectionURL, (error, client) => {
    if(error) {
        console.log('Unable to connect to database')
        return
    }

    console.log('Connected correctly'); 
})

Save mongodb.js and test.  You should see on the console, ‘Connected correctly’.

$ node mongodb.js

Notice that after ‘Connected correctly’ is printed to the console, it appears as if the app is still running.  It is.  The connection stays open until we close it.  We can type CTRL+c to close the connection manually and shut down the node process.

In mongodb.js, remove the console.log showing the connection was created successfully and replace it with the following to create a reference to the database.

const db = client.db(databaseName);

Next, create a mongodb collection named users and add a document for ourselves.

db.collection('users').insertOne({
    name: 'Eric',
    age: 27
})

Test using the following command.  The application should print no output and should not return to the command prompt.

$ node mongodb.js

Let’s use Robo 3T to see the data.  In Robo 3T close the open tabs and when right-clicking on the database connection, choose Refresh.  We should see a new task-manager database listed.  Opening up the database, we see a collection named users was created.  We can view the documents in the collection by right-clicking on the users collection and selecting View Documents.

76. Inserting Documents

The insertOne() method is asynchronous, so we need to pass a callback if we want to handle errors or do something else after operation has completed.

db.collection('users').insertOne({
    name: 'Eric',
    age: 27
}, (error, result) => {

})

In the callback function, we’ll print a message to the console upon error.

db.collection('users').insertOne({
    name: 'Eric',
    age: 27
}, (error, result) => {
    if (error) {
        return console.log('Unable to insert document')
    }
})

The result object that is passed as the second argument has an ops property that we can use to determine the number of documents that were inserted.

db.collection('users').insertOne({
    name: 'Eric',
    age: 27
}, (error, result) => {
    if (error) {
        return console.log('Unable to insert document')
    }

    console.log(result);
})

From the command line we can restart the server and test the insert.  We will see that the another document was added and the result object that is printed has two properties: acknowledged and insertedID.

$ node mongodb.js

To insert many documents we can use the collection’s insertMany method.  Note that the first argument is an array of objects rather than a single object.

db.collection('users').insertMany([{
        name: 'Joe',
        age: 21
    }, {
        name: 'Gunther',
        age: 28
    }], 
    (error, result) => {
        if (error) {
            return console.log('Error inserting documents')
        }

        console.log(result.insertedCount + ' documents inserted')
})

Let’s create a new collection named tasks and insert three documents using insertMany.  Each document should have a description (string) and a completion status (boolean).

db.collection('tasks').insertMany([
    {desc: 'go to store', completed: false},
    {desc: 'do CS homework', completed: true},
    {desc: 'do laundry', completed: false}
],
(error, result) => {
    if (error) {
        return console.log('Unable to insert task documents')
    }
    console.log(result.insertedCount + ' documents inserted')
})

77. The ObectID

We can generate mongodb ids manually by using the ObjectId function which is a property of the mongodb object returned by the call to require that we made at the top of the script.

const mongodb = require(‘mongodb’)
const MongoClient = mongodb.MongoClient
const ObjectId = mongodb.ObjectId

Equivalently we can replace this code with the following destructured code.

const {MongoClient, ObjectId} = require('mongodb');

We can create our own id’s using the ObjectId function.

const id = new ObjectId()

Each id has information embedded in it.  From the mongodb reference (not api) documentation we can view the types of information embedded in an ObjectId.  The information consists of a timestamp, a random value and a counter.

The mongodb api documentation shows us that an ObjectId object has various methods that we can use on it.  One of them is getTimestamp.

console.log(id.getTimestamp())

We can now use the id when we insert a document into a collection by specifying the _id property in the object that we pass to insertOne.

db.collection('users').insertOne({ 
    _id: id,
    name: 'Eric', 
    age: 27 
}, (error, result) => { 
    ...

78. Querying Documents

The mongodb Collection object has functions findOne() and find() that allow us to query the database.  The first argument to findOne() is an object that allows us to input search parameters and the second argument is a callback function.

db.collection('users').findOne({
    name: 'Gunther'
}, (error, user) => {
    if (error) {
        return console.log('Unable to find')
    }
    console.log(user)
})

If we want to search by an id, we need to provide an Object id (not a string) in the search parameters.

db.collection('users').findOne({
    _id: new ObjectId("61bb5723194303dff52c1f22")
}, (error, user) => {
    if (error) {
        return console.log('Unable to find')
    }
    console.log(user)
})

To find multiple documents in a collection we use the find() function.  The first parameter is an object that includes search parameters but find() does not take a callback as a second argument.  The find() method returns a FindCursor object that we can use to view the data that was found using functions like count(), min(), and toArray().

The toArray() and count() methods do take a callback as an argument that we can use to view the requested data.

db.collection('users').find({
    age: 21
}).toArray((error, users) => {
    console.log(users)
})

db.collection('users').find({
    age: 21
}).count((error, count) => {
    console.log(count)
})

79. Promises

Promises replace the callback pattern.  They’re an enhancement to callbacks.

The following code demonstrates why and how callbacks are used.

// getData does some asynchronous work generating data.
// When it is done, it calls the callback with
// appropriate arguments.

const getData = (callback) => {
  setTimeout(() => {
    // simulate generating data
    const arr = [1,2,3]
    //const arr = []

    if (arr.length == 0) {
      return callback('No data found')
    }
    callback(undefined, arr)
  }, 2000)
}

// printsum() and printMin() cannot execute until getData has 
// generated the needed data. So we create them as callback functions.
const printSum = (error, data) => {
  if (error) {
    return console.log(error)
  }
  let sum = 0;
  for(let val of data) {
    sum += val;
  }
  console.log("sum: " + sum)
}

const printMin = (error, data) => {
  if (error) {
    return console.log(error)
  }
  let min = data[0]
  for(let val of data) {
    if (val < min) {
      min = val
    }
  }
  console.log("min: " + min)
}

// call getData, passing to it the callback
getData(printSum)
getData(printMin)

An equivalent segment of code that utilizes Promises is below.

const getData = new Promise((resolve, reject) => {
  setTimeout(() => {
    // simulate generating data
    const arr = [1,2,3]
    //const arr = []
  
    if (arr.length == 0) {
      return reject('No data found')
    }
    resolve(arr)
  }, 2000)
})

// printSum and printMin cannot execute until getData has
// generated the needed data.

const printSum = (data) => {
  let sum = 0;
  for(let val of data) {
    sum += val;
  }
  console.log("sum: " + sum)
}

const printMin = (data) => {
  let min = data[0]
  for(let val of data) {
    if (val < min) {
      min = val
    }
  }
  console.log("min: " + min)
}

const printError = (error) => {
  console.log(error);
}

getData.then(printSum).catch(printError)
getData.then(printMin).catch(printError)

Some terminology…

A Promise is “pending” until either resolve() or reject() is called.  If resolve() is called we say the Promise has been “fulfilled”, else we say it was “rejected”.

80. Updating Documents

To change a value in a document we can use the collection object’s updateOne() or updateMany() function.

The updateOne() function returns a Promise, so we don’t need to pass a callback to it, instead we’ll use the Promise that is returned to see the status of the call. Let’s call updateOne() to change Gunther’s name to Sam.

Note that the first argument to updateOne() is the filter object.  The second argument is a object of type UpdateFilter.  An UpdateFilter is an object that contains one or more predefined properties.  These properties specify how to update the documents in question.  We specify the $set property to set the value of the name field.

const updatePromise = db.collection('users').updateOne({
    _id: ObjectId("61bb5723194303dff52c1f22")
  }, {
  $set: {
    name: 'Sam'
  }
})

updatePromise.then((result) => {
    console.log(result)
  }).catch((error) => {
    console.log(error)
})

Since updateOne() returns a Promise and then() is called on a Promise, we can chain these two blocks of code together as follows.

db.collection('users').updateOne({
    _id: ObjectId("61bb5723194303dff52c1f22")
  }, {
    $set: {
      name: 'Sam'
  }
}).then((result) => {
  console.log(result)
}).catch((error) => {
  console.log(error)
})

To change all of the tasks to completed, we can select all documents that have a completed field set to false and change them to true.

db.collection('tasks').updateMany({
    completed: false
  }, {
  $set: {
    completed: true
  }
}).then((result) => {
  console.log(result)
}).catch((error) => {
  console.log(error)
})

81. Deleting Documents

To delete documents we use deleteOne() or deleteMany().  Let’s delete all users that are 28 years old.

db.collection('users').deleteMany({
  age: 28
}).then((result) => {
  console.log(result)
}).catch((error) => {
  console.log(error)
})

To delete the task that has the description ‘do laundry’.

db.collection('tasks').deleteOne({
  desc: 'do laundry'
}).then((result) => {
  console.log(result)
}).catch((error) => {
  console.log(error)
})

© 2021 – 2024, Eric McGregor. All rights reserved. Unauthorized use, distribution and/or duplication of this material without express and written permission from the author is strictly prohibited.