1. 简介

多租户(Multitenancy)允许多个客户端(租户)共享单一资源,在本文场景中即共享单个数据库实例。核心目标是从共享数据库中隔离每个租户所需的信息

本教程将介绍在 Hibernate 6 中配置多租户的多种实现方式

2. Maven 依赖

pom.xml 中添加 hibernate-core 依赖:

<dependency>
   <groupId>org.hibernate</groupId>
   <artifactId>hibernate-core</artifactId>
   <version>6.1.7.Final</version>
</dependency>

测试使用 H2 内存数据库,添加依赖:

<dependency>
   <groupId>com.h2database</groupId>
   <artifactId>h2</artifactId>
   <version>2.1.214</version>
</dependency>

3. 理解 Hibernate 多租户

根据官方 Hibernate 用户指南,多租户有三种实现策略:

  • 独立模式(Separate Schema):同一物理数据库实例中每个租户使用独立模式
  • 独立数据库(Separate Database):每个租户使用独立的物理数据库实例
  • 分区数据(Partitioned Data):通过鉴别器值(discriminator)分区存储租户数据

Hibernate 抽象了这些策略的实现细节,我们只需实现以下两个接口:

3.1 MultiTenantConnectionProvider

该接口负责为指定租户标识符提供数据库连接。核心方法如下:

interface MultiTenantConnectionProvider extends Service, Wrapped {
    Connection getAnyConnection() throws SQLException;

    Connection getConnection(String tenantIdentifier) throws SQLException;
     // ...
}

当 Hibernate 无法解析租户标识符时,调用 getAnyConnection() 获取通用连接;否则调用 getConnection(tenantIdentifier)

Hibernate 提供两种实现方式

  • 基于 Java 的 DataSource 接口 – 使用 DataSourceBasedMultiTenantConnectionProviderImpl
  • 基于 Hibernate 的 ConnectionProvider 接口 – 使用 AbstractMultiTenantConnectionProvider

3.2 CurrentTenantIdentifierResolver

租户标识符的解析方式灵活多样,例如:

  • 从配置文件读取
  • 从请求路径参数提取

接口定义:

public interface CurrentTenantIdentifierResolver {

    String resolveCurrentTenantIdentifier();

    boolean validateExistingCurrentSessions();
}

Hibernate 通过 resolveCurrentTenantIdentifier() 获取租户标识符。若需验证现有会话是否属于同一租户,validateExistingCurrentSessions() 应返回 true

4. 独立模式策略

此策略在同一物理数据库实例中使用不同模式隔离租户数据。✅ 适用场景:追求极致性能,可牺牲租户级备份等数据库特性。

测试中模拟 CurrentTenantIdentifierResolver 实现动态切换租户:

public abstract class MultitenancyIntegrationTest {

    @Mock
    private CurrentTenantIdentifierResolver currentTenantIdentifierResolver;

    private SessionFactory sessionFactory;

    @Before
    public void setup() throws IOException {
        MockitoAnnotations.initMocks(this);

        when(currentTenantIdentifierResolver.validateExistingCurrentSessions())
          .thenReturn(false);

        Properties properties = getHibernateProperties();
        properties.put(
          AvailableSettings.MULTI_TENANT_IDENTIFIER_RESOLVER, 
          currentTenantIdentifierResolver);

        sessionFactory = buildSessionFactory(properties);

        initTenant(TenantIdNames.MYDB1);
        initTenant(TenantIdNames.MYDB2);
    }

    protected void initTenant(String tenantId) {
        when(currentTenantIdentifierResolver
         .resolveCurrentTenantIdentifier())
           .thenReturn(tenantId);
        createCarTable();
    }
}

MultiTenantConnectionProvider 实现在每次获取连接时动态设置模式

public class SchemaMultiTenantConnectionProvider extends AbstractMultiTenantConnectionProvider {

    private final ConnectionProvider connectionProvider;

    public SchemaMultiTenantConnectionProvider() throws IOException {
        connectionProvider = initConnectionProvider();
    }

    @Override
    protected ConnectionProvider getAnyConnectionProvider() {
        return connectionProvider;
    }

    @Override
    protected ConnectionProvider selectConnectionProvider(String tenantIdentifier) {
        return connectionProvider;
    }

    @Override
    public Connection getConnection(String tenantIdentifier) throws SQLException {
        Connection connection = super.getConnection(tenantIdentifier);
        connection.createStatement()
            .execute(String.format("SET SCHEMA %s;", tenantIdentifier));
        return connection;
    }

    private ConnectionProvider initConnectionProvider() throws IOException {
        Properties properties = new Properties();
        properties.load(getClass().getResourceAsStream("/hibernate-schema-multitenancy.properties"));
        Map<String, Object> configProperties = new HashMap<>();
        for (String key : properties.stringPropertyNames()) {
            String value = properties.getProperty(key);
            configProperties.put(key, value);
        }

        DriverManagerConnectionProviderImpl connectionProvider = new DriverManagerConnectionProviderImpl();
        connectionProvider.configure(configProperties);
        return connectionProvider;
    }

}

配置 hibernate.properties 启用模式隔离:

hibernate.connection.url=jdbc:h2:mem:mydb1;DB_CLOSE_DELAY=-1;\
  INIT=CREATE SCHEMA IF NOT EXISTS MYDB1\\;CREATE SCHEMA IF NOT EXISTS MYDB2\\;
hibernate.multiTenancy=SCHEMA
hibernate.multi_tenant_connection_provider=\
  com.baeldung.hibernate.multitenancy.schema.SchemaMultiTenantConnectionProvider

⚠️ 注意:测试中通过 hibernate.connection.url 初始化模式,生产环境应提前创建模式。

测试验证数据隔离性:

@Test
void givenDatabaseApproach_whenAddingEntries_thenOnlyAddedToConcreteDatabase() {
    whenCurrentTenantIs(TenantIdNames.MYDB1);
    whenAddCar("myCar");
    thenCarFound("myCar");
    whenCurrentTenantIs(TenantIdNames.MYDB2);
    thenCarNotFound("myCar");
}

通过 whenCurrentTenantIs() 动态切换租户上下文。

5. 独立数据库策略

此策略为每个租户分配独立物理数据库实例。✅ 适用场景:需要租户级备份等高级数据库特性,可接受一定性能损耗。

复用前述 MultitenancyIntegrationTestCurrentTenantIdentifierResolver

MultiTenantConnectionProvider 实现使用 Map 维护租户专属连接池

public class MapMultiTenantConnectionProvider extends AbstractMultiTenantConnectionProvider {

    private final Map<String, ConnectionProvider> connectionProviderMap = new HashMap<>();

    public MapMultiTenantConnectionProvider() throws IOException {
        initConnectionProviderForTenant(TenantIdNames.MYDB1);
        initConnectionProviderForTenant(TenantIdNames.MYDB2);
    }

    @Override
    protected ConnectionProvider getAnyConnectionProvider() {
        return connectionProviderMap.values()
            .iterator()
            .next();
    }

    @Override
    protected ConnectionProvider selectConnectionProvider(String tenantIdentifier) {
        return connectionProviderMap.get(tenantIdentifier);
    }

    private void initConnectionProviderForTenant(String tenantId) throws IOException {
        Properties properties = new Properties();
        properties.load(getClass().getResourceAsStream(String.format("/hibernate-database-%s.properties", tenantId)));
        Map<String, Object> configProperties = new HashMap<>();
        for (String key : properties.stringPropertyNames()) {
            String value = properties.getProperty(key);
            configProperties.put(key, value);
        }
        DriverManagerConnectionProviderImpl connectionProvider = new DriverManagerConnectionProviderImpl();
        connectionProvider.configure(configProperties);
        this.connectionProviderMap.put(tenantId, connectionProvider);
    }

}

每个租户通过专属配置文件 hibernate-database-<租户ID>.properties 管理连接参数:

hibernate.connection.driver_class=org.h2.Driver
hibernate.connection.url=jdbc:h2:mem:<租户ID>;DB_CLOSE_DELAY=-1
hibernate.connection.username=sa
hibernate.dialect=org.hibernate.dialect.H2Dialect

更新 hibernate.properties 启用数据库隔离:

hibernate.multiTenancy=DATABASE
hibernate.multi_tenant_connection_provider=\
  com.baeldung.hibernate.multitenancy.database.MapMultiTenantConnectionProvider

复用模式策略的测试用例,验证通过。

6. 结论

本文探讨了 Hibernate 6 中独立数据库独立模式两种多租户策略的实现。通过简化的示例代码,对比了两种策略的核心差异:

特性 独立模式策略 独立数据库策略
数据隔离 模式级 数据库实例级
性能 ⭐⭐⭐⭐⭐ ⭐⭐⭐
租户级特性支持
运维复杂度 简单 复杂

完整代码示例可在 GitHub 项目 获取。


原始标题:A Guide to Multitenancy in Hibernate 5