MongoDB One-to-One relationship tutorial with Mongoose example

Model One-to-One Relationships

Assume that we have 2 entity: Identifier and Customer. For example:

// Identifier
{
   _id: "12345xyz",
   cardCode: "BKD2019",
}

// Customer
{
   _id: "cus123",
   name: "bezkoder",
   age: 29,
   gender: "male"
}

We want to map Identifier and Customer relationship in that: – One Customer has only one Identifier. – One Identifier belongs to only one Customer.

This is called One-to-One Relationship. Now let’s take a look at 2 ways to design schema for this kind of model.

Reference Data Models (Normalization)

In this model, an object A connects to the object B by reference to object B id or a unique identification field.

For example, Identifier has a customer_id field which value is equal to Customer object’s unique _id value.

// Identifier
{
   _id: "12345xyz",
   cardCode: "BKD2019",
   customer_id: "cus123",
}

// Customer
{
   _id: "cus123",  // equal Identifier[customer_id]
   name: "bezkoder",
   age: 29,
   gender: "male"
}

Embedded Data Models (Denormalization)

It’s easy to understand with ‘Embedded’ word. Instead of using a reference, Object A contains the whole object B, or object B is embedded inside object A.

You can see the example below, Identifier will have a nested field customer.

// Identifier
{
   _id: "12345xyz",
   cardCode: "BKD2019",
   customer: {
                _id: "cus123", // Identifier[customer_id]
                name: "bezkoder",
                age: 29,
                gender: "male"
             }
}

You’ve known 2 ways to make One-to-One Relationships. Let’s implement each of them in a Node.js app using Mongoose. After that, I will tell you which model should be used for implementing One-to-One relationship between collections in MongoDb Database.

Mongoose One-to-One relationship example

Setup Nodejs App

First we need to install mongoose, so run the command:

npm install mongoose

Next, we create project structure like this:

src

models

Customer.js

Identifier.js

server.js

package.json

Open server.js, we import mongoose to our app and connect to MongoDB database.

const mongoose = require("mongoose");

mongoose
  .connect("mongodb://localhost/bezkoder_db", {
    useNewUrlParser: true,
    useUnifiedTopology: true
  })
  .then(() => console.log("Successfully connect to MongoDB."))
  .catch(err => console.error("Connection error", err));

The first step is done, we’re gonna create appropriate models and use mongoose to interact with MongoDB database in two ways:

  • Referencing

  • Embedding

Mongoose One-to-One relationship: Referencing

1. Define One-to-One models using Mongoose

Let’s create 2 main model with mongoose.Schema() construtor function.

In models/Customer.js, define Customer with 3 fields: name, age, gender.

const mongoose = require("mongoose");

const Customer = mongoose.model(
  "Customer",
  new mongoose.Schema({
    name: String,
    age: Number,
    gender: String
  })
);

module.exports = Customer;

Identifier object will have cardCode field and a reference customer. So open models/Identifier.js, define Identifier like this:

const mongoose = require("mongoose");

const Identifier = mongoose.model(
  "Identifier",
  new mongoose.Schema({
    cardCode: String,
    customer: {
      type: mongoose.Schema.Types.ObjectId,
      ref: "Customer"
    }
  })
);

module.exports = Identifier;

In the code above, we add customer field, set its type to ObjectId and ref to Customer. What does it help?

Now if we save an Identifier to MongoDB database, a document will be added like this:

{
   _id : ObjectId("5da000be062dc522eccaedeb"),
   cardCode : "5DA000BC06",
   customer : ObjectId("5da000bc062dc522eccaedea"),
   __v : 0
}

Let’s test it, and I will show you how to get an Identifier object with full-fields Customer in this approach.

2. Test with MongoDB database

Open server.js, add the code below:

const Customer = require("./models/Customer");
const Identifier = require("./models/Identifier");

const createCustomer = function(name, age, gender) {
  const customer = new Customer({
    name,
    age,
    gender
  });

  return customer.save();
};

const createIdentifier = function(cardCode, customer) {
  const identifier = new Identifier({
    cardCode,
    customer
  });

  return identifier.save();
};

createCustomer("bezkoder", 29, "male")
  .then(customer => {
    console.log("> Created new Customer\n", customer);
    
    const customerId = customer._id.toString();
    return createIdentifier(customerId.substring(0, 10).toUpperCase(), customerId);
  })
  .then(identifier => {
    console.log("> Created new Identifier\n", identifier);
  })
  .catch(err => console.log(err));

Console shows the result.

> Created new Customer
 { _id: 5da135bd61a1dd3e9c2a6e81,
  name: 'bezkoder',
  age: 29,
  gender: 'male',
  __v: 0 }

> Created new Identifier
 { _id: 5da135bf61a1dd3e9c2a6e82,    
  cardCode: '5DA135BD61',
  customer: 5da135bd61a1dd3e9c2a6e81,
  __v: 0 }

We can check collections in the database.

As I’ve said before, the Identifier has only ObjectId in customer field.

The question is: How to get full items of customer? Oh yeah, we can reference documents in other collections using populate().

Let me show you how to do this.

const showAllIdentifier = async function() {
  const identifiers = await Identifier.find().populate("customer");

  console.log("> All Identifiers\n", identifiers);
};

The result looks like this.

