Hibernate provides out of the box support for multi-tenancy and that is key advantage for converting existing hibernate/JPA based application to multi-tenant. For data access layer you just need to change few hibernate settings and Boom…. All your data access layer is all of a sudden supports multi tenancy. So let’s take a look at all possible change you might need when you are converting your existing single tenant application to multi-tenant.
Assumptions:
a) I am using Hibernate in data access layer.
b) I am using ThreadLocal variable to store and retrieve tenant context in all layers of application.
Steps:
1) I will start with Hibernate settings that we need to change. You might have already figured this out from all different blogs available over internet. If so, you can skip this section.
Solution: Update your persistence.xml file with following elements.
<properties>
<property name="hibernate.cache.use_second_level_cache" value="true"/>
<property name="hibernate.cache.use_query_cache" value="false"/>
<property name="hibernate.dialect" value="org.hibernate.dialect.Oracle10gDialect" />
<property name="hibernate.cache.region.factory_class" value="org.hibernate.cache.ehcache.EhCacheRegionFactory"/>
<property name="jboss.entity.manager.factory.jndi.name" value="java:jboss/emf/productEntityManagerFactory" />
<property name="net.sf.ehcache.configurationResourceName" value ="spring-config/ehcache.xml" />
<property name="hibernate.multiTenancy" value="SCHEMA"/>
<property name="hibernate.tenant_identifier_resolver" value="com.company.product.multitenancy.TenantIdentifierResolver"/>
<property name="hibernate.multi_tenant_connection_provider" value="com.company.product.multitenancy.MultiTenantConnProvider"/>
</properties>
You need to write two new classes as highlighted above.
a. TenantIdentifierResolver: This class will resolve tenant id for each database call. Hibernate will internally calls resolveCurrentTenantIdentifier method for each session it creates. Refer point# 10 below.
Sample Implementation:
public class TenantIdentifierResolver
implements
CurrentTenantIdentifierResolver {
@Override
public String resolveCurrentTenantIdentifier() {
return UserContextHolder.getTenantHeader();
}
@Override
public boolean validateExistingCurrentSessions() {
return false;
}
}
b. MultiTenantConnProvider: This class will provide tenant specific connection for each database operation. Hibernate will use getConnection( String tenantIdentifier ) method to create tenant specific connection for database operation.
Sample Implementation:
@Override
public Connection getConnection( String tenantIdentifier )
throws SQLException {
if ( tenantIdentifier != null ) {
final Connection connection = getAnyConnection();
try (Statement stmt = connection.createStatement()) {
stmt.execute( ALTER_SESSION_SET_CURRENT_SCHEMA + tenantIdentifier );
} catch( SQLException e ) {
try {
connection.close();
} catch( SQLException se ) {
LOGGER.error( "Exception while returning connection to pool.", se );
}
LOGGER.error( "Exception while setting tenant schema. Message : " + e.getMessage() );
throw new DataAccessException(
ExceptionType.DATA_ACCESS_EXCEPTION,
"Could not alter JDBC connection to specified schema [" + tenantIdentifier + "]", e );
}
return connection;
} else {
throw new DataAccessException(
ExceptionType.DATA_ACCESS_EXCEPTION,
"No tenantIdentifier found while getting connection. Please set tenantSchema in UserContextHolder first." );
}
}
2) Write a class, which will store tenant id into ThreadLocal variable so you can use it across all layers of thread execution.
Sample Implementation:
public class UserContextHolder {
private static InheritableThreadLocal<String> tenantHeaderContext = new InheritableThreadLocal<>();
public static String getTenantHeader() {
return tenantHeaderContext.get();
}
public static void setTenantHeader( String tenantID ) {
if ( tenantID == null ) {
tenantHeaderContext.remove();
}
tenantHeaderContext.set( tenantID );
}
}
3) Any third party library you might be using for workflow management like Activiti.
Solution:
a. You need to change that library version if newer version supports multi-tenancy.
OR
b. You need to replace it with some other library.
OR
c. You need to make required changes to support multi-tenancy.
In my case, I was lucky that we were using Activiti and they are supporting multi-tenancy out of the box. I just have to do some configuration changes to provide proper tenant schema name from my application’s thread local.
4) Update caching module.
Being a web application, to improve performance almost all application nowadays use server side caching. It might be implemented using third party library like EHCache in my case. In single tenant application all data is getting stored under single cache.
Solution: Now if you want to convert your application to multi-tenant then you need to create separate cache for each tenant you are on boarding.
5) Update search module.
In our case to improve text based search performance, we were using Solr service for search functionality. It could be any other search library like Lucene (Old version of Solr). Usually these tools consider all data populated is for single tenant. You need to make it multi-tenant compatible.
Solution: If you are using Solr then Solr has concept of multi core installation. You can choose to use different core for each tenant but you might need to modify search index update code wherever application is updating search indexes. You need to take tenant name from application thread local and choose right core before update.
6) Update Static data caches
If application is using any static data for caching purpose like putting some settings into Map to fetch it quickly. You might need to update those Maps to support multiple tenants.
Solution:
Either you can have Map<TenantId, Map> of Map<Object, Object>
OR
Just move these settings into some cache into EHCache.
7) Update Messaging code
If application is using JMS for delayed/asynchronous processing then you might need to handle multi-tenant scenario here. Usually this type of code publish message somewhere and it will have receiver on that queue which will process all messages from queue.
Solution: When you are publishing messages, you already have tenant context in your thread local so you can publish it along with message content and use it into receiver so you can set it back into thread local and if any database update within that receiver processing then it would work fine.
8) Update Auth filters and logout handlers to populate tenant identification
To pass tenant information throughout thread execution.
Solution: We are using spring security so we have various authentication and token checking filters. These filters needs to pass tenant information, which might be coming through request headers into ThreadLocal context. Therefore, it will be available through out that request processing.
When doing logout if there is some clean up code (like cleaning up user session information, user login history etc. in database), which is hitting database, then you need to set tenant information into ThreadLocal context in your logout handler also.
9) Async method calls
Asynchronous call can be using any @Asynchronous(EJB) or @Async(Spring). In our case, these were two types of asynchronous execution calls. As these are being executed in, different threads, which might be coming from thread pool, so ThreadLocal will not be passed to these executions. Any code within this execution, which requires tenant information, will fail.
For spring @Async it creates new thread on the fly so if your thread local is inheritable then it will work without any issue. This issue comes only in case of thread pool is used like in wildfly there is a thread pool for EJB async calls.
Solution: Only solution here is to pass tenant information as parameter to all the methods, which are asynchronous. You can make it little bit generic using Interceptor or AspectJ wrap around proxy, which will take this, tenant info parameter and set into ThreadLocal and on execution end, it will clean up that ThreadLocal so it cannot be misused by another execution.
10) Schedulers or Timer calls.
If your application is, using Schedulers, which will trigger based on @Timeout configuration then that code needs to be changed to run for each tenant now.
Solution: Loop through all tenants and execute main code for each tenant.
11) Switching tenant in same thread execution.
If you are maintaining master schema with all tenant’s information and tenant specific schema with application database. Then there might be a case when you need to change your tenant information for some database execution and for all other operations system will use tenant specific schema. This is not straight away possible in Hibernate as by default, it binds your session to thread local and even though you switch your tenant, it will not use updated information, as it will use existing session with that thread.
Solution: When you switch schema just run that code into different thread so it mandates hibernate to look for tenant information again in thread local. To centralize your changes you can use Interceptor or AspectJ wrap around proxy, which will spawn new thread and return results to call
Disclaimer: This is not exhaustive list of changes. Based on your application architecture, design and various libraries used; there might be some more changes you need to do. I have put together here to help others utilize my experience. If you have any issue please leave a comment and I will be happy to help you.
Comments
Post a Comment