Monday, April 18, 2016

Query tables across multiple tenants (same table name)

Leave a Comment

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;     } 
If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment