(Quick Reference)

8 Multi-Tenancy

Version: 2023.3.0

8 Multi-Tenancy

GORM for MongoDb supports the following multi-tenancy 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.

8.1 Configuring Multi Tenancy

You can configure Multi-Tenancy the same way described in the GORM for Hibernate documenation, simply specify a multi tenancy mode and resolver:

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

Note that if you are using MongoDB and Hibernate together the above configuration will configure both MongoDB and Hibernate to use a multi-tenancy mode of DATABASE.

If you only want to enable multi-tenancy for MongoDB only you can use the following configuration instead:

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

8.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)
    }
}

8.3 Multi Tenancy Modes

As mentioned previously, GORM for MongoDB supports all three multi tenancy modes however there are some considerations to keep in mind.

Database Per Tenant

When using the DATABASE mode, only GORM methods calls are dispatched to the correct tenant. This means the following will use the tenant id:

// switches to the correct client based on the tenant id
Book.list()

However, going directly through the MongoClient will not work:

@Autowired MongoClient mongoClient

// uses the default connection and doesn't resolve the tenant it
mongoClient.getDatabase("book").find()

If you are working directly with the MongoClient instance you need to make sure you obtain the correct instance. For example:

import grails.gorm.multitenancy.*

@Autowired MongoDatastore mongoDatastore
...
MongoClient mongoClient =
        mongoDatastore.getDatastoreForTenantId(Tenants.currentId())
                      .getMongoClient()

Schema Per Tenant

When using the SCHEMA mode, GORM for MongoDB will use a different MongoDB database, but the same MongoClient instance, for each tenant.

However, once again only GORM methods will use the correct database. For example:

// switches to the correct database based on the tenant id
Book.list()

However, getting the database directly from MongoClient will not work:

@Autowired MongoClient mongoClient

// uses the default connection and doesn't resolve the tenant it
mongoClient.getDatabase("book").find()

To resolve this you should always use the DB property of the class which will ensure the right database is used:

// switches to the correct database based on the tenant id
Book.DB.find()

Partitioned Multi-Tenancy

When using the DISCRIMINATOR approach, GORM for MongoDB will store a tenantId attribute in each MongoDB document and attempt to partition the data.

Once again this works only when using GORM methods and even then there are cases where it will not work if you use native MongoDB interfaces.

For example the following works fine:

// correctly includes the `tenantId` in the query
Book.list()

As does this:

import static com.mongodb.client.model.Filters.*;

// correctly includes the `tenantId` in the query
Book.find(eq("title", "The Stand")).first()

But this logic bypasses any built into tenant id interception and inclusion:

Book.collection.find().first()

Since you are operating directly on the collection GORM cannot know when you perform a query on said collection.

In this case you will have to ensure to include the tenantId manually:

import static com.mongodb.client.model.Filters.*;
...
Book.collection.find(eq("tenantId", Tenants.currentId())).first()

And the same is true of write operations such as inserts that are done with the native API.

8.4 Dynamic ConnectionSources

If you are using a multi-tenancy mode of DATABASE then by default the expectation is that all tenants are configured in your application.yml file.

However, it is possible read your Mongo client connection sources dynamically using MongoConnectionSources.

The MongoConnectionSources class will read the mongo client configurations from a Mongo collection called mongo.connections by default. To configure it you must specify the connectionSourcesClass in application.yml:

grails:
    mongodb:
        multiTenancy:
            mode: DATABASE
        connectionSourcesClass: org.grails.datastore.mapping.mongo.connections.MongoConnectionSources
        connectionsCollection: "myconnections"

You can then even add new connections at runtime using the ConnectionSources API:

import grails.gorm.multitenancy.*

@Autowired MongoDatastore mongoDatastore
...
def configuration = [url:"mongodb://localhost/moreBooks"]
MongoClient mongoClient =
        mongoDatastore.connectionSources
                                          .addConnectionSource("moreBooks", configuration)

All new connection sources will be stored within the specified connectionsCollection and if the application is restarted will read from the connectionsCollection.

GORM for MongoDB does not implement provisioning of new MongoDB instances at runtime. This is something that would need to be implemented by a cloud services provider for example.