MultiTenant Plugin

  • Authors: martytime
2 votes
Dependency:
compile ":multi-tenant:1.0.0"

 Documentation

Summary

Installation

grails install-plugin multi-tenant

Description

Please use the plugin located HERE instead of this one. This version will be maintained for a few months to allow users to migrate to the new version. The new plugin version 1.0.0 matches this plugin except for package changes. Migration instructions will be coming shortly.

This plugin is currently being refactored to make it more modular. The project documentation for the new project is be located here . The first release of the new project will match the last release of this project. Both projects will continue to be supported for a short transition period. Please provide input and check out the new plugin components being offered as part of the refactoring.

See Faq section for release notes

The grails multi-tenant plugin allows you to run multiple "customers" (or tenants) from one installation of a grails application with minimum configuration. Application developers who would normally have to install multiple instance of their grails app (one-per-customer) and maintain multiple databases (one-per-customer) can use this plugin to run multiple customers from the same application AND database. The plugin also supports a single-tenant database configuration (one webapp for all tenants, but a datasource for each tenant)

Our goal

We want the plugin to be as non-intrusive and easy to use as possible. You should write your application as if it's for one customer, and the plugin should convert all necessary resources to multi-tenant as needed.

Installation

This plugin only works with 1.1 and newer.
grails install-plugin multi-tenant

Choose Your Database "Mode"

The webapp always runs in multi-tenant mode, but you have the choice on how your database will operate. You can choose either one database per customer (single-tenant), or one database for all customers (multi-tenant).

In Config.groovy, add the following section:

tenant {
    mode = "singleTenant" // OR "multiTenant"
}
multiTenant is the default

For more detailed instructions on how to set up each mode:

Multi-Tenant Database Set Up

Single Tenant Database Set Up

Spring Integration

Some of your beans may contain state that needs to be unique per tenant. A good example of this might be a bean that contains a cache, where they key in the cache would result in collisions if it were shared among all tenants.

With this plugin, you can write the beans as if they are for only one tenant, and the plugin will manage a copy of the bean for each tenant for you and make sure the correct instance is used in the appropriate places. Mark each stateful spring bean as "uniquePerTenant" as follows:

class SomeStatefulBean implements SomeStatefulBeanInterface {

public static boolean uniquePerTenant = true

}

Because of the way Spring AOP works, you have to create an interface for your spring bean and make sure you are wiring the interface, not the implementation itself.

Sometimes, you may not have access to the source code to add "public static boolean uniquePerTenant = true" to the spring bean. You can still make the bean unique by adding the name of the bean to the "tenant.uniquePerTenantBeans" property in Config.groovy:

tenant {
    uniquePerTenantBeans = ["beanName1", "beanName2"]
}

Configure a Tenant Resolver

A tenant resolver is the mechanism that figures out who the current tenant is. The plugin ships with two ways of doing this, and you can also implement your own.

DNS (Subdomain per tenant)
If you have public facing (shopping cart, CMS, etc) areas of your application, you will probably want to use dns to manage your tenants.

DNS Resolver Set Up

Acegi (Uses a "userTenantId" property from your user record)
If you use acegi, you can just have the tenantId derived from the authenticated principal.

Acegi Resolver Set Up

Custom Tenant Resolver

If the domain-name based approach doesn't work for you, you can implement your own TenantResolver by implementing the TenantResolver interface and then adding that class to your spring config:

beans {
    //Make sure to import!
    tenantResolver(MyCustomTenantResolver)
}

Manually changing tenant

Occasionally, you will need to perform data operations for a specific tenant. For example, if you have a signup form on your website and you need to create a bunch of default records for the new tenant. There is a utility class you can use to execute a block of code for a specific tenant:

import com.infusion.tenant.util.TenantUtils
…
//Everything in the following block will be applied to tenantId=15
TenantUtils.doWithTenant(15) {
  Contact c = new Contact(firstName:"Bob")
  c.save() //This record will be saved to tenantId=15
  …
}
//Now the tenantId will be set back to what it was before
...

Tenant Status Checker

You will probably want a way to verify that each tenant has access to their application. If you add a bean to spring named "tenantStatusChecker" that implements the "TenantStatusChecker" interface com.infusion.tenant.TenantStatusChecker, then this plugin will automatically validate each request and redirect to /noaccess.gsp if the tenant doesn't have access.

This is pretty clunky and we have plans to make it much better in the future...

Behind the scenes

Here's what the plugin does behind the scenes:

Multi-Tenant Mode

  • Performs compile time injection to add a "tenantId" column to all domain objects (multi-tenant mode)
  • Adds a nullable constraint for the tenantId column (to avoid annoying validation failures)
  • Intercepts and wraps all hibernate Criteria and Query that force filtering on tenantId so that:
    • All GORM functions backed by a Criteria object always return records ONLY for the current tenant (createCriteria, countBy, exists, find, get, list, listOrderBy, withCriteria)
    • All GORM functions backed by a Query object will inject a named parameter "tenantId" with the current tenantId if a named parameter already exists This is one way you can get around the tenant filtering if you need to.
  • Listens to appropriate hibernate events to check for tenantId:
    • onPreDelete - make sure the record you're deleting matches the current tenantId
    • onLoad - make sure you the record you are loading matches the current tenantId
    • onPreUpdate - make sure the record you are updating matches the current tenantId
    • onPreInsert - set the current tenantId for the record being added.tenantId

Single-Tenant Mode

  • Converts the "dataSource" bean into an AOP proxy that creates a new datasource for each tenant

Other

  • For each spring bean marked as uniquePerTenant:
    • Convert the spring bean definition to prototype (the spring container will need to be able to create a unique instance of the bean for each tenant)
    • Replaces the original bean definition with a Spring AOP proxy definition. The proxy intercepts each method, locates the appropriate bean for the current tenant (or creates a new one if needed), and delegates the method call to that bean.
  • Adds a request filter for all controllers that binds the value from session.tenantId to a threadLocal variable. All code processed during that thread's execution will apply only to that tenant.
  • Adds a request filter for development mode only that sets the current session's tenantId by passing a request parameter, __tenantId.

Centralized Tenant Management Database

Version 0.17 added a centralized database capability. The centralized database is a location for all tenant related data but no application data. It contains tenant configuration data and full support for things like quartz jobs etc. This allows us to centralize the tenant management and not expose that data to client databases. This keep redundancy down and security up. Another plugin is in the works to manage that database. The plugin will use the default datasource or the datasource at tenant 0 to locate tenant configuration data so you will need that one database to contain the tenant configuration data.

Good Luck!