資料庫 migration 要寫成「重跑也安全」:冪等性的實務做法
資料庫的 migration,是把資料表結構改成新樣子的一連串 SQL 檔案。多數教學只告訴你「寫一個檔案、執行它」,但很少談到一件很實際的事:如果它被執行了兩次,會發生什麼事?
在自動化部署的世界,這不是假設性問題。部署可能被中斷後重試、同一支腳本可能被兩個環境各跑一次,或是你自己手滑重複按了。
什麼是冪等
冪等(idempotent)的意思是:執行一次與執行很多次,結果相同。對 migration 來說,就是「跑第二次不會報錯,也不會留下重複的資料」。
哪些 SQL 天生不冪等
ALTER TABLE ... ADD COLUMN:欄位已存在時,重跑會直接報錯。- 單純的
INSERT:重跑會多出一份重複的資料。 CREATE TABLE(沒有IF NOT EXISTS):資料表已存在時報錯。
我自己的一個專案裡,就有幾支 migration 是這種不冪等的類型。我的處理方式是:不讓它們自動執行,只有在明確指定某一支時才跑,而且跑之前先印出檔案的前幾行讓我確認。這是個保守的做法,但它的代價是依賴人的判斷。
讓 migration 變冪等的寫法
建立資料表與索引用 IF NOT EXISTS:
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_example ON example(column_name);
寫入初始資料用「存在就略過」或「存在就更新」:
INSERT OR IGNORE INTO rules (id, name) VALUES (1, 'example');
OR IGNORE 在主鍵或唯一鍵重複時略過;如果希望用新值覆蓋,則用 INSERT OR REPLACE。
新增欄位在 SQLite 系的資料庫(包括 D1)沒有 ADD COLUMN IF NOT EXISTS,這是最麻煩的一個。常見的處理是:把這種變更獨立成一支只跑一次的 migration,並依賴 migration 工具記錄「這支已經套用過」,而不是靠 SQL 本身冪等。
一個重要的坑:別覆蓋使用者的修改
如果你的系統有「管理介面」,使用者或管理員可以直接在介面上修改資料,那就要特別小心。我的一個專案裡,規則是存在資料庫裡的,管理員可以在介面上調整它。如果 migration 用 INSERT OR REPLACE 灌初始資料,下次部署就會把管理員改過的規則全部蓋掉。
所以那個專案的 migration 設計成:只新增不存在的規則,絕不覆蓋已存在的。種子資料與「正式環境的現況」是兩回事,部署不應該重置現況。
別去修改已經套用過的 migration
另一條鐵則:已經套用到正式環境的 migration 檔案,不要再改它的內容。改了之後,新環境與舊環境的結構就會悄悄地分歧,而且很難察覺。要變更,就新增一支新的 migration,檔名編號往後排。
冪等也救不了的情況
- 改變資料的運算,例如「把所有價格乘以 1.1」,重跑就會乘兩次。這種要用 migration 工具的紀錄來確保只執行一次。
- 預覽與正式環境共用同一個資料庫時,在預覽上跑 migration 就等於在正式環境上跑。這時更要確認它是安全的。
小結
寫 migration 時,我會問自己兩個問題:「如果它跑兩次會怎樣?」以及「這會不會蓋掉現在線上的資料?」只要這兩個問題的答案都是「沒事」,部署時就可以睡得安穩一點。