340 lines
9.7 KiB
Markdown
340 lines
9.7 KiB
Markdown
|
|
---
|
|||
|
|
tags: [GORM, Go, ORM, 迁移, AutoMigrate, Migrator, 版本化]
|
|||
|
|
create time: 2026-04-28 00:00
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 迁移工具
|
|||
|
|
|
|||
|
|
## 概述
|
|||
|
|
|
|||
|
|
数据库迁移是应用演进的基础设施——每次修改模型结构都需要将变更同步到数据库。GORM 提供了原生的 `AutoMigrate`,但对于生产环境来说,通常推荐使用**版本化的迁移脚本**来获得更细粒度的控制和回滚能力。
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
flowchart LR
|
|||
|
|
Code["Go struct 变更"] --> AutoMigrate["AutoMigrate<br/>自动同步 schema"]
|
|||
|
|
Code --> MigrationFile["版本化 SQL 文件<br/> goose / golang-migrate"]
|
|||
|
|
|
|||
|
|
AutoMigrate --> Dev{"使用场景?"}
|
|||
|
|
MigrationFile --> Dev
|
|||
|
|
|
|||
|
|
Dev --> |快速原型/个人项目| Quick["✅ AutoMigrate 够用"]
|
|||
|
|
Dev --> |团队协作/生产环境| Strict["✅ 版本化迁移方案"]
|
|||
|
|
|
|||
|
|
style Code fill:#4FC08D,color:#fff
|
|||
|
|
style Quick fill:#3B82F6,color:#fff
|
|||
|
|
style Strict fill:#EAB308,color:#fff
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## AutoMigrate — 一键建表
|
|||
|
|
|
|||
|
|
### 基本用法
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
func migrate(db *gorm.DB) error {
|
|||
|
|
return db.AutoMigrate(
|
|||
|
|
&User{},
|
|||
|
|
&Order{},
|
|||
|
|
&Product{},
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
AutoMigrate 会在以下情况下自动操作:
|
|||
|
|
- **表不存在** → CREATE TABLE
|
|||
|
|
- **列不存在** → ALTER TABLE ADD COLUMN
|
|||
|
|
- **列类型不匹配** → ALTER TABLE MODIFY COLUMN
|
|||
|
|
- **索引不存在** → CREATE INDEX
|
|||
|
|
|
|||
|
|
### 增量更新
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// 第一次:只有 User 和 Order
|
|||
|
|
db.AutoMigrate(&User{}, &Order{})
|
|||
|
|
|
|||
|
|
// 第二次:给 User 加了 Email 字段
|
|||
|
|
type User struct {
|
|||
|
|
ID uint
|
|||
|
|
Name string
|
|||
|
|
Email string `gorm:"size:128;uniqueIndex"` // 新增字段
|
|||
|
|
}
|
|||
|
|
db.AutoMigrate(&User{})
|
|||
|
|
// 不会删除已有的字段,只会添加新列和索引
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> [!warning] AutoMigrate 的限制
|
|||
|
|
> - **不能删除**不再出现在 struct 中的列(需要手动处理)
|
|||
|
|
> - **不能重命名**列(需要先删除再创建)
|
|||
|
|
> - **不能修改主键**定义
|
|||
|
|
> - MySQL 下 `ALTER TABLE MODIFY COLUMN` 可能会重建整张表(锁表!)
|
|||
|
|
> - 某些复杂的类型变更(如 varchar 改 int)可能不被支持
|
|||
|
|
|
|||
|
|
### DryRun 预览变更
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// 查看 AutoMigrate 会做什么,但不执行
|
|||
|
|
err := db.Session(&gorm.Session{DryRun: true}).AutoMigrate(&User{})
|
|||
|
|
if err != nil {
|
|||
|
|
log.Printf("AutoMigrate 将产生的 SQL 错误(仅 DryRun): %v", err)
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> [!tip] DryRun + AutoMigrate 的组合
|
|||
|
|
> DryRun 模式下的 AutoMigrate 会构建 SQL 语句但**不执行**。结合这个特性可以做迁移前校验——在 CI 中检查新模型是否会导致破坏性变更。
|
|||
|
|
|
|||
|
|
## Migrator 接口 —— 细粒度控制
|
|||
|
|
|
|||
|
|
GORM 提供了 `Migrator` 接口来访问底层数据库的迁移能力:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
migrator := db.Migrator()
|
|||
|
|
|
|||
|
|
// 检查表是否存在
|
|||
|
|
if migrator.HasTable(&User{}) {
|
|||
|
|
fmt.Println("users 表已存在")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查列是否存在
|
|||
|
|
if migrator.HasColumn(&User{}, "email") {
|
|||
|
|
fmt.Println("email 列已存在")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取列的类型信息
|
|||
|
|
column, err := migrator.ColumnType(&User{}, "email")
|
|||
|
|
fmt.Printf("列名: %s, 类型: %s, 长度: %d\n",
|
|||
|
|
column.Name(), column.DatabaseTypeName(), column.Length())
|
|||
|
|
|
|||
|
|
// 重命名列(如果驱动支持)
|
|||
|
|
migrator.RenameColumn(&User{}, "name", "username")
|
|||
|
|
|
|||
|
|
// 删除列
|
|||
|
|
migrator.DropColumn(&User{}, "old_field")
|
|||
|
|
|
|||
|
|
// 创建/删除索引
|
|||
|
|
migrator.CreateIndex(&User{}, "IdxEmail")
|
|||
|
|
migrator.HasIndex(&User{}, "IdxEmail")
|
|||
|
|
migrator.DropIndex(&User{}, "IdxEmail")
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> [!note] 不同驱动的 Migrator 实现
|
|||
|
|
> 每个驱动都有自己的 `Migrator` 实现。不是所有操作在所有数据库上都受支持:
|
|||
|
|
> ```go
|
|||
|
|
> // 检查某个驱动是否支持特定操作
|
|||
|
|
> if migrator, ok := db.Migrator().(*mysql.Migrator); ok {
|
|||
|
|
> // MySQL 特有的迁移功能
|
|||
|
|
> }
|
|||
|
|
> ```
|
|||
|
|
|
|||
|
|
## 版本化迁移方案(生产推荐)
|
|||
|
|
|
|||
|
|
在生产环境中,推荐使用专门的迁移工具配合 GORM:
|
|||
|
|
|
|||
|
|
### 方案一:Goose
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# 安装
|
|||
|
|
go install github.com/pressly/goose/v3/cmd/goose@latest
|
|||
|
|
|
|||
|
|
# 创建迁移文件
|
|||
|
|
goose create add_user_email sql
|
|||
|
|
|
|||
|
|
# 生成文件:20260101_001_add_user_email.sql
|
|||
|
|
-- +goose Up
|
|||
|
|
ALTER TABLE users ADD COLUMN email VARCHAR(128) UNIQUE;
|
|||
|
|
ALTER TABLE users ADD COLUMN email_verified BOOLEAN DEFAULT FALSE;
|
|||
|
|
|
|||
|
|
-- +goose Down
|
|||
|
|
ALTER TABLE users DROP COLUMN email;
|
|||
|
|
ALTER TABLE users DROP COLUMN email_verified;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
与 GORM 集成:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
func migrateDB(db *gorm.DB) {
|
|||
|
|
// 先让 GORM 做必要的表存在性检查和基础初始化
|
|||
|
|
db.AutoMigrate(&User{})
|
|||
|
|
|
|||
|
|
// 然后用 goose 执行版本化迁移脚本
|
|||
|
|
goose.Run("up", dsn)
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 方案二:golang-migrate
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# 安装
|
|||
|
|
go install github.com/golang-migrate/migrate/v4/cmd/migrate@latest
|
|||
|
|
|
|||
|
|
# 创建迁移
|
|||
|
|
migrate create -ext sql -dir migrations -seq add_user_email
|
|||
|
|
|
|||
|
|
# migrations/001_add_user_email.sql
|
|||
|
|
CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -- PG 特有
|
|||
|
|
|
|||
|
|
ALTER TABLE users ADD COLUMN IF NOT EXISTS email VARCHAR(128);
|
|||
|
|
ALTER TABLE users ADD CONSTRAINT unique_email UNIQUE (email);
|
|||
|
|
|
|||
|
|
-- <向下迁移>
|
|||
|
|
ALTER TABLE users DROP COLUMN IF EXISTS email;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// 启动时执行迁移
|
|||
|
|
func initDB() {
|
|||
|
|
m, err := migrate.New(
|
|||
|
|
"migrations/file://./migrations",
|
|||
|
|
dsn,
|
|||
|
|
)
|
|||
|
|
if err != nil {
|
|||
|
|
log.Fatal(err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if err := m.Up(); err != nil && err != migrate.ErrNoChange {
|
|||
|
|
log.Fatal(err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 迁移成功后才连接 GORM
|
|||
|
|
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 方案三:自建轻量级迁移表
|
|||
|
|
|
|||
|
|
适合小型项目——用一个表记录当前迁移版本号:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
type MigrationRecord struct {
|
|||
|
|
ID uint `gorm:"primaryKey"`
|
|||
|
|
Version string `gorm:"size:32;uniqueIndex;not null"`
|
|||
|
|
Applied time.Time
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 迁移脚本库
|
|||
|
|
var migrations = []struct {
|
|||
|
|
Version string
|
|||
|
|
SQL string
|
|||
|
|
}{
|
|||
|
|
{"v0.1", "CREATE TABLE IF NOT EXISTS users (id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(64) NOT NULL)"},
|
|||
|
|
{"v0.2", "ALTER TABLE users ADD COLUMN email VARCHAR(128) UNIQUE"},
|
|||
|
|
{"v0.3", "ALTER TABLE users ADD COLUMN created_at DATETIME DEFAULT CURRENT_TIMESTAMP"},
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func ApplyMigrations(tx *gorm.DB) error {
|
|||
|
|
var appliedVersions []string
|
|||
|
|
tx.Model(&MigrationRecord{}).Pluck("version", &appliedVersions)
|
|||
|
|
|
|||
|
|
for _, m := range migrations {
|
|||
|
|
if contains(appliedVersions, m.Version) {
|
|||
|
|
continue // 已执行过,跳过
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
log.Printf("执行迁移: %s", m.Version)
|
|||
|
|
if err := tx.Exec(m.SQL).Error; err != nil {
|
|||
|
|
return fmt.Errorf("迁移 %s 失败: %w", m.Version, err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
tx.Create(&MigrationRecord{Version: m.Version, Applied: time.Now()})
|
|||
|
|
}
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> [!question] 为什么生产环境不推荐只用 AutoMigrate?
|
|||
|
|
|
|||
|
|
| 维度 | AutoMigrate | 版本化迁移 |
|
|||
|
|
|------|------------|-----------|
|
|||
|
|
| 可预测性 | GORM 内部决定执行顺序 | SQL 完全可控 |
|
|||
|
|
| 回滚能力 | ❌ 无内置回滚 | ✅ 有 Down 脚本 |
|
|||
|
|
| 团队协作 | 容易冲突(各自改了 model 就 auto) | 合并 SQL 文件 |
|
|||
|
|
| 数据库差异 | 自动处理方言 | 手写 SQL 需考虑目标 DB |
|
|||
|
|
| CI/CD 集成 | 难判断是否有未执行的变更 | migration_version 表可做 gate |
|
|||
|
|
| 审计追踪 | ❌ | ✅ 谁在什么时候改了什么 |
|
|||
|
|
|
|||
|
|
## 迁移策略对比
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
flowchart TD
|
|||
|
|
ProjectSize{"项目规模?"}
|
|||
|
|
|
|||
|
|
ProjectSize --> |个人项目/<br/>快速原型| Small["AutoMigrate + DryRun 验证"]
|
|||
|
|
ProjectSize --> |小型团队/<br/>单一数据库| Medium["自建迁移版本表<br/>+ AutoMigrate 兜底"]
|
|||
|
|
ProjectSize --> |企业级/<br/>多环境/多数据库| Large["专业迁移工具<br/>goose / migrate"]
|
|||
|
|
|
|||
|
|
style Small fill:#A0AEC0,color:#fff
|
|||
|
|
style Medium fill:#3B82F6,color:#fff
|
|||
|
|
style Large fill:#4FC08D,color:#fff
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 迁移最佳实践
|
|||
|
|
|
|||
|
|
### 1. 幂等性设计
|
|||
|
|
|
|||
|
|
迁移脚本应该能够重复执行而不报错:
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
-- ✅ 幂等写法
|
|||
|
|
ALTER TABLE users ADD COLUMN IF NOT EXISTS phone VARCHAR(20);
|
|||
|
|
CREATE INDEX IF NOT EXISTS idx_users_phone ON users(phone);
|
|||
|
|
|
|||
|
|
-- ❌ 非幂等——重复执行会报 "column already exists" 错误
|
|||
|
|
ALTER TABLE users ADD COLUMN phone VARCHAR(20);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 2. 大表加列的最佳方式
|
|||
|
|
|
|||
|
|
对线上大表(百万级以上)直接 `ALTER TABLE ADD COLUMN` 会锁表导致服务不可用:
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
-- 推荐方式:分步骤进行
|
|||
|
|
-- Step 1: 添加可为 NULL 的新列(不阻塞写入)
|
|||
|
|
ALTER TABLE large_table ADD COLUMN new_column INT;
|
|||
|
|
|
|||
|
|
-- Step 2: 后台任务分批回填数据
|
|||
|
|
-- UPDATE large_table SET new_column = default_val WHERE ... LIMIT 10000;
|
|||
|
|
-- (循环直到全部回填)
|
|||
|
|
|
|||
|
|
-- Step 3: 加上约束(此时数据已经合规了)
|
|||
|
|
ALTER TABLE large_table MODIFY COLUMN new_column INT NOT NULL DEFAULT 0;
|
|||
|
|
ALTER TABLE large_table ADD INDEX idx_new_col (new_column);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3. CI Pipeline 中的迁移检查
|
|||
|
|
|
|||
|
|
```yaml
|
|||
|
|
# .github/workflows/db-check.yml
|
|||
|
|
jobs:
|
|||
|
|
check-migrations:
|
|||
|
|
runs-on: ubuntu-latest
|
|||
|
|
services:
|
|||
|
|
mysql:
|
|||
|
|
image: mysql:8.0
|
|||
|
|
env:
|
|||
|
|
MYSQL_ROOT_PASSWORD: root
|
|||
|
|
ports:
|
|||
|
|
- 3306:3306
|
|||
|
|
steps:
|
|||
|
|
- uses: actions/checkout@v4
|
|||
|
|
|
|||
|
|
- name: Check AutoMigrate consistency
|
|||
|
|
run: |
|
|||
|
|
# 创建一个空的临时 DB,运行 AutoMigrate,然后比较 schema
|
|||
|
|
go run ./cmd/check-schema ${{ secrets.DB_DSN }}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 常见坑点速查
|
|||
|
|
|
|||
|
|
| 问题 | 原因 | 解决方案 |
|
|||
|
|
|------|------|---------|
|
|||
|
|
| AutoMigrate 删不掉旧字段 | GORM 设计上不支持 drop | 手动写迁移脚本或手动 DROP |
|
|||
|
|
| 大表 ALTER 导致服务中断 | InnoDB 全表重建 | 用 pt-online-schema-change 或 gh-ost |
|
|||
|
|
| 迁移脚本不幂等 | 重复部署报错 | 使用 IF NOT EXISTS 条件判断 |
|
|||
|
|
| 本地环境与生产环境不一致 | 不同数据库方言 | Docker 容器里跑相同版本的 DB |
|
|||
|
|
| 忘记执行 Down 脚本 | 无法回滚 | 建立迁移脚本审查流程 |
|
|||
|
|
|
|||
|
|
## 关联笔记
|
|||
|
|
|
|||
|
|
- [[01-安装与初始化]]
|
|||
|
|
- [[02-模型定义]]
|
|||
|
|
- [[08-事务管理]]
|
|||
|
|
- [[13-多数据库支持]]
|