数据库迁移
本目录包含数据库迁移文件和迁移工具。
⚠️ 重要说明:迁移不会覆盖数据
常见误解:每次启动都执行迁移,会不会覆盖线上数据?
答案:不会! 迁移使用 版本控制机制:
┌─────────────────────────────────────────┐
│ schema_migrations 表(自动创建) │
├─────────────────────────────────────────┤
│ version │ dirty │
│ 1 │ false ← 已执行版本 │
└─────────────────────────────────────────┘
第 1 次启动 → 执行 v1 迁移 → 记录 version=1 ✅
第 2 次启动 → 检查 version=1 → 跳过(0 SQL 执行)✅
第 3 次启动 → 检查 version=1 → 跳过(0 SQL 执行)✅
新版本发布 → 检查 version=1 → 仅执行 v2 → 记录 version=2 ✅
关键点:
- ✅ 迁移是增量的,不是全量的
- ✅ 每个版本只执行一次
- ✅ 后续启动会跳过已执行的版本
- ✅ 不会删除或覆盖现有数据
旧库升级:children/guardianships 到 profiles/profile_links
2026-05 的 UC 重构把运行时表切到 profiles / profile_links。已经执行过旧版
000001 的数据库物理表仍是 children / guardianships,不会因为后来修改了
000001_init_schema.up.sql 自动变化。
因此 000002_add_profile_links_profile_id_index 同时承担桥接职责:
- 若
profiles / profile_links 不存在,先创建 v2 运行时表;
- 若旧表
children 存在,把儿童档案复制到 profiles;
- 若旧表
guardianships 存在,把监护关系复制到 profile_links,旧 guardian 关系映射为 v2 的 other;
- 不删除
children / guardianships,上线后保留作审计和回滚依据;
- 再幂等补充
idx_profile_id 索引。
如果某个环境已经用旧的 000002 启动失败,schema_migrations 可能处于
version=2, dirty=1。只有在确认失败点是“profile_links 不存在导致旧 000002
创建索引失败”,且没有手工执行过新 000002 的部分语句时,才可以在备份后把版本恢复为
version=1, dirty=0,再使用包含本修复的镜像重新启动迁移。
📁 目录结构
migration/
├── migrate.go # 迁移工具实现
├── migrations/ # 迁移 SQL 文件(嵌入到二进制)
│ ├── 000001_init_schema.up.sql # 初始化表结构
│ ├── 000001_init_schema.down.sql # 回滚表结构
│ ├── 000005_bootstrap_system_data.up.sql # 最小系统初始化数据
│ ├── 000005_bootstrap_system_data.down.sql # 回滚最小系统初始化数据
│ └── ...
└── README.md # 本文件
🚀 快速开始
1. 安装依赖
# 添加 golang-migrate 依赖
go get -u github.com/golang-migrate/migrate/v4
go get -u github.com/golang-migrate/migrate/v4/database/mysql
go get -u github.com/golang-migrate/migrate/v4/source/iofs
# (可选)安装 CLI 工具,用于创建迁移文件
go install -tags 'mysql' github.com/golang-migrate/migrate/v4/cmd/migrate@latest
2. 在应用中使用
package main
import (
"database/sql"
"fmt"
"github.com/FangcunMount/iam/v2/internal/pkg/migration"
_ "github.com/go-sql-driver/mysql"
)
func main() {
// 1. 连接数据库
db, err := sql.Open("mysql", "user:pass@tcp(localhost:3306)/iam")
if err != nil {
panic(err)
}
defer db.Close()
// 2. 配置迁移器
cfg := &migration.Config{
Enabled: true, // 启用自动迁移
Database: "iam", // 数据库名称
}
// 3. 创建迁移器并执行
migrator := migration.NewMigrator(db, cfg)
if version, applied, err := migrator.Run(); err != nil {
panic(err)
} else if applied {
fmt.Printf("migrated to version %d\n", version)
} else {
fmt.Printf("database already up to date (version %d)\n", version)
}
// 4. 启动应用...
}
3. 创建新的迁移
# 使用 migrate CLI 创建迁移文件
migrate create -ext sql -dir internal/pkg/migration/migrations -seq add_new_feature
# 生成的文件:
# - 000003_add_new_feature.up.sql (升级脚本)
# - 000003_add_new_feature.down.sql (回滚脚本)
编辑生成的文件:
000003_add_new_feature.up.sql:
-- 添加新功能
ALTER TABLE iam_users ADD COLUMN nickname VARCHAR(64) COMMENT '昵称';
000003_add_new_feature.down.sql:
-- 回滚新功能
ALTER TABLE iam_users DROP COLUMN nickname;
🔧 高级用法
手动控制迁移
// 获取当前版本
version, dirty, err := migrator.Version()
// 回滚最近的一次迁移
err = migrator.Rollback()
环境变量配置
# configs/apiserver.prod.yaml
mysql:
host: ${MYSQL_HOST:127.0.0.1}
port: ${MYSQL_PORT:3306}
database: ${MYSQL_DATABASE:iam}
username: ${MYSQL_USER:root}
password: ${MYSQL_PASSWORD:}
migration:
enabled: ${MIGRATION_ENABLED:true}
Docker 部署
# Dockerfile
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY . .
# 构建(迁移文件会被嵌入)
RUN go build -o /apiserver ./cmd/apiserver
# 运行阶段
FROM alpine:latest
COPY --from=builder /apiserver .
CMD ["./apiserver"]
容器启动时会自动执行迁移,无需手动操作。
📊 迁移表
golang-migrate 会自动创建 schema_migrations 表来追踪版本:
mysql> SELECT * FROM schema_migrations;
+---------+-------+
| version | dirty |
+---------+-------+
| 2 | 0 |
+---------+-------+
version: 当前数据库版本号
dirty: 是否处于中间状态(0=正常,1=异常需手动修复)
🔐 安全注意事项
生产环境
-
备份优先
# 迁移前自动备份
mysqldump iam > backup_$(date +%Y%m%d_%H%M%S).sql
-
权限分离
- 应用账号:只需 SELECT, INSERT, UPDATE, DELETE
- 迁移账号:需要 CREATE, DROP, ALTER 等 DDL 权限
-
测试迁移
-
金丝雀发布
开发环境
# 重置数据库到初始状态
cd scripts/sql
./reset-db.sh
# 应用会在启动时自动执行迁移
go run cmd/apiserver/apiserver.go
📚 参考文档
❓ 常见问题
Q: 为什么要使用迁移工具?
A: 在容器化环境中,应用只打包二进制文件。使用 embed.FS 可以将 SQL 文件嵌入到二进制中,启动时自动执行,无需挂载外部文件。
Q: 如何处理 dirty 状态?
A: Dirty 状态表示迁移中途失败。需要:
- 检查日志确定失败原因
- 手动修复数据库到一致状态
- 确认失败版本的 SQL 是否已经部分生效
- 只在数据库状态与目标版本一致时,才更新
schema_migrations
SELECT version, dirty FROM schema_migrations;
不要只把 dirty 改成 0 后重启。对于本轮已知的旧 000002 失败,如果确认失败发生在
CREATE INDEX idx_profile_id ON profile_links 且 profile_links 当时不存在,可以在备份后把
schema_migrations 恢复到 version=1, dirty=0,让修复后的 000002 重新执行。
Q: 生产环境如何禁用自动迁移?
A: 设置环境变量:
export MIGRATION_ENABLED=false
Q: 如何在 Kubernetes 中使用?
A: 使用 Init Container:
initContainers:
- name: migrate
image: your-app:latest
command: ["/app/migrate-only"] # 特殊命令只执行迁移
或者让应用启动时自动执行(推荐)。