快轉到主要內容
把 Liquibase 整合進 CI/CD

把 Liquibase 整合進 CI/CD

·
類別 
後端開發
標籤 
Liquibase Database AI CI/CD AI 協作開發
Eason Chiu
作者
Eason Chiu
一個不做筆記就容易忘記的工程師
目錄

前言
#

前陣子剛好在研究 Liquibase 如何整合進 CI/CD 部署流程,想說來記錄一下。

Liquibase 是管理資料庫變更的 Migration 工具。 一般來說,團隊若要異動正式環境的 Schema 或 Data,通常會由 DBA(Database Administrator) 檢查 Script 是否有問題。但專案一多,DBA 也可能漏看,畢竟也是人。

有些公司沒有專職 DBA,而是由團隊內較資深、對資料較熟悉或敏感的工程師兼任。靠人工逐一審查與執行,投入的 Effort 和把關力道都難免有限。

資料庫一旦在正式環境改壞就很麻煩。 雖然通常會有備份,但處理正式資料庫異動時往往是在跟時間賽跑。手動做 Migration、Rollback 難免有疏失,有些 SQL 還有執行順序要記,這些都會增加 DBA 的負擔。

而現在有 AI Agent 之後,改資料庫變得更快了。只要一句話,AI 就能幫你產生 SQL、分析 Schema,甚至透過 MCP 直接連線執行。

AI 可以快速產生 Migration Script,但執行順序、語法錯誤與資料風險,仍然需要由人負責。 DBA 最後再針對即將部署的資料庫變更與業務風險進行 Review。

這樣的好處是 DBA 只要負責最後要異動的檢查就好,透過 Liquibase + CI/CD 流程,沒過就是退回去給工程師去修改,光是前面的語法檢查與自動化,其實就減少很大負擔了。

為什麼資料庫也需要版本控制?
#

讓工具 + 流程來處理執行順序、語法錯誤的檢查,各個環境的資料庫一致就很重要。

假設專案一開始只有一張 users 表,後來需求慢慢增加:要加上使用者狀態、會員等級、索引、外鍵,甚至要拆表。

如果每個人都是手動連到資料庫執行 SQL,很快就會遇到幾個問題:

  • 本機、測試、正式環境的 Schema 不一致。
  • 不知道某個欄位是什麼時候、為什麼被加上的。
  • 新同事不知道要補跑哪些 SQL 才能把環境建起來。
  • 部署時漏執行 Script,程式上線後才發現欄位不存在。

Liquibase 做的事情,就是把這些變更整理成有順序、可追蹤、可重複執行的 Migration。

Liquibase 是怎麼記錄變更的?
#

Liquibase 的核心單位是 ChangeSet,可以把它想成「一次資料庫變更的提交紀錄」。

  • 每個 ChangeSet 都有唯一的 idauthor
  • 內容是要對資料庫執行的操作,例如建立資料表、增加欄位、建立 Index,或執行一段 SQL。

第一次執行 liquibase update 時,Liquibase 會在資料庫建立管理用資料表:

  • DATABASECHANGELOG:記錄哪些 ChangeSet 已經執行過。
  • DATABASECHANGELOGLOCK:避免同一時間有多個程序執行 Migration。

之後 Liquibase 會根據 DATABASECHANGELOG 判斷哪些變更尚未執行,只套用缺少的 ChangeSet,不會每次從頭重跑。

  • LOCKEDfalse,代表目前沒有程序正在執行 Migration。
  • 因此,共用的 Changelog 可以分別被本機、測試與正式環境執行一次;Liquibase 會依照各環境的歷史紀錄補上缺少的變更。

Liquibase 也會記錄每個 ChangeSet 的 Checksum:

  • 防止已經上線的 Migration 被偷偷修改,Liquibase 會偵測到內容不一致並停止執行。
  • 阻止我們改寫已經發生過的資料庫歷史。

已經執行過的 Migration 不要修改。需要調整時,新增下一個 Migration。

Liquibase 的結構、ChangeSet 與 Spring Boot 設定
#

Liquibase 的目錄結構
#

Liquibase 支援 XML、YAML、JSON 與 SQL。

下面用 SQL 示範一個我比較常用的結構:環境各自有 Changelog,共用 Schema 與環境專用資料分開管理,架構大概會長這樣:

ChangeSet
#

ChangeSet 我是習慣使用 SQL,主要是因為 Migration 檔案也比較好被 Review。

text
src/main/resources/db/changelog/
├── common/
│   └── changes/
│       ├── 20260530-v1-create-users.sql
│       └── 20260531-v2-add-user-status.sql
├── dev/
│   └── db.changelog-dev.yaml
├── test/
│   ├── db.changelog-test.yaml
│   └── changes/
│       └── 20260531-v1-insert-demo-user.sql
└── prod/
    └── db.changelog-prod.yaml

結構相同,但各自可以加入不同的環境專用資料。例如 test/db.changelog-test.yaml

yaml 檔去定義要執行的 sql 與順序(由上往下),Liquibase 沒有規定 Changelog 的檔名格式,但檔名路徑必須唯一。

