Skip to main content

Multitenant Node.js Application with mongoose (MongoDB)

Multitenancy is a reference to the mode of operation of software where multiple independent instances of one or multiple applications operate in a shared environment. The instances (tenants) are logically isolated, but physically integrated.
The term “software multi-tenancy” refers to a software architecture in which a single instance of software runs on a server and serves multiple tenants.

Architecting a multi-tenant system is more complicated than architecting a multi-user system. There are two models of architecting a multi-tenant system: Instance replication and Data-Segregation.
In the instance replication model, the system spins a new instance for every tenant. This is easier to start, but hard to scale. It becomes a nightmare when 100s of tenants signup.
In the data-segregation model, the application is shared between tenants but data of each tenant is stored in separate data stores. Separate data stores could be separate databases or separate schema within the same database.

Data Segregation is of two types
One DB with different schemas for a different tenant.
One DB per tenant.
separate schema ie. different tables within one DB for each tenant.

In this blog, we will discuss multitenancy with Data-Segregation Model with a single DB per tenant.
Create a Node app initialize one MongoDB connection along with the app and export this connection object. create this connection using mongoose createConnection as the createConnection method does not create default connection but return a DB connection object.



const mongoose = require('mongoose');
const log = require('../../../config/log');

const mongoOptions = {
  useNewUrlParser: true,
  useCreateIndex: true,
  useUnifiedTopology: true,
  useFindAndModify: false,
  autoIndex: true,
  poolSize: 10,
  bufferMaxEntries: 0,
  connectTimeoutMS: 10000,
  socketTimeoutMS: 30000,
};

const connect = () => mongoose.createConnection(process.env.MONGODB_URL, mongoOptions);

const connectToMongoDB = () => {
  const db = connect(process.env.MONGODB_URL);
  db.on('open', () => {
    log.info(`Mongoose connection open to ${JSON.stringify(process.env.MONGODB_URL)}`);
  });
  db.on('error', (err) => {
    log.info(`Mongoose connection error: ${err} with connection info ${JSON.stringify(process.env.MONGODB_URL)}`);
    process.exit(0);
  });
  return db;
};

exports.mongodb = (connectToMongoDB)();

Here we are creating a MongoDB connection with mongoose and exporting it from here and it will be imported in config index.js file which will be initialized only once in the app and we can import this MongoDB object anywhere or we can also create and store it in global and can use it anywhere.


const log = require('./log');
const {execute,throwError} = require('./utils');
const mongodb = require('./mongodb');

module.exports = {
  execute,
  throwError,
  mongodb,
  log,
};


importing this MongoDB object and we can create a new MongoDB connection as per our tenant.


const { log, mongodb, throwError } = require('../../../config');
const codes = require('../../../codes.json');


/**
 * Creating New MongoDb Connection obect by Switching DB
 */
const getTenantDB = (tenantId, modelName, schema) => {
  const dbName = `brightlab_${tenantId}`;
  if (mongodb) {
    // useDb will return new connection
    const db = mongodb.useDb(dbName, { useCache: true });
    log.info(`DB switched to ${dbName}`);
    db.model(modelName, schema);
    return db;
  }
  return throwError(500, codes.CODE_8004);
};

/**
 * Return Model as per tenant
 */
exports.getModelByTenant = (tenantId, modelName, schema) => {
  log.info(`getModelByTenant tenantId : ${tenantId}.`);
  const tenantDb = getTenantDB(tenantId, modelName, schema);
  return tenantDb.model(modelName);
};




Mongoose provides useDb function which is used to create a new connection object it takes two params first one is DB name and another is options
Connection.prototype.useDb()
Parameters:
name      «String» The database name
{options} «Object» Configs
options = {useCache: false} «Boolean»  False by default
If set true, cache results so calling useDb() multiple times with the same name only creates 1 connection object.
Returns:
«Connection» New Connection Object
we are it switches to a different database using the same connection pool and Returns a new connection object, with the new DB and using option {useCache: true} will create single connection object for the same tenant if called multiple times.
To get the model from tenant DB, we need to create a schema of our model and pass it along with the tenant to this function, which will return a model. Now we can use this model to perform all of our crud operations.











const mongoose = require('mongoose');

const signatureSchema = new mongoose.Schema({
  nodeId: {
    type: mongoose.Schema.Types.ObjectId,
    required: true,
  },
  requestBy: {
    type: String,
    required: true,
  },
  requestTime: {
    type: Date,
    default: Date.now,
    required: true,
  },
  isActive: {
    type: Boolean,
    default: true,
    required: true,
  },
  labId: {
    type: String,
    required: true,
  },
  comment: String,
});

module.exports = signatureSchema;


In the Data access layer, we will first get the model as per our tenant DB connection and then perform crud operations. In each request, we have to pass tenantId and this tenantId will be used to switch with that tenant DB.



const {
  codes,
  log,
  execute,
  throwError,
} = require('../../../../config');

const { getModelByTenant } = require('./../../../../utils/multitenancy');
const { signatureSchema } = require('../../models/signatureModal');

