讓 AI 幫你寫 SQL Migration:多租戶資料庫的批次變更
如果你還不熟悉多租戶架構的概念,建議先閱讀: 📖 多租戶架構生存指南:一個資料庫一個機構的愛與愁
🎯 前言
手動對 20 個資料庫跑 migration,聽起來就讓人頭痛。
在多租戶 database-per-tenant 的架構下,每次需要變更資料表結構,就意味著同樣的 SQL 要在每個機構的資料庫各跑一次。不只數量多,還要面對:某些機構的資料庫結構因歷史原因已有差異、SQL 跑到一半失敗要怎麼處理、如何確認所有資料庫都成功了。
我用 Claude Code 輔助這個流程有一段時間了,最大的體會是:AI 的價值不是讓你省略思考,而是讓草稿品質提升,使 code review 的速度變快。
一個自己手寫可能要 2 小時的 migration SQL,Claude 可以在 5 分鐘內給你一個完整的草稿(包含 up/down 腳本),你花 20 分鐘 review 就好。
🏗️ 情境說明
我們的系統(AppSystem)採用 database-per-tenant 架構:
- 每個機構一個 MySQL 資料庫(
demo-org-1、demo-org-2… 目前 20+ 個) - MySQL 版本:5.7(注意:這個版本有許多限制)
- 正式環境不能停機,migration 必須在線執行
- 各機構資料庫因歷史原因,部分資料表結構不完全相同
這幾個條件加在一起,讓「跑 migration」這件事比想像中複雜得多。
🤝 讓 Claude Code 起草 Migration SQL
準備資料表結構
要讓 Claude 產生精確的 migration SQL,首先要給它足夠的 context。
我:
請幫我產生一個 migration SQL。
我需要在所有機構的 demo_table 資料表新增一個欄位 config_json。
以下是目前的資料表結構(來自 demo-org-1):
[貼上 SHOW CREATE TABLE 的輸出]
請同時輸出:
1. up.sql(新增欄位)
2. down.sql(回滾)
3. 說明這個 migration 做了什麼、可能的風險
注意:MySQL 版本是 5.7,請確認語法相容。
把 SHOW CREATE TABLE 的完整輸出貼給 Claude,它就能理解目前的結構、現有欄位的命名慣例、engine 和 charset 設定。
Claude 的輸出格式
好的 Claude 輸出應該包含三個部分:
-- ============================================================
-- migration: add_config_json_to_demo_table
-- 說明:新增 config_json 欄位,用於儲存各機構的客製化設定
-- MySQL 版本:5.7
-- 風險評估:低(新增可為空欄位,不影響現有資料)
-- ============================================================
-- up.sql
ALTER TABLE `demo_table`
ADD COLUMN `config_json` TEXT NULL COMMENT '機構客製化設定(JSON 格式)'
AFTER `updated_at`;
-- ============================================================
-- down.sql
ALTER TABLE `demo_table`
DROP COLUMN `config_json`;
注意:如果 Claude 輸出的語法有問題,很可能是 MySQL 5.7 相容性問題,下一節會說明常見錯誤。
❌ AI 常犯的 MySQL 5.7 相容性錯誤
這是我踩過坑整理出來的清單,每次使用 Claude 生成 migration 都要特別留意:
錯誤 1:ADD COLUMN IF NOT EXISTS
-- ❌ MySQL 8.0 才有的語法,5.7 不支援
ALTER TABLE `demo_table`
ADD COLUMN IF NOT EXISTS `config_json` TEXT NULL;
-- ✅ MySQL 5.7 要用 information_schema 判斷(見下一節)
錯誤 2:Window Functions
-- ❌ MySQL 5.7 不支援 window functions
SELECT id, ROW_NUMBER() OVER (PARTITION BY org_id ORDER BY created_at) AS rn
FROM demo_table;
-- ✅ 改用子查詢或 user-defined variables
錯誤 3:utf8 vs utf8mb4
-- ❌ 新欄位用 utf8 會有 emoji 儲存問題
ADD COLUMN `note` VARCHAR(255) CHARACTER SET utf8;
-- ✅ 應該用 utf8mb4
ADD COLUMN `note` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
錯誤 4:Generated Columns 語法
-- ❌ MySQL 5.7 的 generated column 語法與 8.0 不同
-- 且 5.7 的 virtual generated column 有 index 限制
解法:在 CLAUDE.md 中明確記錄 MySQL 版本限制,讓 Claude 每次都會注意到。
✅ 讓 Migration 冪等的模式
在多租戶環境中,migration 必須是冪等的(idempotent):跑第二次也不會出錯。原因是:
- 某個機構的 migration 失敗,修復後重跑
- 不確定哪些已跑過,想全部重跑確認
MySQL 5.7 不支援 ADD COLUMN IF NOT EXISTS,但可以用 information_schema 繞過:
-- MySQL 5.7 冪等 ADD COLUMN 的標準模式
SET @dbname = DATABASE();
SET @tablename = 'demo_table';
SET @columnname = 'config_json';
SET @sql = (
SELECT IF(
COUNT(*) = 0,
CONCAT(
'ALTER TABLE `', @tablename, '` ',
'ADD COLUMN `', @columnname, '` TEXT NULL ',
'COMMENT ''機構客製化設定(JSON 格式)'' ',
'AFTER `updated_at`'
),
'SELECT ''Column already exists, skipping.'''
)
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @dbname
AND TABLE_NAME = @tablename
AND COLUMN_NAME = @columnname
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
這個 PREPARE/EXECUTE 模式是 MySQL 5.7 中處理條件式 DDL 的標準方式。把它納入 CLAUDE.md 範例,Claude 就會在產生 migration 時自動套用。
🛠️ 批次執行腳本
有了好的 migration SQL,接下來要建立能安全批次執行的腳本。
#!/bin/bash
# run_migration.sh - 跨所有機構執行 migration SQL
# 使用方式:./run_migration.sh --file up.sql [--dry-run]
set -euo pipefail
MYSQL_HOST="${MYSQL_HOST:-127.0.0.1}"
MYSQL_PORT="${MYSQL_PORT:-3306}"
MYSQL_USER="${MYSQL_USER:-dbuser}"
MYSQL_PASS="${MYSQL_PASS:-dbpass}"
DRY_RUN=false
SQL_FILE=""
# 解析參數
while [[ $# -gt 0 ]]; do
case $1 in
--file) SQL_FILE="$2"; shift 2 ;;
--dry-run) DRY_RUN=true; shift ;;
*) echo "未知參數:$1"; exit 1 ;;
esac
done
if [[ -z "$SQL_FILE" ]]; then
echo "錯誤:請指定 --file 參數"
exit 1
fi
# 取得所有機構資料庫清單(從系統主庫查詢)
DATABASES=$(mysql -h"$MYSQL_HOST" -P"$MYSQL_PORT" -u"$MYSQL_USER" -p"$MYSQL_PASS" \
-N -e "SELECT db_name FROM system_main.org_list WHERE is_active = 1 ORDER BY db_name;")
SUCCESS_LIST=()
FAIL_LIST=()
echo "================================================"
echo "Migration 腳本:$SQL_FILE"
echo "模式:$([ "$DRY_RUN" = true ] && echo 'DRY RUN(不實際執行)' || echo '實際執行')"
echo "目標資料庫數量:$(echo "$DATABASES" | wc -l | tr -d ' ')"
echo "================================================"
echo ""
for DB in $DATABASES; do
if [ "$DRY_RUN" = true ]; then
echo "[DRY RUN] $DB"
SUCCESS_LIST+=("$DB")
continue
fi
echo -n "執行中:$DB ... "
if mysql -h"$MYSQL_HOST" -P"$MYSQL_PORT" -u"$MYSQL_USER" -p"$MYSQL_PASS" \
--database="$DB" < "$SQL_FILE" 2>/tmp/migration_error_"$DB".log; then
echo "✅ 成功"
SUCCESS_LIST+=("$DB")
else
echo "❌ 失敗"
FAIL_LIST+=("$DB")
# 失敗不中斷,繼續下一個資料庫
fi
done
# 輸出摘要報告
echo ""
echo "================================================"
echo "執行完成"
echo "成功:${#SUCCESS_LIST[@]} 個"
echo "失敗:${#FAIL_LIST[@]} 個"
if [ ${#FAIL_LIST[@]} -gt 0 ]; then
echo ""
echo "失敗的資料庫:"
for DB in "${FAIL_LIST[@]}"; do
echo " - $DB"
echo " 錯誤訊息:$(cat /tmp/migration_error_"$DB".log)"
done
exit 1
fi
執行流程
# 第一步:dry-run,確認腳本沒有語法問題
./run_migration.sh --file up.sql --dry-run
# 第二步:實際執行
./run_migration.sh --file up.sql
# 第三步:執行驗證查詢(見下方)
mysql -u dbuser -pdbpass < verify.sql
🔄 回滾策略
MySQL DDL 操作(ALTER TABLE)不支援 transaction 回滾。BEGIN / ROLLBACK 對 DDL 無效。
這意味著每個 migration 的回滾必須靠 down.sql 手動執行:
migrations/
├── 20260211_add_config_json/
│ ├── up.sql ← 正向 migration
│ ├── down.sql ← 回滾腳本
│ └── verify.sql ← 驗證腳本
重要原則:
- 每個 migration 寫完後,立刻驗證 down.sql 是否真的能回滾
ADD COLUMN的 down 是DROP COLUMN,但方向要對- 如果是
MODIFY COLUMN(改型別),down.sql 要保留原始型別定義
📊 實戰案例:新增 config_json 欄位
以下完整走一遍,讓 AppSystem 的所有機構資料庫新增 config_json 欄位。
Step 1:取得資料表結構
SHOW CREATE TABLE `demo_table`\G
輸出結果貼給 Claude。
Step 2:Claude 產生 migration
-- migrations/20260211_add_config_json/up.sql
-- 冪等模式:欄位已存在時跳過
SET @dbname = DATABASE();
SET @sql = (
SELECT IF(
COUNT(*) = 0,
'ALTER TABLE `demo_table` ADD COLUMN `config_json` TEXT NULL COMMENT ''機構客製化設定'' AFTER `updated_at`',
'SELECT ''config_json already exists, skipping'''
)
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @dbname
AND TABLE_NAME = 'demo_table'
AND COLUMN_NAME = 'config_json'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- migrations/20260211_add_config_json/down.sql
ALTER TABLE `demo_table` DROP COLUMN `config_json`;
-- migrations/20260211_add_config_json/verify.sql
-- 驗證所有機構都有這個欄位
SELECT
TABLE_SCHEMA AS '資料庫',
COLUMN_NAME AS '欄位',
COLUMN_TYPE AS '型別',
IS_NULLABLE AS '允許空值'
FROM information_schema.COLUMNS
WHERE TABLE_NAME = 'demo_table'
AND COLUMN_NAME = 'config_json'
AND TABLE_SCHEMA LIKE 'demo-org-%'
ORDER BY TABLE_SCHEMA;
Step 3:執行與驗證
# Dry run
./run_migration.sh --file migrations/20260211_add_config_json/up.sql --dry-run
# 實際執行
./run_migration.sh --file migrations/20260211_add_config_json/up.sql
# 驗證(應該顯示 20+ 筆,每個機構各一筆)
mysql -u dbuser -pdbpass < migrations/20260211_add_config_json/verify.sql
💡 提示技巧
在 CLAUDE.md 中記錄 MySQL 版本限制
## MySQL 限制(5.7)
重要:產生 migration SQL 時必須遵守以下限制:
- 不支援 `ADD COLUMN IF NOT EXISTS`,改用 information_schema + PREPARE/EXECUTE
- 不支援 window functions(ROW_NUMBER、RANK 等)
- 不支援 JSON_TABLE
- DDL 不支援 transaction 回滾
- 使用 utf8mb4 而非 utf8(避免 emoji 問題)
冪等 ADD COLUMN 標準模式:[貼上上方的 PREPARE/EXECUTE 範例]
Few-Shot 範例
每次請 Claude 產生 migration 時,附上一個已知正確的範例:
以下是一個符合我們專案規範的 migration 範例:
[貼上之前確認過可用的 migration SQL]
請依照相同模式,幫我產生 [新的需求] 的 migration。
這個 few-shot 範例能大幅降低 Claude 犯相容性錯誤的機率。
🎉 結語
在多租戶架構中跑 migration 從來就不是一件容易的事,但 Claude Code 讓這個流程變得更有紀律:
- 起草品質提升:不再從空白開始寫,Claude 的草稿已經考慮到 up/down/說明
- Review 更快:有了明確的 few-shot 範例和 CLAUDE.md 限制記錄,Claude 的錯誤率降低
- 流程標準化:每次 migration 都有相同的目錄結構和腳本模式
AI 不會消除 migration 的風險,但能讓你把心力集中在真正重要的事:仔細 review 變更、確認回滾策略、在非尖峰時段執行。
📎 相關文章: