MongoDB documentation

SQL vs NoSQL

SQL Databases

Structured Query language databases are relational databases. We pre-define a schema of tables before we insert anything.

NoSQL Databases

NoSQL databases do not use SQL. There are many types of NoSQL databases, including document, key-value and graph stores.

Installing MongoDB

MongoDB Installation for Mac

MondoDB Atlas is a cloud based service that needs no installation.

Mongo Shell

To open the Mongo Shell

  1. Start Mongo in the background
    brew services start mongodb-community@5.0
  2. Start the mongo shell
    mongo

Mongo Shell Keywords

Inserting with Mango

MongoDB uses collections within the db. Collections are collections of data within the db. For example, having a collection of users or cities.

If we insert data into a collection that does not yet exist, that collection will be made for us.

Finding/Querying data with Mongo

Updating data with Mongo

Deleting data with Mongo

Additional Mongo operators

A list of comparison operators such as greater than or equal to as well as more complex operators can be found here

Connecting to Mongo with Mongoose

Mongoose is an ODM that helps us connect Node and Javascript to Mongo

Mongoose provides ways for us to model out our application data and define a schema. It offers easy ways to validate data and build complex queries from the comfort of JS.

Installing Mongoose and connecting to a Mongo database

Mongoose Quick Start Guide
  1. Initialize NPM npm init -y
  2. In terminal, npm i mongoose
  3. Create an index.js file
  4. Require Mongoose
    const mongoose = require('mongoose');
  5. Connect to MongoDB database and handle error catching with then/catch
    mongoose.connect('mongodb://localhost:27017/{dbName}', {
    useNewUrlParser: true,
    useUnifiedTopology: true
    }) .then(() => {
    console.log('Connection open');
    }).catch(err => {
    console.log('Oh no, error');
    console.log(err)
    })

Mongoose models

Models are JS classes we make with the assistance of Mongoose that represent information in the Mongo Database/collection

For every collection we plan to use in the Mongo Database, we will need to define a model for each one.

  1. Define a Schema -
    A schema is mapping different data in a collection to a data type such as string or number on the JS side.
    const movieSchema = new mongoose.Schema ({
    title: String,
    year: Number,
    score: Number,
    rating: String
    });
  2. Tell Mongoose we want to make a model from the Schema
    // Pass in a string that names our model, then pass in the schema
    const Movie = mongoose.model('Movie', movieSchema);

Adding data to Mongo using Mongoose

  1. Add the data on the JS side;
    const amadeus = new Movie ({
    title: 'Amadeus',
    year: 1986,
    score: 9.2,
    rating: 'R'
    });
  2. Save the data to MongoDB
  3. amadeus.save();

Adding many pieces of data to Mongo using Mongoose

  1. Use the insert many method (No need to save to MongoDB seperately)
    Movie.insertMany([{
    title: 'Amelie',
    year: 2001,
    score: 8.3,
    rating: 'R'
    },
    {
    title: 'Alien',
    year: 1979,
    score: 8.1,
    rating: 'R'
    },
    {
    title: 'The Iron Giant',
    year: 1999,
    score: 7.5,
    rating: 'PG'
    },
    {
    title: 'Stand By Me',
    year: 1986,
    score: 8.6,
    rating: 'R'
    },
    {
    title: 'Moonrise Kingdom',
    year: 2012,
    score: 7.3,
    rating: 'PG-13'
    }
    ])

Finding/Querying with Mongoose

When finding or querying, it takes time for JS to find so we treat this like a promise. the .find method is treated as a 'thenable' query.

// to find all movies (without the .then, it returns a large object with lots of other data)
Movie.find( {} ).then(data => console.log(data));

// to find all movies with a rating of PG-13
Movie.find( {rating: 'PG-13'} ).then(data => console.log(data));

// to find one movie
Movie.findOne( {} ).then(data => console.log(data));

// to find a movie by ID
Movie.findById( {ID} ).then(data => console.log(data));

