
系列:30 天打造企業級 PLM|面向:設計|素材:領域規格設計、實際資料庫 schema
PLM 到底在管什麼?從一顆螺絲的一生講起:研發畫了圖、開了料號,進了某台機器的 BOM,量產兩年後供應商說要改材質,於是發變更單、跑簽核、放行、改版。三年後客訴,要調閱「當時出貨那一版」的圖面。今天不寫程式,把支撐這一切的四個聚合講清楚。這張圖是全系列的地基,後面 27 天都會用到。

在以前 Oracle Agile PLM 的時代,領域物件的操作是建立在龐大的 Node-based API 與 EJB Entity Bean(或私有物件代理層)之上。
如果寫過 Agile SDK,你一定熟悉 IItem、IChange、ITable、IRow 這一套抽象。在 EJB 架構下,存取一個料號或 BOM 列,後端得透過多層 EJB 攔截器、權限檢驗與 Cell 映射,底層資料表結構則被封裝在私有黑箱中。這種重度封裝雖然保護了資料一致性,但代價是極高的學習曲線、沉重的記憶體開銷,以及無法以標準 SQL/ORM 直接表達清晰的業務聚合。
Mini-PLM 回歸領域驅動設計(DDD)的本質,以 Spring Data JPA 建立乾淨、透明的 POJO 實體模型,將整個 PLM 領域劃分為四大核心聚合:

第一個關鍵決策:Item 與 Revision 拆成兩個實體,而且幾乎所有業務資料都放在 Revision 上。看實際的資料表就懂:
mp_item(料號主檔——只有身分,沒有內容)
item_id bigint PK
item_number varchar(100) UNIQUE ← 料號,全系統唯一
item_name varchar(200)
config_item_type_id bigint FK ← 品項類型(Day 4 的 metadata 入口)
latest_revision_id bigint FK ← 指向「目前版」的捷徑
mp_item_revision(版本——內容的真正載體)
ir_id bigint PK
item_id bigint FK
revision_number varchar(20) ← A、B、C…
revision_status varchar(30) DEFAULT 'DRAFT'
lifecycle_phase varchar(255) DEFAULT 'PRELIMINARY'
release_date timestamp
base_revision_id bigint FK(自參照) ← 這一版從哪一版長出來
is_latest boolean
為什麼不把版本當 Item 的欄位?因為版本下的資訊必須不可變(immutable):B 版發行後,A 版的描述、規格、BOM 都要原封不動可調閱。把所有資訊全部推到 Revision,Item 只留身分。這樣「調閱三年前那一版」就只是換一個 ir_id 查詢,不需要任何歷史快照機制。
base_revision_id 的自參照 FK 也有作用:它記錄版本的族譜。B 版從 A 版長出來、C 版從 B 版長出來,改版歷程靠結構本身,不靠 log。
mp_bom_item
parent_revision_id FK → mp_item_revision ← 父件的「某一版」
child_item_id FK → mp_item ← 子件料號
child_revision_id FK → mp_item_revision ← 子件版本
BOM 的父端掛 Revision 而不是 Item,這是整個模型裡最重要的一條線。機器 A 版的 BOM 和 B 版的 BOM 是兩份獨立結構,改版時把 BOM 複製到新版再修改(Day 14 會講這個複製的效能問題),舊版結構凍結。「三年前出貨那台機器裡面裝了什麼」,答案就躺在舊 Revision 的 BOM 裡,一條 JOIN 查出來。
Form(變更表單)透過 FormItemLink 與品項掛勾,這張中介表是「表單驅動改版」的樞紐:
mp_form_item_link
form_id FK → mp_form
item_id FK → mp_item
original_revision_id FK ← 變更前的版本(diff 的基準)
new_revision_id FK ← 變更產生的新版本
pending_data text ← 簽核中暫存的欄位修改(還沒生效)
target_lifecycle_phase ← 放行後要把生命週期推到哪個階段
設計重點在 pending_data:簽核過程中的修改不直接寫進 Revision,先暫存在連結上;等表單放行,才把 pending 內容落到 new_revision。「簽核中」與「已生效」是兩個分開的世界,簽到一半被駁回,正式資料一個字都沒被動過。original_revision_id 與 new_revision_id 同時存在,則是 Day 15 Redline 差異比對的基礎,diff 的兩端天生就在這張表裡。

版本狀態(RevisionStatusEnum,節錄自程式碼)就三個值:
public enum RevisionStatusEnum {
DRAFT, // 草稿
RELEASED, // 已發佈
OBSOLETE // 已作廢
}
轉換規則同樣簡單:Draft → Released → Obsolete,單向、不可逆。所有表面上的回退(駁回、撤案)都發生在 Form 的狀態機裡(Day 9),Revision 自己的狀態機保持極簡。複雜度放在流程層,資料層保持不可變,整個模型的中心思想就這一句。
早期曾把「目前版本號」與狀態直接放在 Item 上。踩到的問題:改版歷程無法追溯,狀態一蓋掉,「上一版是什麼時候發行的」就沒了;兩張表單同時要改同一顆料,也無從表達各自基於哪一版。移到 Revision 之後這些問題自然消失。
聚合邊界劃錯,後面每個功能都會因為錯誤的設計而增加複雜度。判斷邊界是問自己:這個東西需要歷史嗎?需要歷史的,就該獨立成一個實體。
主檔薄版本厚、BOM 掛版本、表單經 pending 中介、狀態機極簡。四個決策共用同一個中心思想:把「會變的」和「不可變的」分開放。明天進入本系統最有企業味的主題,Day 4:Metadata-driven,不改程式就能加欄位。