ERNI Technology Post No. 48: Multitenancy and its support in hibernate

Multitenant applications have greatly increased in recent years. It is particularly hard to imagine Cloud Computing and SAAS (Software As A Service) without multitenancy.

The implementation of a multitenant application requires multitenant data management because tenants' data have to be isolated from one other.

You are probably asking yourself how such multitenant data management can be implemented using frameworks?

This article will show how multitenant data management can be implemented with the help of Hibernate.

The first part outlines the general principle of a multitenant application and the second part explains the different variants in Hibernate and uses an example to demonstrate these variants step by step.

1.1 PRINCIPLE

We talk of a multitenant application when several customers share software and hardware. This type of multitenant application runs in a single instance. Here, all customers use the same application basis, presentation (UI), business workflows and data management systems. 

However, customers are isolated from each other and can only access their own data as well as any global data. Which parts can be customised to customer needs depends on the degree of potential individualisation of a multitenant application. For example, some applications only offer the option to manage the data of each specific client. In such a case, the application would have to offer a standard user interface as well as out-of-the-box workflows.
Costs can be greatly reduced with a multitenant application. For example, tenants can share the HW and licence costs. Furthermore, the development costs are usually lower because tenants have similar requirements for the application.
On the other hand, the complexity of a multitenant application can be considerably greater than that of a single-tenant solution.

1.2 MULTITENANCY WITH HIBERNATE

In this section we will focus on the question of how data management can be implemented using Hibernate in a way that offers multitenancy and how this data can be isolated for each customer.

Hibernate offers three ways of doing this:
•    Separate instance
•    Separate schema
•    Partitioned data (support planned)

These three options are explained below using an example.

Banks A and B share an application with which they can manage their customers.

Bank A has the following customers:
•    Anna Miller
•    Remo Warren

Bank B has the following customers:
•    Laura Oliver
•    Ella Fraser
•    Jamie Silver

The term tenant key is used in the descriptions below. This refers to the unique, tenant-specific identification key.

Client Tenant Key
Bank A bank_a
Bank B bank_b

1.2.1 SEPARATE INSTANCE

Using the "separate instance" option, banks A and B have their own independent database instances. In our example, these are database instance A from Bank A and database instance B from Bank B. Both instances have a default schema with a client table where the banks' clients are managed.

Banks A and B access the database via the same application instance. Every time the database is accessed Hibernate uses the session context to identify the tenant who is accessing it and its tenant key. The right database instance is identified using this information. Every access by bank A is passed on to database instance A, and every access by bank B to database instance B.

separate instance approach

Image 1: „Separate instance“ approach

Advantages:
•    Independence of the database instances

Disadvantages:
•    The maintenance of several database instances is expensive
•    Modifications to database structure must be completed for each database instance

1.2.2 SEPARATE SCHEMA

Using the "separate schema" option, banks A and B use the same database instance. However, the banks use different database schemas to distinguish themselves from each other. The same table structures can be found in both schemas. In our example, the client table is for managing bank customers.

Banks A and B access the database via the same application instance. Every time the database is accessed, Hibernate uses the session context to identify the tenant who is accessing it and its tenant key. The right database schema is identified using this information. Every access by bank A is passed on to schema A and every access by bank B to schema B.

separate schema approach

Image 2: „Separate schema“ approach

Advantages:
•    The database instance it maintained centrally
•    Cost reduction thanks to sharing licence and HW costs

Disadvantages:
•    Modifications to database structure must be completed for each schema

1.2.3 PARTITIONED DATA

Using the "partitioned data" option, banks A and B have the same database instance, the same database schema and the same tables. In this option, all database items are shared. The difference between the database tuples of bank A and bank B can be found at table level. Each table is implicitly expanded by a so-called discriminator column. In our example, we call this discriminator column "tenant key". In the data sets of Bank A, this column is set to "bank_a", and to "bank_b" for Bank B.

Banks A and B access the database via the same application instance. Every time the database is accessed, Hibernate uses the session context to identify the tenant who is accessing and its tenant key. This information is then restricted to the discriminator column. If bank A accesses the client table, only data sets are selected that have been entered in the discriminator column "bank_a". The same applies to bank B.

The "partitioned data" option is not currently supported in Hibernate. However, support is planned for Hibernate 5.

Partitioned data

Image 3: „Partitioned data“ approach

Advantages:
•    Cost reduction thanks to sharing licence and HW costs
•    The database instance is maintained centrally
•    Modifications to database structure only need to be completed once

