如何構建多租戶應用程序: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 找到