資料層設計指南
文檔版本: v2.1.0 最後更新: 2026-07-29 適用對象: 開發團隊、AI Agent、使用 app-tenant-server 範本的團隊
本文檔記錄 app-tenant-server 專案的資料層設計決策與最佳實踐,確保開發一致性與高效能。
Entity 欄位型別、JSON mapping 與關聯 mapping 的現行規範以 JPA Entity 欄位型別規範 為準;本頁聚焦資料隔離、 索引與目錄結構。
目錄
1. 多租戶架構設計
設計原則
核心理念:以 String tenantId 作為 discriminator,不建立指向 Tenant
資料表的外鍵;執行期隔離由 Hibernate 原生 @TenantId 承擔。
為什麼?
| 考量點 | 傳統方式(@ManyToOne) | 我們的方式(String tenantId) |
|---|---|---|
| 查詢性能 | ❌ 需要 JOIN | ✅ 單表查詢 |
| N+1 問題 | ⚠️ 容易觸發 | ✅ 不存在 |
| 水平擴展 | ❌ 跨分片外鍵困難 | ✅ 支援 sharding by tenant |
| 數據隔離 | ⚠️ 依賴應用層查詢條件 | ✅ Session 建立時自動套用 @TenantId filter |
實作方式
1.1 繼承 TenantAwareEntity
所有需要租戶隔離的 Entity 都應直接繼承 TenantAwareEntity,或繼承應用端
加入審計欄位的 AuditableTenantEntity:
@Entity
@Table(name = "customer")
public class Customer extends AuditableTenantEntity {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
@JdbcTypeCode(SqlTypes.VARCHAR)
@Column(length = 36, nullable = false, updatable = false)
private UUID id;
private String name;
}
框架基類的關鍵映射如下:
@MappedSuperclass
public abstract class TenantAwareEntity implements TenantAware {
@TenantId
@Column(name = "tenant_id", length = 36, nullable = false, updatable = false)
private String tenantId;
}
1.2 Session 建立時綁定租戶
安全流程先解析使用者租戶並設定 TenantContext,交易開始時建立的 Hibernate
Session 再綁定該租戶。查詢與 load-by-key 會自動加入 discriminator:
Optional<Customer> customer =
Optional.ofNullable(entityManager.find(Customer.class, id));
// SQL 概念:
// SELECT ... FROM customer
// WHERE id = ? AND tenant_id = ?
不需要 TenantFilterInterceptor、TenantFilterAspect、手動
enableFilter() 或每個 Repository 的 tenantId 守衛。背景寫入必須先
TenantContext.runAs(tenantId, ...),並在其內建立交易;不可在已建立的
Session 中途切換租戶。
優點總結
- ✅ 性能優先:每個查詢都是單表查詢,無 JOIN 開銷
- ✅ 自動化:Hibernate 在 Session 建立與 INSERT 時處理 tenantId
- ✅ 安全性:查詢含 load-by-key 皆隔離,租戶欄位不可更新
- ✅ 可擴展:支援未來按租戶分片(sharding)
完整的 root 視角、native SQL 與交易邊界限制請參閱 多租戶設計指南。
2. JSON 欄位處理
JSON 只用於原子、資料量有界且不需 SQL 查詢內部欄位的結構。Entity
保留具體 value / collection 型別,使用
AttributeConverter<T, String> + @Column(length = Length.LONG32)。
Converter 對無效 JSON 必須 fail fast,不得靜默回傳空集合、空物件或
null。需要查詢、排序、唯一性或獨立生命週期時改為正規化欄位或子 Entity。
不得使用 native JSON、@Lob、@JdbcTypeCode(SqlTypes.JSON),也不得在
collection 上用 @JdbcTypeCode(SqlTypes.LONGVARCHAR) 取代 converter。
完整判斷與 mapping 見 JPA Entity 欄位型別規範; 現行決策見 ADR-027。
3. Entity 關聯策略
Entity association 與 scalar ID 的選擇依 aggregate boundary、資料完整性、 生命週期與歷史快照需求決定,不以避免 JOIN 作唯一理由。
- Association 明確使用
LAZY。 - Cascade 與
orphanRemoval只給真正的 lifecycle owner。 - Shared Entity 不 cascade remove。
- 有額外屬性的多對多關係建立 join Entity。
- 租戶 discriminator 不是業務 association;
server-tenant依既有 tenant primitive 處理。
完整規則見 JPA Entity 欄位型別規範;決策背景見 ADR-028。
4. Entity 設計檢查清單
新增 Entity 時的檢查項目
✅ 基本設定
-
繼承正確的基類
- 需要租戶隔離 →
TenantAwareEntity - 僅需審計欄位 →
AuditableBase - 無需基類功能 → 無需繼承
- 需要租戶隔離 →
-
設定
@Table索引@Table(name = "customer", indexes = {@Index(name = "idx_customer_tenant", columnList = "tenant_id"),@Index(name = "idx_customer_phone", columnList = "phone"),// ...})
✅ 租戶隔離
- 不要建立
@ManyToOne到Tenant的關聯 - 確認繼承
TenantAwareEntity或應用端的AuditableTenantEntity - 不要重複宣告
@Filter、手動啟用 filter 或在 Repository 補 tenantId 守衛 - 在
@Table.indexes加上tenant_id索引
✅ JSON 欄位
- 使用具體型別 +
AttributeConverter<T, String> - 欄位使用
@Column(length = Length.LONG32) - invalid JSON fail fast;null / empty 語意明確
- 不需在 SQL 查詢 JSON 內部欄位
✅ 關聯設計
- Entity ref / scalar ID 依 aggregate boundary 與完整性選擇
- Cascade / orphanRemoval 只給 lifecycle owner
- Shared Entity 不 cascade remove
- Fetch 類型預設使用
LAZY
✅ 驗證與約束
-
加上適當的 Bean Validation 註解
@NotBlank@Size(max = 200)@Email -
欄位加上
nullable設定@Column(nullable = false)
✅ 業務方法
- 提供有意義的業務方法(非單純 getter/setter)
public void addItem(OrderItem item) { /* ... */ }public boolean canCancel() { /* ... */ }
5. Entity 資料夾架構
設計原則
按業務領域 (Domain) 分組,遵循 DDD (Domain-Driven Design) 的理念。
- 每個資料夾代表一個業務子領域
- 相關的 Entity、Enum、Value Object 放在同一資料夾
- 即使只有一個 Entity 也獨立成資料夾(便於未來擴展)
目前架構
entity/
├── auth/ # 認證授權
│ ├── Account.java
│ └── Authority.java
├── base/ # 共用基礎類別
│ ├── AuditableBase.java
│ ├── Bin.java
│ ├── Code.java
│ ├── Document.java
│ └── TenantAwareEntity.java
├── customer/ # 客戶管理 (CRM)
│ ├── Customer.java
│ ├── CustomerNote.java
│ ├── CustomerStatus.java
│ ├── CustomerTier.java
│ ├── CustomerType.java
│ ├── Address.java
│ ├── Contact.java
│ ├── Gender.java
│ ├── ImportantDate.java
│ └── PaymentTerms.java
├── mail/ # 郵件設定
│ └── MailSetting.java
├── order/ # 訂單管理
│ ├── Order.java
│ ├── OrderItem.java
│ ├── OrderStatus.java
│ └── OrderStatusHistory.java
├── sales/ # 產品/銷售
│ ├── Product.java
│ ├── ProductCategory.java
│ └── ProductStatus.java
└── tenant/ # 多租戶
└── Tenant.java
未來擴展規劃
針對完整的花店管理系統(含 CRM、財務、人事),預計新增以下領域:
| 資料夾 | 業務領域 | 可能包含的 Entity |
|---|---|---|
inventory/ | 庫存管理 | Inventory, StockMovement, Supplier(花材有效期、損耗管理) |
finance/ | 財務 | Invoice, Payment, Receipt, Expense |
hr/ | 人事 | Employee, Schedule, Payroll, Attendance |
supplier/ | 供應商管理 | Supplier, PurchaseOrder |
delivery/ | 配送/物流 | DeliveryRoute, DeliveryRecord |
marketing/ | 行銷活動 | Campaign, Coupon, Promotion |
領域邊界建議
-
Order 與 Payment 分開
order/負責訂單生命週期finance/負責付款記錄- 理由:一個訂單可能有多次付款(訂金 + 尾款)
-
Employee 放
hr/,不放auth/auth/只管 Account(登入帳號)和 Authority(權限)hr/管理員工資訊(Employee)、排班、薪資- 理由:登入認證與人事管理是不同的業務關注點
-
漸進式擴展
- 不需預先建立所有資料夾
- 當實作該功能時再新增對應資料夾
6. 常見問題 (FAQ)
Q1: 為什麼不在資料庫建立 tenantId 外鍵?
A: 性能和擴展性考量。
- 每個查詢都需要過濾
tenant_id,建立外鍵會增加不必要的檢查開銷 - 未來支援按租戶分片(sharding)時,跨分片外鍵難以維護
- Hibernate 原生 discriminator 已在 ORM 邊界提供一致的租戶隔離
Q2: 什麼時候應該使用 JSON 欄位?
A: 符合以下條件時:
- ✅ 數據從屬於主 Entity(如客戶的地址列表)
- ✅ 不需要獨立查詢或過濾
- ✅ 數量有限(通常不超過數百筆)
- ✅ 結構可能變動(靈活性需求)
反之,如果需要:
- ❌ 獨立查詢過濾(如
WHERE address.city = ?) - ❌ 關聯查詢(JOIN)
- ❌ 大量數據
應使用 @OneToMany 建立獨立的 Entity。
Q3: 如何測試跨租戶操作被正確阻擋?
A: 使用整合測試:
@Test
void testCrossTenantAccessBlocked() {
String id = TenantContext.supplyAs("tenant-A", () ->
transactionTemplate.execute(status ->
customerService.create(new Customer()).getId()));
boolean visibleFromTenantB = TenantContext.supplyAs("tenant-B", () ->
transactionTemplate.execute(status ->
customerService.findById(id).isPresent()));
assertThat(visibleFromTenantB).isFalse();
}
每次 runAs 都在內部建立新的交易,確保 Session 綁到正確租戶。API
整合測試則應以兩組不同租戶的認證請求驗證同一契約。
Q4: @TenantId 會自動應用於所有查詢嗎?
A: ORM 管理的查詢會套用,包括:
- ✅ Criteria、JPQL 與一般 EntityManager 查詢
- ✅
entityManager.find()等 load-by-key - ✅ JPQL bulk update/delete
- ⚠️ native SQL 不受保護
若確實需要 native SQL,必須手動加入並測試 tenant_id 條件;一般業務
查詢應優先使用 ORM API。
7. 參考資料
相關文檔
- Authority 權限模型 - R/W/X/D 權限設計
- 多租戶設計指南 - 租戶隔離機制
- JPA Entity 欄位型別規範 - 欄位與關聯 mapping
應用層原始碼(參考實作 app-tenant-server)
以下是參考實作中的相關檔案,供開發者參考:
io.leandev.app.entity.base.AuditableTenantEntity- 應用端審計基類io.leandev.appfuse.jpa.tenant.TenantAwareEntity- 框架多租戶基類io.leandev.appfuse.security.tenant.TenantContext- 上下文工具類
設計決策記錄
| 日期 | 決策 | 原因 |
|---|---|---|
| 2025-12-23 | Entity 資料夾按業務領域分組 | DDD 設計、高內聚低耦合、便於未來微服務拆分 |
| 2025-12-23 | 租戶關聯改用 String tenantId | 性能優化、支援 sharding |
| 2026-07-29 | 欄位、JSON 與關聯規則收斂至 entity-field-types.md | 五資料庫共同基線 |
| 2025-12-22 | JSON 欄位使用文字儲存 | 跨資料庫相容性(H2 支援) |
文檔維護者: Development Team + AI Assistant 最後審閱: 2026-07-29