iT邦幫忙

2026 iThome 鐵人賽

DAY 5
0
Vibe Coding

夢幻甜品師闖工程世界:Vibe Coding vs 專業開發的 0→1 冒險攻略系列 第 5

Day 5|那串摩斯密碼,先從方框開始解:實體與關聯基數,決定你的產品裝得下什麼

  • 分享至 

  • xImage
  •  

昨天講完資料模型設計為什麼重要,也講完修改的風險怎麼分級。今天,終於輪到那些一開始讓我頭皮發麻的摩斯密碼了。

剛打開 BuJo 資料庫的時候,我完全看不懂那些方框跟線在畫什麼。方框裡的縮寫、線的兩端、還有那些長得幾乎一模一樣的小鑰匙圖示,看起來就是一團工程世界的亂碼。

但後來我發現,那張圖其實一點都不隨便——它有一套固定的畫法,只是從來沒有人告訴過我。

工具可以把圖生成給你,卻不會順便解釋圖上每個記號代表什麼。

所以今天,我想從最前面開始:

先搞懂這張圖到底在畫什麼。


原來那些方框,是在把資料拆開再連起來

關聯式資料庫(Relational Database),簡單說,就是把資料拆成一張一張的 資料表(Table),再透過彼此之間的 關聯(Relationship) 把它們連起來,而不是把所有資訊全部塞在同一個地方。

資料庫會把不同類型的資料分開存放。例如,使用者、活動和通知,會分別放在不同的 資料表(Table) 中。

每張資料表都會定義自己需要記錄的欄位(Column),而每個欄位還會設定資料型別(Data Type),用來限制它可以存放哪一種資料。

不同資料表之間,則會透過像 外鍵(Foreign Key) 這類機制建立彼此的對應關係。

如果把這些結構畫成圖,就會像下面這樣:

https://ithelp.ithome.com.tw/upload/images/20260828/20183484St2zuc0Iu5.png

先不用急著把圖上的每個符號都看懂,現在只要知道怎麼讀這張圖就好:

  • 方框代表資料表(Table)
  • 方框內每一行代表一個欄位(Column),例如 iduser_idis_read
  • 欄位左側的 uuidtextdatetime資料型別(Data Type)
  • 方框之間的連線代表資料表之間的關聯(Relationship)

至於圖上另外標出的 ①~④,才是接下來兩天要拆開介紹的四個核心概念:

是什麼 白話說
① 實體 Entity 這個系統要記住哪些「東西」 我的產品世界裡有哪些角色
② 約束 Constraint 資料庫自己負責把關的規則 什麼事絕對不准發生
③ 關聯基數 Cardinality 一邊的一筆,可以對應到另一邊多少筆 這些角色之間是幾對幾
④ 索引 Index 讓查詢跑得更快的機制 找東西要不要翻遍整本書

圖看得懂了,換成 SQL 又長什麼樣子

上面那張圖是視覺化的結果。

而如果把同一份結構換成資料庫真正會執行的 SQL(Structured Query Language),可以長這樣:

CREATE TABLE notifications /* ① */ (
  id UUID PRIMARY KEY /* ② */
    DEFAULT gen_random_uuid() /* ③ */,

  user_id UUID /* ④ */
    NOT NULL /* ⑤ */,

  is_read BOOLEAN /* ④ */
    NOT NULL /* ⑤ */
    DEFAULT false,

  FOREIGN KEY (user_id)
    REFERENCES users(id) /* ⑥ */
    ON DELETE CASCADE /* ⑦ */
);

CREATE INDEX /* ⑧ */ idx_notifications_user_read
ON notifications (user_id, is_read);
位置 對應語法 對應概念
notifications 資料表(Table)——用來承載 Notification 這個 實體(Entity) 的資料
PRIMARY KEY 主鍵(Primary Key, PK)
DEFAULT gen_random_uuid() 預設值(Default Value)
UUIDBOOLEAN 資料型別(Data Type)
NOT NULL 非空約束(NOT NULL Constraint)
FOREIGN KEY … REFERENCES users(id) 外鍵(Foreign Key, FK),建立資料表之間的關聯(Relationship)
ON DELETE CASCADE 參照動作(Referential Action)
CREATE INDEX 索引(Index)

