I thought I'd add my multi tenancy implementation here as it might prove useful for others. We currently have to separate our customer data into their own databases, but not for everything. Our microservices need to know where to store their data with our many IoT devices reporting in.
I choose to only perform model actions on a tenant database via callbacks and proxies. We get a Tenant object from the tenant manager which allows us to perform actions on the tenant in a manner similar to a transaction callback.
const User = use('App/Models/User');
const Device = use('App/Models/Device');
const TenantManager = use ('App/Services/TenantManager');
const tenant = TenantManager.tenant('adonis');
await tenant.perform(async (TenantUser) => {
const newUser = new TenantUser();
newUser.merge({ name: 'Joe' });
await newUser.save();
}, User);
The models passed into the perform method are altered to use the tenants connection. This keeps things separate and ensures that us developers can only interact with a tenant database in a controlled way.
We can choose to use the connection to the tenant if needed. There are also shortcuts to the static calls. await tenant.performWith(async (connection, TenantUser) => { await connection.raw(' ...sql...'); await connection.select('*').from('table'); }, User);
await tenant.save(newUser); // Saves the user model into the tenant database
const joe = await tenant.find(User, 10); // Locates Joe from the tenant database
const bob = await tenant.findBy(User, 'email', 'bob@adonisjs.com'); // Locates Bob from the tenant database
The tenant manager allows us to use middleware to intercept different domains or routes so we can inject the tenant into the http context ready to use within the controller.
The tenant manager handles different connections by tampering with the config. We inject new connection configs when a customer is required and then call the new connection. Our manager keeps track of the open connections and will close stale connections and/or handle a max number of connections by prioritising those frequently used. Although this isn't an ideal way to handle things it's the easiest by far. It also allows us to inject new customers at runtime as our service listens for when new customers are created.
Example connection logic
createConnection ({ database }) {
// Populate the database config with our required connection
const connectionConfig = { ...this.getDefaultConfig() };
connectionConfig.connection.database = database;
this.config.set(`database.${database}`, connectionConfig);
// Use the database manager to establish a connection to the database
this.databaseManager.connection(database);
// Tenant class provides methods to perform actions on the tenant database,
// the callback is called when ever an action is performed so we can keep track
// of active connections
const tenant = new Tenant(database, () => this.track(database));
this.tenants[database] = {
tenant,
database,
// keep track of calls and set the expiry time so we can remove old connections
expires: this.getExpiryDate(),
calls: 0,
};
return tenant;
}
We've also had a lot of fun implementing multi tenancy database migrations but I'm not too happy with the solution, although it does work :)
You can use my approach. Its tested with benchmark
Github Repo:
https://github.com/Salman-Kashfy/Multitenancy-AdonisJS-5/