/intro-mongoose

[mongoose, js, databases, drm]

Primary LanguageJavaScriptOtherNOASSERTION

Intro to Mongoose

Learning Objectives

By the end of this lesson you should be able to...

  • Compare and contrast Mongoose with Mongo
  • Create Mongoose schemas & models
  • Integrate Mongoose with Express
  • Build out index, new, and create routes with Mongoose & Express

Why

Mongoose is an ODM, an Object Document Mapper. It maps documents in a database to JavaScript Objects, making modifying the database possible with easy to use JavaScript helper methods.

"Let's face it, writing MongoDB validation, casting and business logic boilerplate is a drag. That's why we wrote Mongoose."

—Creators of Mongoose.

Terminology

  • Schema: Similar to an object constructor, a Schema is a diagram or blueprint for what every object in the noSQL database will contain. Here's an example of a simple Address Book noSQL database schema:
  var ContactSchema = new Schema({
    firstName: String,
    lastName: String,
    address: String
    phoneNumber: Number,
    email: String,
    professionalContact: Boolean
  });

With the above Schema, we can expect all of our Address Book entries would have a first name, last name, address, and email address in the form of Strings. We can count on the phoneNumber to always be accepted, stored, and returned as a number. Lastly, the boolean value of Professional Contact will always be a true or false

  • Model: A model is a Schema that has been 'activated' with real data and is performing actions such as reading, saving, updating, etc.
  var Contact = mongoose.model('Contact', ContactSchema);

Schema vs. Model

"In mongoose, a schema represents the structure of a particular document, either completely or just a portion of the document. It's a way to express expected properties and values as well as constraints and indexes. A model defines a programming interface for interacting with the database (read, insert, update, etc). So a schema answers "what will the data in this collection look like?" and a model provides functionality like "Are there any records matching this query?" or "Add a new document to the collection". ""

-Peter Lyons Apr 8 '14 at 23:53

In other words, a Schema is like an empty portion plate: portion plate

That your lunchlady (the Model) will be filling to create new Instances of your meal: portion plate

Example (I do)

From the console:

mkdir mongoose_project
cd mongoose_project
npm init -y
npm install --save mongoose

We need to make sure MongoDB is running. From the console, enter this command:

mongod

Create a new Javascript file by typing touch index.js into the terminal.

Let's require mongoose and connect to our database.

var mongoose = require("mongoose");
mongoose.connect("mongodb://localhost/test");

Modeling

Let's create a Book model. A Book has a few different characteristics: title, author, and description.

To create a Book model we have to use a Schema:

var Schema = mongoose.Schema;
var BookSchema = new Schema({
    title: String,
    author: String,
    description: String
});

and finally create the model

var Book = mongoose.model('Book', BookSchema);

Check the docs to see all the different datatypes we can use in a Schema.

###Create: Building and Creating Documents

If you want to build a new Book, you can just do the following:

var book = new Book({title: "Alice's Adventures In Wonderland"});

Then you can still edit it before saving.

book.author = "Lewis Carroll";

Once you're done building you can save the book.

book.save();

Note: you can pass a function that will be called (aka a callback) once the book is done being saved.

If you want to build & save in one step you can use .create. Also we'll pass it a callback to execute once it's done creating the book.

Book.create({title: "The Giver"}, function (err, book) {
  console.log(book);
});

###Read

We can find books by any field, including author:

  Book.find({author: "Lewis Carroll"}, function (err, books) {
    console.log(books);
  });

We can find ALL by specifying any fields we're filtering for, aka an empty object:

Book.find({}, function(err, books){
  console.log(books);
});

Try out some of the other find methods.

.findOne();
.findById();

Reference the docs for more info on what you can do with Mongoose queries and models (use the left-hand side-bar).

###Destroy Removing a Document is as simple as Building and Creating.

Using the remove method:

Book.remove({ title: "The Giver" }, function(err, book) {
  if (err) { return console.log(err) };
  console.log("removal of " + book.title + " successful.");
});

Other removal methods include:

findByIdAndRemove();
findOneAndRemove();

Integrating Mongoose into Express

Well, that's nice. But let's see how mongoose plays with express by building a reminders app. called reminders

Lets create a brand new express application and grab up all the dependencies we'll need

$ mkdir reminders
$ cd reminders
$ npm init -y
$ npm install --save express ejs body-parser mongoose
$ touch index.js

The dependencies we'll be using for this app are:

  1. express - web Frameworks
  2. ejs - view engine
  3. body-parser - allows us to get parameter values from forms
  4. mongoose - our Mongo ODM

Let's first start by defining our schema, models and creating some seed data.

Folders/files:

$ mkdir db
$ mkdir models
$ touch models/reminder.js
$ touch db/seed.js

We will define the structure of our database using schemas

In models/reminder.js:

// requiring mongoose dependency
var mongoose = require('mongoose');

// defining schema for reminders
var ReminderSchema = new mongoose.Schema({
  title: String,
  body: String,
  createdAt: { type : Date, default: new Date() }
});
// define the model
var Reminder = mongoose.model("Reminder", ReminderSchema);
// export the model to any files that `require` this one
module.exports = Reminder;

Great! Now that we have an interface for our models, let's create a seed file so we have some data to work with in our application.

In db/seed.js:

var mongoose = require('mongoose');
var conn = mongoose.connect('mongodb://localhost/reminders');
var Reminder = require("../models/reminder");
Reminder.remove({}, function(err) {
  if (err) {
    console.log("ERROR:", err);
  }
})

