跳至主要内容

Reference Data 設計指南

文檔版本: v1.1.0 最後更新: 2026-01-06 適用對象: 開發團隊、AI Agent、使用 app-server 範本的團隊

本文檔說明 Reference Data(參考資料)的設計原則與實作方式,確保團隊遵循一致的模式。


目錄

  1. 概述
  2. Enum vs Entity vs CodeData 選擇策略
  3. 基礎類別
  4. 統一 API 設計
  5. 新增 Reference Data 類型
  6. 命名慣例
  7. 檢查清單
  8. 現有實作參考
  9. 常見問題 (FAQ)

1. 概述

什麼是 Reference Data?

Reference Data 是系統中相對穩定、用於分類或標準化的資料,通常作為下拉選單選項或業務規則的依據。

Master Data vs Reference Data

特性Master DataReference Data
變動頻率中等(業務成長時增加)低(很少變動)
維護者業務人員開發團隊或系統管理員
範例客戶、產品、供應商幣別、國家、狀態碼
資料量大(數千至數百萬筆)小(通常 < 100 筆)
業務邏輯可能有(如狀態機)

架構概覽

┌─────────────────────────────────────────────────────────────────────┐
│ ReferenceDataController │
│ /api/v1/references/* │
├─────────────────────────────────────────────────────────────────────┤
│ Enum 類型 │ Entity 類型 │ CodeData 類型 │
│ (程式碼定義) │ (有額外欄位) │ (通用代碼資料) │
│ ──────────── │ ──────────── │ ──────────── │
│ • Currency │ • Unit │ • payment-methods │
│ • Country │ → BaseReferenceData │ • delivery-time-slots │
│ • ProductStatus │ • ProductCategory │ • gift-wrapping-options│
│ • OrderStatus │ → TenantAware... │ • card-templates │
│ │ │ → 共用 CodeData Entity │
│ 唯讀 │ 有額外欄位 │ 只有基本欄位,支援 CRUD │
└─────────────────────────────────────────────────────────────────────┘

類型分類

類型說明可寫適用場景
Enum程式碼定義,可有邏輯判斷狀態機、國際標準代碼
Entity有額外欄位的獨立 Entity需要擴充欄位(如 Unit.symbol)
CodeData通用代碼資料,只有基本欄位簡單下拉選單(付款方式、配送時段)

2. Enum vs Entity vs CodeData 選擇策略

決策流程圖

需要新增 Reference Data 類型


┌──────────────┐
│ 程式碼中是否 │ ──是──▶ 使用 Enum
│ 需要判斷邏輯?│ (可帶額外屬性)
│ (if/switch) │
└──────┬───────┘
│否

┌──────────────┐
│ 是否有額外 │ ──是──▶ 使用 Entity
│ 欄位需要 │ (BaseReferenceData 或
│ 後台維護? │ TenantAwareReferenceData)
└──────┬───────┘
│否

┌──────────────┐
│ 是否需要在 │ ──是──▶ 使用 CodeData ★
│ 系統運行中 │ (只需在 CodeDataType 註冊)
│ 由業務人員 │
│ 新增/修改? │
└──────┬───────┘
│否

使用 Enum(簡單版)

選擇矩陣

情境推薦方式範例
國際標準代碼EnumCurrency (ISO 4217), Country (ISO 3166)
狀態機邏輯Enum + 方法OrderStatus(含 isTerminal, isActive)
有額外欄位、全域共用Entity (BaseReferenceData)Unit(計量單位,有 symbol)
有額外欄位、租戶隔離Entity (TenantAwareReferenceData)ProductCategory(可擴充欄位)
只需 code/name、全域CodeData(全域)payment-methods、payment-terms
只需 code/name、租戶隔離CodeData(多租戶)delivery-time-slots、card-templates
純顯示用途、不需邏輯Enum(簡單版)Gender(性別)

3. 基礎類別

3.1 BaseReferenceData(全域共用)

用途:所有租戶共用的 Reference Data

檔案位置entity/base/BaseReferenceData.java

@MappedSuperclass
public abstract class BaseReferenceData extends AuditableBase {

@Id
@Column(length = 36)
@NotBlank
private String code; // 業務代碼(主鍵)

@Column(length = 100, nullable = false)
@NotBlank
private String name; // 顯示名稱

@Column(length = 500)
private String description; // 說明

@Column(name = "sort_order")
private Integer sortOrder = 0; // 排序(預設 0)

@Column(nullable = false)
private Boolean active = true; // 啟用狀態
}

特點

  • 使用 code 作為主鍵(業務有意義的代碼)
  • 適合:計量單位、支付方式等全域資料

3.2 TenantAwareReferenceData(多租戶隔離)

用途:各租戶獨立維護的 Reference Data

檔案位置entity/base/TenantAwareReferenceData.java

@MappedSuperclass
public abstract class TenantAwareReferenceData extends TenantAwareEntity {

@Id
@GeneratedValue(strategy = GenerationType.UUID)
@Column(length = 36)
private String id; // UUID 主鍵

@Column(length = 36, nullable = false)
@NotBlank
private String code; // 業務代碼

@Column(length = 100, nullable = false)
@NotBlank
private String name; // 顯示名稱

@Column(length = 500)
private String description; // 說明

@Column(name = "sort_order")
private Integer sortOrder = 0; // 排序

@Column(nullable = false)
private Boolean active = true; // 啟用狀態

// tenantId 繼承自 TenantAwareEntity
}

特點

  • 使用 UUID id 作為主鍵
  • code 為業務識別碼,配合 tenantId 形成唯一約束
  • 適合:商品分類、自訂標籤等租戶專屬資料

必須設定的唯一約束

@Entity
@Table(name = "product_category",
uniqueConstraints = {
@UniqueConstraint(
name = "uk_product_category_tenant_code",
columnNames = {"tenant_id", "code"}
)
})
@Filter(name = "tenantFilter", condition = "tenant_id = :tenantId")
public class ProductCategory extends TenantAwareReferenceData {
// 繼承所有欄位,可擴充額外欄位
}

3.3 CodeData(通用代碼資料)

用途:不需要額外欄位的簡單 Reference Data,避免為每種類型建立獨立 Entity

檔案位置entity/base/CodeData.java

@Entity
@Table(name = "code_data",
uniqueConstraints = {
@UniqueConstraint(
name = "uk_code_data_type_tenant_code",
columnNames = {"type", "tenant_id", "code"}
)
})
public class CodeData extends AuditableBase {

@Id
@GeneratedValue(strategy = GenerationType.UUID)
private String id;

@Column(length = 50, nullable = false)
private String type; // 類型(如 "payment-methods")

@Column(name = "tenant_id", length = 36)
private String tenantId; // null = 全域,非 null = 租戶專屬

@Column(length = 36, nullable = false)
private String code; // 業務代碼

@Column(length = 100, nullable = false)
private String name; // 顯示名稱

@Column(length = 500)
private String description; // 說明

@Column(name = "sort_order")
private Integer sortOrder = 0; // 排序

@Column(nullable = false)
private Boolean active = true; // 啟用狀態
}

特點

  • 使用 type 欄位區分不同類型的代碼資料
  • tenantId 為 null 表示全域類型,非 null 表示租戶專屬
  • 所有簡單類型共用同一張表,無需為每種類型建立 Entity
  • 透過 CodeDataType Enum 定義可用的類型

CodeDataType 定義

// entity/base/CodeDataType.java
public enum CodeDataType {
// 全域類型
PAYMENT_METHODS("payment-methods", "付款方式", false),
PAYMENT_TERMS("payment-terms", "付款條件", false),

// 多租戶類型
DELIVERY_TIME_SLOTS("delivery-time-slots", "配送時段", true),
GIFT_WRAPPING_OPTIONS("gift-wrapping-options", "禮品包裝", true),
CARD_TEMPLATES("card-templates", "卡片範本", true),
ORDER_SOURCES("order-sources", "訂單來源", true);

private final String code;
private final String label;
private final boolean tenantAware; // true = 多租戶隔離
}

何時使用 CodeData

  • 只需要 code + name(+ description)的簡單下拉選單
  • 不需要在程式碼中判斷邏輯(不需要 if/switch)
  • 可能需要業務人員在系統運行中新增/修改

4. 統一 API 設計

4.1 ReferenceDataController

檔案位置controller/base/ReferenceDataController.java

提供統一的 API 端點,前端可一次取得多種 Reference Data:

查詢端點(所有類型)

端點說明回應
GET /api/v1/references/types取得所有可用類型["currencies", "countries", ...]
GET /api/v1/references/{type}取得單一類型列表[{code, name, ...}, ...]
GET /api/v1/references/{type}/{code}取得單一項目{code, name, ...}
GET /api/v1/references?types=a,b,c批次取得多種類型{a: [...], b: [...], c: [...]}

CodeData CRUD 端點(僅 CodeData 類型)

端點說明權限
POST /api/v1/references/{type}新增 CodeData 項目REFERENCE_DATA_W
PUT /api/v1/references/{type}/{code}更新 CodeData 項目REFERENCE_DATA_W
DELETE /api/v1/references/{type}/{code}刪除 CodeData 項目REFERENCE_DATA_D

注意:Enum 和 Entity 類型不支援 CRUD,嘗試寫入會回傳 500 錯誤。

4.2 統一回應格式(ReferenceDataItem)

檔案位置dto/ReferenceDataItem.java

所有 Reference Data 回傳統一格式:

@Data
@Builder
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ReferenceDataItem {
private String code; // 必填:業務代碼
private String name; // 必填:顯示名稱
private String description; // 選填:說明
private Integer sortOrder; // 選填:排序
private Map<String, Object> extra; // 選填:額外屬性
}

回應範例

{
"code": "TWD",
"name": "新台幣",
"extra": {
"symbol": "NT$",
"decimalPlaces": 0
}
}

4.3 前端使用範例

// 應用程式初始化時,一次取得所有需要的 Reference Data
const response = await api.get('/api/v1/references', {
params: { types: 'currencies,countries,units,product-categories' }
});

// 存入 store 供下拉選單使用
store.setReferenceData(response.data);

// 使用時
<Select options={store.currencies.map(c => ({ value: c.code, label: c.name }))} />

5. 新增 Reference Data 類型

5.1 新增 Enum 類型

步驟

  1. 建立 Enum 類別
// entity/base/PaymentMethod.java
public enum PaymentMethod {
CASH("現金"),
CREDIT_CARD("信用卡"),
BANK_TRANSFER("銀行轉帳"),
LINE_PAY("LINE Pay");

private final String label;

PaymentMethod(String label) {
this.label = label;
}

public String getCode() {
return name(); // 使用 enum name 作為 code
}

public String getLabel() {
return label;
}
}
  1. 在 Controller 新增轉換方法
// ReferenceDataController.java

private List<ReferenceDataItem> getPaymentMethods() {
return Arrays.stream(PaymentMethod.values())
.map(p -> ReferenceDataItem.of(p.getCode(), p.getLabel()))
.collect(Collectors.toList());
}
  1. 註冊到 Registry
private Map<String, Supplier<List<ReferenceDataItem>>> getRegistry() {
return Map.of(
// ... 現有類型
"payment-methods", this::getPaymentMethods // 新增
);
}

5.2 新增 Entity 類型(全域)

步驟

  1. 建立 Entity 類別
// entity/base/PaymentTerm.java
@Entity
@Table(name = "payment_term")
public class PaymentTerm extends BaseReferenceData {

@Column(name = "days_until_due")
private Integer daysUntilDue; // 額外欄位:到期天數
}
  1. 建立 Repository
// repository/base/PaymentTermRepository.java
public interface PaymentTermRepository extends JpaRepository<PaymentTerm, String> {

List<PaymentTerm> findByActiveTrue();

@Query("SELECT p FROM PaymentTerm p WHERE p.active = true ORDER BY p.sortOrder, p.name")
List<PaymentTerm> findAllActive();
}
  1. 在 Controller 注入並新增方法
@RestController
@RequestMapping("/api/v1/references")
@RequiredArgsConstructor
public class ReferenceDataController {

private final PaymentTermRepository paymentTermRepository; // 注入

private List<ReferenceDataItem> getPaymentTerms() {
return paymentTermRepository.findAllActive().stream()
.map(p -> ReferenceDataItem.from(p, Map.of(
"daysUntilDue", p.getDaysUntilDue()
)))
.collect(Collectors.toList());
}
}
  1. 註冊到 Registry
"payment-terms", this::getPaymentTerms
  1. 建立初始資料(選填)
// src/main/resources/data/PaymentTerm.json
[
{
"code": "IMMEDIATE",
"name": "即期付款",
"daysUntilDue": 0,
"sortOrder": 1,
"active": true
},
{
"code": "NET30",
"name": "月結 30 天",
"daysUntilDue": 30,
"sortOrder": 2,
"active": true
}
]

5.3 新增 Entity 類型(多租戶)

步驟

  1. 建立 Entity 類別
// entity/sales/ProductTag.java
@Entity
@Table(name = "product_tag",
uniqueConstraints = {
@UniqueConstraint(
name = "uk_product_tag_tenant_code",
columnNames = {"tenant_id", "code"}
)
})
@Filter(name = "tenantFilter", condition = "tenant_id = :tenantId")
public class ProductTag extends TenantAwareReferenceData {

@Column(length = 7)
private String color; // 額外欄位:標籤顏色(如 #FF5733)
}
  1. 建立 Repository
// repository/sales/ProductTagRepository.java
public interface ProductTagRepository extends JpaRepository<ProductTag, String> {

@Query("SELECT t FROM ProductTag t WHERE t.active = true ORDER BY t.sortOrder, t.name")
List<ProductTag> findAllActive();

Optional<ProductTag> findByCode(String code);

boolean existsByCode(String code);
}
  1. 在 Controller 新增方法並註冊
private final ProductTagRepository productTagRepository;

private List<ReferenceDataItem> getProductTags() {
return productTagRepository.findAllActive().stream()
.map(t -> ReferenceDataItem.from(t, Map.of(
"color", t.getColor() != null ? t.getColor() : ""
)))
.collect(Collectors.toList());
}

// Registry
"product-tags", this::getProductTags

5.4 新增 CodeData 類型(推薦)

最簡單的方式:只需在 CodeDataType 新增一行即可。

步驟

  1. CodeDataType Enum 新增常數
// entity/base/CodeDataType.java
public enum CodeDataType {
// ... 現有類型

// 新增類型
OCCASION_TYPES("occasion-types", "場合類型", true); // true = 多租戶

// 或全域類型
SHIPPING_METHODS("shipping-methods", "運送方式", false); // false = 全域
}
  1. 完成!

Controller 會自動註冊新類型,無需其他修改。

使用 API 管理資料

# 新增項目
curl -X POST /api/v1/references/occasion-types \
-H "Content-Type: application/json" \
-d '{"code": "WEDDING", "name": "婚禮", "sortOrder": 1}'

# 查詢
curl /api/v1/references/occasion-types

# 更新
curl -X PUT /api/v1/references/occasion-types/WEDDING \
-d '{"name": "婚禮慶典", "sortOrder": 2}'

# 刪除
curl -X DELETE /api/v1/references/occasion-types/WEDDING

與 Entity 類型的比較

項目Entity 類型CodeData 類型
新增類型步驟5 步(Entity + Repository + 注入 + 轉換方法 + 註冊)1 步(CodeDataType 加一行)
額外欄位✅ 支援❌ 不支援
獨立資料表✅ 每種類型一張表❌ 所有類型共用 code_data
適用場景需要擴充欄位只需 code + name

6. 命名慣例

6.1 欄位命名

欄位說明範例
code業務代碼,用於程式判斷"TWD", "BOUQUET", "PENDING"
name顯示名稱,用於 UI"新台幣", "花束", "待處理"
description詳細說明"各式鮮花花束,適合送禮"
sortOrder排序順序(數字越小越前面)1, 2, 10
active是否啟用true, false
extra額外屬性(API 回應用){"symbol": "NT$"}

6.2 API 路徑命名

規則範例
使用複數形式/references/currencies(非 /currency
使用 kebab-case/product-categories(非 /productCategories
與類型名稱對應Registry key 與 URL path 一致

6.3 程式碼命名

項目規則範例
Entity 類別PascalCase 單數ProductCategory, Unit
Enum 類別PascalCase 單數Currency, OrderStatus
Enum 值UPPER_SNAKE_CASEPENDING_CONFIRMATION, IN_PRODUCTION
RepositoryEntity + RepositoryProductCategoryRepository
JSON 資料檔PascalCase.jsonProductCategory.json, Unit.json
Registry keykebab-case 複數"product-categories", "order-statuses"

7. 檢查清單

新增 Enum 類型

  • 建立 Enum 類別(含 getCode()getLabel() 方法)
  • ReferenceDataController 新增轉換方法
  • getRegistry() 註冊
  • 更新 API Specification
  • 執行測試確認 API 正常

新增 Entity 類型(全域)

  • 建立 Entity 類別,繼承 BaseReferenceData
  • 建立 Repository,含 findAllActive() 方法
  • ReferenceDataController 注入 Repository
  • 新增轉換方法
  • getRegistry() 註冊
  • 建立 JSON 初始資料檔(如需要)
  • DataInitializer 新增初始化邏輯(如需要)
  • 更新 API Specification
  • 執行測試確認 API 正常

新增 Entity 類型(多租戶)

  • 建立 Entity 類別,繼承 TenantAwareReferenceData
  • 設定 @TableuniqueConstraints(tenant_id + code)
  • 設定 @Filter 註解
  • 建立 Repository,含 findAllActive(), findByCode(), existsByCode() 方法
  • ReferenceDataController 注入 Repository
  • 新增轉換方法
  • getRegistry() 註冊
  • 建立 JSON 初始資料檔(如需要)
  • DataInitializer 新增初始化邏輯(如需要)
  • 更新 API Specification
  • 執行測試確認 API 正常

新增 CodeData 類型(最簡單)

  • CodeDataType Enum 新增常數(指定 code、label、tenantAware)
  • 完成!Controller 會自動註冊

8. 現有實作參考

8.1 Enum 類型

類型檔案位置額外屬性
Currencyentity/base/Currency.javasymbol, decimalPlaces
Countryentity/base/Country.javaalpha3, englishName
ProductStatusentity/sales/ProductStatus.java-
OrderStatusentity/order/OrderStatus.javaisTerminal, isActive

8.2 Entity 類型

類型檔案位置基類額外欄位
Unitentity/base/Unit.javaBaseReferenceDatasymbol, unitType
ProductCategoryentity/sales/ProductCategory.javaTenantAwareReferenceData-

8.3 CodeData 類型

類型全域/多租戶說明
payment-methods全域付款方式
payment-terms全域付款條件
delivery-time-slots多租戶配送時段
gift-wrapping-options多租戶禮品包裝
card-templates多租戶卡片範本
order-sources多租戶訂單來源

相關檔案

  • entity/base/CodeData.java - Entity 類別
  • entity/base/CodeDataType.java - 類型定義 Enum
  • repository/base/CodeDataRepository.java - Repository

8.4 測試資料位置

檔案位置
Unit.jsonsrc/main/resources/data/Unit.json
ProductCategory.jsonsrc/main/resources/data/ProductCategory.json
units.json (共用測試資料)../shared/test-data/units.json
product-categories.json (共用測試資料)../shared/test-data/product-categories.json

9. 常見問題 (FAQ)

Q1: 何時該用 Enum 的 name() vs 自訂 code

A: 建議統一使用 name() 作為 code,除非:

  • 需要與外部系統對接(如 ISO 代碼)
  • 歷史資料已使用不同的 code 格式
// 推薦:使用 name() 作為 code
public String getCode() {
return name();
}

// 例外:外部標準代碼
public enum Currency {
TWD("TWD", "新台幣", ...), // code 與 name() 相同
// ...
}

Q2: Reference Data 應該快取嗎?

A: 建議在以下層級快取:

層級方式適用場景
前端Store/State一次取得,整個 session 使用
後端Spring CacheEntity 類型,減少 DB 查詢
不快取-多租戶 Entity(因租戶隔離)
// 後端快取範例
@Cacheable("units")
public List<Unit> findAllActive() {
return unitRepository.findByActiveTrue();
}

Q3: 如何處理已停用但被參照的 Reference Data?

A: 使用軟刪除(active = false),不要物理刪除:

// Repository 查詢只返回啟用的
@Query("SELECT u FROM Unit u WHERE u.active = true ORDER BY u.sortOrder")
List<Unit> findAllActive();

// 但仍可透過 code 查詢(含停用的),用於顯示歷史資料
Optional<Unit> findByCode(String code);

Q4: 多租戶 Reference Data 的初始資料如何處理?

A: 兩種策略:

  1. 範本資料:新租戶建立時,自動複製一組預設資料
  2. 空白開始:新租戶沒有預設資料,由業務人員自行建立

目前專案使用策略 1(見 DataInitializer)。

Q5: 如何在其他 Entity 中參照 Reference Data?

A: 依據關聯類型選擇:

// Enum 類型:直接使用 Enum
@Enumerated(EnumType.STRING)
@Column(length = 20)
private OrderStatus status;

// Entity 類型(全域):使用 code
@Column(name = "unit_code", length = 36)
private String unitCode;

// Entity 類型(多租戶):使用 code(同租戶內唯一)
@Column(name = "category_code", length = 36)
private String categoryCode;

參考資料

相關文檔

相關程式碼

相關程式碼位於 app-server 參考實作:

  • entity/base/BaseReferenceData.java - 全域 Reference Data 基類
  • entity/base/TenantAwareReferenceData.java - 多租戶 Reference Data 基類
  • controller/base/ReferenceDataController.java - Reference Data API Controller
  • dto/ReferenceDataItem.java - 統一回應 DTO

API Specification

  • bruno/references/ - Reference Data API Specification

文檔維護者: Development Team + AI Assistant 最後審閱: 2025-12-29