Updating with Mongoose

We need to find and then update

We can use the .update method to update one or many

The below methods DO NOT return the updated object.

// to update the first matched query
Movie.updateOne( {title: 'Amadeus'}, {year: 1984} ).then(data => console.log(data));

// to update many
Movie.updateMany( {title: {$in: ['Amadeus', 'Stand By Me']}}, {score: 10} ).then(data => console.log(data));

The below methods DOES return the updated object.

// to update the first matched query
Movie.findOneAndUpdate( {title: 'The Iron Giant'}, {score: 7.0}, {new: true} ).then(data => console.log(data));

Deleting with Mongoose

// to remove 1 thing
Movie.deleteOne( {title: 'Amelie'} ).then(data => console.log(data));

// to remove many things
Movie.deleteMany( {year: {$gte: 1999}} ).then(data => console.log(data));

// to get the removed object returned to us
Movie.findOneAndDelete( {title: 'Alien'} ).then(data => console.log(data));

Mongoose Schema Validations

In the above schema example, those are simplified versions of a schema, we can add additional validations to schema's which are more common.

const productSchema = new mongoose.Schema ({
name: {
type: String,
required: true,
maxLength: 20
},
price: {
type: Number,
required: true,
min: 0
},
onSale: {
type: Boolean,
default: false
},
categories: {
type: [String],
default: ['cycling']
},
qty: {
online: {
type: Number,
default: 0
},
inStore: {
type: Number,
default: 0
}
},
size: {
type: String,
enum: ['S', 'M', 'L']
}
})

Validating Mongoose Updates

In Mongoose, by default, Schema validations do not apply when updating something, only when creating something. So we need to pass the runValidators option when updating something.

// to make schema validations work for updates
Movie.findOneAndUpdate( {title: 'The Iron Giant'}, {score: 7.0}, {new: true, runValidators: true} ).then(data => console.log(data));

Mongoose Validation Errors

We can add error messages within the Schema validations but this method is not so common.

price: {
type: Number,
required: true,
min: [0, 'The price must be above 0']
},

Model Instance Methods (Adding custom methods)

Instance Methods are methods for particular instances such as a product, however Static Methods seen below are methods for an entire model, such as find product by category.

We need to add out custom methods in between where we define our Schema and where we define our model

// Creating custom instance methods
productSchema.methods.toggleOnSale = function () {
this.onSale = !this.onSale;
this.save();
}

productSchema.methods.addCategory = function (newCat) {
this.categories.push(newCat);
return this.save();
}

// Using custom methods
const findProduct = async () => {
const foundProduct = await Product.findOne({
name: 'Mountain Bike' });
console.log(foundProduct)
await foundProduct.toggleOnSale();
console.log(foundProduct)
await foundProduct.addCategory('Outdoors')
}

findProduct();

Adding Model Static Methods

Static Methods are methods that live on the model itself, not just on instances of the model, meaning that static methods are used across the whole model, not on 1 particular object.

// Creating custom static methods
productSchema.statics.fireSale = function () {
return this.updateMany({}, {onSale: true, price: 0})
}

// Using custom static methods
Product.fireSale().then(res => console.log(res))

Mongoose Virtuals

Virtuals allows us to add properties to a schema that does not exist in the database itself. For example, if we store first name and last name in the db but not full name, we can use virtuals to act as if we have full name in the db.

We could just use custom functions to do the same thing, but virtuals behave like an actual property.

Place virtuals between the Schema and the model in the code.

const personSchema = new mongoose.Schema ({
first: String,
last: String
})

// Defining virtuals
personSchema.virtual('fullName').get(function() {
return `${this.first} ${this.last}`
})

// Now we can access Person.fullName as a method on the JS side.

Defining Mongoose Middleware

Mongoose gives us the ability to run code, before or after certain Mongoose methods are called. For example, run some code right before something is removed or before something is saved etc. We can run a Pre or Post hooks before or after methods.

