【问题标题】:Query tables across multiple tenants (same table name)跨多个租户查询表(表名相同)
【发布时间】:2016-04-06 17:28:28
【问题描述】:

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

我希望应用程序在启动时遍历所有租户,读取配置并对其采取行动。在迁移到 Spring Data JPA(由 hibernate 支持)之前,这很容易,因为我分别连接到每个数据库实例。

我认为我不能使用 Spring 的 @Transactional,因为它只建立一个连接。

我希望对相同的 bean 使用相同的存储库接口,因为这在我一次只需要访问一个租户时有效。

我确实有一个class MultiTenantConnectionProviderImpl extends AbstractDataSourceBasedMultiTenantConnectionProviderImpl,它会给我一个给定租户的数据源,但我不确定如何在@Service 类的方法中使用它?

【问题讨论】:

  • 当您说“读取配置”时,您是指每个租户数据库中的一个表吗?你可能想看看这个博客post,它可能会给你一些想法。如果您有一个数据源列表,我认为在您的应用程序启动时循环它们并不难。
  • 使用“配置表”作为示例可能是一个糟糕的选择,因为它会混淆事物。实际上,出于各种原因,我需要在所有租户中阅读一些内容。您提供的博客文章建议预先了解数据源,我不知道。
  • 多租户是指在数据库服务器上有一个名为users 的表,它出现在多个数据库中,如dropbox_dbgoogle_drive_dbsky_drive_db 等?

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


【解决方案1】:

我不确定我是否应该删除我之前的答案,编辑它还是什么。因此,如果 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;
    }
}

然后我只需在创建实体管理器工厂 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);
        }
    }
}

【讨论】:

    【解决方案2】:

    我认为我接近一种解决方案,但我对它不太满意。我很想提出一个更好的答案。

    已编辑:事实证明这不太有效,因为 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;
        }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2014-03-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多