exports.addSignature = async (signaturesBody, tenantId) => {
  log.info(`Add signature called with body ${signaturesBody}`);
  const Signature = getModelByTenant(tenantId, 'signature', signatureSchema);
  // Execute takes a promise and return {null,response} if resolved and return {err,null} if rejected.
  const { err, response } = await execute(Signature.create(signaturesBody));
  if (err || !response) {
    log.error(`Signature creation failed signatureDao.js ${err.message}`);
    throwError(500, codes.signatureAddFailed);
  }
  log.info(`Signature created with response : ${response}`);
  return response;
};



In this approach, we are using the same connection pool which we created at the time of app initialization. Multiple requests with different tenants will create different connection objects which will be timed out after 30 seconds if there is not new request with the same tenant.
There is another approach as well we can implement this will be helpful with limited no of tenants. In this, we can create a global object where the key will be tenantId/tenantName, and value will be the connection object. before every request we will check if the connection is there in this global object otherwise we will create a connection and store it there so in next request from that tenant will be reused.



const { Mongoose } = require('mongoose');
const mongoConfigs = require('../../../config/mongo.conf');
const { log } = require('../../../config');

const multitenantPool = {};

const getTenantDB = function getConnections(tenantId, modelName, schema) {
  // Check connections lookup
  const mCon = multitenantPool[tenantId];
  if (mCon) {
    if (!mCon.modelSchemas[modelName]) {
      mCon.model(modelName, schema);
    }
    return mCon;
  }

  const mongoose = new Mongoose();
  const url = process.env.MONGODB_URL.replace(/brightlab/, `brightlab_${tenantId}`);
  mongoose.connect(url, mongoConfigs);
  multitenantPool[tenantId] = mongoose;
  mongoose.model(modelName, schema);
  mongoose.connection.on('error', err => log.debug(err));
  mongoose.connection.once('open', () => log.info(`mongodb connected to ${url}`));
  return mongoose;
};

exports.getModelByTenant = (tenantId, modelName, schema) => {
  log.info(`getModelByTenant tenantId : ${tenantId}.`);
  const tenantDb = getTenantDB(tenantId, modelName, schema);
  return tenantDb.model(modelName);
};


In this above implementation, there can be a CPU usage issue if the number of the tenants will increase as the app will always have lots of open connection will degrade performance but it can be used in case you have fewer tenants and which will never grow dynamically.




































Comments

Popular posts from this blog

How to use Ngx-Charts in Angular ?

Charts helps us to visualize large amount of data in an easy to understand and interactive way. This helps businesses to grow more by taking important decisions from the data. For example, e-commerce can have charts or reports for product sales, with various categories like product type, year, etc. In angular, we have various charting libraries to create charts.  Ngx-charts  is one of them. Check out the list of  best angular chart libraries .  In this article, we will see data visualization with ngx-charts and how to use ngx-charts in angular application ? We will see, How to install ngx-charts in angular ? Create a vertical bar chart Create a pie chart, advanced pie chart and pie chart grid Introduction ngx-charts  is an open-source and declarative charting framework for angular2+. It is maintained by  Swimlane . It is using Angular to render and animate the SVG elements with all of its binding and speed goodness and uses d3 for the excellent math functio...

Understand Angular’s forRoot and forChild

  forRoot   /   forChild   is a pattern for singleton services that most of us know from routing. Routing is actually the main use case for it and as it is not commonly used outside of it, I wouldn’t be surprised if most Angular developers haven’t given it a second thought. However, as the official Angular documentation puts it: “Understanding how  forRoot()  works to make sure a service is a singleton will inform your development at a deeper level.” So let’s go. Providers & Injectors Angular comes with a dependency injection (DI) mechanism. When a component depends on a service, you don’t manually create an instance of the service. You  inject  the service and the dependency injection system takes care of providing an instance. import { Component, OnInit } from '@angular/core'; import { TestService } from 'src/app/services/test.service'; @Component({ selector: 'app-test', templateUrl: './test.component.html', styleUrls: ['./test.compon...

How to solve Puppeteer TimeoutError: Navigation timeout of 30000 ms exceeded

During the automation of multiple tasks on my job and personal projects, i decided to move on  Puppeteer  instead of the old school PhantomJS. One of the most usual problems with pages that contain a lot of content, because of the ads, images etc. is the load time, an exception is thrown (specifically the TimeoutError) after a page takes more than 30000ms (30 seconds) to load totally. To solve this problem, you will have 2 options, either to increase this timeout in the configuration or remove it at all. Personally, i prefer to remove the limit as i know that the pages that i work with will end up loading someday. In this article, i'll explain you briefly 2 ways to bypass this limitation. A. Globally on the tab The option that i prefer, as i browse multiple pages in the same tab, is to remove the timeout limit on the tab that i use to browse. For example, to remove the limit you should add: await page . setDefaultNavigationTimeout ( 0 ) ;  COPY SNIPPET The setDefaultNav...