├── src └── main │ ├── resources │ ├── schema.sql │ ├── application.properties │ ├── data.sql │ └── applicationContent.xml │ └── java │ └── com │ └── example │ ├── repository │ └── OrderRepository.java │ ├── conf │ ├── TenantContext.java │ ├── CurrentTenantIdentifierResolverImpl.java │ ├── DataSourceBasedMultiTenantConnectionProviderImpl.java │ └── MultiTenantJpaConfiguration.java │ ├── entity │ └── Order.java │ ├── interceptor │ ├── WebMvcConfiguration.java │ └── MultiTenantInterceptor.java │ ├── Mars2MultitenantApplication.java │ └── controller │ └── OrderController.java ├── tenants ├── db1.properties ├── db2.properties ├── db3.properties └── default.properties ├── README.md └── pom.xml /src/main/resources/schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE PRODUCT ( 2 | id int8 NOT NULL, 3 | name VARCHAR(255) 4 | ); -------------------------------------------------------------------------------- /tenants/db1.properties: -------------------------------------------------------------------------------- 1 | name=DB1 2 | datasource.url=jdbc:postgresql://localhost:5432/DB1 3 | datasource.username=postgres 4 | datasource.password=postgres -------------------------------------------------------------------------------- /tenants/db2.properties: -------------------------------------------------------------------------------- 1 | name=DB2 2 | datasource.url=jdbc:postgresql://localhost:5432/DB2 3 | datasource.username=postgres 4 | datasource.password=postgres -------------------------------------------------------------------------------- /tenants/db3.properties: -------------------------------------------------------------------------------- 1 | name=DB3 2 | datasource.url=jdbc:postgresql://localhost:5432/DB3 3 | datasource.username=postgres 4 | datasource.password=postgres -------------------------------------------------------------------------------- /tenants/default.properties: -------------------------------------------------------------------------------- 1 | name=DEFAULT 2 | datasource.url=jdbc:postgresql://localhost:5432/DEFAULT 3 | datasource.username=postgres 4 | datasource.password=postgres -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | # DataSource 2 | spring.datasource.driver-class-name=org.postgresql.Driver 3 | 4 | # Hibernate 5 | spring.jpa.database=postgresql 6 | spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect 7 | spring.jpa.hibernate.ddl-auto=none 8 | server.port=8080 9 | -------------------------------------------------------------------------------- /src/main/java/com/example/repository/OrderRepository.java: -------------------------------------------------------------------------------- 1 | package com.example.repository; 2 | 3 | 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | import com.example.entity.Order; 8 | 9 | @Repository 10 | public interface OrderRepository extends JpaRepository { 11 | 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/example/conf/TenantContext.java: -------------------------------------------------------------------------------- 1 | package com.example.conf; 2 | 3 | public class TenantContext { 4 | 5 | private static ThreadLocal currentTenant = new ThreadLocal<>(); 6 | 7 | public static void setCurrentTenant(String tenantId) { 8 | currentTenant.set(tenantId); 9 | } 10 | 11 | public static String getCurrentTenant() { 12 | return currentTenant.get(); 13 | } 14 | 15 | public static void clear() { 16 | currentTenant.remove(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/example/entity/Order.java: -------------------------------------------------------------------------------- 1 | package com.example.entity; 2 | 3 | import javax.persistence.*; 4 | import java.sql.Date; 5 | 6 | @Entity 7 | @Table(name = "orders") 8 | public class Order { 9 | 10 | public Order() { 11 | } 12 | 13 | public Order(Date date) { 14 | this.date = date; 15 | } 16 | 17 | @Id 18 | @Column(nullable = false, name = "id") 19 | @GeneratedValue(strategy = GenerationType.AUTO) 20 | private int id; 21 | 22 | @Column(nullable = false, name = "date") 23 | private Date date; 24 | } 25 | -------------------------------------------------------------------------------- /src/main/resources/data.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO DATASOURCECONFIG VALUES (1, 'org.h2.Driver', 'jdbc:h2:mem:secondDS', 'secondDS', 'sa', '', true); 2 | INSERT INTO DATASOURCECONFIG VALUES (2, 'org.h2.Driver', 'jdbc:h2:mem:thirdDS', 'thirdDS', 'sa', '', true); 3 | INSERT INTO DATASOURCECONFIG VALUES (3, 'org.h2.Driver', 'jdbc:h2:mem:fourDS', 'fourDS', 'sa', '', true); 4 | 5 | INSERT INTO PRODUCT VALUES (1, 'Product 1'); 6 | INSERT INTO PRODUCT VALUES (2, 'Product 2'); 7 | INSERT INTO PRODUCT VALUES (3, 'Product 3'); 8 | INSERT INTO PRODUCT VALUES (4, 'Product 4'); 9 | INSERT INTO PRODUCT VALUES (5, 'Product 5'); -------------------------------------------------------------------------------- /src/main/java/com/example/interceptor/WebMvcConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.example.interceptor; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.web.servlet.config.annotation.InterceptorRegistry; 5 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; 6 | 7 | @Configuration 8 | public class WebMvcConfiguration extends WebMvcConfigurerAdapter { 9 | 10 | @Override 11 | public void addInterceptors(InterceptorRegistry registry) { 12 | registry.addInterceptor(new MultiTenantInterceptor()); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/example/conf/CurrentTenantIdentifierResolverImpl.java: -------------------------------------------------------------------------------- 1 | package com.example.conf; 2 | 3 | import org.hibernate.context.spi.CurrentTenantIdentifierResolver; 4 | 5 | import com.example.conf.TenantContext; 6 | 7 | public class CurrentTenantIdentifierResolverImpl implements CurrentTenantIdentifierResolver { 8 | 9 | static String DEFAULT_TENANT = "DEFAULT"; 10 | 11 | @Override 12 | public String resolveCurrentTenantIdentifier() { 13 | String currentTenant = TenantContext.getCurrentTenant(); 14 | return currentTenant != null ? currentTenant : DEFAULT_TENANT; 15 | } 16 | 17 | @Override 18 | public boolean validateExistingCurrentSessions() { 19 | return true; 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/example/Mars2MultitenantApplication.java: -------------------------------------------------------------------------------- 1 | package com.example; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; 6 | import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration; 7 | import org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration; 8 | import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; 9 | import org.springframework.context.annotation.ImportResource; 10 | 11 | @SpringBootApplication( 12 | exclude = { DataSourceAutoConfiguration.class, HibernateJpaAutoConfiguration.class} 13 | scanBasePackages = { "com.example" } 14 | ) 15 | public class Mars2MultitenantApplication { 16 | 17 | public static void main(String[] args) { 18 | SpringApplication.run(Mars2MultitenantApplication.class, args); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/example/conf/DataSourceBasedMultiTenantConnectionProviderImpl.java: -------------------------------------------------------------------------------- 1 | package com.example.conf; 2 | 3 | import static com.example.conf.CurrentTenantIdentifierResolverImpl.DEFAULT_TENANT; 4 | 5 | import java.util.Map; 6 | 7 | import javax.sql.DataSource; 8 | 9 | import org.hibernate.engine.jdbc.connections.spi.AbstractDataSourceBasedMultiTenantConnectionProviderImpl; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | 12 | public class DataSourceBasedMultiTenantConnectionProviderImpl extends AbstractDataSourceBasedMultiTenantConnectionProviderImpl { 13 | 14 | private static final long serialVersionUID = 1L; 15 | 16 | @Autowired 17 | private Map mars2DataSources; 18 | 19 | @Override 20 | protected DataSource selectAnyDataSource() { 21 | return this.mars2DataSources.values().iterator().next(); 22 | } 23 | 24 | @Override 25 | protected DataSource selectDataSource(String tenantId) { 26 | return this.mars2DataSources.get(tenantId); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/example/interceptor/MultiTenantInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.example.interceptor; 2 | 3 | import javax.servlet.http.HttpServletRequest; 4 | import javax.servlet.http.HttpServletResponse; 5 | 6 | import org.springframework.web.servlet.ModelAndView; 7 | import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; 8 | 9 | import com.example.conf.TenantContext; 10 | 11 | public class MultiTenantInterceptor extends HandlerInterceptorAdapter { 12 | 13 | private static final String TENANT_HEADER_NAME = "X-TenantID"; 14 | 15 | @Override 16 | public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { 17 | String tenantId = request.getHeader(TENANT_HEADER_NAME); 18 | TenantContext.setCurrentTenant(tenantId); 19 | return true; 20 | } 21 | 22 | @Override 23 | public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView model) throws Exception { 24 | TenantContext.clear(); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/example/controller/OrderController.java: -------------------------------------------------------------------------------- 1 | package com.example.controller; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.http.ResponseEntity; 5 | import org.springframework.transaction.annotation.Transactional; 6 | import org.springframework.web.bind.annotation.RequestMapping; 7 | import org.springframework.web.bind.annotation.RequestMethod; 8 | import org.springframework.web.bind.annotation.RestController; 9 | 10 | import com.example.entity.Order; 11 | import com.example.repository.OrderRepository; 12 | 13 | import java.sql.Date; 14 | 15 | 16 | 17 | @RestController 18 | @Transactional 19 | public class OrderController { 20 | 21 | @Autowired 22 | private OrderRepository orderRepository; 23 | 24 | @RequestMapping(path = "/orders", method= RequestMethod.POST) 25 | public ResponseEntity createSampleOrder() { 26 | 27 | Order newOrder = new Order(new Date(System.currentTimeMillis())); 28 | orderRepository.save(newOrder); 29 | return ResponseEntity.ok(newOrder); 30 | 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Spring Boot Hibernate Multi-tenant Demo 2 | --------------------------------------- 3 | This sample project uses hibernate "SEPARATE DATABASE" multi-tenant strategy, and it also allows you to plugin new database (datasources) at runtime. 4 | 5 | Useful resources 6 | [1](http://tech.asimio.net/2017/01/17/Multitenant-applications-using-Spring-Boot-JPA-Hibernate-and-Postgres.html) 7 | [2](https://fizzylogic.nl/2016/01/24/make-your-spring-boot-application-multi-tenant-aware-in-2-steps) 8 | [3](http://stuartingram.com/2016/10/02/spring-boot-schema-based-multi-tenancy/) 9 | [4](http://anakiou.blogspot.ch/2015/08/multi-tenant-application-with-spring.html) 10 | 11 | ## Running the demo 12 | You need to add a table orders in database (PostgreSQL in this case but ofcourse you can choose anyone) 13 | 14 | ``` 15 | CREATE TABLE "orders" ( 16 | "id" int4 NOT NULL, 17 | "date" date NOT NULL 18 | ); 19 | ``` 20 | 21 | ### Available URLs 22 | 23 | ``` 24 | curl -v POST -H "X-TenantID: DB1" "http://localhost:8080/orders" 25 | curl -v POST -H "X-TenantID: DB2" "http://localhost:8080/orders" 26 | curl -v POST -H "X-TenantID: DB3" "http://localhost:8080/orders" 27 | ``` 28 | -------------------------------------------------------------------------------- /src/main/resources/applicationContent.xml: -------------------------------------------------------------------------------- 1 | 2 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.example 7 | mars2-multitenant 8 | 0.0.1-SNAPSHOT 9 | jar 10 | 11 | mars2-multitenant 12 | Demo project for Spring Boot 13 | 14 | 15 | org.springframework.boot 16 | spring-boot-starter-parent 17 | 1.5.7.RELEASE 18 | 19 | 20 | 21 | 22 | UTF-8 23 | UTF-8 24 | 1.8 25 | 26 | 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-starter-data-jpa 31 | 32 | 33 | org.springframework.boot 34 | spring-boot-starter-web 35 | 36 | 37 | 38 | org.postgresql 39 | postgresql 40 | runtime 41 | 42 | 43 | org.springframework.boot 44 | spring-boot-configuration-processor 45 | true 46 | 47 | 48 | 49 | 50 | 51 | 52 | org.springframework.boot 53 | spring-boot-maven-plugin 54 | 55 | true 56 | false 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /src/main/java/com/example/conf/MultiTenantJpaConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.example.conf; 2 | 3 | import java.io.File; 4 | import java.io.FileInputStream; 5 | import java.io.IOException; 6 | import java.nio.file.Paths; 7 | import java.util.HashMap; 8 | import java.util.LinkedHashMap; 9 | import java.util.Map; 10 | import java.util.Properties; 11 | 12 | import javax.persistence.EntityManagerFactory; 13 | import javax.persistence.PersistenceContext; 14 | import javax.sql.DataSource; 15 | 16 | import org.hibernate.MultiTenancyStrategy; 17 | import org.hibernate.SessionFactory; 18 | import org.hibernate.cfg.Environment; 19 | import org.hibernate.context.spi.CurrentTenantIdentifierResolver; 20 | import org.hibernate.engine.jdbc.connections.spi.MultiTenantConnectionProvider; 21 | import org.springframework.beans.factory.annotation.Autowired; 22 | import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder; 23 | import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; 24 | import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties; 25 | import org.springframework.boot.context.properties.ConfigurationProperties; 26 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 27 | import org.springframework.context.annotation.Bean; 28 | import org.springframework.context.annotation.Configuration; 29 | import org.springframework.context.annotation.ImportResource; 30 | import org.springframework.context.annotation.Primary; 31 | import org.springframework.core.io.ClassPathResource; 32 | import org.springframework.data.jpa.repository.config.EnableJpaRepositories; 33 | import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator; 34 | import org.springframework.orm.hibernate5.HibernateTransactionManager; 35 | import org.springframework.orm.jpa.JpaTransactionManager; 36 | import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; 37 | import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; 38 | import org.springframework.transaction.PlatformTransactionManager; 39 | import org.springframework.transaction.annotation.EnableTransactionManagement; 40 | 41 | import com.example.Mars2MultitenantApplication; 42 | import com.example.entity.Order; 43 | 44 | @Configuration 45 | @EnableConfigurationProperties({ JpaProperties.class }) 46 | //@ImportResource(locations = { "classpath:applicationContent.xml" }) 47 | @EnableTransactionManagement(proxyTargetClass=true) 48 | @EnableJpaRepositories(basePackages= {"com.example.repository"},transactionManagerRef="transactionManager") 49 | public class MultiTenantJpaConfiguration { 50 | 51 | @Autowired 52 | private DataSourceProperties properties; 53 | 54 | @Autowired 55 | private JpaProperties jpaProperties; 56 | 57 | @Bean 58 | public MultiTenantConnectionProvider multiTenantConnectionProvider() { 59 | return new DataSourceBasedMultiTenantConnectionProviderImpl(); 60 | } 61 | 62 | @Bean 63 | public CurrentTenantIdentifierResolver currentTenantIdentifierResolver() { 64 | return new CurrentTenantIdentifierResolverImpl(); 65 | } 66 | 67 | @Bean(name = "mars2DataSources" ) 68 | public Map mars2DataSources() { 69 | 70 | File[] files = Paths.get("tenants").toFile().listFiles(); 71 | Map datasources = new HashMap<>(); 72 | 73 | for(File propertyFile : files) { 74 | Properties tenantProperties = new Properties(); 75 | DataSourceBuilder dataSourceBuilder = new DataSourceBuilder(this.getClass().getClassLoader()); 76 | 77 | try { 78 | tenantProperties.load(new FileInputStream(propertyFile)); 79 | 80 | String tenantId = tenantProperties.getProperty("name"); 81 | 82 | dataSourceBuilder.driverClassName(properties.getDriverClassName()) 83 | .url(tenantProperties.getProperty("datasource.url")) 84 | .username(tenantProperties.getProperty("datasource.username")) 85 | .password(tenantProperties.getProperty("datasource.password")); 86 | 87 | if(properties.getType() != null) { 88 | dataSourceBuilder.type(properties.getType()); 89 | } 90 | 91 | datasources.put(tenantId, dataSourceBuilder.build()); 92 | } catch (IOException e) { 93 | e.printStackTrace(); 94 | return null; 95 | } 96 | } 97 | 98 | return datasources; 99 | } 100 | 101 | @PersistenceContext @Primary @Bean 102 | public LocalContainerEntityManagerFactoryBean entityManagerFactoryBean(MultiTenantConnectionProvider multiTenantConnectionProvider, 103 | CurrentTenantIdentifierResolver currentTenantIdentifierResolver) { 104 | 105 | Map hibernateProps = new LinkedHashMap<>(); 106 | hibernateProps.putAll(this.jpaProperties.getProperties()); 107 | hibernateProps.put(Environment.MULTI_TENANT, MultiTenancyStrategy.DATABASE); 108 | hibernateProps.put(Environment.MULTI_TENANT_CONNECTION_PROVIDER, multiTenantConnectionProvider); 109 | hibernateProps.put(Environment.MULTI_TENANT_IDENTIFIER_RESOLVER, currentTenantIdentifierResolver); 110 | 111 | // No dataSource is set to resulting entityManagerFactoryBean 112 | LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean(); 113 | em.setPackagesToScan(new String[] { Order.class.getPackage().getName() }); 114 | em.setJpaVendorAdapter(new HibernateJpaVendorAdapter()); 115 | em.setJpaPropertyMap(hibernateProps); 116 | 117 | return em; 118 | } 119 | 120 | 121 | @Bean 122 | public EntityManagerFactory entityManagerFactory(LocalContainerEntityManagerFactoryBean entityManagerFactoryBean) { 123 | return entityManagerFactoryBean.getObject(); 124 | } 125 | 126 | 127 | @Bean(name="transactionManager") 128 | public PlatformTransactionManager txManager(EntityManagerFactory entityManagerFactory) { 129 | JpaTransactionManager jpa = new JpaTransactionManager (); 130 | jpa.setEntityManagerFactory(entityManagerFactory); 131 | return jpa; 132 | } 133 | 134 | 135 | private DataSource initialize(DataSource dataSource) { 136 | ClassPathResource schemaResource = new ClassPathResource("schema.sql"); 137 | ClassPathResource dataResource = new ClassPathResource("data.sql"); 138 | ResourceDatabasePopulator populator = new ResourceDatabasePopulator(schemaResource, dataResource); 139 | populator.execute(dataSource); 140 | return dataSource; 141 | } 142 | 143 | } 144 | --------------------------------------------------------------------------------