(18) 使用 Liquibase 建立所需的 Table 與 ChangeSet 的介紹
上一篇文章我們已經將 Liquibase 導入至我們的 Spring Boot 專案中了。而這一篇文章,將會介紹如何使用 ChangeSet 來操作資料庫。所以這個部份,我們將會搭配一些資料庫設計的相關內容,來完成這次主題的展示。
這個系列文的目標主要是做一個管理系統的專案,但其實最終目的是在於學習使用這些工具,因此在系統的架構的部分我們會盡可能的簡化它,以避免模糊學習焦點。
所以要建立甚麼專案?
那麼重點來了,這個管理系統我們打算做甚麼樣的內容呢?考量到後續介紹資料庫的 Table 之間的關聯性,需要想一個「一對一、一對多、多對多」都有使用上的應用。
這裡我們假設以「健身房會員管理系統」為目標,但誠如上方所述,這個系列文重點會放在「工具的使用」,而非「專案的系統完整度」,因此後續所建立的表的內容,就不會花上太多額外的心力在每張表的內容。
如果要簡單的建立健身房會員管理系統,那麼我們可能會遇到下面幾種需求:
- 健身房:紀錄健身房的館場名稱
- 會員卡:會員卡決定可以進入那些健身房,或者依照卡片等級決定甚麼時候可以進入健身房
- 會員:會員卡的持有人,記錄會員的相關資料
- 入場紀錄:紀錄會員的入場紀錄
依照上面的幾項需求,我們就可以簡單建立表的內容,如下:
可以總結一下目前每張表之間的關係:
- Member to Card:1–1
- Card to Gym:*-*
- Member to Logging:1-*
- Gym to Logging:1-*
其中的 Card_Gym 是一張多對多關係中的「中介表」。因此包含這張中介表,在確定了要建立的表的內容後,就可以開始透過 Liquibase 來建立、管理資料庫的表了。
建立表的順序
基於我們要建立的系統,建立表以及FK Constraint(外鍵限制)的順序會如下:
- 建立 Member 表
- 建立 Card 表
- 加入 Member 與 Card 的 FK
- 建立 Gym 表
- 建立Card_Gym 表
- 加入 Member 與 Card 的 FK
- 加入 Card 、 Gym 與 Card_Gym 的 FK
- 加入 Member、 Gym 與 Logging 的 FK
假設你對於資料庫設計已經有一點接觸了,那麼你應該大概知道 FK 的用途是用於確保資料的完整性。但你可能會聽到一些說法是「盡量不要使用 FK」,不過其實這已經是另外一個議題了。這裡還是推薦你看幾篇討論串 (可以在本文的最底端找到推薦文章),相信可以更清楚的知道使用使用 FK 的優點與缺點。
而如同一開始所說的,因為這個系列文著重在工具的使用,所以我們這裡還是會對相關的表加上 FK。
加入 ChangeLog 於 master.xml 中
在上一篇文章中,我們有建立了一個檔案,master.xml
,作為 Liquibase 讀取 ChangeLog 的進入位置。所以當想要加入其他的 ChangeLog 時,就必須在 master.xml
中進行設定。
先來說說建立 ChangeLog File 的小規則,在我們新增 ChangeLog File 時,會統一固定的格式作為建立的 File 名稱,格式可以依照專案管理者來決定怎麼統一,或者像這樣 {yyyyMMddHHmmssSSS}_{type}_{entityName}.xml
的格式,例如 202101081611000_addEntity_Member.xml
,就可以非常明確的知道在甚麼時間點,對哪一個 Entity 做了甚麼事情。
所以假設現在想要加入一個關於 Member 表的 ChangeLog,我們就可以依照下列的步驟進行操作:
- 新增一個
202101081611000_addEntity_Member.xml
的檔案,並假設這個檔案路徑位於src/main/resources/liquibase/changelog
底下 - 將
202101081611000_addEntity_Member.xml
的路徑透過<include>
標籤加入master.xml
中
這裡用到了 <include>
,他主要是用於引用其他的 ChangeLog 時所用的標籤。裡的的參數 file
用於指定路徑,而 relativeToChangelogFile
則用於設定所指定的路徑是否為相對路徑或是 classpath 的路徑。
透過 ChangeSet 建立 Table
我們已經建立好了初始化 Member 表的 ChangeLog,現在要在其中加入 ChangeSet 來 Create Teble。
在開始使用 ChangeSet 前,一樣有幾點需要注意的小規則:
- 一定要加上
id
的屬性值,這個id
會被記錄在DATABASECHANGELOG
裡,用於紀錄哪些 ChangeSet 已經被執行過,所以id
必須得是唯一值。 - 一定要加上
author
的屬性值,因為author
會於DATABASECHANGELOG
裡記錄是誰修改了這次的內容。 - 所有的「新增表、修改表、刪除表」等操作,都會在 ChangeSet 中
在「新增表」的部分,我們就會需要使用 <createTable>
的元素,並且在這個元素內加上 <column>
或者加上 <constraints>
來設定這些表的 Schema 資訊。(參考)
因此,假設我們要建立 Member 表的 Schema,就會如下的程式碼片段所示:
這裡特別需要注意的地方是 <column>
內的 autoIncrement
屬性值,如果想要在資料庫中操作 INSERT 時讓 ID 自動編號,透過 autoIncrement="true"
進行設定就可以讓資料庫自行管理編號的新增。
但是這裡會建議讓 autoIncrement
設定為 false,這牽扯到 Sequence 自動產生的不同方式,所以接著我們來稍微提一下不同的 Sequence 生成策略。
不同的 Sequence 生成策略
對於 Sequence 的產生,有幾種不同的方式:
1. 讓資料庫自行管理每個需要 Sequence 的欄位
如果你想要讓資料庫自動幫你管理這些 Sequence,那麼在 Liquibase 中,就可以如同上述透過 autoIncrement="true"
來完成,或者參考 Liquibase 提供的 AddAutoIncrement 來實現,像是:
而在對應的使用 SQL 命令來建立 Table 時,就如同下方所執行的內容:
CREATE TABLE
PUBLIC.member (
id BIGINT AUTO_INCREMENT NOT NULL,
name VARCHAR(50),
CONSTRAINT PK_MEMBER PRIMARY KEY (id)
)
接著如果你在資料庫中執行插入的命令時,就不需要指定 ID 的值,資料庫會自動使用資料庫所自行管理的 Sequence 對 ID 的部分自行加上 Sequence Value。例如在 PostgreSQL 中:
INSERT INTO member (name) VALUES ('Albert');
這裡需要別注意的是,如果你對愈多個欄位標記上 AUTO_INCREMENT
,那麼資料庫在 Sequence Table 中就會加入愈多個 Sequence 的資料,來管理每一個 Sequence 的當前狀態。
例如在 PostgreSQL 中,就可以用如下命令列,檢視當前的 Sequence:
SELECT * FROM information_schema.sequences;
而若在 <column>
中設定 autoIncrement="false"
(不設定的預設值也是 false) 的話,那麼在對資料庫使用 INSERT 命令列時,ID 的部分也必需要填寫,否則就會得到「null value in column "id" violates not-null constraint」的錯誤訊息。
也因此,我們會需要其他的方式來管理 Sequence 的 Auto-Increment。
2. 透過程式的操作管理 Sequence,以進行 Auto-Increment (適用於有支援 Sequence 的資料庫)
當欄位的 autoIncrement
為 false 時,則資料庫就不會自動管理需要自動遞增的欄位了。這時我們會需要透過程式的部分來自行進行處理,有兩種方式。
2-1. 自行管理一個通用的 Sequence
我們可以在資料庫中,先建立一個統一的 Sequence,假設你在 PostgreSQL 中,就可以使用如下命令建立 一個名為 sequence_generator
的Sequence:
CREATE SEQUENCE PUBLIC.sequence_generator
START WITH 1050 INCREMENT BY 50;
而在 Liquibase 中,如果想要建立 Sequence,可以參考如下,或參考 Liquibase 提供的 CreateSequence 說明:
當你建立完一個通用的 Sequence 後,在程式碼的部分就可以統一將所有需要 Auto-Increment 的欄位指定透過這個 Sequence 來完成,例如剛剛所建立的 sequence_generator
。而該如何指定 Sequence 的部分我們將會在之後文章中的 JPA 篇才會介紹與說明。
2–2. 自行管理多個各別的 Sequence
若你有特殊需求(?),那麼你可以透過如 2-1 的方式,建立多份 Sequence,並在需要不同 Auto-Increment 的欄位指定至對應的 Sequence 中。
3. 自行建立 Sequence Table,以進行 Auto-Increment (適用於沒有支援 Sequence 的資料庫)
這裡稍微簡單的說明一下,其實不是所有資料庫都有提供 Sequence 的功能,像是 MySQL ,所以如果要實現 Sequence 的 Auto-Increment 的話,就必須得先額外建立一張表,專門做為管理 Sequence 的表。
也因此,當我們在使用 Liquibase 來 Create Sequence 或者是使用 Auto-Increment 的時候,也必須稍微注意一下不同料庫之間的特性差異。
為什麼要分為不同的 Sequence 生成策略
其實這個問題是在我寫完上面的內容後突然意識到的問題,為什麼要分成不同的 Sequence 生成策略呢?所以我們來簡單的分析一下這兩者之間的優缺點吧!
- 使用 Auto-increment:使用簡單、方便,由資料庫管理 Sequence,但因為每一個標記 Auto-increment 的欄位都會生成不同的 Sequence Name,因此管理上較為不易。
- 使用 Create Sequence:可以統一使用同一個 Sequence,或者可以自定義使用多個不同的 Sequence,且 Sequence Name 也可以自行定義,所以管理方便。
因此,在甚麼樣的情境下使用哪一種 Sequence 生成策略,其實主要還是看需求。如果你需要客製化較高,或者想要自行管理 Sequence 的話,那麼使用 Create Sequence 就會是比較好的選擇。而如果沒有這麼要求 Sequence 是怎麼產生的話,那麼簡單使用 Auto-increment 來讓資料庫幫我們管理也行。
在這個系列文的專案中,我們將會採取第 2-1 的方式來進行 Sequence 的建立與使用。
透過 <addForeignKeyConstraint> 來加入 FK
加入 Member 與 Card 的 Foreign Key 的順序與規則
如同一開始說的流程:
- 建立 Member 表
- 建立 Card 表
- 加入 Member 與 Card 的 FK
假設我們已經將 Member 以及 Card 透過 <createTable/>
建立完成了,現在我們要將 Member 與Card 之間加入 FK 的約束。
不過在加入 FK 之前,我們要先確定這兩者之間的從屬關係,也就是必須先確定是「先有 Member 才有 Card」這件事情,因為是要先有會員,會員才會有會員卡,而若解除會員,則會員卡可以被註銷,也就是有會員資料卻沒有會員卡資料。
而在 FK 中,我們就要確定是「要在哪張表中加上 FK 」。其實簡單的思考就是:「FK 是外鍵限制,也就是限制該表的某個欄位,必須符合另外一張表的某個欄位」。所以對於 Member 與 Card 來說,則是「要在 Card 中加上 FK,以限制 Card 的 member_id 欄位符合 Member 的 id 欄位」。
加上 Foreign Key
假設我們要在 PostgreSQL 中,對 Card 表加上 FK,如果 Card 表已經存在,那我們只能透過 Alter 表後再加上 FK,像是:
ALTER TABLE Card
ADD CONSTRAINT fk_card_member_id FOREIGN KEY (member_id)
REFERENCES Member (id)
如此一來,在每次加入資料於 Card 時,就會確保 Card 的 member_id 的值一定會包含於 Member 的 id 的值。
而在 Liquibase 中,若想要替 Card 加入 FK,你可以這樣做:
更多關於 <addForeignKeyConstraint/>
的細節可以參考 Liquibase 提供的 addForeignKeyConstraint 說明。
addForeignKeyConstraint 的 onUpdate 與 onDelete 的 CASCADE
這裡有兩個屬性值需要稍微特別提醒一下:
- onUpdate 屬性
- onDelete 屬性
這兩個屬性可以設定的值有:CASCADE、SET NULL、SET DEFAULT、RESTRICT、NO ACTION。特別需要注意的是 CASCADE,如果當這兩個屬性值被設定為 CASCADE,則當 References 的表的欄位被更新或刪除時,加入 Constraint 的欄位也會被更新或者被刪除。
不過其實不太建議直接使用 CASCADE,原因是因為當 FK 的所牽動的功能愈多,那麼可能所造成的 Side Effect 的機會就會更大,或者可能造成錯誤發生時的除錯困難。如果想要避免讓資料庫自行更動資料,想要只透過程式碼來管理資料,那麼就盡量不要設定這些屬性值會是比較好的選擇。
Composite Primary Keys
一張表可以不只有一個欄位可以做為 PK,他可以是複合式的,也就是多個欄位組合為一個 PK,這很常用於多對多的中介表 (Junction Table)。
假設已經完成建立 Card 表以及 Gym 表,接下來就要處理多對多的部分。在中介表中,我們並不會希望 (card_id, gym_id) 的資料重複,所以就會使用到 Composite Primary Keys 複合主鍵的設計。
不論是不是使用 Liquibase 進行操作,在設計表的 Schema 時,你可以在「建立表」的同時加上 Composite PK,亦或者是透過「修改表」的時候加上。
在新增表的同時加入 Composite PK
在 SQL 中,我們可以像這樣完成:
CREATE TABLE PUBLIC.card_gym (
card_id BIGINT NOT NULL,
gym_id BIGINT NOT NULL,
CONSTRAINT PK_CARD_GYM PRIMARY KEY (card_id, gym_id)
)
在 Liquibase 中,可以直接在 Constraints 加上 primaryKey="true"
:
透過修改表的方式加入 Composite PK
在 SQL 中,可以像這樣完成:
ALTER TABLE PUBLIC.card_gym ADD PRIMARY KEY (card_id, gym_id)
在 Liquibase 中,可以透過 <addPrimaryKey>
來完成:
接著為了確保資料的正確性與完整度,因此我們會把這張 Junction Table 的欄位與對應的表的欄位加上 Foreign Key Constrain。細節因為前面有說明過了,因此這邊就不再多贅述~。
最後完成 Logging 表
最後我們來完成這張 Logging 表,這張表主要是拿來記錄會員的進場紀錄。不曉得你有沒有疑惑過為什麼這張表的 FK 是指向 Member 而不是 Card ?
確實,在 Member 中的 id 與 Card 中的 member_id 是一對一的關係,但因為我們考慮到了以下情形:「會員被申請註銷,會員卡的資料包含可前往的 Gym 也需一併被取消,但會員資料可以不一定要被刪除」,因此在 Logging 表中所紀錄的內容,就會是 Member 的 id。
而完成 Logging 表的方式就如同之前所提過的內容一樣如法炮製,如下:
完整的程式碼你可以在文章的最後找到。
執行後的 DATABASECHANGELOG
當所有要加入的表都透過 Liquibase 加入後,並執行專案,Liquibase 就會開始操作資料庫。而 Liquibase 所加入的 DATABASECHANGELOG 表,就會如下圖所示 (以 H2 DB 為例):
如果你想要參考 liquibase 在執行期間到底都做了些甚麼事情,那麼可以參考「liquibase.log」,這裡提供了完整的 log。
文末
我們從這篇文章開始,決定了專案的方向。從需求的設定到資料庫設計,先定義出需要甚麼表以及每張表之間的關聯,最後詳述了怎麼使用 Liquibase 對資料庫進行操作,展示如何新增 <changeSet>
、如何透過 <createTable>
來新增表、怎麼使用 <column>
新增欄位、怎麼搭配 <constraints>
設定欄位的約束 (例如 NOT NULL)、如何透過 <addForeignKeyConstraint>
來修改 FK 等等過程。
因此對於 Liquibase 的基本操作我們已經在這個章節詳述的算完整了,後續如果有更多進階的功能,則會在後續有需求時再提起。因為礙於篇幅的關係,所以如果你迫不及待的想知道更多關於 Liquibase 的細節,也可以上 Liquibase 的官方網站查看更多的功能細節。
Liquibase 的使用介紹大概就會在這邊告一段落,因為我們已經完整的將專案的架構擬建出來了,因次下一篇文章就會開始介紹,如何透過 ORM 的概念來與資料庫進行溝通,例如資料的取得、新增、修改、刪除等基本功能。
那我們就開始進入下一個環節吧 ─ Spring Data Jpa!
前往
本篇程式碼
上一篇
下一篇
所有文章列表
其他外部參考