白話翻譯

我要建立一張 notifications 表。

每筆通知都有自己的主鍵 id,沒給值時就自動產生 UUID。

user_id 一定要指向真的存在的使用者;使用者被刪掉時,他的通知也一起刪掉。

is_read 不可以留空,預設是 false

最後替 user_id + is_read 建一個索引,讓「查某個人的未讀通知」這類查詢更快。

這張表就是這兩天的詞彙表。

原本我以為,把這張圖上的符號一次解完應該不難。

但真的開始為了這篇文章查資料、重新把每個概念拆開來看之後,我才發現,資料庫裡每一個看起來很小的符號,背後其實都有很多值得繼續深入研究的東西。

全部塞在同一天,反而很容易每個都只講到一點點。

所以這一關我決定拆成兩篇。

今天先講實體(Entity)和關聯基數(Cardinality);明天再繼續拆約束(Constraint)和索引(Index)。


實體(Entity):這個產品世界裡有哪些角色

實體(Entity),可以先理解成「系統裡需要被獨立記錄、有自己身分的一個對象」。

那怎麼知道有哪些實體?

實務上有一個很直覺的起手式,可以從 名詞短語分析(Noun Phrase Analysis) 開始——把產品的需求描述念一遍,把裡面的名詞全部圈出來,那些名詞就可以先列為實體的候選人。

使用者可以建立活動,活動可以有多個候選時段,其他參加者對候選時段回報空閒時間,系統會發送通知。」

圈出來之後,再對每一個問幾個問題:

這個東西會不會重複出現很多次?它自己有沒有專屬的屬性?它需不需要被獨立識別或被其他資料參照?

如果答案大多是「有」,它就很值得被列入候選實體(Candidate Entity),再進一步判斷是不是需要獨立建模。

反過來說,如果你在某張表上看到 xxx1xxx2xxx3 這種重複編號的欄位,通常就是一個值得警覺的 Schema Smell

這種寫法有個正式名稱,叫 重複群組(Repeating Group),它違反的是資料庫設計裡的 第一正規化(First Normal Form, 1NF) ——同一類資料不應該靠編號欄位橫向長出去,而應該直向存成一筆一筆的資料列。

換句話說,這組資料多半更適合拆成另一張表。

BuJo 的 UserActivityNotification 都是獨立的實體,因為它們各自都有很多筆、也各自有專屬的屬性。

User 為例:

