上一篇,我們從 MES 裡相對單純的「機台」開始。
建立 Machine Table、完成 CRUD,再加上一些基本的 Validation。
到這裡為止,我們的資料其實還很單純:
Machine
每一台機台都是獨立存在的資料。
但從今天開始,事情要稍微變複雜一點。
因為我們準備建立第二個重要的基礎資料:
製程(Process)。
而這一次,我們不只要建立 Process。
我們還要開始回答一個問題:
「這個製程,可以使用哪些機台?」
資料與資料之間,第一次產生了關聯。
先不考慮 Machine。
單純看 Process,其實跟上一篇的 Machine 很像。
例如我們可能有:
PROCESS_UUID
PROCESS_CODE
PROCESS_NAME
ENABLE
CREATE_TIME
UPDATE_TIME
建立幾筆資料:
P001 組裝
P002 測試
P003 檢驗
API 也差不多:
POST /processes
GET /processes
GET /processes/{process_uuid}
PUT /processes/{process_uuid}
做到這裡,其實還是在上一篇提到的:
CRUD + Validation
例如:
if process_code_exists(data["process_code"]):
raise Exception("Process code already exists.")
如果今天 Process 只是這樣,那 Day 05 好像也只是把 Machine 換成 Process 而已。
真正有趣的是下一個需求。
假設工廠現在有三台機台:
MC-001 組裝機 01
MC-002 組裝機 02
MC-003 測試機 01
而我們有兩個製程:
P001 組裝
P002 測試
現場告訴我們:
組裝
├── MC-001
└── MC-002
測試
└── MC-003
也就是說:
P001 可以使用 MC-001、MC-002。
P002 可以使用 MC-003。
這時候 Process 和 Machine 就不再是兩張完全沒有關係的 Table。
我們需要把它們串起來。
第一次遇到這種需求,很容易想到:
那我在 PROCESS Table 加一個 MACHINE_UUID 不就好了?
變成:
PROCESS
-----------------------
PROCESS_UUID
PROCESS_CODE
PROCESS_NAME
MACHINE_UUID
ENABLE
這樣確實可以表示:
P001 → MC-001
問題是:
如果 P001 可以使用兩台機台呢?
P001
├── MC-001
└── MC-002
一個 MACHINE_UUID 就不夠用了。
那要不要改成:
MACHINE_UUID_1
MACHINE_UUID_2
MACHINE_UUID_3
技術上當然可以做。
但是馬上又會出現問題:
如果未來一個製程可以使用 10 台機台呢?
那我們是不是要一路加:
MACHINE_UUID_4
MACHINE_UUID_5
MACHINE_UUID_6
...
這顯然不是一個很容易維護的設計。
另外一個很直覺的做法可能是:
MACHINE_UUID =
"uuid-001,uuid-002,uuid-003"
看起來很方便。
一個欄位就可以放很多台 Machine。
但是未來如果我要查:
MC-001 現在被哪些製程使用?
事情就開始變麻煩。
而且資料庫原本可以幫我們處理的關聯、約束、JOIN,也會變得比較難使用。
所以面對這種需求,我們通常不會把多個 ID 塞進同一個欄位。
而是建立第三張 Table。
我們可以建立:
PROCESS
MACHINE
PROCESS_MACHINE
其中 PROCESS_MACHINE 專門負責記錄:
哪一個 Process 可以使用哪一台 Machine?
概念上可以長這樣:
PROCESS_MACHINE
--------------------------------
PROCESS_UUID
MACHINE_UUID
假設:
P001 → MC-001
P001 → MC-002
P002 → MC-003
資料就會變成:
PROCESS_UUID MACHINE_UUID
----------------------------
P001 MC-001
P001 MC-002
P002 MC-003
這時候關係就清楚很多。
因為我們現在真正要記錄的,不只是 Process,也不是 Machine。
而是:
Process 與 Machine 之間的「關係」。
換個方式看:
Process
│
│
├── Machine
├── Machine
└── Machine
一個 Process 可以對應多台 Machine。
而如果未來:
同一台 Machine 也可能被不同 Process 使用。
例如:
Process-A ──┐
├── Machine-001
Process-B ──┘
那這就會形成典型的:
Many-to-Many(多對多)
這時候中間的 Relation Table 就會變得非常重要。
上一篇建立 Machine 時,我們收到的資料可能很單純:
{
"machine_code": "MC-001",
"machine_name": "組裝機 01"
}
但是建立 Process 時,現在可能會收到:
{
"process_code": "P001",
"process_name": "組裝",
"machine_uuid_list": [
"machine-uuid-001",
"machine-uuid-002"
]
}
這時候後端就不再只是:
process = Process(**data)
db.add(process)
db.commit()
而是要開始處理兩件事情:
建立 Process
↓
建立 Process 與 Machine 的關係
概念上可能像:
process = Process(
process_code=data["process_code"],
process_name=data["process_name"]
)
db.add(process)
db.flush()
for machine_uuid in data["machine_uuid_list"]:
relation = ProcessMachine(
process_uuid=process.process_uuid,
machine_uuid=machine_uuid
)
db.add(relation)
db.commit()
到了這裡,我們第一次開始感受到:
功能看起來只是「新增製程」,Database 實際上可能寫了不只一張 Table。
上面的程式還多了一個上一篇沒有特別提到的東西:
db.flush()
為什麼不是直接 commit()?
因為我們建立 Process 之後,接下來馬上還需要它的 UUID 去建立 PROCESS_MACHINE。
但這整件事情,我們希望它仍然是:
同一次 Transaction。
概念上就是:
BEGIN
建立 PROCESS
取得 PROCESS_UUID
建立 PROCESS_MACHINE
建立 PROCESS_MACHINE
建立 PROCESS_MACHINE
COMMIT
如果中間任何一步失敗:
ROLLBACK
而不是 Process 建成功了,Machine Relation 卻只寫了一半。
這也是當資料開始產生關聯後,研發需要開始注意的另一件事情:
資料一致性。
現在 API 傳進來:
{
"machine_uuid_list": [
"machine-001",
"machine-999"
]
}
但是:machine-999
根本不存在。
那我們應該怎麼辦?
最簡單的想法就是直接寫入,讓 Foreign Key 幫我們擋。
但從系統設計的角度,我通常還是希望在 Service Layer 先確認:
machines = get_machines(data["machine_uuid_list"])
if len(machines) != len(set(data["machine_uuid_list"])):
raise Exception("Machine not found.")
甚至還要檢查:
if not machine.enable:
raise Exception("Machine is disabled.")
所以新增 Process 的流程開始變成:
收到資料
↓
檢查 Process Code
↓
檢查 Machine 是否存在
↓
檢查 Machine 是否可使用
↓
建立 Process
↓
建立 Relation
↓
Commit
這已經比上一篇的 Machine CRUD 多了一些東西。
這時候上一篇留下來的問題也開始有答案了。
假設:
MC-001
已經被:
P001
使用。
關係變成:
P001 → MC-001
現在使用者突然說:
我要刪除 MC-001。
如果我們真的把 Machine Delete 掉:PROCESS_MACHINE 怎麼辦?
這就是為什麼上一篇提到:
Master Data 很多時候比起 Delete,更適合使用 Disable。
例如:
MC-001
ENABLE = False
它仍然存在。
過去的關係與歷史資料也還在。
但是建立新的設定時,我們不再讓使用者選擇它。
到這裡,上一篇談的設計選擇就真的開始產生影響了。
假設 P001 原本可以使用:
MC-001
MC-002
現在使用者修改成:
MC-002
MC-003
那我們真正要更新的其實不只是:
UPDATE PROCESS
還包括:
原本:
P001 → MC-001
P001 → MC-002
修改後:
P001 → MC-002
P001 → MC-003
也就是 Relation 本身也需要同步。
這時候 Update 的概念就變成:
更新 Process
+
更新 Process-Machine Relation
所以我現在會把 CRUD 理解成:
CRUD 是操作的入口,不一定代表背後只會執行一個 INSERT、SELECT、UPDATE 或 DELETE。
真正決定複雜度的,通常是後面的 Business Rule 與資料關係。
上一篇 Machine 很單純:
Machine
今天加入 Process 之後:
Process
│
└── Process-Machine
│
└── Machine
我們第一次開始處理:
Foreign Key
Relation Table
Many-to-Many
Validation
Transaction
資料一致性
而這些概念,接下來會一直出現在 MES 裡。
因為 MES 本身就是一個充滿關係的系統。
產品跟途程有關。
途程跟製程有關。
工單跟產品、客戶、途程有關。
Lot 又跟工單有關。
當系統越往後開發,這張關係網只會越來越大。
現在我們已經有:
Machine
↓
Process
但還是不夠。
因為目前系統只知道:
「工廠有哪些製程。」
它還不知道:
「產品到底要先做哪一道,再做哪一道?」
例如:
組裝
測試
檢驗
到底是:
組裝 → 測試 → 檢驗
還是:
測試 → 組裝 → 檢驗
這就是下一個功能要解決的問題。
下一篇:
「第一次把多個製程串成一條生產流程。」
而這一次除了 Relation 以外,我們還會多出一個新的問題:
順序(Sequence)。