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) :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 | /** * 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… :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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 :
1 2 3 4 5 6 7 8 9 10 11 | 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…
Leave a comment