iT邦幫忙

2026 iThome 鐵人賽

DAY 3
0
Software Development

30天打造一套企業PLM系列 第 3

Day 3:PLM 領域模型怎麼設計

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20260820/201612900y77kgTMHL.jpg

系列:30 天打造企業級 PLM|面向:設計|素材:領域規格設計、實際資料庫 schema

問題場景

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

https://ithelp.ithome.com.tw/upload/images/20260820/20161290xroASwoOeK.jpg

商業邏輯設計

  • 料號是跨部門的共同語言。研發看規格、採購看供應商、生管看用量、品保看檢驗標準,同一顆料四種視角,這決定了「料號本體要薄、擴充欄位要活」(Day 4 的伏筆)
  • 改版是商業行為。成本重算、供應鏈通知、庫存處置都掛在版本切換上,所以版本切換必須有人簽字,這就是 Form 存在的理由
  • 變更管理的價值在稽核:把「誰決定改、誰知道改了」變成可追溯的紀錄

核心內容:四大聚合與一張圖

架構演進:從 EJB Node-based 模型到乾淨的 JPA 領域聚合

在以前 Oracle Agile PLM 的時代,領域物件的操作是建立在龐大的 Node-based API 與 EJB Entity Bean(或私有物件代理層)之上。

如果寫過 Agile SDK,你一定熟悉 IItemIChangeITableIRow 這一套抽象。在 EJB 架構下,存取一個料號或 BOM 列,後端得透過多層 EJB 攔截器、權限檢驗與 Cell 映射,底層資料表結構則被封裝在私有黑箱中。這種重度封裝雖然保護了資料一致性,但代價是極高的學習曲線、沉重的記憶體開銷,以及無法以標準 SQL/ORM 直接表達清晰的業務聚合。

Mini-PLM 回歸領域驅動設計(DDD)的本質,以 Spring Data JPA 建立乾淨、透明的 POJO 實體模型,將整個 PLM 領域劃分為四大核心聚合:

https://ithelp.ithome.com.tw/upload/images/20260820/20161290vdd59SFKmg.jpg

1. Item / ItemRevision:主檔薄、版本厚

第一個關鍵決策: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。

2. BOM:掛在版本上,不是掛在料號上

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 查出來。

3. Form:變更的載體,也是簽核的載體

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_idnew_revision_id 同時存在,則是 Day 15 Redline 差異比對的基礎,diff 的兩端天生就在這張表裡。

https://ithelp.ithome.com.tw/upload/images/20260820/20161290HhRksn5fv6.jpg

4. 版本狀態機

版本狀態(RevisionStatusEnum,節錄自程式碼)就三個值:

public enum RevisionStatusEnum {
    DRAFT,    // 草稿
    RELEASED, // 已發佈
    OBSOLETE  // 已作廢
}

轉換規則同樣簡單:Draft → Released → Obsolete,單向、不可逆。所有表面上的回退(駁回、撤案)都發生在 Form 的狀態機裡(Day 9),Revision 自己的狀態機保持極簡。複雜度放在流程層,資料層保持不可變,整個模型的中心思想就這一句。

踩坑記錄:邊界劃錯的代價

早期曾把「目前版本號」與狀態直接放在 Item 上。踩到的問題:改版歷程無法追溯,狀態一蓋掉,「上一版是什麼時候發行的」就沒了;兩張表單同時要改同一顆料,也無從表達各自基於哪一版。移到 Revision 之後這些問題自然消失。

聚合邊界劃錯,後面每個功能都會因為錯誤的設計而增加複雜度。判斷邊界是問自己:這個東西需要歷史嗎?需要歷史的,就該獨立成一個實體。

小結

主檔薄版本厚、BOM 掛版本、表單經 pending 中介、狀態機極簡。四個決策共用同一個中心思想:把「會變的」和「不可變的」分開放。明天進入本系統最有企業味的主題,Day 4:Metadata-driven,不改程式就能加欄位。


上一篇
Day 2:Monorepo 與建置管線——前後端解耦開發、靜態資源打包,如何用一個 repo 管好兩個世界?
下一篇
Day 4:Metadata-driven—不改程式就能加欄位
系列文
30天打造一套企業PLM4
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言