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 Multi-Tenancy
Version: 2023.3.0-SNAPSHOT
Table of Contents
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:
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 |
---|---|
|
Resolves against a fixed tenant id |
|
Resolves the tenant id from a system property called |
|
Resolves the tenant id from the subdomain via DNS |
|
Resolves the current tenant from an HTTP cookie named |
|
Resolves the current tenant from the HTTP session using the attribute |
|
Resolves the current tenant from the request HTTP Header using the header name |
The tenant resolvers in the org.grails.datastore.mapping.multitenancy.web
package require the grails-datastore-web
dependency:
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.
|