Skip to content

Instantly share code, notes, and snippets.

@tridungle
Last active May 23, 2023 23:00
Show Gist options
  • Save tridungle/d14f9713f14033b6d71fbc2fdeb45ce1 to your computer and use it in GitHub Desktop.
Save tridungle/d14f9713f14033b6d71fbc2fdeb45ce1 to your computer and use it in GitHub Desktop.
multi tenancy implementation with adonis

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 :)

@Salman-Kashfy
Copy link

You can use my approach. Its tested with benchmark

Github Repo:
https://github.com/Salman-Kashfy/Multitenancy-AdonisJS-5/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment