如何构建多租户应用程序:Hibernate 教程
已发表: 2022-03-11当我们谈论每个客户端都有自己独立数据的云应用程序时,我们需要考虑如何存储和操作这些数据。 即使有所有优秀的 NoSQL 解决方案,有时我们仍然需要使用良好的旧关系数据库。 可能想到的第一个分离数据的解决方案是在每个表中添加一个标识符,以便可以单独处理它。 这行得通,但是如果客户要求提供他们的数据库怎么办? 检索所有隐藏在其他记录中的记录将非常麻烦。
Hibernate 团队不久前想出了一个解决这个问题的方法。 它们提供了一些扩展点,使人们能够控制应从何处检索数据。 该解决方案可以选择通过标识符列、多个数据库和多个模式来控制数据。 本文将介绍多模式解决方案。
所以,让我们开始工作吧!
入门
如果您是经验丰富的 Java 开发人员并且知道如何配置所有内容,或者您已经拥有自己的 Java EE 项目,则可以跳过本节。
首先,我们必须创建一个新的 Java 项目。 我使用的是 Eclipse 和 Gradle,但您可以使用您喜欢的 IDE 和构建工具,例如 IntelliJ 和 Maven。
如果你想使用和我一样的工具,你可以按照以下步骤来创建你的项目:
- 在 Eclipse 上安装 Gradle 插件
- 单击文件->新建->其他...
- 找到 Gradle (STS) 并点击 Next
- 告知名称并为示例项目选择 Java 快速入门
- 点击完成
伟大的! 这应该是初始文件结构:
javaee-mt |- src/main/java |- src/main/resources |- src/test/java |- src/test/resources |- JRE System Library |- Gradle Dependencies |- build |- src |- build.gradle
您可以删除源文件夹中的所有文件,因为它们只是示例文件。
为了运行这个项目,我使用 Wildfly,我将展示如何配置它(同样你可以在这里使用你最喜欢的工具):
- 下载 Wildfly:http://wildfly.org/downloads/(我使用的是 10 版)
- 解压文件
- 在 Eclipse 上安装 JBoss Tools 插件
- 在 Servers 选项卡上,右键单击任何空白区域并选择 New -> Server
- 选择 Wildfly 10.x(如果 10 不可用,9.x 也可以使用,具体取决于您的 Eclipse 版本)
- 单击下一步,选择创建新运行时(下一页)并再次单击下一步
- 选择您解压缩 Wildfly 的文件夹作为主目录
- 点击完成
现在,让我们配置 Wildfly 以了解数据库:
- 转到 Wildfly 文件夹中的 bin 文件夹
- 执行 add-user.bat 或 add-user.sh(取决于您的操作系统)
- 按照步骤将您的用户创建为经理
- 在 Eclipse 中,再次转到 Servers 选项卡,右键单击您创建的服务器并选择 Start
- 在浏览器上,访问 http://localhost:9990,即管理界面
- 输入您刚刚创建的用户的凭据
- 部署数据库的驱动程序 jar:
- 转到部署选项卡,然后单击添加
- 单击下一步,选择您的驱动程序 jar 文件
- 单击下一步并完成
- 转到配置选项卡
- 选择子系统 -> 数据源 -> 非 XA
- 单击添加,选择您的数据库,然后单击下一步
- 为您的数据源命名,然后单击下一步
- 选择 Detect Driver 选项卡并选择您刚刚部署的驱动程序
- 输入您的数据库信息,然后单击下一步
- 如果要确保上一步的信息正确,请单击测试连接
- 点击完成
- 返回 Eclipse 并停止正在运行的服务器
- 右键单击它,选择添加和删除
- 将您的项目添加到右侧
- 点击完成
好的,我们已经将 Eclipse 和 Wildfly 配置在一起了!
这是项目之外所需的所有配置。 让我们继续进行项目配置。
自举项目
现在我们已经配置了 Eclipse 和 Wildfly 并创建了我们的项目,我们需要配置我们的项目。
我们要做的第一件事是编辑 build.gradle。 它应该是这样的:
apply plugin: 'java' apply plugin: 'war' apply plugin: 'eclipse' apply plugin: 'eclipse-wtp' sourceCompatibility = '1.8' compileJava.options.encoding = 'UTF-8' compileJava.options.encoding = 'UTF-8' compileTestJava.options.encoding = 'UTF-8' repositories { jcenter() } eclipse { wtp { } } dependencies { providedCompile 'org.hibernate:hibernate-entitymanager:5.0.7.Final' providedCompile 'org.jboss.resteasy:resteasy-jaxrs:3.0.14.Final' providedCompile 'javax:javaee-api:7.0' }
依赖项都被声明为“providedCompile”,因为这个命令不会在最终的war文件中添加依赖项。 Wildfly 已经拥有这些依赖项,否则会与应用程序的依赖项发生冲突。
此时,您可以右键单击您的项目,选择 Gradle (STS) -> Refresh All 以导入我们刚刚声明的依赖项。
是时候创建和配置“persistence.xml”文件了,该文件包含 Hibernate 需要的信息:
- 在 src/main/resource 源文件夹中,创建一个名为 META-INF 的文件夹
- 在此文件夹中,创建一个名为 persistence.xml 的文件
该文件的内容必须类似于以下内容,更改 jta-data-source 以匹配您在 Wildfly 中创建的数据源,并将package com.toptal.andrehil.mt.hibernate
与您将在下一个中创建的数据源相匹配部分(除非您选择相同的包名称):
<?xml version="1.0" encoding="UTF-8" ?> <persistence xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd" version="2.0" xmlns="http://java.sun.com/xml/ns/persistence"> <persistence-unit name="pu"> <jta-data-source>java:/JavaEEMTDS</jta-data-source> <properties> <property name="hibernate.multiTenancy" value="SCHEMA"/> <property name="hibernate.tenant_identifier_resolver" value="com.toptal.andrehil.mt.hibernate.SchemaResolver"/> <property name="hibernate.multi_tenant_connection_provider" value="com.toptal.andrehil.mt.hibernate.MultiTenantProvider"/> </properties> </persistence-unit> </persistence>
休眠类
添加到 persistence.xml 的配置指向两个自定义类 MultiTenantProvider 和 SchemaResolver。 第一类负责提供配置有正确模式的连接。 第二个类负责解析要使用的模式的名称。

