Skip to content

Instantly share code, notes, and snippets.

@frankstepanski
Last active June 12, 2021 00:12
Show Gist options
  • Save frankstepanski/0e9a0d53a4c065530aacb67c4ba123e9 to your computer and use it in GitHub Desktop.
Save frankstepanski/0e9a0d53a4c065530aacb67c4ba123e9 to your computer and use it in GitHub Desktop.
MongoDB and Mongoose

MongoDB and Mongoose

MongoDB is the most popular non-relational database in use today. It is a document-oriented database that can store data in the form of documents, which are similar to objects in javascript. Instead of tables in SQL, we have collections. Instead of columns in SQL, we have keys. Instead of rows in SQL, we have documents.

NoSQL databases like MongoDB are inherently more flexible than SQL databases as they do not need to follow a model. While a SQL table can only contain rows that conform to the specific schema of columns, collections in a NoSQL database can contain documents with different kinds of data that don't neatly fit into a schema. They may even contain arrays and objects that are nested.

Mongoose is a node module published on npm that not only provides a way to access a MongoDB database from a node server, but also adds a data schema to MongoDB collections.

While not being beholden to a schema may be a feature of a NoSQL database, it can also be a cause of confusion and error if data is not organized in a predictable structure. Mongoose aims to solve that by letting developers create schemas and use models created off of that schema to interact with MongoDB.

Connections to a MongoDB will look similiar to this:

const mongoose = require('mongoose');
const URL = "mongodb+srv://<user>:<password>@cluster0.ivtom.mongodb.net/<database>?retryWrites=true&w=majority";

mongoose.connect(URL, { useNewUrlParser: true, useUnifiedTopology: true });

CRUD - CREATE

A schema maps to a MongoDB collection.

It defines the shape of the documents within that collection. Schemas are building block for Models. They can be nested to create complex models. A model allows you to create instances of your objects, called documents.

We will create a person schema called personSchema having this prototype:

- Person Prototype -
--------------------
name : string [required]
age :  number
favoriteFoods : array of strings (*)

Creating the model will be like this:

let Schema = mongoose.Schema;

let personSchema = new Schema({
    name: String,
    age: Number,
    favoriteFoods: [String]
});

let Person = mongoose.model('Person', personSchema);

Create and Save a Record of a Model

We will create a function that creates a document instance using the Person model constructor. It passes to the constructor an object having the fields name, age, and favoriteFoods with their types conforming to the ones in the personSchema.

It then calls the method document.save() on the returned document instance and passes to it a callback using the Node convention.

This is a common pattern; all the following CRUD methods take a callback function like this as the last argument.

let createAndSavePerson = function (done) {
    var personInstance = new Person({name: 'Frank', age: 25, favoriteFoods: ['Sushi', 'Gluten free pasta']});
    personInstance.save((err, data) => err ? done(err) : done(null, data));
};

Create Many Records with model.create()

Sometimes you need to create many instances of your models, e.g. when seeding a database with initial data. Model.create() takes an array of objects like [{name: 'John', ...}, {...}, ...] as the first argument, and saves them all in the db.\

let arrayOfPeople = [
    {name: "Test", age: 24, favoriteFoods: ['Pizza', 'Cheese']},
    {name: "TestPerson", age: 22, favoriteFoods: ['Coke', 'Garlic Bread']}
];

let createManyPeople = function (arrayOfPeople, done) {

    Person.create(arrayOfPeople, (err, data) => {
        if (err) {
            return done(err);
        }
        return done(null, data);
    });
};

Use model.find() to Search Your Database

In its simplest usage, Model.find() accepts a query document (a JSON object) as the first argument, then a callback. It returns an array of matches. It supports an extremely wide range of search options.

let findPeopleByName = function (personName, done) {
    Person.find({name: personName}, (err, data) => {
        if (err) {
            return done(err);
        }
        return done(null, data);
    });
};

Use model.findOne() to Return a Single Matching Document from Your Database

Model.findOne() behaves like Model.find(), but it returns only one document (not an array), even if there are multiple items. It is especially useful when searching by properties that you have declared as unique.

let findOneByFood = function (food, done) {

    Person.findOne({favoriteFoods: food}, (err, data) => {
        if (err) {
            return done(err);
        }
        return done(null, data);
    });

};

Use model.findById() to Search Your Database By _id

When saving a document, MongoDB automatically adds the field id, and set it to a unique alphanumeric key. Searching by id is an extremely frequent operation, so Mongoose provides a dedicated method for it.

let findPersonById = function (personId, done) {

    Person.findById({_id: personId}, (err, data) => {
        if (err) {
            return done(err);
        }
        return done(null, data);
    });

};

Perform Classic Updates by Running Find, Edit, then Save

In the good old days, this was what you needed to do if you wanted to edit a document, and be able to use it somehow (e.g. sending it back in a server response). Mongoose has a dedicated updating method: Model.update(). It is bound to the low-level mongo driver. It can bulk-edit many documents matching certain criteria, but it doesn’t send back the updated document, only a 'status' message. Furthermore, it makes model validations difficult, because it just directly calls the mongo driver.

let findEditThenSave = function (personId, done) {
    let foodToAdd = 'hamburger';

    Person.findById(personId, function (err, data) {
        if (err) {
            done(err);
        }

        data.favoriteFoods.push(foodToAdd);
        data.save((err, data) => (err ? done(err) : done(null, data)));
    });
};

Perform New Updates on a Document Using model.findOneAndUpdate()

Recent versions of Mongoose have methods to simplify documents updating. Some more advanced features (i.e. pre/post hooks, validation) behave differently with this approach, so the classic method is still useful in many situations. findByIdAndUpdate() can be used when searching by id.

let findAndUpdate = function (personName, done) {

    Person.findOneAndUpdate({name: personName}, {$set: {age: 20}}, {new: true}, function (err, data) {
        if (err) {
            done(err);
        }
        done(null, data);
    });
};

Delete One Document Using model.findByIdAndRemove

findByIdAndRemove and findOneAndRemove are like the previous update methods. They pass the removed document to the db. As usual, use the function argument personId as the search key.

let removeById = function (personId, done) {

    Person.findOneAndRemove(personId, function (err, data) {
        if (err) {
            done(err);
        }
        done(null, data);
    });

};

Chain Search Query Helpers to Narrow Search Results

If you don’t pass the callback as the last argument to Model.find() (or to the other search methods), the query is not executed. You can store the query in a variable for later use. This kind of object enables you to build up a query using chaining syntax. The actual db search is executed when you finally chain the method .exec(). You always need to pass your callback to this last method.

let queryChain = function (done) {
    var foodToSearch = "burrito";

    Person.find({favoriteFoods: foodToSearch}).sort({name: "asc"}).limit(2).select("-age").exec(function (err, data) {
        if (err) {
            done(err);
        }
        done(null, data);
    });
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment