I have a system where there is an unknown number of tenants (different database instances on same database server). I have working code where a user logs in and the correct tenant is selected, and I can read the configuration table for that tenant.
I want the application at start time to loop through all tenants, read the configuration and act upon it. Prior to moving to Spring Data JPA (backed by hibernate) this was easy as I was connecting to each database instance separately.
I don't think I can use Spring's @Transactional
as it only sets up a single connection.
I hope to use the same repository interface with the same bean, as this works when i only need to hit one tenant at a time.
I do have a class MultiTenantConnectionProviderImpl extends AbstractDataSourceBasedMultiTenantConnectionProviderImpl
that will give me a dataSource for a given tenant, but I'm not sure how to use that in a @Service
class's method?
2 Answers
Answers 1
I'm not sure if I should remove my previous answer, edit it or what. So if a MOD can let me know proper procedure I'll be happy to comply.
Turns out I was right about the use of @Transactional
not going to work. I ended up using an custom implementation of and AbstractRoutingDataSource
to replace my MultiTenantConnectionProviderImpl
and CurrentTenantResolverImpl
. I use this new data source instead of setting the hibernate.multiTenancy
hibernate.multi_tenant_connection_provider
and hibernate.tenant_identifier_resolver
My temporary override class looks like this:
public class MultitenancyTemporaryOverride implements AutoCloseable { static final ThreadLocal<String> tenantOverride = new NamedThreadLocal<>("temporaryTenantOverride"); public void setCurrentTenant(String tenantId) { tenantOverride.set(tenantId); } public String getCurrentTenant() { return tenantOverride.get(); } @Override public void close() throws Exception { tenantOverride.remove(); } }
My TenantRoutingDataSource looks like this:
@Component public class TenantRoutingDataSource extends AbstractDataSource implements InitializingBean { @Override public Connection getConnection() throws SQLException { return determineTargetDataSource().getConnection(); } @Override public Connection getConnection(String username, String password) throws SQLException { return determineTargetDataSource().getConnection(username, password); } @Override public void afterPropertiesSet() throws Exception { } protected String determineCurrentLookupKey() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); String database = "shared"; if (authentication != null && authentication.getPrincipal() instanceof MyUser) { MyUser user = (MyUser) authentication.getPrincipal(); database = user.getTenantId(); } String temporaryOverride = MultitenancyTemporaryOverride.tenantOverride.get(); if (temporaryOverride != null) { database = temporaryOverride; } return database; } protected DataSource determineTargetDataSource() { return selectDataSource(determineCurrentLookupKey()); } public DataSource selectDataSource(String tenantIdentifier) { //I use C3P0 for my connection pool PooledDataSource pds = C3P0Registry.pooledDataSourceByName(tenantIdentifier); if (pds == null) pds = getComboPooledDataSource(tenantIdentifier); return pds; } private ComboPooledDataSource getComboPooledDataSource(String tenantIdentifier) { ComboPooledDataSource cpds = new ComboPooledDataSource(tenantIdentifier); cpds.setJdbcUrl("A JDBC STRING HERE"); cpds.setUser("MyDbUsername"); cpds.setPassword("MyDbPassword"); cpds.setInitialPoolSize(10); cpds.setMaxConnectionAge(10000); try { cpds.setDriverClass("com.informix.jdbc.IfxDriver"); } catch (PropertyVetoException e) { throw new RuntimeException("Weird error when setting the driver class", e); } return cpds; } }
Then i just provide my custom data source to my Entity Manager factory bean when creating it.
@Service public class TestService { public void doSomeGets() { List<String> tenants = getListSomehow(); try(MultitenancyTemporaryOverride tempOverride = new MultitenancyTemporaryOverride()) { for(String tenant : tenants) { tempOverride.setCurrentTenant(tenant); //do some work here, which only applies to the tenant } } catch (Exception e) { logger.error(e); } } }
Answers 2
I think I'm close to one solution, but I'm not too entirely happy with it. I would love for a better answer to come up.
EDITED: turns out this doesn't quite work, as Spring or Hibernate appears to only call the current tenant identifier resolver once, not for each time a @Transactional method is called
It involves changing the CurrentTenantIdentifierResolver
implementation to not only look at the current user (if it is set) to get their current tenant id (up to implementor to figure out how to set that)...it also needs to look at a thread local variable to see if an override has been set.
Using this approach, I can temporarily set the tenantID...call a service method with my multi tenancy transaction manager specified and then get the data.
My Test Service:
@Service public class TestService { @Transactional(transactionManager = "sharedTxMgr") public void doSomeGets() { List<String> tenants = getListSomehow(); try(MultitenancyTemporaryOverride tempOverride = new MultitenancyTemporaryOverride()) { for(String tenant : tenants) { tempOverride.setCurrentTenant(tenant); doTenantSpecificWork(); } } catch (Exception e) { logger.error(e); } } @Transactional(transactionManager = "tenantSpecificTxMgr") public void doTenantSpecificWork() { //do some work here, which only applies to the tenant } }
My class that wraps setting ThreadLocal, implementing AutoCloseable to help make sure variable is cleaned up
public class MultitenancyTemporaryOverride implements AutoCloseable { static final ThreadLocal<String> tenantOverride = new ThreadLocal<>(); public void setCurrentTenant(String tenantId) { tenantOverride.set(tenantId); } public String getCurrentTenant() { return tenantOverride.get(); } @Override public void close() throws Exception { tenantOverride.remove(); } }
My tenant resolver implementation that uses the thread local
public class CurrentTenantIdentifierResolverImpl implements CurrentTenantIdentifierResolver { @Override public String resolveCurrentTenantIdentifier() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); logger.debug(ToStringBuilder.reflectionToString(authentication)); String database = "shared"; if (authentication != null && authentication.getPrincipal() instanceof MyUser) { MyUser user = (MyUser) authentication.getPrincipal(); database = user.getTenantId(); } String temporaryOverride = MultitenancyTemporaryOverride.tenantOverride.get(); if(temporaryOverride != null) { database = temporaryOverride; } return database; }
0 comments:
Post a Comment