多租户:使用 Spring Data JPA 管理多个数据源

Posted

技术标签:

【中文标题】多租户:使用 Spring Data JPA 管理多个数据源【英文标题】:Multi-tenancy: Managing multiple datasources with Spring Data JPA 【发布时间】:2018-09-20 10:18:55 【问题描述】:

我需要创建一个可以管理多个数据源的服务。 这些数据源在应用程序第一次运行时不一定存在,实际上一个端点会创建新的数据库,我希望能够切换到它们并创建数据。

例如,假设我有 3 个数据库,A、B 和 C,然后我启动应用程序,我使用创建 D 的端点,然后我想使用 D。

这可能吗?

如果存在其他数据源,我知道如何切换到其他数据源,但我目前看不到任何可以使我的请求成为可能的解决方案。 你有什么想法吗?

谢谢

【问题讨论】:

【参考方案1】:

要使用 Spring Boot 实现多租户,我们可以使用 AbstractRoutingDataSource 作为所有“租户数据库”的基础 DataSource 类。

它有一个我们必须重写的抽象方法determineCurrentLookupKey。它告诉AbstractRoutingDataSource 它目前必须提供哪个租户数据源才能使用。因为它工作在多线程环境中,所以所选租户的信息应该存储在ThreadLocal变量中。

AbstractRoutingDataSource 将租户数据源的信息存储在其私有Map<Object, Object> targetDataSources 中。此映射的键是 租户标识符(例如 String 类型)和值 - 租户数据源。要将我们的租户数据源放入此地图,我们必须使用其设置器 setTargetDataSources

如果没有我们必须使用方法setDefaultTargetDataSource(Object defaultTargetDataSource) 设置的“默认”数据源,AbstractRoutingDataSource 将无法工作。

设置租户数据源和默认数据源后,我们必须调用方法afterPropertiesSet() 告诉AbstractRoutingDataSource 更新其状态。

所以我们的“MultiTenantManager”类可以是这样的:

@Configuration
public class MultiTenantManager 

    private final ThreadLocal<String> currentTenant = new ThreadLocal<>();
    private final Map<Object, Object> tenantDataSources = new ConcurrentHashMap<>();
    private final DataSourceProperties properties;

    private AbstractRoutingDataSource multiTenantDataSource;

    public MultiTenantManager(DataSourceProperties properties) 
        this.properties = properties;
    

    @Bean
    public DataSource dataSource() 
        multiTenantDataSource = new AbstractRoutingDataSource() 
            @Override
            protected Object determineCurrentLookupKey() 
                return currentTenant.get();
            
        ;
        multiTenantDataSource.setTargetDataSources(tenantDataSources);
        multiTenantDataSource.setDefaultTargetDataSource(defaultDataSource());
        multiTenantDataSource.afterPropertiesSet();
        return multiTenantDataSource;
    

    public void addTenant(String tenantId, String url, String username, String password) throws SQLException 

        DataSource dataSource = DataSourceBuilder.create()
                .driverClassName(properties.getDriverClassName())
                .url(url)
                .username(username)
                .password(password)
                .build();

        // Check that new connection is 'live'. If not - throw exception
        try(Connection c = dataSource.getConnection()) 
            tenantDataSources.put(tenantId, dataSource);
            multiTenantDataSource.afterPropertiesSet();
        
    

    public void setCurrentTenant(String tenantId) 
        currentTenant.set(tenantId);
    

    private DriverManagerDataSource defaultDataSource() 
        DriverManagerDataSource defaultDataSource = new DriverManagerDataSource();
        defaultDataSource.setDriverClassName("org.h2.Driver");
        defaultDataSource.setUrl("jdbc:h2:mem:default");
        defaultDataSource.setUsername("default");
        defaultDataSource.setPassword("default");
        return defaultDataSource;
    

简要说明:

map tenantDataSources 这是我们放置到 setTargetDataSources setter 的本地租户数据源存储;

DataSourceProperties properties用于从'application.properties'的spring.datasource.driverClassName获取租户数据库的Database Driver Class名称(例如org.postgresql.Driver);

方法addTenant 用于将新租户及其数据源添加到我们的本地租户数据源存储中。 我们可以即时执行此操作 - 感谢afterPropertiesSet() 方法;

方法setCurrentTenant(String tenantId) 用于“切换”到给定租户的数据源。我们可以使用这种方法,例如,在 REST 控制器中处理使用数据库的请求时。请求应包含“tenantId”,例如在X-TenantId 标头中,我们可以检索并放入此方法;

defaultDataSource() 是使用内存 H2 数据库构建的,以避免使用工作 SQL 服务器上的默认数据库。

注意:您必须spring.jpa.hibernate.ddl-auto 参数设置为 none 以禁用 Hibernate 在数据库架构中进行更改。您必须事先创建租户数据库的架构。

此课程的完整示例以及更多您可以在我的 repo 中找到。

更新

这个branch 演示了一个使用专用数据库存储租户数据库属性而不是属性文件的示例(请参阅下面的@MarcoGustavo 问题)。

【讨论】:

以上是关于多租户:使用 Spring Data JPA 管理多个数据源的主要内容,如果未能解决你的问题,请参考以下文章

具有多租户休眠的 Spring-Data JPA

继承所有存储库方法的 Spring Data JPA 全局过滤器子句

多个数据库的 Spring Data JPA 配置

如何通过 Spring Data JPA 了解底层数据库名称

使用 spring-data-elasticsearch 的多租户

Spring Boot + Spring Data 多租户