Hibernate.orgCommunity Documentation

Chapter 16. Multi-tenancy

Table of Contents

16.1. What is multi-tenancy?
16.2. Multi-tenant data approaches
16.2.1. Separate database
16.2.2. Separate schema
16.2.3. Partitioned (discriminator) data
16.3. Multi-tenancy in Hibernate
16.3.1. MultiTenantConnectionProvider
16.3.2. CurrentTenantIdentifierResolver
16.3.3. Caching
16.3.4. Odds and ends
16.4. Strategies for MultiTenantConnectionProvider implementors

The term multi-tenancy in general is applied to software development to indicate an architecture in which a single running instance of an application simultaneously serves multiple clients (tenants). This is highly common in SaaS solutions. Isolating information (data, customizations, etc) pertaining to the various tenants is a particular challenge in these systems. This includes the data owned by each tenant stored in the database. It is this last piece, sometimes called multi-tenant data, on which we will focus.

There are 3 main approaches to isolating information in these multi-tenant systems which goes hand-in-hand with different database schema definitions and JDBC setups.

Using Hibernate with multi-tenant data comes down to both an API and then integration piece(s). As usual Hibernate strives to keep the API simple and isolated from any underlying integration complexities. The API is really just defined by passing the tenant identifier as part of opening any session.


Additionally, when specifying configuration, a org.hibernate.MultiTenancyStrategy should be named using the hibernate.multiTenancy setting. Hibernate will perform validations based on the type of strategy you specify. The strategy here correlates to the isolation approach discussed above.

NONE

(the default) No multi-tenancy is expected. In fact, it is considered an error if a tenant identifier is specified when opening a session using this strategy.

SCHEMA

Correlates to the separate schema approach. It is an error to attempt to open a session without a tenant identifier using this strategy. Additionally, a org.hibernate.service.jdbc.connections.spi.MultiTenantConnectionProvider must be specified.

DATABASE

Correlates to the separate database approach. It is an error to attempt to open a session without a tenant identifier using this strategy. Additionally, a org.hibernate.service.jdbc.connections.spi.MultiTenantConnectionProvider must be specified.

DISCRIMINATOR

Correlates to the partitioned (discriminator) approach. It is an error to attempt to open a session without a tenant identifier using this strategy. This strategy is not yet implemented in Hibernate as of 4.0 and 4.1. Its support is planned for 5.0.

When using either the DATABASE or SCHEMA approach, Hibernate needs to be able to obtain Connections in a tenant specific manner. That is the role of the org.hibernate.service.jdbc.connections.spi.MultiTenantConnectionProvider contract. Application developers will need to provide an implementation of this contract. Most of its methods are extremely self-explanatory. The only ones which might not be are getAnyConnection and releaseAnyConnection. It is important to note also that these methods do not accept the tenant identifier. Hibernate uses these methods during startup to perform various configuration, mainly via the java.sql.DatabaseMetaData object.

The MultiTenantConnectionProvider to use can be specified in a number of ways:

  • Use the hibernate.multi_tenant_connection_provider setting. It could name a MultiTenantConnectionProvider instance, a MultiTenantConnectionProvider implementation class reference or a MultiTenantConnectionProvider implementation class name.

  • Passed directly to the org.hibernate.service.ServiceRegistryBuilder.

  • If none of the above options match, but the settings do specify a hibernate.connection.datasource value, Hibernate will assume it should use the specific org.hibernate.service.jdbc.connections.spi.DataSourceBasedMultiTenantConnectionProviderImpl implementation which works on a number of pretty reasonable assumptions when running inside of an app server and using one javax.sql.DataSource per tenant. See its javadocs for more details.

org.hibernate.context.spi.CurrentTenantIdentifierResolver is a contract for Hibernate to be able to resolve what the application considers the current tenant identifier. The implementation to use is either passed directly to Configuration via its setCurrentTenantIdentifierResolver method. It can also be specified via the hibernate.tenant_identifier_resolver setting.

There are 2 situations where CurrentTenantIdentifierResolver is used:

Additionally, if the CurrentTenantIdentifierResolver implementation returns true for its validateExistingCurrentSessions method, Hibernate will make sure any existing sessions that are found in scope have a matching tenant identifier. This capability is only pertinent when the CurrentTenantIdentifierResolver is used in current-session settings.


The approach above is valid for the DATABASE approach. It is also valid for the SCHEMA approach provided the underlying database allows naming the schema to which to connect in the connection URL.

Example 16.3. Implementing MultiTenantConnectionProvider using single connection pool

/**
 * Simplisitc implementation for illustration purposes showing a single connection pool used to serve
 * multiple schemas using "connection altering".  Here we use the T-SQL specific USE command; Oracle
 * users might use the ALTER SESSION SET SCHEMA command; etc.
 */
public class MultiTenantConnectionProviderImpl
		implements MultiTenantConnectionProvider, Stoppable {
	private final ConnectionProvider connectionProvider = ConnectionProviderUtils.buildConnectionProvider( "master" );

	@Override
	public Connection getAnyConnection() throws SQLException {
		return connectionProvider.getConnection();
	}

	@Override
	public void releaseAnyConnection(Connection connection) throws SQLException {
		connectionProvider.closeConnection( connection );
	}

	@Override
	public Connection getConnection(String tenantIdentifier) throws SQLException {
		final Connection connection = getAnyConnection();
		try {
			connection.createStatement().execute( "USE " + tenanantIdentifier );
		}
		catch ( SQLException e ) {
			throw new HibernateException(
					"Could not alter JDBC connection to specified schema [" +
							tenantIdentifier + "]",
					e
			);
		}
		return connection;
	}

	@Override
	public void releaseConnection(String tenantIdentifier, Connection connection) throws SQLException {
		try {
			connection.createStatement().execute( "USE master" );
		}
		catch ( SQLException e ) {
			// on error, throw an exception to make sure the connection is not returned to the pool.
			// your requirements may differ
			throw new HibernateException(
					"Could not alter JDBC connection to specified schema [" +
							tenantIdentifier + "]",
					e
			);
		}
		connectionProvider.closeConnection( connection );
	}

	...
}

This approach is only relevant to the SCHEMA approach.