Contents

Hibernate 5 多租户简介

1. 简介

多租户 允许多个客户端或租户使用单个资源,或者在本文的上下文中,单个数据库实例。目的是将每个租户需要的信息与共享数据库隔离开来

在本教程中,我们将介绍在 Hibernate 5 中配置多租户的各种方法。

2. Maven依赖

我们需要在 pom.xml 文件中包含hibernate-core 依赖项:

<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中有三种多租户方法:

  • 单独的架构——同一物理数据库实例中的每个租户一个架构
  • 独立数据库——每个租户一个独立的物理数据库实例
  • 分区(鉴别器)数据——每个租户的数据按鉴别器值分区

Hibernate 尚不支持分区(鉴别器)数据方法。跟进此 JIRA 问题 以获取未来进展。

像往常一样,Hibernate 抽象了每种方法实现的复杂性。

我们只需要提供这两个接口的实现

在浏览数据库和模式方法示例之前,让我们更详细地了解每个概念。

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.blogdemo.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-tenant_identifier.properties 填充,其中包含所有连接详细信息:

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.blogdemo.hibernate.multitenancy.database.MapMultiTenantConnectionProvider

如果我们运行与模式方法完全相同的测试,则测试再次通过。