> All Identifiers
 [ { _id: 5da135bf61a1dd3e9c2a6e82,
    cardCode: '5DA135BD61',
    customer:
     { _id: 5da135bd61a1dd3e9c2a6e81,
       name: 'bezkoder',
       age: 29,
       gender: 'male',
       __v: 0 },
    __v: 0 } ]

Mongoose Hide _id & __v in result

If you don’t want to get customer._id & customer.__v in the result, just add second parameters to populate() function like this:

const identifiers = await Identifier.find()
    .populate("customer", "-_id -__v");

Check the result:

> All Identifiers
 [ { _id: 5da135bf61a1dd3e9c2a6e82,
    cardCode: '5DA135BD61',
    customer: { name: 'bezkoder', age: 29, gender: 'male' },
    __v: 0 } ]

How about __v in parent object? We use select() function to remove it.

const identifiers = await Identifier.find()
    .populate("customer", "-_id -__v");
    .select("-__v");

The result will be:

> All Identifiers
 [ { _id: 5da135bf61a1dd3e9c2a6e82,
    cardCode: '5DA135BD61',
    customer: { name: 'bezkoder', age: 29, gender: 'male' } } ]

Mongoose One-to-One relationship: Embedding

1. Define One-to-One models using Mongoose

The way we define models for Embedded Documents will be different from Referenced Documents.

In models/Customer.js, we also define Customer like code above, but also export CustomerSchema.

const mongoose = require("mongoose");

const CustomerSchema = new mongoose.Schema({
  name: String,
  age: Number,
  gender: String
});

const Customer = mongoose.model("Customer", CustomerSchema);

module.exports = { Customer, CustomerSchema };

Identifier object will still have cardCode field & customer field. But instead using a reference, we assign import and CustomerSchema directly.

models/Identifier.js

const mongoose = require("mongoose");
const CustomerSchema = require("./Customer").CustomerSchema;

const Identifier = mongoose.model(
  "Identifier",
  new mongoose.Schema({
    cardCode: String,
    customer: CustomerSchema
  })
);

module.exports = Identifier;

2. Test with MongoDB database

Open server.js, the definition of createCustomer & createIdentifier functions will be the same as Referencing.

It’s a little change in how we call createIdentifier(). Instead passing customerId, we use customer object.

const Customer = require("./models/Customer").Customer;
const Identifier = require("./models/Identifier");

const createCustomer = function(name, age, gender) {
  const customer = new Customer({
    name,
    age,
    gender
  });

  return customer.save();
};

const createIdentifier = function(cardCode, customer) {
  const identifier = new Identifier({
    cardCode,
    customer
  });

  return identifier.save();
};

createCustomer("bezkoder", 29, "male")
  .then(customer => {
    console.log("> Created new Customer\n", customer);

    return createIdentifier(
      customer._id.toString().substring(0, 10).toUpperCase(),
      customer
    );
  })
  .then(identifier => {
    console.log("> Created new Identifier\n", identifier);
  })
  .catch(err => console.log(err));

Run the code, then check result in the console.

> Created new Customer
 { _id: 5da1406de666c118c89bba28,
  name: 'bezkoder',
  age: 29,
  gender: 'male',
  __v: 0 }

> Created new Identifier
 { _id: 5da1406fe666c118c89bba29,  
  cardCode: '5DA1406DE6',
  customer:
   { _id: 5da1406de666c118c89bba28,
     name: 'bezkoder',
     age: 29,
     gender: 'male',
     __v: 0 },
  __v: 0 }

Now look at our MongoDB database. Full Customer object is embedded in Identifier object now.

To get the items in identifiers collections, just use find() function.

const showAllIdentifier = async function() {
  const identifiers = await Identifier.find();

  console.log("> All Identifiers\n", identifiers);
};

And you can see all things you want in the result:

> All Identifiers
 [ { _id: 5da1406fe666c118c89bba29,  
    cardCode: '5DA1406DE6',
    customer:
     { _id: 5da1406de666c118c89bba28,
       name: 'bezkoder',
       age: 29,
       gender: 'male',
       __v: 0 },
    __v: 0 } ]

Mongoose Hide _id & __v in result

With embedded documents, to hide _id & __v is easy with only one select() function.

For example, if I want to remove Identifier __v, also embedded document (Customer) fields such as customer._id & customer.__v, just write select() with parameter like this.

const showAllIdentifier = async function() {
  const identifiers = await Identifier.find()
    .select("-__v -customer.__v -customer._id");

  console.log("> All Identifiers\n", identifiers);
};

Now the result is pretty.

> All Identifiers
 [ { _id: 5da1406fe666c118c89bba29,
    cardCode: '5DA1406DE6',
    customer: { name: 'bezkoder', age: 29, gender: 'male' } } ]

Referencing or Embedding for One-to-One Relationships

To make One-to-One Relationship between documents, we can reference or embed a document in the other. Which should be used in general?

Now remind what we’ve done and you can see that we have to use an additional function called populate() after find() function for Referenced Data Model.

Embedding stores related information directly inside the objects. So we only use find() function to get everything. It gives us better performance with a single query. This Model also helps us update the embedded data in just one query using Dot Notation.

So the answer is that modeling One-to-One relationships with Embedded documents is the better choice.

Last updated