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 MacMondoDB Atlas is a cloud based service that needs no installation.
Mongo Shell
To open the Mongo Shell
- Start Mongo in the background
brew services start mongodb-community@5.0 - Start the mongo shell
mongo
Mongo Shell Keywords
- db shows the current database we are using.
- show dbs shows all databases.
- use {db name} Creates a new database / switches to the existing database.
- show collections shows the collections in the current database.
- db.{collectionName}.insert() Insert 1 or many documents into a collection.
- db.{collectionName}.find() Search collections.
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.
- To insert data into a collection (and create the collection if it does not exist);
// insert 1 document
db.{collectionName}.insert({keyValue pairs of data})
// insert many documents
db.{collectionName}.insert([{keyValue pairs of data}, {keyValue pairs of data}])
Finding/Querying data with Mongo
- To search for data within a collection passing through some filter
// to find all that matches the filter
db.{collectionName}.find({name: 'Mavi'})
// to find ONE that matches the filter
db.{collectionName}.findOne({name: 'Mavi'})
Updating data with Mongo
- To update data within a collection;
// update 1 document ( First value = to find the right document, Second value with '$set:' changes that value )
db.{collectionName}.updateOne({name: 'Charlie'}, {$set: { age: 4 }, $currentDate: { lastModified: true }})
// update many documents matching the search criteria
db.{collectionName}.updateMany({catFriendly: true}, {$set: { isAvailable: false }})
// replaces a whole document with new data but keeping the id
db.{collectionName}.replaceOne()
Deleting data with Mongo
- To delete data within a collection;
// delete 1 document
db.{collectionName}.deleteOne({name: 'Charlie'})
// delete many documents matching the search criteria
db.{collectionName}.deleteMany({catFriendly: true})
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
- To find documents based on nested values
db.{collectionName}.find({'personality.childFriendly': true }) - To find documents filtering multiple values
db.{collectionName}.find({'personality.childFriendly': true, age: 10 })
Connecting to Mongo with Mongoose
Mongoose is an ODM that helps us connect Node and Javascript to Mongo
- ODM (Object data mapper) ODMs like Mongoose map documents coming from a database into usable Javascript objects.
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- Initialize NPM npm init -y
- In terminal, npm i mongoose
- Create an index.js file
- Require Mongoose
const mongoose = require('mongoose'); - 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.
- 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
}); - 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
- Add the data on the JS side;
const amadeus = new Movie ({
title: 'Amadeus',
year: 1986,
score: 9.2,
rating: 'R'
}); - Save the data to MongoDB
amadeus.save();
Adding many pieces of data to Mongo using Mongoose
- 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
- Install NPM
- Install Express, EJS & Mongoose
- Make index.js file
- Make a views folder
- Do a basic setup in the index.js file
- require express
- Execute express
- Require path
- Set views directory
- Set view engine to EJS
- Listen on port 3000
- Connect Mongoose
- Require Mongoose
- mongoose.connect with error handling / connect to correct db
- Open up the Mongo demon in the terminal in a new tab
Creating our model
- Make a new folder called models and a file within that folder called product.js
- In product.js
- Require Mongoose
- Define our Schema
- Compile our model
- Export the model to use it in a different file
module.exports = Product; - Back in index.js
- Require the product.js
const Product = require('./models/product'); - Create a Seeds file to seed data for dev purposes seperately to the rest of the app
- Create seed.js in the main directory
- In seeds.js
- require Mongoose
- require product model
- connect to Mongo db via Mongoose
- Create some products and save
Products Index page
- In the index.js file
- Create a get route for all products (/products)
- Create a products folder in the views folder
- Create an index.ejs file in the products folder
- In the get route, render the ejs file and pass through the products.
- Within index.ejs, create webpage for the products get route
- Display products in the index.ejs file
Product Details page
- In the index.js file
- Create a get route for the product details pages (/products/:id)
- Find product by ID
- Render the correct EJS file passing through the product
- Create an ejs file in the products folder within the view directory.
- Build page in the ejs file.
Creating products
- In index.js
- Make a get request to a new route to the new product page (/products/new)
- Render a page with a form to submit new products
- Create a new.ejs file in the views directory
- In the new.ejs file
- Make a form with action set to /products and method set to POST
- In the index.js file
- Make a post request to a post route (/products)
- Set app.use url encoded to extended true to be able to parse form data.
- Console.log req.body to see if the form data gets parsed.
- Create a new product with the info from req.body
- Save the new product to the db
- Redirect the user to the show page
Updating products
- In index.js
- Create a get request to /products/:id/edit
- Get the ID from the URL
- Find product by ID
- Render edit page, passing through the product
- Create an edit.ejs file in the views directory
- In the edit.ejs file
- Create a form with the action /products/product._id and method to POST
- In the form, set all fields values to the product information.
- In the index.js file
- Create a put request to /products/:id (same as action in form)
- Install npm package - method override
- In index.js file
- Require method override package.
- App.set the method override package
- In the edit.js file
- Change the form action to - /products/product._id?_method=PUT to overide the forms method to PUT.
- In the index.js file > within the PUT request - we need to update the product using Mongoose
- Get the id from the URL
- Find the product by ID and update with req.body + runValidators: true and new: true
- Redirect to show page
Deleting products
- On the product show page, show.ejs file
- Create a form with action to /products/productID?_method=DELETE and method set to POST
- In index.js
- Create a delete request to /products/:id
- Get the ID from the URL
- Find the product by ID and delete
- Redirect back to index page
Filtering by category
- Add a category link in the show page
- In the get request to /products
- Get category from req.query
- Create if/else statement
- If
- find products with category
- Render products page passing through the product
- Else
- Find all products
- 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
- Favour embedding unless there is a reason not to
- Needing to access an object on its own is a compelling reason not to embed
- 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.
- 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.
- 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.
- 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.
- Create a get route to /farms/:id/products/new where we rendered the new products form.
- Updated the action method in the form to farms/:id/products
- Create a post route for the form data submission to /farms/:id/products and within the post route,
- Find farm by ID
- Create new product
- Add the products into the farm model
- Add the farm into the products model
- Save to both product and farm seperately
- Redirect back to farms show page
farm.products.push(product)
product.farm = farm
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.
- Set up the delete route to delete the relevant data.
- Set up the delete form/button and fake the delete method with method override
- 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