Class TenantContextIdentifierResolver

java.lang.Object
io.leandev.appfuse.jpa.tenant.TenantContextIdentifierResolver
All Implemented Interfaces:
org.hibernate.context.spi.CurrentTenantIdentifierResolver<String>

public class TenantContextIdentifierResolver extends Object implements org.hibernate.context.spi.CurrentTenantIdentifierResolver<String>

TenantContext 接到 Hibernate 原生 discriminator 多租戶的 resolver

TenantId 使用:Hibernate 於每個 Session 建構時呼叫 resolveCurrentTenantIdentifier(),據以決定該 Session 的租戶分區。

為何是「類別 + 設定」而非 Spring `@Bean`

Hibernate 的 hibernate.tenant_identifier_resolver 接受類名字串並自行實例化,故本類別 刻意保持無依賴、可無參建構TenantContext 全靜態)。這是必要的——一旦任何實體掛 @TenantId_tenantId filter 即在 SessionFactory 層定義,於是每一個 Session 建構都會走 AbstractSharedSessionContractsetUpMultitenancy,取不到租戶識別 就丟 HibernateException: SessionFactory configured for multi-tenancy, but no tenant identifier specified。若 resolver 只是應用層 @Configuration 裡的 @Bean不載入該 config 的 @DataJpaTest slice 會整個 context 起不來(實測)。改由設定注入即無此問題,且不需 autoconfiguration(見 ADR-014:框架刻意不採 autoconfiguration)。

ROOT_TENANT:無租戶 context 的表達

Hibernate 不接受 resolver 回 null(見上述 setUpMultitenancy),故「無租戶」以 ROOT_TENANT 哨兵表達,並由 isRoot(String) 宣告它可存取所有分區——Hibernate 據此跳過 租戶 filter:

// AbstractSharedSessionContract#setUpMultitenancy
if ( resolver == null || !resolver.isRoot( tenantIdentifier ) ) {
    // turn on the filter, unless this is the "root" tenant with access to all partitions
    loadQueryInfluencers.enableFilter( TenantIdBinder.FILTER_NAME )...
}

兩類到達 root 的執行緒,皆為刻意

  • 系統管理員:其帳號不屬於任何租戶(租戶 ID 為 null)→ 解析鏈回 null → root → 跨租戶視野
  • 背景執行緒(排程 / @Async):天生無 context → root → 得以跨租戶撈取

root 亦可於寫入時顯式指定租戶(TenantIdGeneration:「the root tenant is allowed to set the tenant id explicitly」),故 seed/管理工具可跨租戶建資料。

root 的讀寫權界(決策)

  • 跨租戶讀取:允許(本機制的目的)。
  • 修改他人租戶的既有資料允許——經決策接受,對齊 Hibernate 自身模型 (CurrentTenantIdentifierResolver.isRoot(T) 的定義即「a root tenant with access to all partitions」)。租戶歸屬不受影響:@TenantId 欄位為 updatable = false。舊機制在此情境 會拋 IllegalStateException,此為刻意放寬。
  • 建立租戶業務資料:須指明租戶——以 TenantContext.runAs(String, Runnable) 或顯式 setTenantId(...) (後者為 Hibernate 支援的 root 路徑)。兩者皆無時,注入的會是 ROOT_TENANT 哨兵、該列 對所有租戶皆不可見(靜默污染),故 TenantAwareEntity@PrePersist 守衛擋下。

三者一致:放行、不動歸屬故放行、建立只擋「租戶無處可得」這一種必然污染的情況。

See Also:
  • Field Details

    • ROOT_TENANT

      public static final String ROOT_TENANT
      無租戶 context 時回報的 root 租戶識別(可存取所有分區)
      See Also:
  • Constructor Details

    • TenantContextIdentifierResolver

      public TenantContextIdentifierResolver()
  • Method Details

    • resolveCurrentTenantIdentifier

      public String resolveCurrentTenantIdentifier()
      Specified by:
      resolveCurrentTenantIdentifier in interface org.hibernate.context.spi.CurrentTenantIdentifierResolver<String>
    • validateExistingCurrentSessions

      public boolean validateExistingCurrentSessions()
      Specified by:
      validateExistingCurrentSessions in interface org.hibernate.context.spi.CurrentTenantIdentifierResolver<String>
    • isRoot

      public boolean isRoot(String tenantId)
      Specified by:
      isRoot in interface org.hibernate.context.spi.CurrentTenantIdentifierResolver<String>