In my previous posts (part 1, part 2) about Saas style multi-tenant web applications, the focus was on how multi-tenancy is achieved. In this post I will share how tenants can be added without restarting the application.
In this post, I will show you how you can add the tenant information in a separate 'master' database and the multi-tenant application will pick up the existing tenants and any tenant added while your application is running. If you were to add a new tenant in the application.yml file, then you would have to restart the application. With the technique I am about to describe, you do not need to restart your application.
The application.yml file is still required for application related properties and values, but the tenant database information need not be stored in it.
It is a simple table with the datasource related information about each tenant, like url, username, password, etc.
The datasource for the 'master_tenant' table needs to be setup first as this table contains the tenant information. Once this datasource is set up, it can be used to read the tenant information and set up a datasource per tenant.
Please pay attention to the package structures. The master and tenant related datasource configurations are kept in separate packages and the master datasource is set up first.
The following source code listings show the way this is done.
Happy coding :)
Update 4th Sep 2018
Many readers of this blog post have emailed me saying that they run the code but cannot login. This is because they have not populated the user table with username and password for a user nor have they created the necessary databases and schema. In many cases, the readers are not familiar with Spring Security. I request that you please read up on Spring Security first. Here is an excellent example from the official Spring documentation site.Purpose of this blog post
In my previous posts, I showed how to read the details of the tenant databases from the application.yml file. The focus was on how to set up SaaS style database per tenant multitenancy.In this post, I will show you how you can add the tenant information in a separate 'master' database and the multi-tenant application will pick up the existing tenants and any tenant added while your application is running. If you were to add a new tenant in the application.yml file, then you would have to restart the application. With the technique I am about to describe, you do not need to restart your application.
Structure of the application
In the previous posts, the application learnt about the tenants from the application.yml file. In this post, the tenant information is not learnt from the application.yml file but from a separate table in the master database.The application.yml file is still required for application related properties and values, but the tenant database information need not be stored in it.
Datasources - One for the master database and one each for every tenant
The tenant information will be stord in the master database. For example, if the master database is called 'masterdb' and the table for tenant information is called 'master_tenant', then the information might look like this:It is a simple table with the datasource related information about each tenant, like url, username, password, etc.
Order of setting up datasources and entity managers
It is important how the data sources are set up. Earlier, the tenant information was from the application.yml file. So the only datasources being setup were for each tenant.The datasource for the 'master_tenant' table needs to be setup first as this table contains the tenant information. Once this datasource is set up, it can be used to read the tenant information and set up a datasource per tenant.
Please pay attention to the package structures. The master and tenant related datasource configurations are kept in separate packages and the master datasource is set up first.
The following source code listings show the way this is done.
File: MasterDatabaseConfig.java
This sets up the master datasource to connect to the masterdb database.package com.sunitkatkar.blogspot.master.config; import java.util.Properties; import javax.persistence.EntityManagerFactory; import javax.sql.DataSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.dao.annotation. PersistenceExceptionTranslationPostProcessor; import org.springframework.data.jpa.repository.config. EnableJpaRepositories; import org.springframework.orm.jpa.JpaTransactionManager; import org.springframework.orm.jpa.JpaVendorAdapter; import org.springframework.orm.jpa. LocalContainerEntityManagerFactoryBean; import org.springframework.orm.jpa.vendor. HibernateJpaVendorAdapter; import org.springframework.transaction.annotation. EnableTransactionManagement; import com.sunitkatkar.blogspot.master.model.MasterTenant; import com.sunitkatkar.blogspot.master.repository. MasterTenantRepository; import com.zaxxer.hikari.HikariDataSource; /** * Configuration of the master database which holds information about tenants in * the application. * * @author Sunit Katkar, sunitkatkar@gmail.com * (https://sunitkatkar.blogspot.com/) * @since ver 1.0 (May 2018) * @version 1.0 */ @Configuration @EnableTransactionManagement @EnableJpaRepositories(basePackages = { "com.sunitkatkar.blogspot.master.model", "com.sunitkatkar.blogspot.master.repository" }, entityManagerFactoryRef = "masterEntityManagerFactory", transactionManagerRef = "masterTransactionManager") public class MasterDatabaseConfig { private static final Logger LOG = LoggerFactory .getLogger(MasterDatabaseConfig.class); /** * Master database configuration properties like username, password, etc. */ @Autowired private MasterDatabaseConfigProperties masterDbProperties; /** * Creates the master datasource bean which is required for creating the * entity manager factory bean <br/> * <br/> * Note that using names for beans is not mandatory but it is a good * practice to ensure that the intended beans are being used where required. * * @return */ @Bean(name = "masterDataSource") public DataSource masterDataSource() { LOG.info("Setting up masterDataSource with: " + masterDbProperties.toString()); HikariDataSource ds = new HikariDataSource(); ds.setUsername(masterDbProperties.getUsername()); ds.setPassword(masterDbProperties.getPassword()); ds.setJdbcUrl(masterDbProperties.getUrl()); ds.setDriverClassName(masterDbProperties.getDriverClassName()); ds.setPoolName(masterDbProperties.getPoolName()); // HikariCP settings // Maximum number of actual connection in the pool ds.setMaximumPoolSize(masterDbProperties.getMaxPoolSize()); // Minimum number of idle connections in the pool ds.setMinimumIdle(masterDbProperties.getMinIdle()); // Maximum waiting time for a connection from the pool ds.setConnectionTimeout(masterDbProperties.getConnectionTimeout()); // Maximum time that a connection is allowed to sit idle in the pool ds.setIdleTimeout(masterDbProperties.getIdleTimeout()); LOG.info("Setup of masterDataSource succeeded."); return ds; } /** * Creates the entity manager factory bean which is required to access the * JPA functionalities provided by the JPA persistence provider, i.e. * Hibernate in this case. <br/> * <br/> * Note the <b>{@literal @}Primary</b> annotation which tells Spring boot to * create this entity manager as the first thing when starting the * application. * * @return */ @Primary @Bean(name = "masterEntityManagerFactory") public LocalContainerEntityManagerFactoryBean masterEntityManagerFactory(){ LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean(); // Set the master data source em.setDataSource(masterDataSource()); // The master tenant entity and repository need to be scanned em.setPackagesToScan( new String[]{MasterTenant.class.getPackage().getName(), MasterTenantRepository.class.getPackage().getName()}); // Setting a name for the persistence unit as Spring sets it as // 'default' if not defined em.setPersistenceUnitName("masterdb-persistence-unit"); // Setting Hibernate as the JPA provider JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter(); em.setJpaVendorAdapter(vendorAdapter); // Set the hibernate properties em.setJpaProperties(hibernateProperties()); LOG.info("Setup of masterEntityManagerFactory succeeded."); return em; } /** * This transaction manager is appropriate for applications that use a * single JPA EntityManagerFactory for transactional data access. <br/> * <br/> * Note the <b>{@literal @}Qualifier</b> annotation to ensure that the * <tt>masterEntityManagerFactory</tt> is used for setting up the * transaction manager. * * @param emf * @return */ @Bean(name = "masterTransactionManager") public JpaTransactionManager masterTransactionManager( @Qualifier("masterEntityManagerFactory") EntityManagerFactory emf) { JpaTransactionManager transactionManager = new JpaTransactionManager(); transactionManager.setEntityManagerFactory(emf); return transactionManager; } /** * Bean post-processor that automatically applies persistence exception * translation to any bean marked with Spring's @Repository annotation, * adding a corresponding PersistenceExceptionTranslationAdvisor to the * exposed proxy (either an existing AOP proxy or a newly generated proxy * that implements all of the target's interfaces). * * @return */ @Bean public PersistenceExceptionTranslationPostProcessor exceptionTranslation() { return new PersistenceExceptionTranslationPostProcessor(); } /** * The properties for configuring the JPA provider Hibernate. * * @return */ private Properties hibernateProperties() { Properties properties = new Properties(); properties.put(org.hibernate.cfg.Environment.DIALECT, "org.hibernate.dialect.MySQL5Dialect"); properties.put(org.hibernate.cfg.Environment.SHOW_SQL, true); properties.put(org.hibernate.cfg.Environment.FORMAT_SQL, true); properties.put(org.hibernate.cfg.Environment.HBM2DDL_AUTO, "update"); return properties; } }
File: TenantDatabaseConfig.java
This class sets up the datasources for the tenant databases. Note that this class requires the 'MultiTenantConnectionProvider' for the tenant datasources. These datasources are configured in the 'DataSourceBasedMultiTenantConnectionProviderImpl' class, shown later on in this post.package com.sunitkatkar.blogspot.tenant.config; import java.util.HashMap; import java.util.Map; import javax.persistence.EntityManagerFactory; import org.hibernate.MultiTenancyStrategy; import org.hibernate.context.spi.CurrentTenantIdentifierResolver; import org.hibernate.engine.jdbc.connections.spi.MultiTenantConnectionProvider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; import org.springframework.orm.jpa.JpaTransactionManager; import org.springframework.orm.jpa.JpaVendorAdapter; import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; import org.springframework.transaction.annotation.EnableTransactionManagement; import com.sunitkatkar.blogspot.tenant.model.User; import com.sunitkatkar.blogspot.tenant.repository.UserRepository; import com.sunitkatkar.blogspot.tenant.service.UserService; /** * This is the tenant data sources configuration which sets up the multitenancy. * * @author Sunit Katkar, sunitkatkar@gmail.com * (https://sunitkatkar.blogspot.com/) * @since ver 1.0 (May 2018) * @version 1.0 */ @Configuration @EnableTransactionManagement @ComponentScan(basePackages = { "com.sunitkatkar.blogspot.tenant.repository", "com.sunitkatkar.blogspot.tenant.model" }) @EnableJpaRepositories(basePackages = { "com.sunitkatkar.blogspot.tenant.repository", "com.sunitkatkar.blogspot.tenant.service" }, entityManagerFactoryRef = "tenantEntityManagerFactory", transactionManagerRef = "tenantTransactionManager") public class TenantDatabaseConfig { private static final Logger LOG = LoggerFactory .getLogger(TenantDatabaseConfig.class); @Bean(name = "tenantJpaVendorAdapter") public JpaVendorAdapter jpaVendorAdapter() { return new HibernateJpaVendorAdapter(); } @Bean(name = "tenantTransactionManager") public JpaTransactionManager transactionManager( EntityManagerFactory tenantEntityManager) { JpaTransactionManager transactionManager = new JpaTransactionManager(); transactionManager.setEntityManagerFactory(tenantEntityManager); return transactionManager; } /** * The multi tenant connection provider * * @return */ @Bean(name = "datasourceBasedMultitenantConnectionProvider") @ConditionalOnBean(name = "masterEntityManagerFactory") public MultiTenantConnectionProvider multiTenantConnectionProvider() { // Autowires the multi connection provider return new DataSourceBasedMultiTenantConnectionProviderImpl(); } /** * The current tenant identifier resolver * * @return */ @Bean(name = "currentTenantIdentifierResolver") public CurrentTenantIdentifierResolver currentTenantIdentifierResolver() { return new CurrentTenantIdentifierResolverImpl(); } /** * Creates the entity manager factory bean which is required to access the * JPA functionalities provided by the JPA persistence provider, i.e. * Hibernate in this case. * * @param connectionProvider * @param tenantResolver * @return */ @Bean(name = "tenantEntityManagerFactory") @ConditionalOnBean(name = "datasourceBasedMultitenantConnectionProvider") public LocalContainerEntityManagerFactoryBean entityManagerFactory( @Qualifier("datasourceBasedMultitenantConnectionProvider") MultiTenantConnectionProvider connectionProvider, @Qualifier("currentTenantIdentifierResolver") CurrentTenantIdentifierResolver tenantResolver) { LocalContainerEntityManagerFactoryBean emfBean = new LocalContainerEntityManagerFactoryBean(); //All tenant related entities, repositories and service classes must be scanned emfBean.setPackagesToScan( new String[] { User.class.getPackage().getName(), UserRepository.class.getPackage().getName(), UserService.class.getPackage().getName() }); emfBean.setJpaVendorAdapter(jpaVendorAdapter()); emfBean.setPersistenceUnitName("tenantdb-persistence-unit"); Map<String, Object> properties = new HashMap<>(); properties.put(org.hibernate.cfg.Environment.MULTI_TENANT, MultiTenancyStrategy.SCHEMA); properties.put( org.hibernate.cfg.Environment.MULTI_TENANT_CONNECTION_PROVIDER, connectionProvider); properties.put( org.hibernate.cfg.Environment.MULTI_TENANT_IDENTIFIER_RESOLVER, tenantResolver); // ImprovedNamingStrategy is deprecated and unsupported in Hibernate 5 // properties.put("hibernate.ejb.naming_strategy", // "org.hibernate.cfg.ImprovedNamingStrategy"); properties.put(org.hibernate.cfg.Environment.DIALECT, "org.hibernate.dialect.MySQL5Dialect"); properties.put(org.hibernate.cfg.Environment.SHOW_SQL, true); properties.put(org.hibernate.cfg.Environment.FORMAT_SQL, true); properties.put(org.hibernate.cfg.Environment.HBM2DDL_AUTO, "update"); emfBean.setJpaPropertyMap(properties); LOG.info("tenantEntityManagerFactory set up successfully!"); return emfBean; } }
File: DataSourceBasedMultiTenantConnectionProviderImpl.java
This sets up the multi-tenant connection properties required by Hibernate. This is different than the code you have seen in the past 2 blog posts. Here, the datasources are read from the 'master_tenant' table and then stored in a map. This map is used to look up the datasource based on the tenant id. If a tenant is added to the 'master_tenant' table while the application is running, then a request for that tenant will prompt this class to read the 'master_tenant' table once again and recreate the datasource look up map. This is how the application becomes 'dynamic' about using a tenant added at runtime.package com.sunitkatkar.blogspot.tenant.config; import java.util.List; import java.util.Map; import java.util.TreeMap; import javax.sql.DataSource; import org.hibernate.engine.jdbc.connections. spi.AbstractDataSourceBasedMultiTenantConnectionProviderImpl; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import com.sunitkatkar.blogspot.master.model.MasterTenant; import com.sunitkatkar.blogspot.master.repository.MasterTenantRepository; import com.sunitkatkar.blogspot.util.DataSourceUtil; /** * This class does the job of selecting the correct database based on the tenant * id found by the {@link CurrentTenantIdentifierResolverImpl} * * @author Sunit Katkar, sunitkatkar@gmail.com * (https://sunitkatkar.blogspot.com/) * @since ver 1.0 (May 2018) * @version 1.0 * */ @Configuration public class DataSourceBasedMultiTenantConnectionProviderImpl extends AbstractDataSourceBasedMultiTenantConnectionProviderImpl { private static final Logger LOG = LoggerFactory .getLogger(DataSourceBasedMultiTenantConnectionProviderImpl.class); private static final long serialVersionUID = 1L; /** * Injected MasterTenantRepository to access the tenant information from the * master_tenant table */ @Autowired private MasterTenantRepository masterTenantRepo; /** * Map to store the tenant ids as key and the data source as the value */ private Map<String, DataSource> dataSourcesMtApp = new TreeMap<>(); @Override protected DataSource selectAnyDataSource() { // This method is called more than once. So check if the data source map // is empty. If it is then rescan master_tenant table for all tenant // entries. if (dataSourcesMtApp.isEmpty()) { List<MasterTenant> masterTenants = masterTenantRepo.findAll(); LOG.info(">>>> selectAnyDataSource() -- Total tenants:" + masterTenants.size()); for (MasterTenant masterTenant : masterTenants) { dataSourcesMtApp.put(masterTenant.getTenantId(), DataSourceUtil .createAndConfigureDataSource(masterTenant)); } } return this.dataSourcesMtApp.values().iterator().next(); } @Override protected DataSource selectDataSource(String tenantIdentifier) { // If the requested tenant id is not present check for it in the master // database 'master_tenant' table if (!this.dataSourcesMtApp.containsKey(tenantIdentifier)) { List<MasterTenant> masterTenants = masterTenantRepo.findAll(); LOG.info(">>>> selectDataSource() -- tenant:" + tenantIdentifier + " Total tenants:" + masterTenants.size()); for (MasterTenant masterTenant : masterTenants) { dataSourcesMtApp.put(masterTenant.getTenantId(), DataSourceUtil .createAndConfigureDataSource(masterTenant)); } } return this.dataSourcesMtApp.get(tenantIdentifier); } }
Resources
The complete source code is checked into GitHub. Its a standard Maven project which you can import into your IDE.In conclusion
That's all there is to reading tenant information at run time. For a production grade application, I recommend that you create a separate Spring Boot app or a microservice to manage the tenant information in the master_tenant table. So this keeps the management of the application separate from the actual application. The master database can be used to store other kinds of app related information and the separate app can be used by the administrators for other app related activities.Happy coding :)
Update - 10 July 2018
A reader of my blog - Jordan Mackie - found an error which was a mistake on my part in publishing a small but important part of the code. In the file TenantDatabaseConfig.java you need to explicitly name the tenant entity manager factory and use that when passing it in to the transaction manager. So the code should look like@Bean(name = "tenantTransactionManager") public JpaTransactionManager transactionManager( @Qualifier("tenantEntityManagerFactory") EntityManagerFactory tenantEntityManager) { JpaTransactionManager transactionManager = new JpaTransactionManager(); transactionManager.setEntityManagerFactory(tenantEntityManager); return transactionManager; }