(Quick Reference)

12 Multi-Tenancy

Version: 2023.3.0-SNAPSHOT

12 Multi-Tenancy

Multi-Tenancy, as it relates to software developments, is when a single instance of an application is used to service multiple clients (tenants) in a way that each tenants' data is isolated from the other.

This type of architecture is highly common in Software as a Service (SaaS) and Cloud architectures. There are a variety of approaches to multi-tenancy and GORM tries to be flexible in supporting as many as possible.

12.1 Multi-Tenancy Modes

GORM supports the following different multitenancy modes:

  • DATABASE - A separate database with a separate connection pool is used to store each tenants data.

  • SCHEMA - The same database, but different schemas are used to store each tenants data.

  • DISCRIMINATOR - The same database is used with a discriminator used to partition and isolate data.

The above modes are listed from least probable to leak data between tenants to most probable. When using a DISCRIMINATOR approach much greater care needs to be taken to ensure tenants don’t see each other’s data.

12.2 Multi-Tenancy Transformations

The following transformations can be applied to any class to simplify greatly the development of Multi-Tenant applications. These include:

  • @CurrentTenant - Resolve the current tenant for the context of a class or method

  • @Tenant - Use a specific tenant for the context of a class or method

  • @WithoutTenant - Execute logic without a specific tenant (using the default connection)

For example:

import grails.gorm.multitenancy.*

// resolve the current tenant for every method
@CurrentTenant
class TeamService {

    // execute the countPlayers method without a tenant id
    @WithoutTenant
    int countPlayers() {
        Player.count()
    }

    // use the tenant id "another" for all GORM logic within the method
    @Tenant({"another"})
    List<Team> allTwoTeams() {
        Team.list()
    }

    List<Team> listTeams() {
        Team.list(max:10)
    }

    @Transactional
    void addTeam(String name) {
        new Team(name:name).save(flush:true)
    }
}

12.3 Database Per Tenant

Using a database per tenant is the most secure way to isolate each tenants data and builds upon GORM’s existing support for Multiple Data Sources.

Configuration

In order to activate database-per-tenant multi-tenancy you need to set the multi-tenancy mode to DATABASE in your configuration and supply a TenantResolver:

grails:
    gorm:
        multiTenancy:
            mode: DATABASE
            tenantResolverClass: org.grails.datastore.mapping.multitenancy.web.SubDomainTenantResolver
dataSource:
    dbCreate: create-drop
    url: jdbc:h2:mem:books
dataSources:
    moreBooks:
        url: jdbc:h2:mem:moreBooks
    evenMoreBooks:
        url: jdbc:h2:mem:evenMoreBooks

The above example uses a built-in TenantResolver implementation that works with Grails or Spring Boot and evaluates the current tenant id from the DNS sub-domain in a web application. However, you can implement whatever tenant resolving strategy you choose.

Multi Tenant Domain Classes

With the above configuration in place you then to need to implement the MultiTenant trait in the domain classes you want to be regarded as multi tenant:

class Book implements MultiTenant<Book> {
    String title
}

With that done whenever you attempt to execute a method on a domain class the tenant id will be resolved via the TenantResolver. So for example if the TenantResolver returns moreBooks then the moreBooks connection will be used when calling GORM methods such as save(), list() and so on.

Multi Tenancy and the Session Factory

Note that if you reference the default SessionFactory or PlatformTransactionManager in your classes that are injected via Spring, these will not be tenant aware and will point directly to default data source.

If you wish to obtain a specific SessionFactory or PlatformTransactionManager then you can use the getDatastoreForConnection(name) method of the HibernateDatastore class:

@Autowired
HibernateDatastore hibernateDatastore
...
Serializable tenantId = Tenants.currentId(HibernateDatastore)
SessionFactory sessionFactory = hibernateDatastore
                                    .getDatastoreForConnection(tenantId.toString())
                                    .getSessionFactory()

Multi Tenancy with Sessions

When working with GORM typically you need a session bound to the current thread in order for GORM and transactions to work consistently. If you switch to a different tenant then it may be that the session bound to the current thread no longer matches the underlying SessionFactory being used by the tenant. With this in mind you may wants to use the Tenants class to ensure the correct session is bound for the current tenant:

import static grails.gorm.multitenancy.Tenants.*

List<Book> books = withCurrent {
    Book.list()
}

You can also use a specify tenant id using the withId method:

import static grails.gorm.multitenancy.Tenants.*

List<Book> books = withId("moreBooks") {
    Book.list()
}

Note that if you are using more than one GORM implementation, it may be necessary to specify the implementation type:

import static grails.gorm.multitenancy.Tenants.*
import org.grails.orm.hibernate.*

List<Book> books = withId(HibernateDatastore, "moreBooks") {
    Book.list()
}

Adding Tenants at Runtime

Provisioning and creating SQL databases at runtime is non-trivial and beyond the scope of what GORM offers, however the ConnectionSources API does provide a way to hook into GORM to make this possible.

By default an InMemoryConnectionSources object is used which will read the connection sources from the application configuration and store them in-memory.

However, it is possible to configure an alternate implementation:

grails:
    gorm:
        connectionSourcesClass: com.example.MyConnectionSources
        multiTenancy:
            mode: DATABASE

The implementation could read the connection sources at startup from another database table and implement logic by overriding the addConnectionSource method to provision a new databases at runtime.

If you are interested in more examples, an implementation exists for MongoDB that reads connection sources from a MongoDB collection. However, it doesn’t implement support for provision MongoDB instances at runtime.

12.4 Schema Per Tenant

Schema-per-tenant is when a single database is used, but a different database schema is used for each tenant.

Configuration

In order to activate schema-per-tenant multi-tenancy you need to set the multi-tenancy mode to SCHEMA in your configuration and supply a TenantResolver:

grails:
    gorm:
        multiTenancy:
            mode: SCHEMA
            tenantResolverClass: foo.bar.MySchemaResolver
dataSource:
    dbCreate: create-drop
    url: jdbc:h2:mem:books

The TenantResolver can optionally implement the AllTenantsResolver interface and return the schema names of all tenants. This gives you the option to hard code these, place them in configuration or read them from the default schema dynamically.

If the AllTenantsResolver resolver is not implemented then GORM will use the configured SchemaHandler to resolve all the schema names from the database which it will use for the tenants.

Runtime Schema Creation

On startup, if dataSource.dbCreate is set to create the database at runtime, then GORM will attempt to create the schemas if they are missing.

To do this it uses an instance SchemaHandler which by default uses the CREATE SCHEMA [name] syntax, but can be overridden and customized for other database dialects as necessary.

If you want to use this feature and create schemas at runtime depending on the database you may need to configure an alternate implementation:

dataSource:
    dbCreate: create-drop
    schemaHandler: foo.bar.MySchemaHandler

You can disable completely runtime schema creation by removing the dbCreate option or setting it to none.

If you wish to add a schema whilst the application is running then you can use the addTenantForSchema of the HibernateDatastore class:

HibernateDatastore datastore = ...
datastore.addTenantForSchema("myNewSchema")
If dbCreate is disabled then you will have to create the schema manually prior to invoking this method

Schema-Per-Tenant Caveats

In order to support a schema-per-tenant, just like the DATABASE Multi-Tenancy mode, GORM uses a unique SessionFactory per tenant. So all of the same considerations regarding session management apply.

12.5 Partitioned Multi-Tenancy

Partitioned multi-tenancy is when a discriminator column is used in each multi-tenant class in order to partition the data.

When using a discriminator all data for all tenants is stored in a single database. This is means that, as a developer, you have to take much greater care to ensure that each tenants' data is correctly isolated.

Configuration

In order to activate discriminator-based multi-tenancy you need to set the multi-tenancy mode to DISCRIMINATOR in your configuration and supply a TenantResolver:

grails:
    gorm:
        multiTenancy:
            mode: DISCRIMINATOR
            tenantResolverClass: foo.bar.MyTenantResolver
dataSource:
    dbCreate: create-drop
    url: jdbc:h2:mem:books
The specified TenantResolver will also need to implement the AllTenantsResolver interface, which has an additional method to resolve all known tenants.

Mapping Domain Classes

Like the other forms of multi-tenancy you then need to implement the MultiTenant trait in the domain classes that are multi-tenant. In addition, you also need to specify a discriminator column. The default discriminator column is called tenantId, for example:

class Book implements MultiTenant<Book> {
    Long tenantId
    String title
}

However, the tenant identifier can be any type or name. For example:

class Book implements MultiTenant<Book> {
    String publisher
    String title

    static mapping = {
        tenantId name:'publisher'
    }
}

Querying and Caveats

The discriminator-based multi-tenancy transparently adds a Hibernate filter that is activated when you query the domain class and you can also use the Tenants API to switch between tenants:

import static grails.gorm.multitenancy.Tenants.*

List<Book> books = withCurrent {
    Book.list()
}
...
List<Book> books = withId("moreBooks") {
    Book.list()
}

Note that this automatic activation of the Hibernate filter using GORM eventing and therefore only works when you use GORM methods, if you perform any raw Hibernate queries on the session factory you will need to activate the filter manually:

import static grails.gorm.multitenancy.Tenants.*
import org.grails.orm.hibernate.*

// get a reference to the datastore and sessionFactory, probably from Spring
HibernateDatastore datastore = ..
SessionFactory sessionFactory = ..
datastore.enableMultiTenancyFilter()

// do work with the session factory
sessionFactory.openSession()

12.6 Understanding Tenant Resolvers

As mentioned previously the TenantResolver interface is how you define the value of the current tenant.

This section will cover a few more details about implementing TenantResolver instances.

Specifying the Tenant Resolver

As already mentioned you can specify the TenantResolver via configuration using the grails.gorm.tenantResolverClass setting:

grails:
    gorm:
        multiTenancy:
            mode: DATABASE
            tenantResolverClass: org.grails.datastore.mapping.multitenancy.web.SubDomainTenantResolver

However, if you are using Grails or Spring Boot then the TenantResolver can also be specified as a Spring bean and it will automatically be injected, this allows you to use dependency injection to configure the dependencies of the resolver.

Built-in Tenant Resolvers

The following table contains all of the TenantResolver implementations that ship with GORM and are usable out of the box. The package name has been shorted from org.grails.datastore.mapping to o.g.d.m for brevity:

name description

o.g.d.m.multitenancy.resolvers.FixedTenantResolver

Resolves against a fixed tenant id

o.g.d.m.multitenancy.resolvers.SystemPropertyTenantResolver

Resolves the tenant id from a system property called gorm.tenantId

o.g.d.m.multitenancy.web.SubDomainTenantResolver

Resolves the tenant id from the subdomain via DNS

o.g.d.m.multitenancy.web.CookieTenantResolver

Resolves the current tenant from an HTTP cookie named gorm.tenantId by default

o.g.d.m.multitenancy.web.SessionTenantResolver

Resolves the current tenant from the HTTP session using the attribute gorm.tenantId by default

o.g.d.m.multitenancy.web.HttpHeaderTenantResolver

Resolves the current tenant from the request HTTP Header using the header name gorm.tenantId by default

The tenant resolvers in the org.grails.datastore.mapping.multitenancy.web package require the grails-datastore-web dependency:

build.gradle
compile "org.grails:grails-datastore-web:2023.3.0-SNAPSHOT"

The AllTenantsResolver interface

If you are using discriminator-based multi-tenancy then you may need to implement the AllTenantsResolver interface in your TenantResolver implementation if you want to at any point iterate over all available tenants.

Typically with discriminator-based multi-tenancy the tenants are identified by some other domain class property. So for example an implementation would look like:

Iterable<Serializable> resolveTenantIds() {
    new DetachedCriteria(Company)
            .distinct('name')
            .list()
}

The above example uses the distinct names of each Company domain class to resolve all of the tenant identifiers.

Implementing Web Tenant Resolvers

If you wish to implement your own tenant resolver for Grails or Spring Boot then it is possible do so using the RequestContextHolder class without needing to inject any dependencies. For example the SubDomainTenantResolver implementation is as follows:

Serializable resolveTenantIdentifier() {

    RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes()
    if(requestAttributes instanceof ServletWebRequest) {

        String subdomain = ((ServletWebRequest)requestAttributes).getRequest().getRequestURL().toString();
        subdomain = subdomain.substring(subdomain.indexOf("/") + 2);
        if( subdomain.indexOf(".") > -1 ) {
            return subdomain.substring(0, subdomain.indexOf("."))
        }
        else {
            return ConnectionSource.DEFAULT
        }
    }
    throw new TenantNotFoundException("Tenant could not be resolved outside a web request")
}
If the tenant id is not found a TenantNotFoundException should be thrown.