Egor Panok

Full Stack JavaScript Developer

How To Emulate A Foreign Key Constraint Check on Mongo with Mongoose

November 11, 2018 - 3 min read

How To Emulate A Foreign Key Constraint Check on Mongo with Mongoose

Document-oriented database engines like Mongo offer the benefit of fast handling large volumes of semi-structured or unstructured data. But the issue of data integrity is valid regardless of the database’s type. If a database engine doesn’t help us to maintain data-integrity we as developers need to care about it by ourselves. Let’s find out how to emulate foreign key constraint check on Mongo with Mongoose.

Use Case

Let’s suppose we want to build a blog using Mongo. The minimalistic, oversimplified database structure consists of two collections - posts and tags. Each post can be associated with multiple tags and each tag can be used to mark multiple posts. So we have many-to-many relationship between posts and tags.

We want to make sure that when we add a blog post and associate it with tags by ids, those tags actually exist in the database.

Solution

We can achieve it by using Mongoose models and async validators. Here’s how it could look like:

// /models/tag.js

const mongoose = require("mongoose")
const Schema = mongoose.Schema

const TagSchema = new Schema({
    name: {
        type: String,
        unique: true,
        required: true
    },
    description: {
        type: String,
        unique: false,
        required: false
    }
})

module.exports = mongoose.model("Tag", TagSchema)
// /models/post.js

const mongoose = require("mongoose")
const Schema = mongoose.Schema
const FKHelper = require("./helpers/foreign-key-helper")

const PostSchema = new Schema({
    title: {
        type: String,
        unique: false,
        required: true
    },
    body: {
        type: String,
        unique: false,
        required: true
    },
    tags: [
        {
            type: Schema.ObjectId,
            ref: "Tag",
            validate: {
                isAsync: true,
                validator: function(v) {
                    return FKHelper(mongoose.model("Tag"), v)
                },
                message: `Tag doesn't exist`
            }
        }
    ],
    isPublished: {
        type: Boolean,
        required: true
    }
})

module.exports = mongoose.model("Post", PostSchema)

Note tags property of the Post model. It constructs the many-to-many relationship between posts and tags. And for each tag in the array we emulate foreign key constraint check by using async validator function.

Let’s examine the code of the FKHelper module, which is fairly straightforward:

// models/helpers/foreign-key-helper.js

module.exports = (model, id) => {
    return new Promise((resolve, reject) => {
        model.findOne({ _id: id }, (err, result) => {
            if (result) {
                return resolve(true)
            } else
                return reject(
                    new Error(
                        `FK Constraint 'checkObjectsExists' for '${id.toString()}' failed`
                    )
                )
        })
    })
}

Now the validator function will be fired every time model.save() is run, not allowing to save a post if at least one associated tag doesn’t exist.

If we want to have the validation run on model.findOneAndUpdate() method we have to make sure we use the runValidators flag set to true in the options, like this:

// somewhere in the app, i.e. in the PUT /posts/{id} endpoint

Post.findOneAndUpdate(
    { _id: id },
    { $set: setObject },
    { new: true, runValidators: true }
)

Summary

Data integrity should not be compromised when we use Mongo. We can easily achieve it by validating data with Mongoose async validators.

Happy coding!

Photo by Lance Anderson on Unsplash


Written by Egor Panok, full stack JavaScript Developer who loves building useful things. Follow him on Twitter

© 2020, Egor Panok, Full Stack JavaScript Developer