跨多个租户查询表(相同的表名)

时间:2016-04-06 17:28:28

标签: java hibernate spring-data-jpa multi-tenant

我的系统中存在未知数量的租户(同一数据库服务器上的不同数据库实例)。我有一个工作代码,用户登录并选择了正确的租户,我可以读取该租户的配置表。

我希望应用程序在开始时循环遍历所有租户,阅读配置并对其进行操作。在迁移到Spring Data JPA(由hibernate支持)之前,这很简单,因为我分别连接到每个数据库实例。

我不认为我可以使用Spring Application,因为它只设置一个连接。

我希望使用与同一个bean相同的存储库接口,因为当我只需要一次命中一个租户时就可以使用。

我有一个@Transactional,它会为给定的租户提供一个dataSource,但我不确定如何在class MultiTenantConnectionProviderImpl extends AbstractDataSourceBasedMultiTenantConnectionProviderImpl类的方法中使用它?< / p>

2 个答案:

答案 0 :(得分:2)

我不确定我是否应该删除之前的答案,编辑它或者是什么。因此,如果MOD可以让我知道正确的程序,我将很乐意遵守。

事实证明我使用@Transactional无法正常工作是正确的。我最终使用了AbstractRoutingDataSource的自定义实现来替换我的MultiTenantConnectionProviderImplCurrentTenantResolverImpl。我使用这个新数据源而不是设置hibernate.multiTenancy hibernate.multi_tenant_connection_providerhibernate.tenant_identifier_resolver

我的临时覆盖类如下所示:

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();
    }
}

我的TenantRoutingDataSource看起来像这样:

@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;
    }
}

然后我在创建时将我的自定义数据源提供给我的Entity Manager工厂bean。

@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);
        }
    }
}

答案 1 :(得分:0)

我认为我接近一个解决方案,但我对此并不十分满意。我希望得到一个更好的答案。

EDITED:事实证明这并不是很有效,因为Spring或Hibernate似乎只调用当前的租户标识符解析器一次,而不是每次调用@Transactional方法

它涉及将CurrentTenantIdentifierResolver实现更改为不仅查看当前用户(如果已设置)以获取其当前租户ID(由实现者来决定如何设置)...它还需要查看线程局部变量以查看是否已设置覆盖。

使用这种方法,我可以暂时设置tenantID ...使用指定的多租户事务管理器调用服务方法,然后获取数据。

我的测试服务:

@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
    }
}

我的类包装设置ThreadLocal,实现AutoCloseable以帮助确保变量被清理

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();
    }

}

我的租户解析器实现使用线程本地

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;
    }