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 抽象了这些策略的实现细节,我们只需实现以下两个接口:
- MultiTenantConnectionProvider – 提供租户专属数据库连接
- CurrentTenantIdentifierResolver – 解析当前租户标识符
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. 独立数据库策略
此策略为每个租户分配独立物理数据库实例。✅ 适用场景:需要租户级备份等高级数据库特性,可接受一定性能损耗。
复用前述 MultitenancyIntegrationTest
和 CurrentTenantIdentifierResolver
。
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 项目 获取。