CREATE TABLE users (
  id           UUID PRIMARY KEY,
  display_name TEXT NOT NULL,
  avatar_url   TEXT,
  created_at   TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

在資料模型裡,我們把「使用者」視為一個 實體(Entity);到了關聯式資料庫裡,則用 users 這張 資料表(Table) 來保存它的資料。

iddisplay_nameavatar_urlcreated_at 則是資料表裡的 欄位(Column),對應到這個實體需要保存的屬性。


關聯基數:這些角色之間到底是幾對幾

關聯基數(Cardinality),描述的是兩個實體之間,一邊的一筆資料可以對應到另一邊多少筆資料。

這篇先從最常見的三種關係型態開始看:

  • 一對多(1:N)
  • 一對一(1:1)
  • 多對多(M:N)

一對多(1:N)

最常見的一種。

一個活動可以開很多個候選時段,但每一個候選時段只屬於一個活動。

典型的做法,是把外鍵放在「多」的那一邊:

CREATE TABLE candidate_slots (
  id          UUID PRIMARY KEY,
  activity_id UUID NOT NULL,

  FOREIGN KEY (activity_id)
    REFERENCES activities(id)
);

每一筆候選時段只會記一個 activity_id,所以它只屬於一個活動;但同一個 activity_id 可以出現在很多筆候選時段裡。

因此形成:

Activity 1 → N CandidateSlots

也就是在 1:N 的關係裡,外鍵通常會放在 N 的那一端,讓每一筆資料記住「我屬於誰」。

一對一(1:1)

1:1 常見的一種實作方式,是在外鍵上再加一條 唯一性約束(Unique Constraint),讓同一個對象最多只能被對應一次。

例如活動和排程設定:

CREATE TABLE activity_schedules (
  activity_id UUID NOT NULL UNIQUE,

  FOREIGN KEY (activity_id)
    REFERENCES activities(id)
);

activity_id 一方面記著「這份排程設定屬於哪一個活動」,另一方面又有 UNIQUE,所以同一個活動最多只能在這張表裡出現一次。

也就是:

一個活動最多只會對應到一份排程設定。

這裡要注意的是,這個結構保證的是「最多一份」,但它沒有保證「一定有一份」。

這件事在 ER 模型裡也有自己的名字,叫 參與度(Participation),講的是最小基數——這一邊的資料,是「一定要」有對應的對象,還是「可以沒有」也沒關係。

UNIQUE 管的是最多幾筆,參與度管的是最少幾筆。

至於每個活動是不是一定都要有一份排程設定,就要看產品本身的 業務規則(Business Rule) 怎麼定了。

多對多(M:N)

一個使用者可以參加很多活動,一個活動也可以有很多參加者——兩邊都是「多」

在關聯式資料庫裡,這種關係通常會用一張 中介表(Join Table) 來記錄兩邊的配對。

BuJo 的做法可以簡化成:

CREATE TABLE activity_participants (
  activity_id UUID NOT NULL,
  user_id     UUID NOT NULL,
  status      TEXT NOT NULL,

  FOREIGN KEY (activity_id)
    REFERENCES activities(id),

  FOREIGN KEY (user_id)
    REFERENCES users(id),

  UNIQUE (activity_id, user_id)
);

每一筆資料都代表:

「某個人參加了某個活動。」

activity_id 指向活動,user_id 指向使用者。

而像 status 這種欄位,描述的不是使用者、也不是活動,而是這一段參加關係本身,所以也最適合放在中介表裡。

最後:

UNIQUE (activity_id, user_id)

則限制同一個人和同一個活動的組合,只能有一筆參加紀錄。


三種基數放回 BuJo,一張表就看懂

基數 BuJo 的例子 常見做法
1:N 活動 → 候選時段 在候選時段表放 activity_id 外鍵
1:1 活動 ↔ 排程設定 外鍵再加 UNIQUE
M:N 使用者 ↔ 活動 用中介表 activity_participants 承接

還有一件事值得記著:

昨天那張「一旦錯了會很貴」的清單裡,關聯基數就在上面

從 1:N 改成 M:N,通常代表要新建一張中介表、修改原本的關聯方式,還可能要搬資料。

反過來,如果原本一筆資料可以對應很多個對象,後來卻改成只能留一個,就得重新決定:

原本那些多出來的關係,到底該怎麼處理?

所以基數不是隨手決定的東西。

它是那種:

現在多想十分鐘,之後省下三天。


方框和線解開了,下一關是那些小字

到這裡,這張圖上的方框跟線,我大概都能講出它們在說什麼了——有哪些角色、彼此是幾對幾、哪些東西該獨立成一張表。

但一開始讓我瞇著眼睛看半天的那些 PKFKNOT NULLUNIQUE,還沒有真正拆開。

而且我後來發現,還有很多重要規則不是只看方框和線就能知道:

這個欄位可不可以空著?

同一組資料能不能重複?

活動被刪掉的時候,底下的資料要一起消失嗎?

為什麼有些查詢會突然變慢?

前面那張詞彙表裡的 ②PRIMARY KEY、⑤NOT NULL、⑦ON DELETE CASCADE、⑧CREATE INDEX,也都還在等著解鎖。

明天,就來拆開 約束(Constraint)和索引(Index)

一個負責守住資料規則,一個負責讓資料更快被找到。


上一篇
Day 4|工具備好了,那食材呢?資料模型設計,決定這道菜以後改不改得動
下一篇
Day 6|食譜寫得再美,也擋不住有人把鹽當成糖:約束是資料庫自己的把關機制,索引負責找得快
系列文
夢幻甜品師闖工程世界:Vibe Coding vs 專業開發的 0→1 冒險攻略9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言