Mongoose

CSCI-UA.0480-008

NoSQL Database, ODM

What's a NoSQL database again??

  • a database that doesn't model data using tables and relations between those tables
  • instead, usually a key value store
  • or a document store


And what's an ODM?

  • object document mapper
  • maps application objects (objects in your code) to documents in your database
    • allows CRUD operations on those documents


We're using MongoDB as our database and Mongoose as our ODM

Mongoose

mongoose

Vocabulary

What's a document… and what's a collection?

In MongoDB

  • document - a single row or object in your database (like… an instance of a pizza or cat)
  • collection - a group of documents, similar to a table in a relational database

What's a schema, model… and object?


In Mongoose…

  • schema - describes a collection, provides properties and other constraints (think class)
  • model - built from a schema, a constructor that allows you to create objects (think constructor for class)
  • objects/model instances - represent a document

Designing a Data Model

Let's try designing movies. What are some properties that a Movie should have?

  • title
  • year
  • director
    • maybe first name
    • maybe last name

Embedded vs Reference

So the director can be:

  • just a first name and last name in your movie document
  • a separate document embedded in your movie document
  • a separate document referenced by your movie document


See mongodb's docs on data modeling.


What are the advantages / disadvantages of each?

Back to Mongoose - Schemas

A schema is analogous to a collection. We create a schema with the mongoose.Schema constructor.

  • the convention is that your schema's name will match a lowercase, plural collection in your database
  • the Schema constructor takes an object with keys as names of keys that the documents created from this schema will have
  • …and values that represent the configuration of these keys (for example, type)

Models

Once you have a schema, you can then register a model. A model is a constructor for creating objects that represent MongoDB Documents.

Instance Methods

  • save (create a new document)


Static Methods

  • find
  • findOne
  • findOneAndUpdate

A Note About Warnings

You may get some deprecation warnings

  1. DeprecationWarning: Mongoose: mpromise (mongoose's default ...
    • this basically means that you have to pick which promise library to use
    • promises are a way of handling async operations without nexted callbacks
    • to use ES6 native promises: mongoose.Promise = global.Promise;
  2. DeprecationWarning: open() is deprecated in mongoose...
    • mongoose uses some deprecated calls for connecting to the database
    • for now, add this second argument to mongoose.connect
    • {useMongoClient: true}

Plugins

Some Schema / model functionality is so common that they're implemented on multiple schemas

  • to add functionality on the Schema level, you can use a plugin
  • from the mongoose docs:
    • "Schemas are pluggable, that is, they allow for applying pre-packaged capabilities to extend their functionality. This is a very powerful feature."
  • for example, some useful pluggable functionality for a schema may be:
    1. last modified
    2. access control
    3. …and slugs

Slugs and Plugins (Slug-ins?)

So… one useful plug-in is mongoose-url-slugs

  • can be used to generate a slug (human readable string that's unique for each document) for all of your objects
  • without having to manually specify slug in the schema!


To use:

  1. install via npm install
  2. require
  3. call SchemaName.plugin(...) to activate the plugin for that schema

Pizza

Let's try creating a schema for a pizza and toppings.

  • it should allow pizzas to have a size and crust
  • it should associate pizzas with toppings
  • the pizza should have a short-name (a slug)
  • toppings should have a name, and some way of noting whether or not you'd like 'extra' toppings

{
	size: 'medium',
	crust: 'thin',
	slug: 'medium-thin-2'
	toppings: [{name:'mushroom', extra:true}, {name:'peppers'}]
}

Let's Start With Some Setup

Make sure that you have the required modules for connecting to the database… and creating a slug!

  • mongoose
  • mongoose-url-slugs



npm install --save mongoose mongoose-url-slugs

Require and connect…


const mongoose = require('mongoose'),
	URLSlugs = require('mongoose-url-slugs');

// more stuff goes here

mongoose.model('Pizza', Pizza);
mongoose.model('Topping', Topping);
mongoose.connect('mongodb://localhost/pizzadb');

Types / Embedded Documents

One way to define relationships is to embed one document in another…

  • for example, this specifies that field Foo contains an Array / list of Bar objects
  • Foo: [Bar]


Additionally, instead of specifying the type outright, you can use an object that defines some field specifications:

  • type
  • max
  • min
  • required
  • default (for default value)

Your Schema

Schemas represent collections (tables). Notice the different ways of specifying the type of a field:


const Topping = new mongoose.Schema({
	name: String,
	extra: {type: Boolean, default:false}
});

const Pizza = new mongoose.Schema({
	size: {type: String, enum: ['small', 'medium', 'large']},
	crust: String,
	toppings: [Topping]
});

// note that we left out slug from the schema... 
// (the plugin will add it for you!)
// this should go before registering model!
Pizza.plugin(URLSlugs('size crust'));

Models

With mongoose, a model allows you to:

  • create new instances and save them
  • find saved instances (using a static method)
  • update existing instances



Pizza = mongoose.model('Pizza');

Creating and Saving


const pizza1 = new Pizza({
	size: 'small',
	crust: 'thin'
});

pizza1.save(function(err, pizza, count) {
	console.log('made me some pizza', pizza, count, err);
});

// call mongoose.disconnect() in callback function to close
// database connection;

Finding / Retrieving

Ok… just like the commandline client, we can use find:


// find all (try with query/criteria)
Pizza.find(function(err, pizzas, count) {
	console.log(err, pizzas, count);
});

Notice that we get back an Array!

Finding Only One!

But I only want one! Sometimes it's annoying to have to index into an Array if you only want one of something, so there's also findOne


// find only one (returns first)
Pizza.findOne({slug: 'small-2' }, function(err, pizza, count) {
	console.log(err, pizza, count);
});

Finding Then Updating

In Mongoose… instead of using the push operator (like in the commandline client), we have a method, push, that can be called on a property if it represents a list / Array of embedded values:


// update one after finding (hello callbacks!)
Pizza.findOne({slug: 'small-2' }, function(err, pizza, count) {
    // we can call push on toppings!
	pizza.toppings.push({name: 'mushroom'});
	pizza.save(function(saveErr, savePizza, saveCount) {
		console.log(savePizza);	
	});
});

Finding Then Updating Take Two

But of course… we can actually use push in an update query. In this case, we're using findOneAndUpdate to do the find and update all at once!


// find one and update it; maybe better than previous?
// ...notice $push?
Pizza.findOneAndUpdate({slug:'small-2'}, {$push: {toppings: {name:'peppers'}}}, function(err, pizza, count) {
	console.log(err, pizza, count);
});

Finding by Embedded Documents

We can also adjust our query to find by an embedded document. In this case, we use the property of the list of embedded documents… and use another object that describes the embedded document that we'd like to match.


Pizza.find({toppings: {name:'mushroom'}}, function(err, pizzas, count) {
	console.log(pizzas);
});

Finding and Updating Multiple Embedded Documents

Notice that when we update an embedded document, before we save the parent, we have to let mongoose know that we made changes to embedded documents. (shrug)


Pizza.findOne({slug:'small-2'}, function(err, pizza, count) {
	for (let i = 0; i < pizza.toppings.length; i++) {
		pizza.toppings[i].extra = true;
	}
	pizza.markModified('toppings');
	pizza.save(function(err, modifiedPizza, count) {
		console.log(err, modifiedPizza);
	});
});

(whew!)