How to Join Collections using Mongoose

Introduction

If you are considering or already using the npm module Mongoose to handle your MongoDB interactions at some point you’ll probably need to know how to join two collections. If you’re unfamiliar with the concept of joins, a join would be like if you had a collection of Posts and each post had a number of Comments attached to it. In this scenario Posts might be one collection, and Comments might be another collection. If you wanted to query a post and get all the pertinent Comments, you’d use a join to get both the product and all the related Comments.

Joins are extremely common and it’s a good idea to get a handle on them when you start using a new technology because they offer such valuable functionality with very little code.

Prerequisites

  • You should have MongoDB installed and running.
  • You should have installed Mongoose using npm.
  • Some command line experience is recommended.
  • Familiarity with Postman is recommended.

Goals

We have a basic starter application that we’ll be using this demo. We want to focus on 1. how to setup the Schema and 2. how to use a join with Mongoose. We love to demonstrate by example so we will be imagining that we are creating a database for a small grocery store and our database will be called grocerydb. Inside our grocerydb we will have both a collection for Products and a collection for Reviews. Our goal will be to create routes to create both Products and Product and Reviews, as well as ( here comes the join functionality ) a GET request that can retrieve a product along with the associated associated Review. We don’t want to get distracted by any user interface so we’ll only be dealing with the command line and Postman to verify that our app is working as expected.

The Starter App

Let’s take a look at the app we’ll start out with. We’ll start adding the functionality to reach our goal but you should get acquianted with our starting point. The only route that exists at this point is a homepage route which just returns a hello message. Once we’ve finished our Schema definitions, we’ll get to the routing.

var express = require("express");
var mongoose = require("mongoose");

// Require all models
var db = require("./models");
console.log(db);

// Connect to MongoDB
mongoose.connect("mongodb://localhost/grocerydb", { useNewUrlParser: true });

var PORT = 3000;

// Initialize Express
var app = express();

// Parse request body as JSON
app.use(express.urlencoded({ extended: true }));
app.use(express.json());

// Make public static folder
app.use(express.static("public"));

// Routes

// Home route. Currently just to make sure app is running returns hello message.
app.get("/", function(req, res) {
  res.send("Hello from demo app!");
});


// Start the server
app.listen(PORT, function() {
  console.log("Listening on port " + PORT + ".");
});

If you run this app $node app.js you should get a message in the console that it is Listening on port 3000..

Create the Schema

Let’s take a look at the Schema we’d use to have the relationship we desire between Products and Reviews. In our demo scenario each Product should a single Review attached to it. Let’s see how we’d do this in code:

File: /demo/Product.js

var mongoose = require("mongoose");

// Get the Schema constructor
var Schema = mongoose.Schema;

// Using Schema constructor, create a ProductSchema
var ProductSchema = new Schema({
  name: {
    type: String,
    required: true
  },
  quantity: {
    type: Number,
    required: true
  },
  departments: {
    type: Array,
    required: true
  },
  review: {
    type: Schema.Types.ObjectId,
    ref: "Review"
  }
});

// Create model from the schema
var Product = mongoose.model("Product", ProductSchema);

// Export model
module.exports = Product;

The main thing to notice here is that under the definition for “review” we use type: Schema.Types.ObjectId and ref: "Review" which let’s Mongo know to expect our Review object to be populated there.

  review: {
    type: Schema.Types.ObjectId,
    ref: "Review"
  }

Now let’s define the Review:

File: /demo/Review.js

var mongoose = require("mongoose");

// Get the Schema constructor
var Schema = mongoose.Schema;

// Using Schema constructor, create a ProductSchema
var ReviewSchema = new Schema({
  stars: {
    type: Number,
    required: true
  },
  review: {
    type: String,
    required: true
  }
});

// Create model from the schema
var Review = mongoose.model("Review", ReviewSchema);

// Export model
module.exports = Review;

We could have done both Schemas in one file but we think it’s cleaner to separate them and then import them both using an index.js.

File: /demo/index.js

// Exporting one object containing all models
module.exports = {
    Product: require("./Product"),
    Review: require("./Review")
  };

Snippet from File: /demo/app.js

// Require all models
var db = require("./models");

If we require an entire folder like we did in this last snippet, it will look for the index file and import that file.

Create Routes

We’ll be utilizing the Express framework to create our routes.

Here’s our route to get all products:

// Route to get all products
app.get("/products", function(req,res) {
  db.Product.find({})
  .then(function(dbProducts) {
    res.json(dbProducts);
  })
  .catch(function(err) {
    res.json(err);
  })
});

Here’s our route to get all Reviews:

// Route to get all reviews
app.get("/reviews", function(req,res) {
  db.Review.find({})
  .then(function(dbReviews) {
    res.json(dbReviews);
  })
  .catch(function(err) {
    res.json(err);
  })
});

Here’s our to create a Product:

// Route for creating a new Product
app.post("/product", function(req, res) {
  db.Product.create(req.body)
    .then(function(dbProduct) {
      // If we were able to successfully create a Product, send it back to the client
      res.json(dbProduct);
    })
    .catch(function(err) {
      // If an error occurred, send it to the client
      res.json(err);
    });
});

In order for our joins to start coming into play we’ll need to Review on Product. Here’s our route that takes in the id of an existing Product and creates a Review for it. We use Mongoose to first create the Review db.Review.create(req.body) and if that succeeds we find the associated Product and update it’s review field. For this we use the findOneAndUpdate() function. If everything is successful then we return the updated Product.

// Route for creating a new Review and updating Product "review" field with it
app.post("/product/:id", function(req, res) {
  // Create a new note and pass the req.body to the entry
  db.Review.create(req.body)
    .then(function(dbReview) {
      // If a Review was created successfully, find one Product with an `_id` equal to `req.params.id`. Update the Product to be associated with the new Review
      // { new: true } tells the query that we want it to return the updated Product -- it returns the original by default
      // Since our mongoose query returns a promise, we can chain another `.then` which receives the result of the query
      return db.Product.findOneAndUpdate({ _id: req.params.id }, { review: dbReview._id }, { new: true });
    })
    .then(function(dbProduct) {
      // If we were able to successfully update a Product, send it back to the client
      res.json(dbProduct);
    })
    .catch(function(err) {
      // If an error occurred, send it to the client
      res.json(err);
    });
});

Lastly, here’s our route to create get a single Product, the related Review including all the Review fields. Here we use the populate() function to populate the review field of the Product with all the Review fields.

// Route for retrieving a Product by id and populating it's Review.
app.get("/products/:id", function(req, res) {
  // Using the id passed in the id parameter, prepare a query that finds the matching one in our db...
  db.Product.findOne({ _id: req.params.id })
    // ..and populate all of the notes associated with it
    .populate("review")
    .then(function(dbProduct) {
      // If we were able to successfully find an Product with the given id, send it back to the client
      res.json(dbProduct);
    })
    .catch(function(err) {
      // If an error occurred, send it to the client
      res.json(err);
    });
});

Create some demo data

Now if we want to test this out we need to create some demo data. Luckily we have made a route for that so we can easily go into Postman and make the following POST requests with these JSON bodies to the url http://localhost:3000/product.

POST Request:

{
    "name": "Soda",
    "quantity": 12,
    "deparments": ["Beverages", "Checkout"]
}

Response:

{
    "departments": [],
    "_id": "5cc724e9288cda2c3184f5d3",
    "name": "Soda",
    "quantity": 12,
    "__v": 0
}

POST Request: `js {


"name": "Bread",
"quantity": 8,
"deparments": ["Bakery"]

} ` Response:

{
    "departments": [],
    "_id": "5cc7254e288cda2c3184f5d4",
    "name": "Bread",
    "quantity": 8,
    "__v": 0
}

POST Request:

{
    "name": "Soy Milk",
    "quantity": 2,
    "deparments": ["Dairy"]
}

Response:

{
    "departments": [],
    "_id": "5cc72576288cda2c3184f5d5",
    "name": "Soy Milk",
    "quantity": 2,
    "__v": 0
}

Now we have 3 Products in our database and we can use the http://localhost:3000/product/:id route to create a Review on an existing Product, all we need is the id of the Product. You can see all the ids from the responses above. So let’s post a Review about the Soda (_id=5cc724e9288cda2c3184f5d3). We can go into Postman and create a POST request to the url `http://localhost:3000/product/5cc724e9288cda2c3184f5d3 with the following review as the body:

{
    "stars": 2,
    "review": "Soda was too sugary."
}

And now you can retrieve that Product with the full review attached by using Postman to make a GET request to http://localhost:3000/products/5cc724e9288cda2c3184f5d3 and you will get the following:

{
    "departments": [],
    "_id": "5cc724e9288cda2c3184f5d3",
    "name": "Soda",
    "quantity": 12,
    "__v": 0,
    "review": {
        "_id": "5cc733d522b290304b0579bb",
        "stars": 2,
        "review": "Soda was too sugary.",
        "__v": 0
    }
}

Conclusion

In this demo we have shown you how to use Mongoose to create two Schemas that can be joined together. We also demonstrated how to use Mongoose to actually join the data together with the populate function. This was a one-to-one relationship but the basic concepts and core code here can easily be translated to the one-to-many or many-to-many relationship. We hope you found this demonstration valuable and it helps in your specific application. If you have any feedback or need help with your database please don’t hesitate to reach out us.

Pilot the ObjectRocket Platform Free!

Try Fully-Managed CockroachDB, Elasticsearch, MongoDB, PostgreSQL (Beta) or Redis.

Get Started

Keep in the know!

Subscribe to our emails and we’ll let you know what’s going on at ObjectRocket. We hate spam and make it easy to unsubscribe.