Skip to content

KIN_DATA/ASSOC_DATA 雙行鏡像存儲的結構性問題與建模方向(含 restore 路徑破口) #1222

Description

@frankslin

背景

親屬(KIN_DATA)與社會關係(ASSOC_DATA)目前採「一條關係存兩行鏡像」方案:A→B 一行、B→A 一行,互逆碼靠 KINSHIP_CODES.c_kin_pair1/2ASSOC_CODES.c_assoc_pair/pair2 登記。一致性由應用層(RelationshipMirrorService + BiogMainRepository 的四個 sync 方法、四種鏡像例外、前端 409 確認閘)在維持;讀取側完全單向(PersonBrowserService 只查 WHERE c_personid = ?,無反向補查),因此鏡像一旦缺失,直接表現為「A 頁看得到、B 頁看不到」。

本 issue 彙整 2026-08-05 的一次結構性調查(prod 實測 + 全碼路徑梳理),記錄現行方案的問題清單與更 elegant 的關係型建模方向,供後續討論與排期。

Prod 實測(2026-08-05)

指標 KIN_DATA ASSOC_DATA
總行數 560,128 189,724
完全無反向行 2,885 11
反向行存在但碼不在登記的互逆碼集 ≈23,948(4.3%) 134

ASSOC 的低漂移說明鏡像機制上線(2026-06-26)後有效;KIN 的欠賬主要是歷史資料與繞過路徑造成。

問題清單

  1. 同一事實存兩份,但兩行之間沒有任何配對鍵。 找鏡像行只能靠啟發式(人 id 互換 + 碼落在互逆碼集;assoc 另加 c_text_titlec_assoc_first_year)。此定位器天然歧義:互逆碼集可命中多行(父→子/長子/三子皆合法)、KINSHIP_CODES 配對登記本身非對稱(75.pair2=180 但 180 不回指 75)、c_autogen_notes 兩側天生不同。內部工作項 66/70/77/82/87/88(編號見 docs/RELATIONSHIP_MIRROR_INLINE_DESIGN.md 與程式註釋,非本 repo 的 issue 編號)一連串修補全是在補這個啟發式。
  2. 一致性無資料庫保證,只有應用層紀律。 已確認的現存破口:operations 的 restore 路徑完全沒有鏡像處理OperationsController::restoreUpdate/restoreDelete 單表單列直寫)——還原一次刪除只重建正向行;還原一次改碼後,對面留在新碼上,且因 c_kin_code 是 PK 一部分,定位器從此找不到對面。另外 assoc 刪除的回退層在多筆命中時只寫 log 靜默跳過(正向刪了、鏡像留著),與 kin 的 409 確認閘不對稱。
  3. 30+ 個屬性欄在兩行間全量複製(年份、地點、機構、出處、備註…),每次更新都要同步兩份;preserveMirrorCode 之類細碎語義是為此付出的複雜度。
  4. 寬複合 PK 是負擔。 ASSOC_DATA 主鍵 9 欄,連 c_text_titlec_assoc_first_year 都在 PK 內;無 surrogate id,operations/audit/提案都要背 9 欄 PK;改書名或首年等於換身份(delete+insert)。CompositePrimaryKey::SCHEMAS 的欄序與 SQL PK 欄序不一致,全靠約定。
  5. 鏡像變換含不可推導的私有約定。 assoc 鏡像行的 c_kin_idc_assoc_kin_id 都被設為本人 personid 而非按語義互換;刪除定位器第一級依賴此約定,不符約定的舊資料只能落回退層。

建模方向(關係型資料庫內的替代方案)

關鍵前提:反向標籤本身是資訊(B 是 A 的父,A 可以是 B 的子/長子/三子,是編目者的選擇、不可推導)——但這只構成「一行存兩個碼」的理由,不構成「存兩行」的理由。

方案 A(kinship 理想形):單行 + 雙端碼。

KIN_REL(id PK, person_a, person_b, code_a_to_b, code_b_to_a,
        c_source, c_pages, c_notes, 稽核欄…)

一致性 by construction,「鏡像缺失/碼不符」兩類問題不再存在,整套鏡像同步機制可移除。讀取用 UNION ALL 相容視圖(欄名對齊現行 c_personid/c_kin_id/c_kin_code)即可讓現有單向查詢原樣工作。

方案 B(association 理想形):關係實體 + 參與者表(reification)。 ASSOC_DATA 本質是事件(時間、地點、場合、文本、中介人、見證人、雙方各自的經由親屬):

ASSOC_EVENT(id PK, 雙端 assoc 碼, 年份/年號/月日, c_addr_id,
            c_text_title, occasion/topic/genre, c_source, c_pages, c_notes)
ASSOC_ROLE(event_id FK, person_id FK, role, kin_via_code, kin_via_person)

中介人、見證人從 nullable 欄變成參與者行;查某人全部關係=ASSOC_ROLE WHERE person_id = ? 一個索引查詢。

方案 C(務實增量,不換表形):給兩行加配對鍵。 若 schema 須與 Access 版 CBDB/上游同步保持相容(大概率是硬約束),最小有效改進:加 surrogate id + 共享 relation_uid(或互指 mirror_id)把兩行顯式配對+唯一約束。鏡像同步邏輯保留,但定位器從「啟發式猜」變成「按鍵取」——多筆命中 409、回退層靜默跳過、autogen_notes 干擾整類問題消失;restore 也只需按 relation_uid 帶上對面那行。資料遷移可用現有定位器做一次性配對(唯一命中者自動配,歧義者留修復頁),遷移本身即一次全庫體檢。

建議優先序

  1. 先堵 restore 破口(含 assoc 靜默跳過)——這是現在進行式的髒資料來源,與選型無關。
  2. 中期走方案 C:相容代價最小,卻消滅「啟發式定位」這個最大複雜度來源,並為任何後續重構留好配對基礎。
  3. 方案 A/B 是無相容包袱時的目標形態;現有讀路徑全是單向 WHERE c_personid = ?,恰是相容視圖最容易偽裝的形態,屆時遷移風險可壓很低。

一個支持「方案 C 足夠好」的數據點:ASSOC_DATA 在鏡像機制上線後僅 11 行缺失、134 行碼不符——應用層同步本身有效,缺的不是更多同步碼,而是讓同步不必「猜」的那把鍵。

相關材料

  • docs/RELATIONSHIP_MIRROR_INLINE_DESIGN.md(鏡像內化設計,未涵蓋 restore 路徑)
  • docs/UNIDIRECTIONAL_RELATIONSHIP_REPAIR.md(單向關係修復頁)
  • app/Services/RelationshipMirrorService.phpapp/Repositories/BiogMainRepository.php(sync 四方法)
  • app/Http/Controllers/OperationsController.php(restore,無鏡像處理)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions