Using (and reusing) multiple mongoose database con

2020-07-22 18:07发布

问题:

I'm looking for the easiest & performant way to make a multitenant express.js app for managing projects.

Reading several blogs and articles, I figured out that, for my application, would be nice to have a database per tenant architecture.

My first try has been to use subdomains to detect the tenant, and then map the subdomain to a mongodb database.

I came up with this express middlewares

var mongoose = require('mongoose');
var debug = require('debug')('app:middleware:mongooseInstance');
var conns [];
function mongooseInstance (req, res, next) {
    var sub = req.sub = req.subdomains[0] || 'app';
    // if the connection is cached on the array, reuse it
    if (conns[sub]) {
        debug('reusing connection', sub, '...');
        req.db = conns[sub];
    } else {
        debug('creating new connection to', sub, '...');
        conns[sub] = mongoose.createConnection('mongodb://localhost:27017/' + sub);
        req.db = conns[sub];
    }
    next();
}
module.exports = mongooseInstance;

Then I register the models inside another middleware:

var fs = require('fs');
var debug = require('debug')('app:middleware:registerModels');
module.exports = registerModels;

var models = [];
var path = __dirname + '/../schemas';

function registerModels (req, res, next) {
    if(models[req.sub]) {
        debug('reusing models');
        req.m = models[req.sub];
    } else {
        var instanceModels = [];
        var schemas = fs.readdirSync(path);
        debug('registering models');
        schemas.forEach(function(schema) {
            var model = schema.split('.').shift();
            instanceModels[model] = req.db.model(model, require([path, schema].join('/')));
        });
        models[req.sub] = instanceModels;
        req.m = models[req.sub];
    }
    next();
}

Then I can proceed normally as any other express.js app:

var express = require('express');
var app = express();
var mongooseInstance = require('./lib/middleware/mongooseInstance');
var registerModels = require('./lib/middleware/registerModels');

app.use(mongooseInstance);
app.use(registerModels);

app.get('/', function(req, res, next) {
    req.m.Project.find({},function(err, pets) {
        if(err) {
            next(err);
        }
        res.json({ count: pets.length, data: pets });
    });
});

app.get('/create', function (req, res) {
    var p = new req.m.Project({ name: 'Collin', description: 'Sad' });
    p.save(function(err, pet) {
        res.json(pet);
    });
});

app.listen(8000);

The app is working fine, I don't have more than this right now, and I'd like to get some feedback before I go on, so my questions would be:

Is this approach is efficient? Take into account that a lot will be happening here, multiple tenants, several users each, I plan to setup webhooks in order to trigger actions on each instance, emails, etc...

Are there any bottlenecks/pitfalls I'm missing? I'm trying to make this scalable from the start.

What about the model registering? I didn't found any other way to accomplish this.

Thanks!

回答1:

Is this approach is efficient? Are there any bottlenecks/pitfalls I'm missing?

This all seems generally correct to me

What about the model registering?

I agree with @narc88 that you don't need to register models in middleware.

For lack of a better term, I would use a factory pattern. This "factory function" would take in your sub-domain, or however you decide to detect tenants, and return a Models object. If a given middleware wants to use its available Models you just do

var Models = require(/* path to your Model factory */);

...

// later on inside a route, or wherever
var models = Models(req.sub/* or req.tenant ?? */);
models.Project.find(...);

For an example "factory", excuse the copy/paste

var mongoose = require('mongoose');
var fs = require('fs');
var debug = require('debug')('app:middleware:registerModels');

var models = [];
var conns = [];
var path = __dirname + '/../schemas';

function factory(tenant) {
    // if the connection is cached on the array, reuse it
    if (conns[tenant]) {
        debug('reusing connection', tenant, '...');
    } else {
        debug('creating new connection to', tenant, '...');
        conns[tenant] = mongoose.createConnection('mongodb://localhost:27017/' + tenant);
    }

    if(models[tenant]) {
        debug('reusing models');
    } else {
        var instanceModels = [];
        var schemas = fs.readdirSync(path);
        debug('registering models');
        schemas.forEach(function(schema) {
            var model = schema.split('.').shift();
            instanceModels[model] = conns[tenant].model(model, require([path, schema].join('/')));
        });
        models[tenant] = instanceModels;
    }
    return models[tenant];
}

module.exports = factory;

Aside from potential (albeit probably small) performance gain, I think it also has the advantage of:

  • doesn't clutter up the request object as much
  • you don't have to worry as much about middleware ordering
  • allows more easily abstracting permissions for a given set of models, i.e. the models aren't sitting on the request for all middleware to see
  • This approach doesn't tie your models to http requests, so you might have flexibility to use the same factory in a job queue, or whatever.