Disadvantages:
•    If the database instance fails, all tenants are affected

1.3 IMPLEMENTATION OF THE "SEPARATE SCHEMA" APPROACH

Implementation of the "separate schema" approach is illustrated in the following section.
This approach was chosen because, of those that can currently be implemented, it is closest to the single instance approach.

A PostgreSQL database and Hibernate 4.3.5 were used for implementation. Furthermore, for the sake of simplicity, Native Hibernate and self-managed JDBC connections (without connection pool) were used.

1.3.1 RESTRICTIONS

The database structure must be created on the database manually. Hibernate does not support automatic schema export in a multitenancy environment.

1.3.2 CLIENT ENTITY

The client entity represents the customers of a bank. For the sake of simplicity, this entity only has two attributes — first Name and last Name — to manage first and last names as well as a primary key.

@Entity
@Table(name="client")
public class Client {
    
    @Id
    @GeneratedValue(generator="increment")
    @GenericGenerator(name="increment", strategy = "increment")
    private Long id;
    
    @Column
    private String firstname;
    
    @Column
    private String lastname;
    
    public Client() {
        
    }

    public Client(String firstname, String lastname) {
        this.firstname = firstname;
        this.lastname = lastname;
    }

    public String getFirstname() {
        return firstname;
    }

    public void setFirstname(String firstname) {
        this.firstname = firstname;
    }

    public String getLastname() {
        return lastname;
    }

    public void setLastname(String lastname) {
        this.lastname = lastname;
    }
    
}

Code-Listing 1: Client Entity

1.3.3 TENANT-IDENTIFIER-RESOLVER

It is the job of the Tenant-Identifier-Resolver to identify the accessing tenant. We will identify the tenant on the basis of the session context. This session context contains tenant-specific information. To do this, you have to implement a dedicated class, which derives the CurrentTenantIdentifierResolver interface from the Hibernate framework. The interface allows a lot of freedom as to how to identify the tenant. In this example, we are retrieving the current tenant from the session context and returning the tenant key.

public class TenantResolver implements CurrentTenantIdentifierResolver {

    @Override
    public String resolveCurrentTenantIdentifier() {
        return SessionContext.getCurrentTenant().getKey();
    }

    @Override
    public boolean validateExistingCurrentSessions() {
        return false;
    }

}

Code-Listing 2: TenantResolver

1.3.4 MULTI-TENANT-CONNECTION-PROVIDER 

The job of this class is to make the JDBC connections available on the database. To do this, you have to implement a class, which derives the MultiTenantConnectionProvider interface from Hibernate framework. Since only one interface is prescribed, there is a lot of freedom here too. The developer can decide whether container-managed or self-managed connections are returned. In our example self-managed JDBC connections are returned to the database instance.

We implement the getAnyConnection() method so that it returns a JDBC connection to the default schema and getConnection(String tenantKey) so that the connection to the tenant-specific schema is established. This tenant key is identified by the tenant identifier resolver during the running time. The tenant key matches the schema name.
The disadvantage of the aforementioned freedom is that you have to undertake the schema switch yourself using T-SQL and the following solution therefore probably does not work with every database system.
An alternative could be to use two JDBC connections, which point directly to the two schemas.

public class MultiTenantConnectionProviderImpl implements MultiTenantConnectionProvider {

    private static final long serialVersionUID = 2810274398511638803L;
    
    private static final String USER_NAME = "postgres";
    private static final String PASSWORD = "postgres";
    private static final String CONNECTION_URL = "jdbc:postgresql://localhost:5432/multitenancy";
    
    
    @Override
    public Connection getAnyConnection() throws SQLException {
        return DriverManager.getConnection(CONNECTION_URL, USER_NAME, PASSWORD);
    }

    @Override
    public Connection getConnection(String tenantKey) throws SQLException {
        final Connection connection = getAnyConnection();
        connection.createStatement().execute("SET SCHEMA '" + tenantKey + "'");
        return connection;
    }
    
    @Override
    public void releaseAnyConnection(Connection connection) throws SQLException {
        if(connection != null) {
            connection.close();
        }
    }
    
    @Override
    public void releaseConnection(String tenantKey, Connection connection)
            throws SQLException {
        releaseAnyConnection(connection);
        
    }

    ….
}

Code-Listing 3: MultiTenantConnectionProviderImpl (incomplete)

1.3.5 HIBERNATE CONFIGURATION

