D1 Migrations:用版本控制管理資料表結構 | Cloudflare 完整教學
上一篇《D1 入門:Serverless SQLite》我們學會了在 Workers 裡用 SQL 建表、查資料、做交易。但真實專案的資料表結構不會一次定案:今天加個
posts表、明天幫users加個欄位、後天補幾個索引。如果每次都手動wrangler d1 execute一堆 SQL、還要用人腦記「本地套過沒、遠端套過沒」,遲早會亂。這就是 D1 Migrations(資料庫遷移)要解決的問題——它讓你像 Git 管理程式碼一樣,把 schema 的每一次變更變成有編號、可追蹤、可重播、跨環境一致的版本。這篇我們把wrangler d1 migrations create/list/apply一路走完,含遷移檔命名順序、--localvs--remote、種子資料、CI 自動遷移,以及和 Drizzle ORM 搭配的思路。
前言
**D1 Migrations(資料庫遷移)**是管理資料表結構(schema)版本的標準機制。每一次你要改動資料庫結構——新增一張表、幫既有的表加欄位、建立或刪除索引、調整外鍵關聯——都寫成一個獨立、有遞增編號的 SQL 檔案。Wrangler 會自動追蹤「哪些遷移已經套用過」,確保你在任何環境(本地、staging、production)都能把 schema 精準演進到同一個狀態。
打個比方:如果說你的程式碼是靠 Git 一個一個 commit 累積演進的——每個 commit 記錄一次改動、有先後順序、任何人 clone 下來重播就能得到一模一樣的程式碼——那 Migrations 就是「資料表結構的 Git」。你的 schema 不再是某個人腦裡「我記得好像改過」的模糊狀態,而是一疊按編號堆疊的變更檔案:0001 建初始表、0002 加文章表、0003 加使用者角色……。任何人拿到專案,只要「重播」這疊遷移,就能得到正確的資料表結構。手動 execute 就像不用版控、直接在檔案上覆蓋修改——短期能動,長期必亂。
這篇聚焦 D1 Migrations,承接上一篇的 D1 基礎(建庫、建表、查詢),把「schema 怎麼安全地演進」這件事講透。讀完你會掌握:
- Migrations 是什麼、為何需要——版本控管 schema 的心智模型、
d1_migrations追蹤表如何運作、跟手動execute的本質差異 - 三個核心指令——
wrangler d1 migrations create建檔、apply套用、list查狀態,以及遷移檔的命名與順序規則 - 本地 vs 遠端——
--local與--remote各自維護一份追蹤表,標準的「先本地後遠端」流程 - 種子資料與 CI——如何灌初始種子資料(seed data)、在 CI/CD 中自動套用遷移、部署順序為何是「先遷移再部署」
- 與 Drizzle ORM 搭配——用
drizzle-kit從 TypeScript schema 自動產生遷移檔的思路
核心概念
什麼是 Migration?一疊有編號的 SQL 變更檔
一個 migration(遷移)就是一個 SQL 檔案,描述資料庫從「舊版 schema」到「新版 schema」的一次變更。Wrangler 把這些檔案集中放在一個目錄(預設 migrations/),並用遞增數字前綴確保它們的套用順序:
migrations/
├── 0001_initial_schema.sql ← 第一次:建立 users、sessions 表
├── 0002_add_posts_table.sql ← 第二次:新增 posts 表
└── 0003_add_user_roles.sql ← 第三次:幫 users 加欄位、建 roles 表
關鍵在於編號決定順序。D1 一定會由小到大依序套用這些檔案,而且每個檔案只套用一次。這帶來三個重要性質:
- 可追蹤(traceable):每次 schema 變更都是一個檔案,存進 Git,誰改了什麼、何時改的一目了然。
- 可重播(replayable):任何人拿到專案,重播這疊檔案就能重建出一模一樣的 schema。
- 跨環境一致(consistent):本地、staging、production 都跑同一疊遷移,schema 保證同步。
d1_migrations 追蹤表:D1 怎麼知道哪些套過了
你可能會問:D1 怎麼知道 0001、0002 已經套用、0003 還沒?答案是——它會在每個資料庫裡自動建立一張追蹤表 d1_migrations,記錄已套用的遷移檔名與套用時間:
-- D1 自動建立與維護的追蹤表(你不需要手動建,了解結構即可)
CREATE TABLE d1_migrations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE, -- 遷移檔名,例如 0001_initial_schema.sql
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -- 套用時間
);
每次你執行 wrangler d1 migrations apply,D1 會:
- 讀取
migrations/目錄裡所有.sql檔,依編號排序。 - 查
d1_migrations表,找出還沒被記錄的遷移檔。 - 只執行那些未套用的,一個一個依序跑,跑完就在
d1_migrations插入一筆紀錄。
因為追蹤表存在資料庫本身,所以本地資料庫和遠端資料庫各自維護一份——這也是為什麼你必須對 --local 和 --remote 分別套用遷移(稍後詳談)。
遷移檔的命名與順序規則
檔名格式是 NNNN_描述.sql(四位數編號 + 底線 + 描述)。你不用手動想編號——wrangler d1 migrations create 會自動找出目前最大編號、加一,幫你產生正確前綴。你只需要提供一個有意義的描述(用底線連接),讓人看檔名就知道這次改了什麼:
| 好的描述 | 不好的描述 |
|---|---|
add_posts_table | update |
add_index_on_email | fix |
add_role_column_to_users | new_migration |
最重要的鐵律:遷移檔不可變(immutable)
這是 Migrations 唯一、也最容易犯的錯:已經套用過的遷移檔,永遠不要回頭編輯它。因為 D1 是用「檔名」判斷某個遷移套過沒——一旦 0002_add_posts.sql 在遠端被標記為已套用,你再改它的內容,apply 會直接跳過(因為檔名已在 d1_migrations 裡),修改不會生效,還會造成本地與遠端 schema 分岔。
正確做法永遠是往前修:寫錯了、要調整,就新建一個更高編號的遷移檔去修正,而不是改舊的。這跟 Git 一樣——你不會去竄改已 push 的歷史 commit,而是新增一個 commit 修正它。
關鍵術語速覽
| 術語 | 一句話定義 |
|---|---|
| migration | 一個描述 schema 變更的 .sql 檔,有遞增編號決定順序 |
migrations/ | 存放所有遷移檔的目錄(可在 wrangler.jsonc 設定 migrations_dir) |
d1_migrations | D1 自動維護的追蹤表,記錄哪些遷移已套用 |
migrations create | 建立一個新的空白遷移檔(自動加編號前綴) |
migrations apply | 套用所有「尚未套用」的遷移到指定資料庫 |
migrations list | 列出已套用/待套用的遷移狀態 |
| seed data | 種子資料——資料庫初始化時預先塞入的基礎資料(如角色、分類) |
實作範例
我們用一組可執行的 CLI 與 SQL,把整個遷移流程從無到有走一遍。假設你已經照上一篇建好資料庫 my-app-db 並綁定好 wrangler.jsonc。
1. 設定 migrations 目錄
先在 wrangler.jsonc 的 d1_databases 裡指定遷移目錄(不設定的話預設就是 migrations,但明確寫出來比較清楚):
{
"name": "my-app",
"main": "src/index.ts",
"compatibility_date": "2026-01-01",
"d1_databases": [
{
"binding": "DB", // 程式中用 env.DB 存取
"database_name": "my-app-db", // 建立後不可更改
"database_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"migrations_dir": "migrations", // 遷移檔所在目錄(預設值)
"migrations_table": "d1_migrations" // 追蹤表名稱(預設值,通常不用改)
}
]
}
2. 建立第一個遷移檔:migrations create
用 wrangler d1 migrations create 建立遷移檔。注意第一個參數是資料庫名稱、第二個是描述:
# 語法:wrangler d1 migrations create <資料庫名稱> <描述>
wrangler d1 migrations create my-app-db initial_schema
# 輸出範例:
# ✅ Successfully created Migration '0001_initial_schema.sql'!
#
# 此檔案位於:migrations/0001_initial_schema.sql
# 請在裡面寫入你的 SQL,然後用 apply 套用。
Wrangler 自動幫檔案加了 0001_ 前綴。這個指令只建立空檔案、不執行任何 SQL,你需要打開它、寫入這次的 schema 變更。
3. 寫入遷移內容:初始 schema
打開剛建的 migrations/0001_initial_schema.sql,寫入建立基礎表格與索引的 SQL:
-- migrations/0001_initial_schema.sql
-- 建立基本使用者表與 session 表,含常用索引
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'user',
active INTEGER NOT NULL DEFAULT 1, -- SQLite 沒有原生 boolean,用 0/1
created_at TEXT NOT NULL DEFAULT (datetime('now')) -- 日期用 TEXT(ISO 8601)
);
-- 常查詢的欄位建索引
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
CREATE INDEX IF NOT EXISTS idx_users_role ON users(role);
CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY, -- UUID
user_id INTEGER NOT NULL,
expires_at TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions(user_id);
4. 套用到本地:migrations apply --local
遷移寫好後,先套用到本地模擬資料庫測試。這一步不會碰到任何線上資料:
# 套用所有尚未套用的遷移到本地模擬庫
wrangler d1 migrations apply my-app-db --local
# 輸出範例:
# Migrations to be applied:
# ┌─────────────────────────────┐
# │ Name │
# ├─────────────────────────────┤
# │ 0001_initial_schema.sql │
# └─────────────────────────────┘
# ✅ 0001_initial_schema.sql 已成功套用
套用後,可以啟動本地開發伺服器驗證資料表確實建好了:
# wrangler dev 預設連本地模擬庫,安全
wrangler dev
# 或直接下命令確認表存在
wrangler d1 execute my-app-db --local --command="SELECT name FROM sqlite_master WHERE type='table'"
# 應該看到 users、sessions,以及 D1 自動建的 d1_migrations
5. 查看狀態:migrations list
migrations list 讓你隨時看清楚「哪些套過了、哪些還沒」。本地和遠端要分開查(因為各自一份追蹤表):
# 查本地狀態
wrangler d1 migrations list my-app-db --local
# 查遠端狀態
wrangler d1 migrations list my-app-db --remote
# 遠端輸出範例(還沒套用任何遷移到遠端時):
# ┌─────────────────────────────┬──────────────────┐
# │ Name │ Applied At │
# ├─────────────────────────────┼──────────────────┤
# │ 0001_initial_schema.sql │ ⏳ Not yet applied │
# └─────────────────────────────┴──────────────────┘
6. 套用到遠端:migrations apply --remote
本地驗證無誤後,才套用到雲端正式資料庫。這一步才會真正改到線上 schema:
# 套用到 Cloudflare 上的正式資料庫
wrangler d1 migrations apply my-app-db --remote
# 套用後再 list 一次確認
wrangler d1 migrations list my-app-db --remote
# 這次 0001 應該顯示已套用的時間戳記,而非 "Not yet applied"
記住這個標準節奏:寫遷移 → --local 套用測試 → 確認無誤 → --remote 套用上線。
7. 演進 schema:新增第二個遷移
需求變了——要加文章功能。不要去改 0001,而是新建 0002:
wrangler d1 migrations create my-app-db add_posts_table
# 建立 migrations/0002_add_posts_table.sql
寫入新表與對既有表的變更(這裡示範 ALTER TABLE 加欄位):
-- migrations/0002_add_posts_table.sql
-- 新增文章表,並幫 users 補上 last_login 欄位
CREATE TABLE IF NOT EXISTS posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
author_id INTEGER NOT NULL,
title TEXT NOT NULL,
slug TEXT UNIQUE NOT NULL,
content TEXT,
published INTEGER NOT NULL DEFAULT 0,
published_at TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE SET NULL
);
CREATE INDEX IF NOT EXISTS idx_posts_author_id ON posts(author_id);
CREATE INDEX IF NOT EXISTS idx_posts_published ON posts(published, published_at DESC);
-- 對既有 users 表新增欄位(schema 演進)
ALTER TABLE users ADD COLUMN last_login TEXT;
再跑一次 apply,D1 會只套用 0002(因為 0001 已在 d1_migrations 裡,自動跳過):
wrangler d1 migrations apply my-app-db --local
wrangler d1 migrations apply my-app-db --remote
8. 灌種子資料(seed data)
**種子資料(seed data)**指資料庫初始化時要預先塞入的基礎資料,例如角色、分類、預設設定。有兩種做法:
做法一:把種子資料寫進遷移檔(適合「屬於 schema 一部分」的固定資料,如角色定義):
-- migrations/0003_seed_roles.sql
-- 建立角色表並塞入預設角色(這類固定資料適合放進遷移)
CREATE TABLE IF NOT EXISTS roles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE NOT NULL,
permissions TEXT NOT NULL DEFAULT '[]' -- 用 JSON 字串存權限陣列
);
INSERT INTO roles (name, permissions) VALUES
('admin', '["read","write","delete","manage_users"]'),
('editor', '["read","write"]'),
('viewer', '["read"]');
做法二:用獨立的 seed 檔搭配 execute(適合「只在開發環境用」的測試假資料,不該進正式庫也不該進 d1_migrations 追蹤):
-- seed-dev.sql:僅供本地開發的測試假資料(不放進 migrations/)
INSERT INTO users (email, name, role) VALUES
('alice@example.com', 'Alice', 'admin'),
('bob@example.com', 'Bob', 'editor');
# 用 execute 灌測試資料,只灌本地,不進追蹤表
wrangler d1 execute my-app-db --local --file=./seed-dev.sql
判斷準則:資料是「應用邏輯依賴的固定基礎資料」(角色、國家列表)→ 放進遷移檔,連 production 都要;資料是「開發時方便測試的假資料」→ 用獨立 seed 檔配 --local execute,別進遷移。
9. 在 CI/CD 中自動套用遷移
手動記得跑 --remote 很容易忘,正式團隊都把它放進 CI/CD,由 pipeline 在部署前自動執行。關鍵是順序:一定要先套用遷移、再部署 Worker——這樣新程式碼跑起來時,它依賴的資料表結構已經就緒:
# .github/workflows/deploy.yml(節錄)
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
# 步驟一:先套用資料庫遷移(schema 先就緒)
- name: Apply D1 migrations
run: npx wrangler d1 migrations apply my-app-db --remote
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
# 步驟二:再部署 Worker(此時新程式碼依賴的表已存在)
- name: Deploy Worker
run: npx wrangler deploy
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
為什麼順序不能反?如果先部署新版 Worker、再套遷移,那在遷移完成前的空窗期,新程式碼會去查一張還不存在的表或欄位,直接報
no such table/no such column。「先遷移、後部署」才能避免這段空窗。
10. 與 Drizzle ORM 搭配的遷移思路
如果你用 Drizzle ORM(目前與 D1 整合最完整的 ORM),遷移思路會反過來:你不手寫 SQL 遷移檔,而是改 TypeScript 定義的 schema,再讓 drizzle-kit 自動幫你「diff」出遷移 SQL。
// src/schema.ts:用 TypeScript 定義 schema(這是唯一真相來源)
import { sqliteTable, text, integer, index } from "drizzle-orm/sqlite-core";
import { sql } from "drizzle-orm";
export const users = sqliteTable("users", {
id: integer("id").primaryKey({ autoIncrement: true }),
email: text("email").notNull().unique(),
name: text("name").notNull(),
active: integer("active", { mode: "boolean" }).notNull().default(true),
createdAt: text("created_at").notNull().default(sql`(datetime('now'))`),
}, (t) => ({
emailIdx: index("idx_users_email").on(t.email),
}));
流程是:改 schema.ts → drizzle-kit generate 產生遷移檔到 migrations/ → 一樣用 wrangler d1 migrations apply 套用:
# 1. drizzle-kit 比對 schema.ts 與現有遷移,自動產生新的遷移 SQL
npx drizzle-kit generate
# 產生 migrations/0001_xxx.sql(Drizzle 命名的遷移檔)
# 2. 用熟悉的 wrangler 指令套用(先本地後遠端)
wrangler d1 migrations apply my-app-db --local
wrangler d1 migrations apply my-app-db --remote
要讓 drizzle-kit generate 的產物被 wrangler d1 migrations apply 正確吃到,drizzle.config.ts 的 out 要指向同一個 migrations/ 目錄。核心觀念不變:遷移檔仍是那疊有編號、不可變、可重播的變更,只是「產生方式」從手寫 SQL 變成 Drizzle 自動 diff 而已。
常見錯誤與最佳實踐
坑一:手動 execute 改 schema,不留任何遷移紀錄。
臨時用 wrangler d1 execute --command="ALTER TABLE ..." 改 production schema,當下能動,但這次變更沒有進 migrations/、沒進 Git、沒進 d1_migrations 追蹤表。結果就是:同事的環境、你的本地、production 三者 schema 悄悄分岔,某天重建資料庫時,這個手動改的欄位憑空消失。正確做法:任何 schema 變更都走遷移檔,execute 只用來查詢或灌開發用假資料。
坑二:只套了 --local,忘了 --remote。
本地開發一切正常,一部署上線就 no such table / no such column——因為遠端資料庫根本沒跑過那次遷移。本地和遠端各有一份 d1_migrations,必須分別套用:
# ❌ 常見疏漏:只套本地就以為完成了
wrangler d1 migrations apply my-app-db --local
# ✅ 正確:本地驗證後,遠端也要套(或交給 CI 自動做)
wrangler d1 migrations apply my-app-db --local
wrangler d1 migrations apply my-app-db --remote
最穩的做法是把 --remote 套用放進 CI/CD,由 pipeline 在部署前自動執行,人就不會漏。
坑三:回頭編輯已套用的遷移檔。
改錯了想修,直接去編輯 0002_add_posts.sql 的內容——這是最致命的錯。D1 用檔名判斷遷移套過沒,已套用的檔案再改內容也不會被重新執行(直接跳過),還會讓本地(重建時套到新內容)與遠端(還是舊內容)永久分岔。正確做法是往前修:新建更高編號的遷移檔來修正:
# ❌ 錯誤:直接改已套用的 0002_add_posts.sql
# ✅ 正確:新建 0004 往前修(補欄位 / 改索引)
wrangler d1 migrations create my-app-db fix_posts_add_excerpt
-- migrations/0004_fix_posts_add_excerpt.sql
-- 用新遷移修正:補上當初漏掉的 excerpt 欄位
ALTER TABLE posts ADD COLUMN excerpt TEXT;
坑四:破壞性變更沒想清楚,資料回不來。
DROP TABLE、DROP COLUMN、DELETE FROM 這類破壞性變更一旦套到 production,資料就沒了。SQLite 對 ALTER TABLE DROP COLUMN 的支援也有限制,重構有外鍵關聯的表時,可能要先暫時停用外鍵檢查:
-- migrations/0005_restructure.sql
-- 重組有外鍵關聯的表時,先延遲外鍵檢查(交易結束自動重置)
PRAGMA defer_foreign_keys = true;
DROP TABLE IF EXISTS post_tags;
CREATE TABLE post_tags (
post_id INTEGER NOT NULL,
tag_id INTEGER NOT NULL,
PRIMARY KEY (post_id, tag_id),
FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE,
FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE
);
破壞性變更前,務必先確認 D1 的 Time Travel(時間點還原)能救回資料(免費方案保留 7 天、付費 30 天),必要時先手動 wrangler d1 export 備份。
最佳實踐小結:把握 Migrations 的心法——任何 schema 變更一律走遷移檔(絕不手動 execute 改結構);遷移檔不可變,要修就新建更高編號往前修;先 --local 驗證、再 --remote 上線,兩者各自一份追蹤表;把 --remote 套用放進 CI,且順序是「先遷移、後部署」;固定基礎資料放進遷移、開發假資料用獨立 seed 檔;破壞性變更前先想清楚、留好備份。守住這幾條,你的 schema 就能像程式碼一樣,安全、可追蹤地演進。
小結
上一篇《D1 入門:Serverless SQLite》,我們學會在 Workers 裡用熟悉的 SQL 建表、查資料、做原子交易;這一篇,我們補上讓資料表結構安全演進的關鍵機制——D1 Migrations:
- Migrations 是什麼——一疊有編號、不可變、可重播的 SQL 變更檔,是「資料表結構的 Git」;D1 用自動維護的
d1_migrations追蹤表記錄哪些套過了,只套用尚未套用的。 - 三個核心指令——
migrations create建檔(自動加編號)、apply套用(依序、只套未套用的)、list查狀態。 - 本地 vs 遠端——
--local與--remote各自一份追蹤表,標準流程是先本地驗證、再遠端上線;最穩是把--remote放進 CI,順序「先遷移、後部署」。 - 種子資料與 ORM——固定基礎資料放進遷移、開發假資料用獨立 seed 檔;搭配 Drizzle ORM 時改 TypeScript schema、用
drizzle-kit generate自動產生遷移,再用 wrangler 套用。
到這裡,你的 D1 資料庫已經能安全地建立、查詢、並隨需求演進 schema 了。但當你的應用開始擴張到全球、讀取流量變大,你會想:能不能讓不同地區的使用者就近讀到資料、而不用每次都繞回主要資料庫?這正是 D1 的 **Sessions API 與讀取複本(Read Replication)**要解決的。下一篇《D1 Sessions API 與讀取複本》,我們就來看 D1 如何在全球六個地區部署唯讀複本、用書籤(Bookmark)機制在低延遲與一致性之間取得平衡。
想先查閱官方對 D1 Migrations 的完整說明,可以隨時參考 Cloudflare D1 Migrations 官方文件。schema 版本控管這一課已就緒,我們下一篇《D1 Sessions API 與讀取複本》見。