Multi-tenant Hibernate

DevEnglish

Let’s say you have an application that needs to deal with several tenants and that each tenant requires to have its own database located where he wants.
In our example we will have the same database shcema for all tenant but it is not a problem to have several schemas.
The solution presented here loads one SessionFactory per Tenant with one hibernate.properties file per tenant that allows you to have one tenant on PostgreSQL, one tenant on Oracle, and 2 other tenants on MySQL, etc…

In our application each tenant has a unique tenant_key (composed of 8 standard letters, ex : tgfdscvb) used for database schema, tenant directories, tenant property files…

First here is the HibernateUtil.java file (logs and comments were removed below, to reduce the number of lines) :

/**
 *             DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
 *                           Version 2, December 2004
 *
 * Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
 *
 * Everyone is permitted to copy and distribute verbatim or modified
 * copies of this license document, and changing it is allowed as long
 * as the name is changed.
 *
 *            DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
 *   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
 *
 *  0. You just DO WHAT THE FUCK YOU WANT TO.
 *
 */
package com.ledruide.helper.hibernate;

import java.io.FileInputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.HibernateException;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.cfg.Configuration;

public class HibernateUtil {

    private static Log                                     log              = LogFactory.getLog( HibernateUtil.class );
    private static Map<String , SessionFactory>            sessionFactories = new HashMap<String , SessionFactory>();

    public static final ThreadLocal<Map<String , Session>> threadLocal      = new ThreadLocal<Map<String , Session>>();

    /**
     * This method looks into all the session factories in orded to find the good one based on the
     * tenant.
     * 
     * @param key represents the tenant's name (UNIQUE in database)
     * @return the current session for the user based on the right SessionFactory
     * @throws HibernateException if an error occurs
     */
    public static Session currentSession( String key ) throws HibernateException {
        Map<String , Session> sessionMaps = ( Map<String , Session> ) threadLocal.get();

        if ( sessionMaps == null ) {
            sessionMaps = new HashMap<String , Session>();
            threadLocal.set( sessionMaps );
        }
        // Open a new Session, if this Thread has none yet
        Session s = ( Session ) sessionMaps.get( key );
        if ( s == null ) {
            s = ( ( SessionFactory ) sessionFactories.get( key ) ).openSession();
            sessionMaps.put( key , s );
        }
        return s;
    }

    /**
     * Should not be used...
     * 
     * @return an empty session
     * @throws HibernateException
     */
    public static Session currentSession() throws HibernateException {
        throw new HibernateException( "This method : currentSession() must not be used!" );
    }

    /**
     * Closes all the currents session.
     * 
     * @throws HibernateException
     */
    public static void closeSessions() throws HibernateException {
        Map<String , Session> sessionMaps = ( HashMap<String , Session> ) threadLocal.get();
        threadLocal.set( null );
        if ( sessionMaps != null ) {
            for ( Session session : sessionMaps.values() ) {
                if ( session.isOpen() ) session.close();
            };
        }
    }

    /**
     * Closes the current session from the ThreadLocal
     */
    public static void closeSession( String key ) {
        Map<String , Session> sessionMaps = ( HashMap<String , Session> ) threadLocal.get();
        if ( sessionMaps != null ) {
            Session session = sessionMaps.get( key );
            try {
                if ( session != null && session.isOpen() ) {
                    session.flush();
                    session.close();
                }
            }
            catch ( Throwable th ) {
                log.error( "Could not flush nor close session for tenant " + key + " with session "
                        + session.hashCode() , th );
                th.printStackTrace();
            }
            finally {
                sessionMaps.remove( key );
                session = null;
            }
        }
    }

    /**
     * Rollback the database transaction.
     */
    public static void rollback( String key ) throws HibernateException {
        Map<String , Session> sessionMaps = ( HashMap<String , Session> ) threadLocal.get();
        Session session = currentSession( key ) ;
        Transaction tx = session.getTransaction();
        try {
            sessionMaps.remove( key );
            if ( tx != null && !tx.wasCommitted() && !tx.wasRolledBack() ) {
                tx.rollback();
            }
        }
        catch ( HibernateException ex ) {
            throw new HibernateException( ex );
        }
        finally {
            closeSession(key);
        }
    }

    /**
     * Builds all the SessionFactories from a Map containning the tenant as a key and a Properties
     * Object as a value.
     * 
     * @param propertiesList Map with key = tenant and value = PropertyFileName in order to build
     *            the SessionFactory
     * @throws Exception
     */
    public static void buildSessionFactories( List<String> propertiesList ) throws Exception {
        // For each element (tenant) we create a SessionFactory
        for ( String tenantKey : propertiesList ) {
            buildSessionFactory( tenantKey );
        }
    }

    /**
     * Builds all the SessionFactories from a Map containning the tenant as a key and a Properties
     * Object as a value.
     * 
     * @param propertiesList Map with key = tenant and value = Properties in order to build the
     *            SessionFactory
     */
    public static void buildSessionFactory( String tenantKey ) throws Exception {
        buildSessionFactory( tenantKey , true );
    }

    /**
     * Builds all the SessionFactories from a Map containning the tenant as a key and a Properties
     * Object as a value.
     * 
     * @param propertiesList Map with key = tenant and value = Properties in order to build the
     *            SessionFactory
     */
    public static void buildSessionFactory( String tenantKey , boolean replace ) throws Exception {
        try {
            if ( replace || sessionFactories.get( tenantKey ) == null ) {
                Configuration conf = new Configuration();
                // Use your own file emplacement mecanism... this one is for brevity
                String fileName = "/hibernate_" + tenantKey + ".properties";
                Properties props = new Properties();
                props.load( new FileInputStream( fileName ) );
                conf.addProperties( props );

                Properties propsHibernateMapping = new Properties();
                // This hibernate.cfg file is only a list of all hbm.xml objects (details below)
                propsHibernateMapping.load( HibernateUtil.class.getResourceAsStream( "/hibernate.cfg" ) );
                for ( Object key : propsHibernateMapping.keySet() ) {
                    conf.addResource( ( String ) key );
                }
                SessionFactory sessionFactory = conf.buildSessionFactory();
                sessionFactories.put( tenantKey , sessionFactory );
            }
        }
        catch ( Throwable ex ) {
            log.error( "Initial SessionFactory creation failed." , ex );
            throw new Exception( ex );
        }
    }

    public static Map<String , SessionFactory> getSessionFactories() {
        return sessionFactories;
    }

}

How to use this class ?
The next code loads every tenant on application startup, I removed everything that checked the databases versions and the automatic upgrades/downgrades procedures… :

    for ( Tenant tenant : tenants ) {
        try {
            // Creates the SessionFactory based on the tenant HibernateProperty file
            HibernateUtil.buildSessionFactory( tenant.getDatasourceUrl() );
        }
        catch ( Exception e ) {
            // File for TENANT WAS NOT FOUND
            // nothing else to do than logging the problem
            log.warn( "\n\n--------------------------------------------------------"
                   + "\n----- DATASOURCE FILE NOT FOUND FOR TENANT : " + tenant.getName() + " ("
                   + tenant.getDatasourceUrl() + ")" + tenant.getName() + " ("
                   + tenant.getDatasourceUrl() + ")!" );
        }
    }

You have to create a hibernate.cfg file to attach all your model objects to the tenant SessionFactory :

com/ledruide/project1/domain/model/Quality.hbm.xml
com/ledruide/project1/domain/model/LimitFamilyVariant.hbm.xml
com/ledruide/project1/domain/model/ExternalActionRef.hbm.xml
com/ledruide/project1/domain/model/WfObjectType.hbm.xml
com/ledruide/project1/domain/model/Software.hbm.xml
com/ledruide/project1/domain/model/TaskType.hbm.xml
com/ledruide/project1/domain/model/RepairInfo.hbm.xml
com/ledruide/project1/domain/model/OperationStatus.hbm.xml
com/ledruide/project1/domain/model/EventCategory.hbm.xml
com/ledruide/project1/domain/model/PclassMref.hbm.xml
...

More detail on demand…

Previous
CentOS 5.5 – Install MySQL 6
Next
Question existentielle – Ordinateur portable ou fixe ?

Leave a comment

Your email address will not be published. Required fields are marked *

Time limit is exhausted. Please reload the CAPTCHA.