前面幾篇,我們已經完成:
Machine
↓
Process
↓
Route
而且系統的複雜度也慢慢增加。
Machine 的時候,我們主要處理:
CRUD、Validation、Disable。
到了 Process,開始出現:
資料之間的關聯。
到了 Route,又多了:
Process + SEQ,以及流程控制。
今天我們稍微離開生產流程,來建立另外一個之後工單會使用到的 Master Data:
Customer(客戶)。
乍看之下 Customer 好像又回到了最簡單的 CRUD。
但如果一個客戶可以有多個聯絡人,事情就會開始變得不太一樣。
如果今天只需要記錄客戶基本資料,Table 其實很單純。
例如:
CUSTOMER
-------------------------
CUSTOMER_UUID
COMPANY_CODE
COMPANY_NAME
ADDRESS
ENABLE
CREATE_TIME
UPDATE_TIME
建立一個客戶:
COMPANY_CODE : C001
COMPANY_NAME : 客戶 A
ADDRESS : 桃園市...
ENABLE : True
API 也跟前面的 Machine 非常像:
POST /customers
GET /customers
GET /customers/{customer_uuid}
PUT /customers/{customer_uuid}
如果文章寫到這裡就結束,那其實沒什麼好講的。
因為本質上就是:
收到資料
↓
Validation
↓
INSERT CUSTOMER
先前的 Machine 已經做過一次了。
所以今天真正想看的不是 Customer 本身。
而是:
Customer 底下的 Contact。
假設今天建立:
客戶 A
實際跟這間公司合作時,可能不會永遠只找同一個人。
例如:
客戶 A
│
├── 王先生
│ └── 生產相關窗口
│
├── 陳小姐
│ └── 品質相關窗口
│
└── 林先生
└── 出貨相關窗口
這時候問題就來了。
我們要怎麼把這三個聯絡人存進 Customer?
最直覺的方法可能是:
CUSTOMER
-------------------------
CUSTOMER_UUID
COMPANY_CODE
COMPANY_NAME
CONTACT_NAME_1
CONTACT_MOBILE_1
CONTACT_NAME_2
CONTACT_MOBILE_2
CONTACT_NAME_3
CONTACT_MOBILE_3
如果一個客戶最多三個聯絡人,好像可以。
但是很快就會遇到一個問題:
如果某個客戶有第四個聯絡人呢?
那就繼續增加:
CONTACT_NAME_4
CONTACT_MOBILE_4
以後如果有人有十個:
CONTACT_NAME_10
CONTACT_MOBILE_10
顯然會越來越難維護。
所以這裡跟前面的 Route 有點像。
當資料的數量不固定時,不應該一直增加 Column。
我們可以把它拆成另外一張 Table。
這時候資料結構可以變成:
CUSTOMER
│
├── CONTACT
├── CONTACT
└── CONTACT
例如:
CUSTOMER
-------------------------
CUSTOMER_UUID
COMPANY_CODE
COMPANY_NAME
ADDRESS
ENABLE
另外建立:
CUSTOMER_CONTACT
-------------------------
CONTACT_UUID
CUSTOMER_UUID
CONTACT_NAME
MOBILE
EMAIL
SEQ
資料可能長成:
CUSTOMER
C001 客戶 A
然後:
CUSTOMER_CONTACT
CUSTOMER CONTACT MOBILE
--------------------------------
C001 王先生 09xxxxxxxx
C001 陳小姐 09xxxxxxxx
C001 林先生 09xxxxxxxx
這樣不管一個客戶有多少個聯絡人 Customer Table 本身都不用改。
現在 Customer 與 Contact 之間的關係是:
Customer 1
│
├── Contact A
├── Contact B
└── Contact C
也就是:
一個 Customer,可以擁有多個 Contact。
這種資料關係通常稱為:
One-to-Many(一對多)。
前一篇 Process 與 Machine,我們碰到的是資料之間的 Relation。
今天則開始碰到另外一種很常見的資料結構:
Header
│
└── Detail
在這個例子裡:
Customer = Header
Contact = Detail
如果只有 Customer,Request 很簡單:
{
"company_code": "C001",
"company_name": "客戶 A",
"address": "桃園市..."
}
現在多了 Contact 之後,可能變成:
{
"company_code": "C001",
"company_name": "客戶 A",
"address": "桃園市...",
"contacts": [
{
"contact_name": "王先生",
"mobile": "09xxxxxxxx",
"email": "user1@example.com"
},
{
"contact_name": "陳小姐",
"mobile": "09xxxxxxxx",
"email": "user2@example.com"
}
]
}
這種結構開始跟之前不太一樣。
Request 裡面除了 Customer 本身,還包了一組:
contacts[]
也就是:
Nested Data。
建立 Customer 的流程可能是:
收到 Request
↓
建立 Customer
↓
取得 CUSTOMER_UUID
↓
建立 Contact 1
↓
建立 Contact 2
↓
Commit
概念上的程式可能像:
customer = Customer(
company_code=data["company_code"],
company_name=data["company_name"],
address=data["address"]
)
db.add(customer)
db.flush()
for contact in data["contacts"]:
customer_contact = CustomerContact(
customer_uuid=customer.customer_uuid,
contact_name=contact["contact_name"],
mobile=contact["mobile"],
email=contact["email"]
)
db.add(customer_contact)
db.commit()
跟上一篇建立 Route 有一點像。
先建立:
Customer
再利用 Customer UUID 建立:
Customer Contact
而且最好放在同一個 Transaction。
如果其中一筆 Contact 建立失敗:
Customer → 成功
Contact 1 → 成功
Contact 2 → 失敗
我們通常不希望 Database 最後留下:
Customer
└── Contact 1
這種只完成一半的狀態。
所以失敗時應該:
ROLLBACK
讓整次操作一起失敗。
Create 其實還滿直覺的。
真正比較有意思的是:
修改 Customer。
假設資料庫原本是:
客戶 A
│
├── 王先生
└── 陳小姐
使用者打開編輯頁面之後:
保留王先生。
刪除陳小姐。
新增林先生。
修改後變成:
客戶 A
│
├── 王先生
└── 林先生
從使用者的角度來看:
改完,按「儲存」。
就結束了。
但從後端的角度來看,我們收到新資料之後,其實要知道三件事情。
王先生 → 原本就存在
陳小姐 → 原本存在,現在不見了
林先生 → 原本不存在
也就是:
王先生 → KEEP / UPDATE
陳小姐 → DELETE / DISABLE
林先生 → INSERT
這時候 Update 就不再只是:
UPDATE CUSTOMER
而是綜合以下功能:
更新 Customer
比較 Contact 新舊資料
新增 Contact
修改 Contact
刪除 / 停用 Contact
這時候 CONTACT_UUID 就開始變得重要。
假設前端送回:
{
"contacts": [
{
"contact_uuid": "UUID-001",
"contact_name": "王先生"
},
{
"contact_uuid": null,
"contact_name": "林先生"
}
]
}
Database 原本有:
UUID-001 → 王先生
UUID-002 → 陳小姐
我們就可以比較。
前端傳回:
UUID-001
Database 也有:
UUID-001
所以:
UUID-001 → 保留 / 更新
前端沒有傳回:
UUID-002
代表:
UUID-002 → 被移除
而:
林先生
沒有 UUID。
代表:
林先生 → 新增
最後就得到:
原本
----------------
UUID-001 王先生
UUID-002 陳小姐
修改後
----------------
UUID-001 王先生
NEW 林先生
這就是處理 Header / Detail Update 時很常遇到的情況。
看到這裡可能會想:
有必要這麼麻煩嗎?
其實還有一個非常簡單的方法。
每次 Update:
DELETE 原本所有 Contact
↓
INSERT 前端傳來的所有 Contact
例如:
原本:
王先生
陳小姐
↓ DELETE ALL
修改後重新 INSERT:
王先生
林先生
程式確實會簡單很多。
而且在某些單純的設定資料裡,這種做法也不是完全不能用。
但要注意一件事情:
原本資料的 Identity 會消失。
例如王先生明明沒有變:
原本:
UUID-001 → 王先生
重新建立後:
UUID-999 → 王先生
對使用者來說還是同一個王先生。
對 Database 來說:
已經是完全不同的一筆資料。
如果未來有其他資料引用:
CONTACT_UUID = UUID-001
這時候問題就出現了。
所以到底要:
全部刪掉重建
還是:
比較差異後進行 Insert / Update / Delete
沒有永遠唯一的答案。
要看這筆 Detail 未來會不會被其他資料引用,以及我們需不需要保留它的 Identity 與歷史。
Customer 做到這裡,其實可以看到一個很常見的開發問題。
建立資料的時候很單純:
新增 Customer
↓
新增 Contact
有幾個聯絡人,就新增幾筆資料。
但到了修改,就沒有這麼單純了。
假設原本是:
客戶 A
│
├── 王先生
└── 陳小姐
使用者修改之後變成:
客戶 A
│
├── 王先生
└── 林先生
從使用者的角度來看,只是:
修改資料 → 按下儲存
但從系統的角度來看,其實要做的是:
王先生 → 保留 / 更新
陳小姐 → 刪除 / 停用
林先生 → 新增
所以「更新」不一定只是執行一次:
UPDATE
而是要先比較:
原本的資料
↓
使用者修改後的資料
↓
找出差異
最後再決定哪些資料需要:
INSERT
UPDATE
DELETE / DISABLE
這也是為什麼我們會替每一筆 Contact 保留自己的 UUID。
因為系統需要知道:
這筆資料是原本就存在的,還是這次才新增的?
看起來只是很普通的「客戶資料修改」,背後其實已經不只是單純修改幾個欄位。
隨著系統裡的資料越來越複雜,這種:
比較修改前後的差異,再決定資料要新增、修改還是移除。
也會是開發 MES 時經常遇到的問題。
接下來我們要開始建立另一個非常重要的 Master Data:
Product(產品)。
因為到了後面的 Work Order,我們至少需要回答:
這張工單是替哪個客戶,生產哪一個產品?
而 Product 做完之後,我們距離真正建立 Work Order,也就越來越近了。
下一篇:
「產品是由基礎資料組合而成的。」