ADR-004: 方法論對資料隔離策略中性化
ADR 編號: 004 狀態: 已接受 (Accepted) 決策日期: 2026-06-21 決策者: AppFuse Team + AI Assistant 範圍: docs-methodology(開發方法論)
摘要
AppFuse 方法論原本把多租戶寫死成預設:m-server-common.md 規定「業務 Entity 必須繼承 AuditableTenantEntity」、/domain-model//us//epic//api-spec 在產出時一律假定租戶隔離。結果是——單租戶專案即使跑了 /remove-multi-tenancy(限 server 模組)把程式碼轉乾淨,方法論層仍會反覆「再生」多租戶痕跡:下次跑 /domain-model 又建議 AuditableTenantEntity、/epic 又長出「多租戶考量」段。
本 ADR 將方法論對「資料怎麼隔離」中性化:不偏袒租戶或擁有權任一模式,改由 project.json 的 defaultDataIsolation 欄位作為單一事實來源宣告專案取向,消費此欄位的 skill 讀它分流。隔離策略回歸其本來面目——per-entity 的決策(即使多租戶專案,認證層 Entity 也免租戶),defaultDataIsolation 只設定業務 Entity 的預設,個別 Entity 可覆寫。
背景 (Context)
問題陳述
框架 jar(appfuse-server)本身已經中性——它同時提供 AuditableTenantEntity(租戶隔離)與 AuditableBase(無租戶欄位),兩者平等、不強制。非中性的不是框架碼,而是方法論的預設建議:
| 再生源頭 | 寫死的內容 |
|---|---|
m-server-common.md 義務表 | 「業務 Entity 必須繼承 AuditableTenantEntity」——硬義務 |
/domain-model | 候選實體預設推 AuditableTenantEntity 基類 |
/epic、/us | 內建「多租戶考量」範本段與澄清提問 |
/api-spec | tenant_id 注入註記、不可更新欄位含 tenantId |
這些把「per-entity 的隔離決策」硬塞成「project 全域的多租戶預設」。
核心張力
單租戶專案的轉換工具 /remove-multi-tenancy 限 server 模組(見其「範圍邊界」):它清得了程式碼與模組 rule,但碰不到 workspace 層的共享方法論(m-server-common.md 與 /domain-model 等 skill)。於是:
- 清乾淨的程式碼,會被下次的
/domain-model//epic依「多租戶預設」重新污染。 - 若改採「逐檔在 scaffold 副本塞單租戶 banner」止血,等於把單一專案的決策 fork 進通用方法論檔,且真相散在 N 個 banner、漏一個就再生。
缺的是:一個讓方法論不偏袒、由專案宣告取向的機制。
關鍵識別:隔離是 per-entity,不是全域二元
即使在「多租戶」專案,認證層 Entity 也繼承 AuditableBase(免租戶)——m-server-common.md 自己就寫了「認證層 Entity 例外」。所以租戶性本來就是 per-entity 的屬性;一個「單租戶專案」只是所有業務 Entity 的隔離答案剛好一致為「無租戶」。
這意味著「全域 tenant on/off flag」是個洩漏的抽象——它假裝租戶性是全域,與框架實際的 per-entity 處理相左。
限制條件
/domain-model的澄清循環已經在問「此實體是業務層還是認證層」來選基類——擴成「隔離策略軸」是延伸既有機制,非新增負擔。- 多數使用框架的專案是多租戶 SaaS——預設值必須是
tenant以保留現行行為、向後相容(欄位缺省=tenant)。 - 花店參考實作(app-server)須維持多租戶,作為租戶路徑的可運行 demo。
考量的方案 (Options Considered)
方案 A:逐檔塞單租戶 banner(止血)
在每個 scaffold 出的專案副本,手動於 m-server-common.md、/us、/domain-model 等加「本專案單租戶」banner。
- ✅ 不動框架、立即可行
- ❌ 污染共享方法論檔;真相散在 N 個 banner、漏一個就再生;每個單租戶專案重複 fork
方案 B:全域 tenancy flag(tenancy: multi|single)
project.json 加一個全域開關,skill 讀它二分。
- ✅ 單一事實來源、一處改全對齊
- ❌ 「全域二元」是洩漏抽象——假裝租戶性是 project 全域,與框架的 per-entity 處理(認證層免租戶)相左
方案 C:中性方法論 + per-entity 隔離策略宣告(採用)
方法論對兩種隔離模式中性;project.json 宣告預設隔離策略 defaultDataIsolation(tenant/ownership),作為業務 Entity 的預設基類來源;/domain-model 沿既有澄清循環逐 Entity 套用、per-entity 可覆寫。
- ✅ 單一事實來源;忠於框架的 per-entity 模型(預設+覆寫,而非全域開關)
- ✅ 方法論檔保持中性,對 multi/single 皆成立,零 per-file banner
- ✅
/remove-multi-tenancy退化為「翻預設 + 批次重宣告」,不再需要動共享方法論 - ⚠️ 需要同步改動數個方法論 skill/rule(一次性成本)
決策 (Decision)
採方案 C。具體:
project.json新增defaultDataIsolation:"tenant"(預設,向後相容)/"ownership"。語意權威定義落在m-server-common.md的「資料隔離策略」節。m-server-common.md中性化:義務表「必須繼承AuditableTenantEntity」改為依defaultDataIsolation分流;新增「資料隔離策略」節為單一事實來源,列出消費此欄位的 skill。/domain-model擴成隔離策略軸:業務 Entity 預設基類讀defaultDataIsolation,澄清循環逐 Entity 釐清(業務/認證層 × 租戶/擁有權),per-entity 可覆寫。/epic、/us、/api-spec中性化:「多租戶考量」→「資料隔離考量」(涵蓋租戶/擁有權兩路);tenant_id 註記改條件性適用。/scaffold-project收集defaultDataIsolation:建立新專案時詢問並寫入。/remove-multi-tenancy補完:Step 4i 由「點名清單」改「系統化 grep 整個{module}/.claude/」(修復10-framework/13-transaction等漏網);新增 Step 4j 翻轉project.json的defaultDataIsolation(tenant→ownership)。
「預設值」與「中性」不衝突:方法論讀欄位分流(中性),欄位本身有合理預設 tenant(向後相容+多數場景零摩擦)。預設值不等於方法論硬編。
未跳過 RFC:本決策由唯一框架維護者直接落地(RFC 的價值是與消費者/其他維護者社會化,無對象則為負擔),但保留決策記錄——即本 ADR。
後果 (Consequences)
正面
- 單租戶專案跑完
/remove-multi-tenancy後,方法論不再再生多租戶痕跡(/domain-model依ownership建議AuditableBase)。 - 共享方法論檔保持中性、不被任何專案 fork,框架升級時無 per-project banner 雜訊。
- 隔離策略的心智模型對齊框架實況(per-entity,預設+覆寫)。
負面/成本
- 一次性改動數個方法論 skill/rule(已於本次落地)。
- 多租戶專案的文檔段落由「多租戶考量」更名為「資料隔離考量」——既有 reference 文檔的措辭與新範本不完全一致(不影響功能,漸進對齊)。
中性
- 花店參考實作維持多租戶(
defaultDataIsolation: tenant),程式碼不動。 - 框架 jar 不變——本 ADR 純屬方法論層,無框架 API 變更。
相關文檔
m-server-common.md「資料隔離策略」節(defaultDataIsolation權威語意與消費 skill)/remove-multi-tenancyskill(單租戶轉換的執行端,Step 4j 翻轉宣告)- 多租戶設計指南 與 ADR-001 多租戶數據隔離策略(框架租戶機制細節)
- ADR-003 Headless 契約驅動 API 軌(同屬方法論層的軌道/模式分流決策)