For example, if a user removes their FB profile, FB would need to then remove all comments, photos and data of that user, we can use Mongoose Middleware for this.

More information can be found here

// Defining middleware rules
personSchema.pre('save', async function () {
console.log('About to save!')
})

personSchema.post('save', async function () {
console.log('Just saved!')
})

Express + Mongoose

Express + Mongoose basic setup

  1. Install NPM
  2. Install Express, EJS & Mongoose
  3. Make index.js file
  4. Make a views folder
  5. Do a basic setup in the index.js file
    1. require express
    2. Execute express
    3. Require path
    4. Set views directory
    5. Set view engine to EJS
    6. Listen on port 3000
  6. Connect Mongoose
    1. Require Mongoose
    2. mongoose.connect with error handling / connect to correct db
    3. Open up the Mongo demon in the terminal in a new tab

Creating our model

  1. Make a new folder called models and a file within that folder called product.js
  2. In product.js
    1. Require Mongoose
    2. Define our Schema
    3. Compile our model
    4. Export the model to use it in a different file
      module.exports = Product;
  3. Back in index.js
    1. Require the product.js
      const Product = require('./models/product');
  4. Create a Seeds file to seed data for dev purposes seperately to the rest of the app
    1. Create seed.js in the main directory
    2. In seeds.js
      1. require Mongoose
      2. require product model
      3. connect to Mongo db via Mongoose
      4. Create some products and save

Products Index page

  1. In the index.js file
    1. Create a get route for all products (/products)
  2. Create a products folder in the views folder
  3. Create an index.ejs file in the products folder
  4. In the get route, render the ejs file and pass through the products.
  5. Within index.ejs, create webpage for the products get route
  6. Display products in the index.ejs file

Product Details page

  1. In the index.js file
    1. Create a get route for the product details pages (/products/:id)
    2. Find product by ID
    3. Render the correct EJS file passing through the product
  2. Create an ejs file in the products folder within the view directory.
  3. Build page in the ejs file.

Creating products

  1. In index.js
    1. Make a get request to a new route to the new product page (/products/new)
    2. Render a page with a form to submit new products
    3. Create a new.ejs file in the views directory
  2. In the new.ejs file
    1. Make a form with action set to /products and method set to POST
  3. In the index.js file
    1. Make a post request to a post route (/products)
    2. Set app.use url encoded to extended true to be able to parse form data.
    3. Console.log req.body to see if the form data gets parsed.
    4. Create a new product with the info from req.body
    5. Save the new product to the db
    6. Redirect the user to the show page

Updating products

  1. In index.js
    1. Create a get request to /products/:id/edit
    2. Get the ID from the URL
    3. Find product by ID
    4. Render edit page, passing through the product
    5. Create an edit.ejs file in the views directory
  2. In the edit.ejs file
    1. Create a form with the action /products/product._id and method to POST
    2. In the form, set all fields values to the product information.
  3. In the index.js file
    1. Create a put request to /products/:id (same as action in form)
  4. Install npm package - method override
  5. In index.js file
    1. Require method override package.
    2. App.set the method override package
  6. In the edit.js file
    1. Change the form action to - /products/product._id?_method=PUT to overide the forms method to PUT.
  7. In the index.js file > within the PUT request - we need to update the product using Mongoose
    1. Get the id from the URL
    2. Find the product by ID and update with req.body + runValidators: true and new: true
    3. Redirect to show page

Deleting products

  1. On the product show page, show.ejs file
    1. Create a form with action to /products/productID?_method=DELETE and method set to POST
  2. In index.js
    1. Create a delete request to /products/:id
    2. Get the ID from the URL
    3. Find the product by ID and delete
    4. Redirect back to index page

Filtering by category

  1. Add a category link in the show page
  2. In the get request to /products
    1. Get category from req.query
    2. Create if/else statement
    3. If
      1. find products with category
      2. Render products page passing through the product
    4. Else
      1. Find all products
      2. Render products page passing through the product