In the Hibernate configuration (hibernate.cfg.xml), you have to define which option you want to use. In our example, we want to use the "separate schemas" option. This corresponds to the value SCHEMA, which we therefore specify for the property "hibernate.multiTenancy".
Furthermore, it is necessary to define a Tenant-Identifier-Resolver and a multitenant connection provider. Here, we therefore define the two classes TenantResolver and MultiTenantConnectionProviderImpl, which we have already implemented.

Hibernate.cfg.xml code listing (not complete)

<hibernate-configuration>
    <session-factory>
        <property name="hibernate.dialect">org.hibernate.dialect.PostgreSQLDialect</property>
        <property name="show_sql">true</property>
        
        <!-- Configuration for multitenancy separate schema support -->
        <property name="hibernate.multiTenancy">SCHEMA</property>
        <property name="hibernate.tenant_identifier_resolver">
          multitenancy.sample.TenantResolver</property>
        <property name="hibernate.multi_tenant_connection_provider">
          multitenancy.sample.MultiTenantConnectionProviderImpl</property>
        
        <!-- Entity mapping -->
        <mapping class="multitenancy.sample.Client"/>
    </session-factory>
</hibernate-configuration>

Code-Listing 4: hibernate.cfg.xml (incomplete)

1.3.6 SIMULATING HIBERNATE ACCESS

To simulate access by the application to hibernate and therefore to the database, we use the following Java class MultitenancyAccessSimulator.

For bank A we create the bank customers Anna Miller and Remo Warren and, for bank B, the bank customers Laura Oliver, Ella Fraser and Jamie Silver.

Code listing MultitenancyAccessSimulator

Publicc class MultitenancyAccessSimulator {

    public static void main(String[] args) {
        final SessionFactory sessionFactory = MultitenancyAccessSimulator.createSessionFactory();
        Session session = null;
        
        
        SessionContext.setCurrentTenant(Tenant.BANK_A);
        session = sessionFactory.openSession();
        
        session.beginTransaction();
        
        session.save(new Client("Anna", "Miller"));
        session.save(new Client("Remo", "Warren"));
        
        session.getTransaction().commit();
        session.close();
        
        
        SessionContext.setCurrentTenant(Tenant.BANK_B);
        session = sessionFactory.openSession();
        session.beginTransaction();
        
        session.save(new Client("Laura", "Oliver"));
        session.save(new Client("Ella", "Fraser"));
        session.save(new Client("Jamie", "Silver"));
        
        session.getTransaction().commit();
        
        sessionFactory.close();
    }
    
    private static SessionFactory createSessionFactory() {
        final Configuration configuration = new Configuration();
        configuration.configure();
        final StandardServiceRegistry serviceRegistry = new StandardServiceRegistryBuilder().applySettings(
                configuration.getProperties()).build();
        return configuration.buildSessionFactory(serviceRegistry);
    }

}

Code-Listing 5: MultitenancyAccesSimulator

Then we check the result directly in the database with an SQL query:

select * from bank_a.client;

Result:

id firstname lastname
1 Anna Miller
2 Remo Warren

select * from bank_b.client;

1.4 Summary

In this article we have learned about the three approaches for implementing multitenant data management. The "partitioned data" approach, which is closest to the single instance approach, is not yet supported by Hibernate.
For this reason, we implemented the "separate schema" option using an example. The findings from the implementation are that Hibernate offers a lot of scope for development (JDBC connection management) how a corresponding option is implemented. The schema switch must be implemented by the developer and is therefore not done automatically by the framework.
Overall, this approach can be implemented easily and works as required in conjunction with a PostgreSQL database.
The consequence of the aforementioned scope is that the "separate instance" option can be implemented in almost exactly the same way. Instead, in the Multi-Tenant-Connection-Provider, you have to manage one connection per database instance and return it to the tenant accordingly.
In terms of configuration, it makes no difference for either approach whether you specify SCHEMA or DATABASE for the "hibernate.multiTenancy" property in the Hibernate configuration (hibernate.cfg.xml). The only thing that is checked by Hibernate is whether a Tenant-Identifier-Resolver has been defined.
The question is therefore why a distinction has to be made at all in terms of configuration between the two options, "Separate instance" and "separate schema", in Hibernate. Hibernate completely leaves control and implementation to the user.
We are eager to see how the Hibernate developers will implement "Partitioned data". We hope that Hibernate finds an elegant solution for handling the discriminator column expansion so that you do not have to spoil your entities unnecessarily with tenant-specific expansions.

References

 

posted on 17.09.2014
Categories: 
by: Bruno Krieg