1.简介
多租户允许多个客户端或租户使用单个资源,或者在本文的上下文中使用单个数据库实例。目的是将每个租户需要的信息与共享数据库隔离开来。
在本教程中,我们将介绍在Hibernate 5中配置多租户的各种方法。
2. Maven依赖
我们需要包括在休眠核心依赖于pom.xml的文件:
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>5.2.12.Final</version>
</dependency>
为了进行测试,我们将使用H2内存数据库,因此我们还要将此依赖项添加到pom.xml文件中:
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.196</version>
</dependency>
3.了解Hibernate中的多租户
正如Hibernate官方用户指南中所提到的,Hibernate中有三种多租户方法:
单独的架构 -同一物理数据库实例中每个租户一个架构 单独的数据库 -每个租户一个单独的物理数据库实例 分区(Discriminator)数据 -每个租户的数据由鉴别器值分区 该分区(鉴别)数据的方法尚未被Hibernate的支持。关注此JIRA问题以了解未来的进展。
像往常一样,Hibernate抽象出每种方法实现的复杂性。
我们所需要的只是提供这两个接口的实现:
MultiTenantConnectionProvider - 为每个租户提供连接
CurrentTenantIdentifierResolver - 解析要使用的租户标识符
在浏览数据库和模式方法示例之前,让我们更详细地了解每个概念。
3.1。 MultiTenantConnectionProvider
基本上,此接口为具体租户标识符提供数据库连接。
让我们看看它的两个主要方法:
interface MultiTenantConnectionProvider extends Service, Wrapped {
Connection getAnyConnection() throws SQLException;
Connection getConnection(String tenantIdentifier) throws SQLException;
// ...
}
如果Hibernate无法解析要使用的租户标识符,它将使用方法getAnyConnection来获取连接。否则,它将使用方法getConnection。
Hibernate根据我们定义数据库连接的方式提供了这个接口的两个实现:
使用Java中的DataSource接口 - 我们将使用DataSourceBasedMultiTenantConnectionProviderImpl实现 使用Hibernate 的ConnectionProvider接口 - 我们将使用AbstractMultiTenantConnectionProvider实现
3.2。 CurrentTenantIdentifierResolver
有许多方法可以解析租户标识符。例如,我们的实现可以使用配置文件中定义的一个租户标识符。
另一种方法是使用路径参数中的租户标识符。
我们来看看这个界面:
public interface CurrentTenantIdentifierResolver {
String resolveCurrentTenantIdentifier();
boolean validateExistingCurrentSessions();
}
Hibernate调用方法resolveCurrentTenantIdentifier来获取租户标识符。如果我们希望Hibernate验证属于同一租户标识符的所有现有会话,则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接口的实现将设置每次请求连接时使用的模式:
class SchemaMultiTenantConnectionProvider
extends AbstractMultiTenantConnectionProvider {
private ConnectionProvider connectionProvider;
public SchemaMultiTenantConnectionProvider() throws IOException {
this.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.properties"));
DriverManagerConnectionProviderImpl connectionProvider
= new DriverManagerConnectionProviderImpl();
connectionProvider.configure(properties);
return connectionProvider;
}
}
因此,我们将使用一个具有两个模式的内存H2数据库 - 每个租户一个。
让我们配置hibernate.properties以使用模式多租户模式和MultiTenantConnectionProvider接口的实现:
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属性来创建两个模式。这对于实际应用程序来说不是必需的,因为模式应该已经到位。
对于我们的测试,我们将在租户myDb1中添加一个Car条目。我们将验证此条目是否存储在我们的数据库中,并且它不在租户myDb2中:
@Test
void whenAddingEntries_thenOnlyAddedToConcreteDatabase() {
whenCurrentTenantIs(TenantIdNames.MYDB1);
whenAddCar("myCar");
thenCarFound("myCar");
whenCurrentTenantIs(TenantIdNames.MYDB2);
thenCarNotFound("myCar");
}
正如我们在测试中看到的那样,我们在调用whenCurrentTenantIs方法时更改了租户。
5.数据库方法
数据库多租户方法对每个租户使用不同的物理数据库实例。由于每个租户都是完全隔离的,因此当我们需要特殊的数据库功能(如每个租户备份)而不是我们需要最佳性能时,我们应该选择此策略。
对于数据库方法,我们将使用与上面相同的MultitenancyIntegrationTest类和CurrentTenantIdentifierResolver接口。
对于MultiTenantConnectionProvider接口,我们将使用Map集合来获取每个租户标识符的ConnectionProvider:
class MapMultiTenantConnectionProvider
extends AbstractMultiTenantConnectionProvider {
private 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)));
DriverManagerConnectionProviderImpl connectionProvider
= new DriverManagerConnectionProviderImpl();
connectionProvider.configure(properties);
this.connectionProviderMap.put(tenantId, connectionProvider);
}
}
每个ConnectionProvider都通过配置文件hibernate-database-
hibernate.connection.driver_class=org.h2.Driver
hibernate.connection.url=jdbc:h2:mem:<Tenant Identifier>;DB_CLOSE_DELAY=-1
hibernate.connection.username=sa
hibernate.dialect=org.hibernate.dialect.H2Dialect
最后,让我们再次更新hibernate.properties以使用数据库多租户模式和MultiTenantConnectionProvider接口的实现:
hibernate.multiTenancy=DATABASE
hibernate.multi_tenant_connection_provider=\
com.baeldung.hibernate.multitenancy.database.MapMultiTenantConnectionProvider
如果我们运行与模式方法完全相同的测试,则测试再次通过。