Data relationships with Mongo

This is about how to structure our database and relationships of data within MongoDB.

In SQL databases, you will have different tables of data, where you can reference other tables to connect a relationship.

One to few

One to few relationships are things such as stored addresses for an eCommerce user. They will only store a few addresses.

These are typically embed directly in the document without the need to split up documents and reference other documents.

One to many

Store the data seperately, then store references to documents IDs inside the parent document.

const farmSchema = new mongoose.Schema({
name: String,
city: String,
products: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Product' }]
})

{
farmName: 'Full Belly Farms',
location: 'Guinda, CA',
products: [
ObjectID('165738910387'),
ObjectID('897656412780'),
ObjectID('451267738913'),
]
}

// the above is where the products data within the Farm Schema is referencing a products Schema instead of copy and pasting the same data into the farm schema. The ref attribute references which model the data is in.

Mongoose Populate

When Mongo models reference other data from other models, the object ID is used, but sometimes we want to see all the data we are referencing, instead of just the object ID. This is called Populating

Farm.findOne({ 'Full Belly Farms' })
.populate('Products')
.then(farm => console.log(farm))

// the populate method needs the property name of what we want to populate to be passed through. Then Mongoose populates the data in the referenced model.

One to bajillions

With thousands of documents, or more, it's more efficient to store reference to the parent on the child document. (Instead of storing the reference of the child, in the parent document)

{
tweetText: 'LOL I just crashed my car because I was busy tweeting',
tags: ['Stupid', 'Moron', 'YOLO'],
user: ObjectId('2343434123') }

// We can reference the parent element within the child element. (The opposite to one to many) to be more efficient.

Mongo Schema Design

6 rules of thumb by MongoDB

  1. Favour embedding unless there is a reason not to
  2. Needing to access an object on its own is a compelling reason not to embed
  3. Arrays should not grow without bound. If there are more than a couple hundred documents on the 'many' side, don't embed them. If there are more than a few thousand documents on the 'many' side, dont use an array of Object ID references. High cardinality arrays are a compelling reason not to embed.
  4. Don’t be afraid of application-level joins: if you index correctly and use the projection specifier (as shown in part 2) then application-level joins are barely more expensive than server-side joins in a relational database.
  5. Consider the write/read ratio when denormalizing. A field that will mostly be read and only seldom updated is a good candidate for denormalization: if you denormalize a field that is updated frequently then the extra work of finding and updating all the instances is likely to overwhelm the savings that you get from denormalizing.
  6. As always with MongoDB, how you model your data depends – entirely – on your particular application’s data access patterns. You want to structure your data to match the ways that your application queries and updates it.

Mongo relationships with Express

Connecting Farms model with products model

In this example, we want to add products to a particular farm.

  1. Create a get route to /farms/:id/products/new where we rendered the new products form.
  2. Updated the action method in the form to farms/:id/products
  3. Create a post route for the form data submission to /farms/:id/products and within the post route,
    1. Find farm by ID
    2. Create new product
    3. Add the products into the farm model
    4. Add the farm into the products model
    5. farm.products.push(product)
      product.farm = farm
    6. Save to both product and farm seperately
    7. Redirect back to farms show page

Deletion Mongoose Middleware

If we have related Mongo collections, when someone deletes their account etc, we need to manage how we delete the relevant data across the connected models.

  1. Set up the delete route to delete the relevant data.
  2. Set up the delete form/button and fake the delete method with method override
  3. Setup a middleware in the models schema before compiling the model.
    // creating a middleware for after the delete query is made
    farmSchema.post('findOneAndDelete', async function (farm) {
    // run if there are products
    if (farm.products.length) {
    // deleting all products indluded in the farm
    await Product.deleteMany({_id: {$in: farm.products}})
    }
    })

List of sources

    Colt Steeles web developer bootcamp 2021