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

问题描述 投票:0回答:1

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

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

这可能吗?

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

spring-boot spring-data-jpa multi-tenant
1个回答
44
投票

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

它有一个我们必须重写的抽象方法 defineCurrentLookupKey。它告诉

AbstractRoutingDataSource
目前必须提供哪个租户数据源才能使用。由于它工作在多线程环境中,因此所选租户的信息应存储在
ThreadLocal
变量中。

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

如果没有“默认”数据源,

AbstractRoutingDataSource
将无法工作,我们必须使用方法
setDefaultTargetDataSource(Object defaultTargetDataSource)
进行设置。

设置租户数据源和默认数据源后,我们必须调用方法

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
    获取租户数据库的数据库驱动类名称(例如,
    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的问题)。

© www.soinside.com 2019 - 2024. All rights reserved.