背景
親屬(KIN_DATA)與社會關係(ASSOC_DATA)目前採「一條關係存兩行鏡像」方案:A→B 一行、B→A 一行,互逆碼靠 KINSHIP_CODES.c_kin_pair1/2 與 ASSOC_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 的欠賬主要是歷史資料與繞過路徑造成。
問題清單
- 同一事實存兩份,但兩行之間沒有任何配對鍵。 找鏡像行只能靠啟發式(人 id 互換 + 碼落在互逆碼集;assoc 另加
c_text_title、c_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 編號)一連串修補全是在補這個啟發式。
- 一致性無資料庫保證,只有應用層紀律。 已確認的現存破口:operations 的 restore 路徑完全沒有鏡像處理(
OperationsController::restoreUpdate/restoreDelete 單表單列直寫)——還原一次刪除只重建正向行;還原一次改碼後,對面留在新碼上,且因 c_kin_code 是 PK 一部分,定位器從此找不到對面。另外 assoc 刪除的回退層在多筆命中時只寫 log 靜默跳過(正向刪了、鏡像留著),與 kin 的 409 確認閘不對稱。
- 30+ 個屬性欄在兩行間全量複製(年份、地點、機構、出處、備註…),每次更新都要同步兩份;
preserveMirrorCode 之類細碎語義是為此付出的複雜度。
- 寬複合 PK 是負擔。 ASSOC_DATA 主鍵 9 欄,連
c_text_title、c_assoc_first_year 都在 PK 內;無 surrogate id,operations/audit/提案都要背 9 欄 PK;改書名或首年等於換身份(delete+insert)。CompositePrimaryKey::SCHEMAS 的欄序與 SQL PK 欄序不一致,全靠約定。
- 鏡像變換含不可推導的私有約定。 assoc 鏡像行的
c_kin_id 與 c_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 帶上對面那行。資料遷移可用現有定位器做一次性配對(唯一命中者自動配,歧義者留修復頁),遷移本身即一次全庫體檢。
建議優先序
- 先堵 restore 破口(含 assoc 靜默跳過)——這是現在進行式的髒資料來源,與選型無關。
- 中期走方案 C:相容代價最小,卻消滅「啟發式定位」這個最大複雜度來源,並為任何後續重構留好配對基礎。
- 方案 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.php、app/Repositories/BiogMainRepository.php(sync 四方法)
app/Http/Controllers/OperationsController.php(restore,無鏡像處理)
背景
親屬(KIN_DATA)與社會關係(ASSOC_DATA)目前採「一條關係存兩行鏡像」方案:A→B 一行、B→A 一行,互逆碼靠
KINSHIP_CODES.c_kin_pair1/2與ASSOC_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)
ASSOC 的低漂移說明鏡像機制上線(2026-06-26)後有效;KIN 的欠賬主要是歷史資料與繞過路徑造成。
問題清單
c_text_title、c_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 編號)一連串修補全是在補這個啟發式。OperationsController::restoreUpdate/restoreDelete單表單列直寫)——還原一次刪除只重建正向行;還原一次改碼後,對面留在新碼上,且因c_kin_code是 PK 一部分,定位器從此找不到對面。另外 assoc 刪除的回退層在多筆命中時只寫 log 靜默跳過(正向刪了、鏡像留著),與 kin 的 409 確認閘不對稱。preserveMirrorCode之類細碎語義是為此付出的複雜度。c_text_title、c_assoc_first_year都在 PK 內;無 surrogate id,operations/audit/提案都要背 9 欄 PK;改書名或首年等於換身份(delete+insert)。CompositePrimaryKey::SCHEMAS的欄序與 SQL PK 欄序不一致,全靠約定。c_kin_id與c_assoc_kin_id都被設為本人 personid 而非按語義互換;刪除定位器第一級依賴此約定,不符約定的舊資料只能落回退層。建模方向(關係型資料庫內的替代方案)
關鍵前提:反向標籤本身是資訊(B 是 A 的父,A 可以是 B 的子/長子/三子,是編目者的選擇、不可推導)——但這只構成「一行存兩個碼」的理由,不構成「存兩行」的理由。
方案 A(kinship 理想形):單行 + 雙端碼。
一致性 by construction,「鏡像缺失/碼不符」兩類問題不再存在,整套鏡像同步機制可移除。讀取用
UNION ALL相容視圖(欄名對齊現行c_personid/c_kin_id/c_kin_code)即可讓現有單向查詢原樣工作。方案 B(association 理想形):關係實體 + 參與者表(reification)。 ASSOC_DATA 本質是事件(時間、地點、場合、文本、中介人、見證人、雙方各自的經由親屬):
中介人、見證人從 nullable 欄變成參與者行;查某人全部關係=
ASSOC_ROLE WHERE person_id = ?一個索引查詢。方案 C(務實增量,不換表形):給兩行加配對鍵。 若 schema 須與 Access 版 CBDB/上游同步保持相容(大概率是硬約束),最小有效改進:加 surrogate
id+ 共享relation_uid(或互指mirror_id)把兩行顯式配對+唯一約束。鏡像同步邏輯保留,但定位器從「啟發式猜」變成「按鍵取」——多筆命中 409、回退層靜默跳過、autogen_notes 干擾整類問題消失;restore 也只需按relation_uid帶上對面那行。資料遷移可用現有定位器做一次性配對(唯一命中者自動配,歧義者留修復頁),遷移本身即一次全庫體檢。建議優先序
WHERE c_personid = ?,恰是相容視圖最容易偽裝的形態,屆時遷移風險可壓很低。一個支持「方案 C 足夠好」的數據點:ASSOC_DATA 在鏡像機制上線後僅 11 行缺失、134 行碼不符——應用層同步本身有效,缺的不是更多同步碼,而是讓同步不必「猜」的那把鍵。
相關材料
docs/RELATIONSHIP_MIRROR_INLINE_DESIGN.md(鏡像內化設計,未涵蓋 restore 路徑)docs/UNIDIRECTIONAL_RELATIONSHIP_REPAIR.md(單向關係修復頁)app/Services/RelationshipMirrorService.php、app/Repositories/BiogMainRepository.php(sync 四方法)app/Http/Controllers/OperationsController.php(restore,無鏡像處理)