下面是这两个类的实现:
public class MultiTenantProvider implements MultiTenantConnectionProvider, ServiceRegistryAwareService { private static final long serialVersionUID = 1L; private DataSource dataSource; @Override public boolean supportsAggressiveRelease() { return false; } @Override public void injectServices(ServiceRegistryImplementor serviceRegistry) { try { final Context init = new InitialContext(); dataSource = (DataSource) init.lookup("java:/JavaEEMTDS"); // Change to your datasource name } catch (final NamingException e) { throw new RuntimeException(e); } } @SuppressWarnings("rawtypes") @Override public boolean isUnwrappableAs(Class clazz) { return false; } @Override public <T> T unwrap(Class<T> clazz) { return null; } @Override public Connection getAnyConnection() throws SQLException { final Connection connection = dataSource.getConnection(); return connection; } @Override public Connection getConnection(String tenantIdentifier) throws SQLException { final Connection connection = getAnyConnection(); try { connection.createStatement().execute("SET SCHEMA '" + tenantIdentifier + "'"); } catch (final SQLException e) { throw new HibernateException("Error trying to alter schema [" + tenantIdentifier + "]", e); } return connection; } @Override public void releaseAnyConnection(Connection connection) throws SQLException { try { connection.createStatement().execute("SET SCHEMA 'public'"); } catch (final SQLException e) { throw new HibernateException("Error trying to alter schema [public]", e); } connection.close(); } @Override public void releaseConnection(String tenantIdentifier, Connection connection) throws SQLException { releaseAnyConnection(connection); } }
上述语句中使用的语法适用于 PostgreSQL 和其他一些数据库,如果您的数据库有不同的语法来更改当前模式,则必须更改此语法。
public class SchemaResolver implements CurrentTenantIdentifierResolver { private String tenantIdentifier = "public"; @Override public String resolveCurrentTenantIdentifier() { return tenantIdentifier; } @Override public boolean validateExistingCurrentSessions() { return false; } public void setTenantIdentifier(String tenantIdentifier) { this.tenantIdentifier = tenantIdentifier; } }
此时,已经可以测试应用程序了。 目前,我们的解析器直接指向一个硬编码的公共模式,但它已经被调用了。 为此,如果服务器正在运行,请停止它并重新启动它。 您可以尝试在调试模式下运行它并在上述类的任何位置放置断点以检查它是否正常工作。
解析器的实际使用
那么,解析器如何真正包含模式的正确名称?
实现这一点的一种方法是在所有请求的标头中保留一个标识符,然后创建一个过滤器来注入模式的名称。
让我们实现一个过滤器类来举例说明用法。 解析器可以通过 Hibernate 的 SessionFactory 访问,因此我们将利用它来获取它并注入正确的模式名称。
@Provider public class AuthRequestFilter implements ContainerRequestFilter { @PersistenceUnit(unitName = "pu") private EntityManagerFactory entityManagerFactory; @Override public void filter(ContainerRequestContext containerRequestContext) throws IOException { final SessionFactoryImplementor sessionFactory = ((EntityManagerFactoryImpl) entityManagerFactory).getSessionFactory(); final SchemaResolver schemaResolver = (SchemaResolver) sessionFactory.getCurrentTenantIdentifierResolver(); final String username = containerRequestContext.getHeaderString("username"); schemaResolver.setTenantIdentifier(username); } }
现在,当任何类获得 EntityManager 来访问数据库时,它已经配置了正确的模式。
为简单起见,此处显示的实现是直接从标头中的字符串获取标识符,但最好使用身份验证令牌并将标识符存储在令牌中。 如果您有兴趣了解有关此主题的更多信息,我建议您查看 JSON Web Tokens (JWT)。 JWT 是一个简单易用的令牌操作库。
如何使用所有这些
配置完所有内容后,在与EntityManager
交互的实体和/或类中无需执行任何其他操作。 您从 EntityManager 运行的任何内容都将被定向到由创建的过滤器解析的架构。
现在,您需要做的就是在客户端拦截请求并将标识符/令牌注入到要发送到服务器端的标头中。
文末的链接指向写这篇文章的项目。 它使用 Flyway 创建 2 个模式,并包含一个名为 Car 的实体类和一个名为CarService
的休息服务类,可用于测试项目。 您可以按照以下所有步骤操作,但您可以克隆并使用这个项目,而不是创建自己的项目。 然后,在运行时,您可以使用一个简单的 HTTP 客户端(如 Chrome 的 Postman 扩展)并使用标题 key:value 向 http://localhost:8080/javaee-mt/rest/cars 发出 GET 请求:
- 用户名:乔; 要么
- 用户名:弗雷德。
通过这样做,请求将返回不同的值,这些值位于不同的模式中,一个称为 joe,另一个称为“fred”。
最后的话
这不是在 Java 世界中创建多租户应用程序的唯一解决方案,但它是实现此目的的简单方法。
要记住的一件事是 Hibernate 在使用多租户配置时不会生成 DDL。 我的建议是看看 Flyway 或 Liquibase,它们是控制数据库创建的优秀库。 即使您不打算使用多租户,这也是一件好事,因为 Hibernate 团队建议不要在生产中使用他们的自动数据库生成。
用于创建本文和环境配置的源代码可以在 github.com/andrehil/JavaEEMT 找到