自動化更新資料庫 Schema,告別手動執行 SQL 的風險。
新功能需要改資料庫 Schema 時,傳統做法是有人手動連進資料庫執行 ALTER TABLE。這種做法有兩個明確的風險:一是容易忘記執行、或在錯誤的環境執行;二是「程式碼版本」與「資料庫版本」是分離的兩套狀態,沒有機制保證兩者對得上。新版本的程式碼一啟動,如果資料庫還是舊 Schema,直接崩潰。
Liquibase 把 Schema 變更寫成版本化的 changeset,記錄在程式碼庫裡(services/user-service/src/main/resources/db/changelog/),跟應用程式碼一起進版控。應用程式啟動時,Liquibase 會自動比對資料庫目前的狀態與 changelog,把還沒套用的 changeset 依序執行。
<changeSet id="20260519-baseline-schema" author="wafer-bi">
<preConditions onFail="MARK_RAN">
<not><tableExists tableName="auth_groups"/></not>
</preConditions>
<createTable tableName="auth_groups">
<column name="id" type="SERIAL"><constraints primaryKey="true" nullable="false"/></column>
<column name="name" type="VARCHAR(255)"><constraints unique="true" nullable="false"/></column>
</createTable>
...
</changeSet>
preConditions 是關鍵:每個 changeset 執行前會先檢查前提條件(例如「這個 table 還不存在」),已經套用過的 changeset 不會重複執行。Liquibase 會在資料庫裡維護一張 DATABASECHANGELOG 表,記錄每個 changeset 的 id、作者、執行時間——這張表本身就是遷移歷史的真相來源。
直接示範一次完整流程。新增 002-add-last-login.xml:
<changeSet id="20260721-add-last-login" author="carrot">
<preConditions onFail="MARK_RAN">
<not><columnExists tableName="users" columnName="last_login_at"/></not>
</preConditions>
<addColumn tableName="users">
<column name="last_login_at" type="TIMESTAMP"/>
</addColumn>
</changeSet>
加進 db.changelog-master.xml 的 <include> 清單,重新 build image、kubectl rollout restart deployment/user-service——過程中沒有連進資料庫下任何一行 SQL:

▲ Liquibase 自動遷移:rollout 前後直接對比 users table 的欄位變化,DATABASECHANGELOG 記錄新 changeset 的真實執行時間
rollout 前 users table 沒有 last_login_at;rollout 後這個欄位自動出現,且 DATABASECHANGELOG 裡多了一筆對應的紀錄,時間戳記是 Pod 啟動的當下——這證明遷移是應用程式啟動時自動跑的,不是我手動介入的結果。
這個機制在 K8S 場景下特別有價值:Pod 隨時可能因為 HPA 擴容、滾動更新、或節點遷移而重新調度到不同節點、甚至產生新的 Pod。只要 image 裡帶著正確版本的 changelog,Schema 遷移就會在任何一個新啟動的 Pod 上自動核對並執行——不需要額外寫一支「遷移用的 Job」或請人記得手動操作。
多副本同時啟動時 Liquibase 有內建的鎖機制(DATABASECHANGELOGLOCK 表)避免多個 Pod 同時搶著跑遷移造成衝突,只有取得鎖的那個 Pod 會實際執行 changeset,其餘的等待鎖釋放後發現版本已經一致,直接跳過。
Schema 變更跟應用程式碼綁定在同一次發布裡,遷移歷史有真實紀錄可查、可追溯。明天討論另一個層面的風險管理:如果整個叢集或資料真的損毀了,要怎麼復原。