Raspberry Pi 4 的後端修鍊 (3) - foreign key

  1. Raspberry Pi 4 的後端修鍊 (3) - foreign key
    1. 增加一個有 foreign key 的資料表[1]
    2. 「讀者」資料表
      1. 經過實測
    3. ON DELECT 和 ON UPDATE 的四種模式[2][3]
      1. 經過實測
    4. 「出版社」資料表
    5. 「作者」資料表
    6. 「書籍」資料表
    7. 「實體書」資料表
    8. 「書評」資料表
    9. 「借書單」資料表
    10. 建立 migration

Raspberry Pi 4 的後端修鍊 (3) - foreign key

增加一個有 foreign key 的資料表[1]

關聯式資料庫,的「關聯」之處,在於 foreign key 的關係。
有些開發者不喜歡 foreign key ,直接鍵一個欄位記錄別的 TABLE 的 key 值,但只有他自己知道,其它的人不知道,看資料庫也看不出來。

我自己比較喜歡用 foreign key 所以在此特別介紹一下如何用 foreign key 。

圖書館資料庫為例。

除了 user 之外的表,照理來說都會有 foreign key

「讀者」資料表

使用者與讀者之間的關係是「一對一」

CREATE TABLE reader (
id CHAR(10) NOT NULL,
password VARCHAR(128),
slack_id VARCHAR(50),
line_id VARCHAR(50),
phone CHAR(10),
email VARCHAR(50),
PRIMARY KEY (id),
created_at DATETIME,
updated_at DATETIME,
creator_id INT NOT NULL,
FOREIGN KEY (creator_id) REFERENCES user (id)
) ENGINE = InnoDB;
  • id 想要記錄讀者的身份證字號,可以用身份證就可以借書了。
  • password 和 user 一樣,
  • creator_idFOREIGN KEY 參考 user.id
DESCRIBE reader;
+------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------+--------------+------+-----+---------+----------------+
| id | char(10) | NO | PRI | <null> | auto_increment |
| password | varchar(128) | YES | | <null> | |
| slack_id | varchar(50) | YES | | <null> | |
| line_id | varchar(50) | YES | | <null> | |
| phone | char(10) | YES | | <null> | |
| email | varchar(50) | YES | | <null> | |
| creator_id | int(11) | NO | MUL | <null> | |
+------------+--------------+------+-----+---------+----------------+

8 rows in set
Time: 0.046s

透過最簡單的語法 FOREIGN KEY (creator_id) REFERENCES user (id) 定義了一個 FOREIGN KEY。

在此有兩個預設值需要注意

ON DELETE
ON UPDATE

這兩個的預設值各別是如何?

經過實測

先刪除 reader 沒問題
但如先刪除 user,會出現錯誤訊息

Query 1 ERROR: 
Cannot delete or update a parent row:
a foreign key constraint fails (
`good_ideas_lib`.`reader`,
CONSTRAINT `reader_ibfk_1` FOREIGN KEY (`creator_id`)
REFERENCES `user` (`id`)
)

修改 reader 的 creator_id 不會出現問題
但修改的 key 沒有存在的話,就會出錯誤訊息。

Query 1 ERROR: 
Cannot add or update a child row:
a foreign key constraint fails (
`good_ideas_lib`.`reader`,
CONSTRAINT `reader_ibfk_1` FOREIGN KEY (`creator_id`)
REFERENCES `user` (`id`)
)

修改 user 的 id,會出現錯誤訊息

Query 1 ERROR: 
Cannot delete or update a parent row:
a foreign key constraint fails (
`good_ideas_lib`.`reader`,
CONSTRAINT `reader_ibfk_1` FOREIGN KEY (`creator_id`)
REFERENCES `user` (`id`)
)

ON DELECT 和 ON UPDATE 的四種模式[2][3]

  • RESTRICT(約束):預設模式。當在父表刪除資料時,會比對子表是否有對應的資料,如果有則不允許刪除。
  • CASCADE(級聯):當在父表刪除資料時,會對比子表是否有對應的資料,如果有則會一起刪除。
  • SET NULL:當在父表刪除資料時,會對比子表是否有對應的資料,如果有則設置子表的外鍵欄位為 NULL,子表資料不會被刪除。(欄位需允許 NULL)
  • NO ACTION:與 RESTRICT 相同。

看來預設都是 RESTRICT

如果想要正確的更新與刪除,在設定 foreign key 的同時,需要好好的思考資料關聯間的基本關係。

以此為例,只需要思考「因為 user 刪除,reader 資料的建立者不能留空白,也不能無法參考,所以要限制無法刪除;如果 user 更新了 id ,那麼 reader 的 creator_id 也需要一起更新」

而且不用考慮刪除/更新 reader 的 creator_id 的情況,因為這種情況應該是自由的,想怎麼做都行,不受約束。

設定如下,再試一次

CREATE TABLE reader (
id CHAR(10) NOT NULL,
password VARCHAR(128),
slack_id VARCHAR(50),
line_id VARCHAR(50),
phone CHAR(10),
email VARCHAR(50),
PRIMARY KEY (id),
created_at DATETIME,
updated_at DATETIME,
creator_id INT NOT NULL,
FOREIGN KEY (creator_id) REFERENCES user (id)
ON DELETE RESTRICT
ON UPDATE CASCADE
) ENGINE = InnoDB;
DESC reader;
+------------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+------------+--------------+------+-----+---------+-------+
| id | char(10) | NO | PRI | <null> | |
| password | varchar(128) | YES | | <null> | |
| slack_id | varchar(50) | YES | | <null> | |
| line_id | varchar(50) | YES | | <null> | |
| phone | char(10) | YES | | <null> | |
| email | varchar(50) | YES | | <null> | |
| created_at | datetime | YES | | <null> | |
| updated_at | datetime | YES | | <null> | |
| creator_id | int(11) | NO | MUL | <null> | |
+------------+--------------+------+-----+---------+-------+

9 rows in set
Time: 0.039s

經過實測

確實修改 user 的 id 之後,reader 的 creator_id 會自動修改。

「出版社」資料表

CREATE TABLE publisher (
id INT NOT NULL AUTO_INCREMENT,
name VARCHAR(50) NOT NULL,
PRIMARY KEY (id),
created_at DATETIME,
updated_at DATETIME,
creator_id INT NOT NULL,
FOREIGN KEY (creator_id) REFERENCES user (id)
ON UPDATE CASCADE
) ENGINE = InnoDB;

這段 SQL,新增了一個 publisher 的資料表
此資料表有 5 個欄位

  1. id: 出版社的 id
    • 只要流水號,自動生成,必填(不可是 NULL),唯一碼 PRIMARY KEY
  2. name: 出版社名字,字串可變長度 50
    • 變動字元 (50),必填
  3. creator_id: 使用者 id
    • 使用者修改,建立者隨使用者變動而變動
    • 使用者刪除,不可在有出版社資料時,使用者被刪除。
  4. created_at: 新增日期時間
  5. updated_at: 更新日期時間

增加了兩個與資料表同名的檔案 publisher.frm, publisher.ibd

:/var/lib/mysql/good_ideas_lib# ls
db.opt publisher.frm publisher.ibd user.frm user.ibd

「作者」資料表

CREATE TABLE author (
id INT NOT NULL AUTO_INCREMENT,
name VARCHAR(50) NOT NULL,
PRIMARY KEY (id),
created_at DATETIME,
updated_at DATETIME,
creator_id INT NOT NULL,
FOREIGN KEY (creator_id) REFERENCES user (id)
ON UPDATE CASCADE
) ENGINE = InnoDB;

「書籍」資料表

CREATE TABLE book (
id INT AUTO_INCREMENT NOT NULL,
title VARCHAR(50),
isbn VARCHAR(13),
author_id INT NOT NULL,
FOREIGN KEY (author_id) REFERENCES author (id)
ON DELETE RESTRICT
ON UPDATE CASCADE,
publisher_id INT NOT NULL,
FOREIGN KEY (publisher_id) REFERENCES publisher (id)
ON DELETE RESTRICT
ON UPDATE CASCADE,
date_public DATE NOT NULL,
description TEXT,
PRIMARY KEY (id),
created_at DATETIME,
updated_at DATETIME,
creator_id INT NOT NULL,
FOREIGN KEY (creator_id) REFERENCES user (id)
ON DELETE RESTRICT
ON UPDATE CASCADE
) ENGINE = InnoDB;

「實體書」資料表

CREATE TABLE abook (
id INT AUTO_INCREMENT NOT NULL,
PRIMARY KEY (id),
book_id INT NOT NULL,
FOREIGN KEY (book_id) REFERENCES book (id)
ON DELETE RESTRICT
ON UPDATE CASCADE,
storage VARCHAR(50),
owner_id CHAR(10),
FOREIGN KEY (owner_id) REFERENCES reader (id)
ON DELETE RESTRICT
ON UPDATE CASCADE,
created_at DATETIME,
updated_at DATETIME,
creator_id INT NOT NULL,
FOREIGN KEY (creator_id) REFERENCES user (id)
ON DELETE RESTRICT
ON UPDATE CASCADE
) ENGINE = InnoDB;

「書評」資料表

CREATE TABLE thought (
id INT AUTO_INCREMENT NOT NULL,
PRIMARY KEY (id),
book_id INT NOT NULL,
FOREIGN KEY (book_id) REFERENCES book (id)
ON DELETE RESTRICT
ON UPDATE CASCADE,
note TEXT,
created_at DATETIME,
updated_at DATETIME,
creator_id INT NOT NULL,
FOREIGN KEY (creator_id) REFERENCES user (id)
ON DELETE RESTRICT
ON UPDATE CASCADE
) ENGINE = InnoDB;

「借書單」資料表

CREATE TABLE borrowing (
id INT AUTO_INCREMENT NOT NULL,
PRIMARY KEY (id),
reader_id CHAR(10) NOT NULL,
FOREIGN KEY (reader_id) REFERENCES reader (id)
ON DELETE RESTRICT
ON UPDATE CASCADE,
abook_id INT NOT NULL,
FOREIGN KEY (abook_id) REFERENCES abook (id)
ON DELETE RESTRICT
ON UPDATE CASCADE,
start_at DATETIME,
end_at DATETIME,
created_at DATETIME,
updated_at DATETIME,
creator_id INT NOT NULL,
FOREIGN KEY (creator_id) REFERENCES user (id)
ON DELETE RESTRICT
ON UPDATE CASCADE
) ENGINE = InnoDB;

以上!
照著貼,就可以建立出一個資料庫囉!

SHOW TABLES;
+--------------------------+
| Tables_in_good_ideas_lib |
+--------------------------+
| abook |
| author |
| book |
| borrowing |
| publisher |
| reader |
| thought |
| user |
+--------------------------+

8 rows in set
Time: 0.026s

實際跑一下 TablePlus 的外掛,畫出來是這樣

image

建立 migration

手動的將所有的資料表都建立好了。
但如果想要重新再建一個,好像又要重新做一次語法。
所以就想到如果可以像 node 一樣執行 .js 檔,那不就好了嗎?

還真讓我找到[4]語法執行,真的就好了。

再分檔案與日期 即可建立了一個有 migration 概念的機制。


  1. Foreign Keys ↩︎

  2. actions - constraints ↩︎

  3. 以 MySQL 為例解釋外鍵(Foreign Key) - 一顆藍莓 ↩︎

  4. command line - How to run SQL script in MySQL? - Stack Overflow ↩︎