How to do a One-to-Many Join using Mongoose

Have a Database Problem? Speak with an Expert for Free
Get Started >>

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 perform a join that has a one-to-many relationship. One-to-many joins are an extremely common common relationship. An example of a one-to-many relationship would be that one user can have many phone numbers ( work phone, a home phone, and a cell phone). You might handle this with a collection of users and another collection of phone numbers and to get all the phone numbers a user has you’ll need to join the data in both collections together. In this tutorial we’ll show you a simple example of a one-to-many join using the popular Mongoose library which lets you work with a Mongo Database.

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.

The Demo Application Use Case

In this demo we’ll be imagining that we’re creating a database grocerydb for a small grocery store. We’ll make a collection of Products and a collection of Reviews for each Product. Each Product should be able to have multiple (many) Reviews. We’ll be using Express to help us create the routes to interact with our app.

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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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 one-to-many relationship we desire between Products and Reviews. In our demo scenario each Product could have multiple Reviews attached to it. Let’s see how we’d do this in code:

File: /demo/Product.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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
  },
  reviews: [{
    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 the reviews field we have used square brackets to indicate that we will hold of an array of the type defined inside the curly brackets. Inside the curly brackets we use type: Schema.Types.ObjectId and ref: "Review" which let’s Mongo know to expect an Array of Reviews.

1
2
3
4
  review: {
    type: Schema.Types.ObjectId,
    ref: "Review"
  }

Now let’s define the Review:

File: /demo/Review.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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;

This definition is straightforward. We have two fields one for stars ( a star rating ) and one for review ( a written text 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

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

This is how we require all schemas in our server file app.js. Snippet from File: /demo/app.js

1
2
// 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 which in turns imports both our schemas.

Create Routes

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

Here’s our route to get all products:

1
2
3
4
5
6
7
8
9
10
// 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:

1
2
3
4
5
6
7
8
9
10
// 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:

1
2
3
4
5
6
7
8
9
10
11
12
// 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 start creating Reviews on Products. 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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 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 }, {$push: {reviews: 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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 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("reviews")
    .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:

1
2
3
4
5
{
    "name": "Soda",
    "quantity": 12,
    "deparments": ["Beverages", "Checkout"]
}

Response:

1
2
3
4
5
6
7
{
    "reviews": [],
    "_id": "5cc78c5b71dc9046e740f715",
    "name": "Soda",
    "quantity": 12,
    "__v": 0
}

POST Request: `js {


1
2
3
"name": "Bread",
"quantity": 8,
"deparments": ["Bakery"]

} ` Response:

1
2
3
4
5
6
7
{
    "reviews": [],
    "_id": "5cc78c7071dc9046e740f716",
    "name": "Bread",
    "quantity": 8,
    "__v": 0
}

POST Request:

1
2
3
4
5
{
    "name": "Soy Milk",
    "quantity": 2,
    "deparments": ["Dairy"]
}

Response:

1
2
3
4
5
6
7
{
    "reviews": [],
    "_id": "5cc78c8071dc9046e740f717",
    "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=5cc78c5b71dc9046e740f715). We can go into Postman and create a POST request to the url `http://localhost:3000/product/5cc78c5b71dc9046e740f715 with the following reviews as the body. Post them one at a time.

1
2
3
4
{
    "stars": 2,
    "review": "Soda was too sugary."
}
1
2
3
4
{
    "stars": 3,
    "review": "The soda didn't tasted old."
}

You can see in the response that our Soda Product is being returned with the array of Reviews populated. This is the essence of a one-to-many relationship. For this example it is specifically, One Product to Many Reviews.

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
    "reviews": [
        {
            "_id": "5cc78d0371dc9046e740f718",
            "stars": 2,
            "review": "Soda was too sugary.",
            "__v": 0
        },
        {
            "_id": "5cc78d6471dc9046e740f719",
            "stars": 3,
            "review": "The soda didn't tasted old.",
            "__v": 0
        }
    ],
    "_id": "5cc78c5b71dc9046e740f715",
    "name": "Soda",
    "quantity": 12,
    "__v": 0
}

Conclusion

In this demo we have shown you how to use Mongoose to create two Schemas that can be joined together in a one-to-many relationship. We also demonstrated how to use Mongoose to actually join the data together with the populate function. This was a one-to-many relationship but the basic concepts and core code here can easily be translated to the one-to-one or many-to-many relationships. 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.