var reminders = [
  {
    title: "Cat",
    body: "Figure out his halloween costume for next year"
  },
  {
    title: "Laundry",
    body: "Color-code socks"
  },
  {
    title: "Spanish",
    body: "Learn to count to ten to impress the ladies"
  }
];

Reminder.create(reminders, function(err, docs) {
  if (err) {
    console.log("ERROR:", err);
  } else {
    console.log("Created:", docs)
    mongoose.connection.close();
  }
});

Feel free to personalize the reminders to suit your own interests and errands.

Now run the seed file in order to add these default values to our Database, by typing node db/seed.js in the terminal.

Server Setup

Now we've got all of our models and seed data set. Let's start building out the reminders application. Let's update our main application file to include the dependencies we'll need.

In index.js:

// Dependencies
var express = require('express');
var app = express();
var mongoose = require('mongoose');
var bodyParser = require('body-parser');

// Configuration
mongoose.connect('mongodb://localhost/reminders');
process.on('exit', function() { mongoose.disconnect() }); // Shutdown Mongoose correctly
app.set("view engine", "ejs");  // sets view engine to EJS
app.use(bodyParser.json());  // allows for parameters in JSON and html
app.use(bodyParser.urlencoded({extended:true}));
app.use(express.static(__dirname + '/public'));  // looks for assets like stylesheets in a `public` folder
var port = 3000;  // define a port to listen on

// Controllers
var remindersController = require("./controllers/remindersController");

// Routes
app.get("/reminders", remindersController.index);

// Start server
app.listen(port, function() {
  console.log("app is running on port:", port);
});

Challenge: Build out your own server file. Make sure it works by running it with node or nodemon.

Reminders Index

As we can see on the last line of the code above, we have just one route. It's using remindersController.index as a callback, but it hasn't been defined yet. Let's define it now:

$ mkdir controllers
$ touch controllers/remindersController.js

And then add this into our new remindersController.js file:

var Reminder = require("../models/reminder")

var remindersController = {
  index: function(req, res) {
    Reminder.find({}, function(err, docs) {
      res.render("reminders/index", {reminders: docs});
    });
  }
}

module.exports = remindersController;

Now we're referncing an ejs view that doesn't exist yet. Lets create that now, as well as our layout view:

$ mkdir views
$ touch views/layout.ejs
$ mkdir views/reminders
$ touch views/reminders/index.ejs

In views/layout.ejs:

<!doctype html>
<html>
  <head>
    <link rel="stylesheet" type="text/css" href="/css/styles.css">
  </head>
  <body>
    <header>
      <h1><a href="/reminders">Reminder.ly</a></h1>
      <a href="/reminders/new">+ Reminder</a>
    </header>
    <hr>
    <!-- the below `triple-stash`, avoid escaping any html -->
   <%- body %>
  </body>
</html>

In views/reminders/index.ejs:

<ul>
  <% for(var i=0; i< reminders.length; i++) {%>
    <li class="reminder">
      <a class="reminder-title" href="<%= '/reminders/'+reminders[i]._id %>"><%= reminders[i].title %>:</a>
      <span class="reminder-body"><%= reminders[i].body %></span>
    </li>
  <% } %>
</ul>

Challenge: Create these template files in your own project. What happens when you try to go to localhost:3000/reminders?

New Reminder

We'll need to update our routes in index.js to add #new and #create actions.

app.get("/reminders", remindersController.index);
app.get("/reminders/new", remindersController.new);
app.post("/reminders", remindersController.create);

Our views/reminders/new.ejs should look something like this:

<h2>Create a New Reminder</h2>
<form action="/reminders" method="post">
  <label for="reminder-title">Title:</label>
  <input id="reminder-title" type="text" name="title">
  <label for="reminder-body">Body:</label>
  <input id="reminder-body" type="text" name="body">
  <input type="submit">
</form>

Finally our updated remindersController will be:

var Reminder = require("../models/reminder")

var remindersController = {
  index: function(req, res) {
    Reminder.find({}, function(err, docs) {
      res.render("reminders/index", {reminders: docs});
    });
  },
  new: function(req, res) {
    res.render("reminders/new")
  },
  create: function(req, res) {
    // strong params
    var title = req.body.title;
    var body = req.body.body;
    Reminder.create({title: title, body: body}, function(err, doc) {
      // if there there is an error: redirect to reminders#new; else: redirect to reminders#index
      err ? res.redirect("/reminders/new") : res.redirect("/reminders");
    })
  }
}

module.exports = remindersController;

You Did It!

congrats

  • First, verify that a user can see reminders on localhost:3000/reminders. Also, check that a user can make a new reminder by going to localhost:3000/reminders/new.

Bonus!

Show a Single ToDo

  • A user should be able to click on any reminder's title on the reminders page and be directed to the specific reminder page that displays information about that reminder. Add that functionality to your reminders page. Bonus: add more information to this page that only shows up on this page, but not on the index page with all reminders.

CRUDing in the Console

  • Checkout the solution-code directory and examine the file db/console.js. It drops you into a REPL session with the database and all your models pre-loaded. You can run node db/console.js to see for yourself. CRUD some data in this REPL.

Note: If for some reason it's not working, try restarting your mongod process.

Refactor

  • How would you refactor your index.js so that all the configuration logic lived in a file config/config.js?
  • How about refactoring the code so that your routes live in config/routes.js?

Licensing

All content is licensed under a CC­BY­NC­SA 4.0 license. All software code is licensed under GNU GPLv3. For commercial use or alternative licensing, please contact legal@ga.co.