yaml
databaseChangeLog:
  - include:
      file: db/changelog/common/changes/20260530-v1-create-users.sql
  - include:
      file: db/changelog/common/changes/20260531-v2-add-user-status.sql
  - include:
      file: db/changelog/test/changes/20260531-v1-insert-demo-user.sql

而我自己的習慣是日期+版本,例如 20260530-v1-create-users:日期方便追蹤,v1 用來區分同一天的變更。

例如 test/changes/20260531-v1-insert-demo-user.sql

sql
--liquibase formatted sql
--changeset eason:20260531-v1-insert-demo-user

INSERT INTO users (email) VALUES ('[email protected]');

Test 會套用共用 Migration 和測試資料。Prod 只套用共用 Migration。

先建立 20260530-v1-create-users.sql

sql
--liquibase formatted sql
--changeset eason:20260530-v1-create-users

CREATE TABLE users (
    id BIGINT IDENTITY(1,1) NOT NULL,
    email NVARCHAR(255) NOT NULL,
    created_at DATETIME2(7) CONSTRAINT df_users_created_at DEFAULT SYSUTCDATETIME() NOT NULL,
    CONSTRAINT pk_users PRIMARY KEY (id),
    CONSTRAINT uk_users_email UNIQUE (email)
);

--rollback DROP TABLE users
  • 已執行的 ChangeSet 不要修改,要新增下一個 ChangeSet。
  • 一個 Migration 做一件事。
  • 盡量提供 Rollback。

DROP TABLE 適合用在還沒有正式資料的表。這個例子的 Rollback 是 DROP TABLE users,會刪除資料表與資料。

但有資料時,Rollback 可能造成資料遺失。 Rollback 是風險評估,不是保證能把所有資料時光倒流。

SQL Server 多數 DDL 支援 Transaction,尚未 COMMIT 就失敗時可以回滾。Oracle 的 DDL 會在執行前後自動 COMMIT,因此不能只靠 Transaction Rollback,只能使用 Liquibase Rollback 或備份復原。

需求改成要記錄帳號狀態時,再新增一個 Migration,而不是回頭修改 20260530-v1-create-users.sql

sql
--liquibase formatted sql
--changeset eason:20260531-v2-add-user-status
--preconditions onFail:HALT
--precondition-not columnExists tableName:users columnName:status

ALTER TABLE users ADD
    status NVARCHAR(20) CONSTRAINT df_users_status DEFAULT 'ACTIVE' NOT NULL;

--rollback ALTER TABLE users DROP COLUMN status

liquibase 會先檢查欄位是否已存在,如果存在就停止執行。 SQL 的 Precondition 要放在 ChangeSet 前面。

索引也是一樣,建立獨立 ChangeSet:

sql
--liquibase formatted sql
--changeset eason:20260601-v1-add-user-email-index

CREATE INDEX idx_users_email ON users (email);

--rollback DROP INDEX idx_users_email

Migration 跑成功不代表線上一定安全。比如 Table 加 index 或欄位限制,可能造成鎖表,必要時會拆成多步驟。

Spring Boot 設置
#

Spring Boot 上使用只要加到 Maven 就可以了:

xml
<dependency>
    <groupId>org.liquibase</groupId>
    <artifactId>liquibase-core</artifactId>
</dependency>
<dependency>
    <groupId>com.microsoft.sqlserver</groupId>
    <artifactId>mssql-jdbc</artifactId>
</dependency>

接著在 src/main/resources/application.properties 設定 SQL Server 連線與 Changelog 位置

properties
spring.datasource.url=jdbc:sqlserver://localhost:1433;databaseName=demo_db;encrypt=false
spring.datasource.username=demo_user
spring.datasource.password=${DB_PASSWORD}
spring.datasource.driver-class-name=com.microsoft.sqlserver.jdbc.SQLServerDriver
spring.liquibase.change-log=classpath:/db/changelog/${APP_ENV}/db.changelog-${APP_ENV}.yaml

例如設定 APP_ENV=test 時,Liquibase 會載入 db/changelog/test/db.changelog-test.yaml。正式環境的密碼建議改用環境變數或 Secret 管理。

把 Liquibase 整合進 CI/CD
#

Migration 資料庫時常會發生某個環境忘了執行,導致最後每個環境的 Schema 都不一樣。所以 ChangeSet 應該跟程式碼一起進 Git 版控,讓 CI/CD 幫我們執行。

不同階段的資料庫大概會這樣分:

階段 使用的資料庫 這個階段主要做什麼
本機 / Dev 自己 Local 的 DB 或 Dev DB update 套用 ChangeSet,確認功能開發沒有問題
PR / CI 每次重新建立的乾淨 DB 從零開始執行 Migration,確認整份 Changelog 可以跑完
Test 測試環境的 DB 跑整合測試、回歸測試與 UAT
Production 正式 DB 部署時由 Pipeline 執行 update,並保留執行紀錄

這裡比較重要的是 PR 階段不要只拿共用的 Test DB 來驗證。因為其他分支可能已經改過資料庫,反而會把漏寫 ChangeSet 或執行順序錯誤的問題蓋掉。每次 PR Push 後,都重新建立一個乾淨資料庫,執行 validateupdate-sqlupdate,再啟動應用程式跑 Integration Tests:

text
新增或修改 ChangeSet
PR Review
CI:validate → update-sql → 乾淨 DB update → 自動測試
Merge
CD:Dev / Test 執行 Update
核准後執行 Production Update

在 PR 還沒合併以前,ChangeSet 可以依照 Review 意見反覆修改。

但一旦合併,或已經在 Dev、Test 等共用環境執行過,就不要再回頭修改原本的檔案,後續調整應該新增下一個 ChangeSet。

到了 Production,則 交給 Pipeline 執行,並在核准後才套用變更

AI 時代下,Liquibase 應該怎麼用?
#

AI 很適合快速撰寫 Migration 的執行 Script,但不應該取得正式資料庫的直接寫入權限。

以上面流程為例,主要把資料庫異動拆成下面三個步驟:

  1. AI:分析需求、參考 Schema 與歷史 Migration、產生 ChangeSet 初稿
  2. 工程師:確認資料模型、風險、SQL 與 Rollback,決定是否放行
  3. Liquibase:依照被版控的 ChangeSet,在受控流程中執行與記錄歷史

這樣分工的好處是,AI 產生的每一個改動都會先變成一個可閱讀、可討論、可 Code Review 的檔案,而不是一段直接打進資料庫的指令。

先給 AI 足夠的 Context,再請它產生初稿
#

如果只對 AI 說「幫我加會員等級欄位」,它可能會產生看起來合理、但不符合現況的 SQL。它不知道目前有沒有舊資料、欄位命名規則是什麼、資料庫是 SQL Server 還是 PostgreSQL,也不知道既有 Migration 用了哪些設計。

所以我通常會提供的 Context 包含:

  • 這次功能的 Spec 與資料規則。
  • 相關的 Entity、API、Validation 與查詢程式碼。
  • 目前的 Schema,或至少相關資料表的結構。
  • 同一個模組最近幾支 Migration。
  • 資料庫種類與版本,以及團隊的命名規範。

Prompt 不需要很花俏,但要把背景與邊界講清楚。重點不是讓 AI 寫得更華麗,而是把它能做與不能做的事情寫清楚。

不過現在的 Agent 其實都會把專案當作 Context 了,大部分的情境只要把需求跟邊界情況講清楚就好。

AI 產生後,工程師要 Review 什麼?
#

最少會確認這幾件事:

  • 欄位型別、長度、預設值與 Nullable 是否符合商業規則。
  • 新舊資料能否相容,是否需要先回填資料再加 Constraint。
  • Index 是否真的需要,會不會讓寫入成本增加或造成長時間鎖定。
  • 外鍵與 Unique Constraint 是否符合既有資料。
  • Rollback 是否真的可行,以及是否有資料遺失風險。
  • Migration 的順序是否正確,有沒有依賴還不存在的表或欄位。
  • update-sql 輸出的 SQL 是否與資料庫方言及預期一致。

AI 可以很快寫出執行的 SQL,但它無法替你承擔資料庫壞掉的風險。

尤其是「先新增欄位、程式改成雙寫或讀取新欄位、回填歷史資料、最後才加限制」這種多階段演進,通常比一次修改 Schema 更安全。這類順序與取捨,才是我們人類應該保留的決策權。

不建議讓 AI 直接操作正式資料庫
#

現在用 MCP 讓 AI 讀資料庫 Schema 很方便,讓它直接執行 SQL 也不難。但正式資料庫的直接寫入權限,跟讀取設計稿、產生程式碼不是同一個風險等級。

理想的架構大概會長這樣:

不過實際情況,有時還真的不太需要 MCP,等待串接也要時間,也就是一句 select 就能處理的情況,大部分的情境專案上的 DTO、Entity 程式當作 Context 就很夠了。

主要重點還是在:AI 負責產生變更,工程師負責決策,Liquibase 負責治理。

結語
#

Liquibase 不是因為用了 AI 才需要。即使沒有 AI,只要專案有多人協作、多個環境或持續部署,資料庫 Migration 就很值得被好好管理。

但 AI 出現後,它的重要性反而更明顯。AI 讓產生 SQL 的成本變得很低,也讓「一不小心改太快」的風險跟著提高。 此時把資料庫變更收斂成 ChangeSet、放進 Git、經過 Review、由 CI/CD 執行,就不只是流程形式,而是一道真的能保護的圍欄。

自從導入 Liquibase 後,我其實都讓 Agent 去產 Migration 的 SQL 了,我自己就比較像是 DBA Review 前的第一道防線,最後上線前才是 DBA 進來 Review。

相關文章

AI IDE - Cursor 介紹 與 使用心得
類別 
AI
標籤 
Cursor AI 協作開發 Prompt Engineering
把 AI Agent 融入開發流程:從專案打地基、建立 Skill 到用 Obsidian 管理專案
類別 
AI
標籤 
AI Agent AI 協作開發 AI Skill Obsidian MCP
Docker Compose 常用指令、操作
類別 
後端開發
標籤 
Docker Compose 指令筆記