vault backup: 2026-04-28 20:56:51
This commit is contained in:
+163
-57
@@ -13,21 +13,23 @@ create time: 2026-04-28 00:00
|
||||
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["✅ 版本化迁移方案"]
|
||||
|
||||
|
||||
AutoMigrate --> UseCase{"使用场景?"}
|
||||
MigrationFile --> UseCase
|
||||
|
||||
UseCase --> |快速原型/个人项目| Quick["✅ AutoMigrate 够用"]
|
||||
UseCase --> |团队协作/生产环境| Strict["✅ 版本化迁移方案"]
|
||||
|
||||
style Code fill:#4FC08D,color:#fff
|
||||
style Quick fill:#3B82F6,color:#fff
|
||||
style Strict fill:#EAB308,color:#fff
|
||||
```
|
||||
|
||||
## AutoMigrate — 一键建表
|
||||
## 正文
|
||||
|
||||
### 基本用法
|
||||
### AutoMigrate — 一键建表
|
||||
|
||||
#### 基本用法
|
||||
|
||||
```go
|
||||
func migrate(db *gorm.DB) error {
|
||||
@@ -39,13 +41,18 @@ func migrate(db *gorm.DB) error {
|
||||
}
|
||||
```
|
||||
|
||||
AutoMigrate 会在以下情况下自动操作:
|
||||
以上代码展示了最基础的用法。调用时传入所有需要管理的模型即可——GORM 会依次处理每个模型对应的表,并执行以下四种操作:
|
||||
|
||||
- **表不存在** → CREATE TABLE
|
||||
- **列不存在** → ALTER TABLE ADD COLUMN
|
||||
- **列类型不匹配** → ALTER TABLE MODIFY COLUMN
|
||||
- **索引不存在** → CREATE INDEX
|
||||
|
||||
### 增量更新
|
||||
> [!question] AutoMigrate 每次运行都会重新建表吗?
|
||||
>
|
||||
> 不会。它基于 Go struct tag 与数据库 schema 的差异做**增量比对**:只在确实缺少某个东西时才发出 ALTER 语句,已存在的结构会被跳过。
|
||||
|
||||
#### 增量更新
|
||||
|
||||
```go
|
||||
// 第一次:只有 User 和 Order
|
||||
@@ -61,6 +68,8 @@ db.AutoMigrate(&User{})
|
||||
// 不会删除已有的字段,只会添加新列和索引
|
||||
```
|
||||
|
||||
理解了增量更新的特性后,需要了解它的**边界**——AutoMigrate 设计上是"只加不改"的:
|
||||
|
||||
> [!warning] AutoMigrate 的限制
|
||||
> - **不能删除**不再出现在 struct 中的列(需要手动处理)
|
||||
> - **不能重命名**列(需要先删除再创建)
|
||||
@@ -68,7 +77,9 @@ db.AutoMigrate(&User{})
|
||||
> - MySQL 下 `ALTER TABLE MODIFY COLUMN` 可能会重建整张表(锁表!)
|
||||
> - 某些复杂的类型变更(如 varchar 改 int)可能不被支持
|
||||
|
||||
### DryRun 预览变更
|
||||
#### DryRun 预览变更
|
||||
|
||||
有时在正式执行迁移之前,你想知道 GORM 会生成哪些 SQL。DryRun 模式可以帮到你:
|
||||
|
||||
```go
|
||||
// 查看 AutoMigrate 会做什么,但不执行
|
||||
@@ -78,12 +89,12 @@ if err != nil {
|
||||
}
|
||||
```
|
||||
|
||||
> [!tip] DryRun + AutoMigrate 的组合
|
||||
> DryRun 模式下的 AutoMigrate 会构建 SQL 语句但**不执行**。结合这个特性可以做迁移前校验——在 CI 中检查新模型是否会导致破坏性变更。
|
||||
> [!tip] 生产场景建议
|
||||
> 在团队项目中,可以将 DryRun + AutoMigrate 加入 CI Pipeline——每次 PR 提交时校验新模型不会引入破坏性变更。如果 DryRun 报错,说明 struct tag 变更无法在当前数据库环境下执行。
|
||||
|
||||
## Migrator 接口 —— 细粒度控制
|
||||
### Migrator 接口 —— 细粒度控制
|
||||
|
||||
GORM 提供了 `Migrator` 接口来访问底层数据库的迁移能力:
|
||||
AutoMigrate 适合自动化场景,但在某些情况下你需要对迁移过程进行**更精细的控制**。比如:在迁移之前先检查某个表是否已存在、获取列的元数据信息、或者手动重命名列。这时就可以通过 `db.Migrator()` 获取 Migrator 实例来调用具体的操作:
|
||||
|
||||
```go
|
||||
migrator := db.Migrator()
|
||||
@@ -115,29 +126,35 @@ migrator.HasIndex(&User{}, "IdxEmail")
|
||||
migrator.DropIndex(&User{}, "IdxEmail")
|
||||
```
|
||||
|
||||
从代码可以看出,Migrator 暴露了**逐个字段级别**的操作能力——你可以精确地检查、创建、删除某个具体的列或索引。这对于在迁移脚本中执行复杂变更非常有用。
|
||||
|
||||
> [!note] 不同驱动的 Migrator 实现
|
||||
> 每个驱动都有自己的 `Migrator` 实现。不是所有操作在所有数据库上都受支持:
|
||||
> 每个驱动都有自己的 `Migrator` 实现(如 `*mysql.Migrator`、`*postgres.Migrator`)。并非所有操作在所有数据库上都受支持:
|
||||
>
|
||||
> ```go
|
||||
> // 检查某个驱动是否支持特定操作
|
||||
> // 利用类型断言检查具体驱动
|
||||
> if migrator, ok := db.Migrator().(*mysql.Migrator); ok {
|
||||
> // MySQL 特有的迁移功能
|
||||
> }
|
||||
> ```
|
||||
|
||||
## 版本化迁移方案(生产推荐)
|
||||
### 版本化迁移方案(生产推荐)
|
||||
|
||||
在生产环境中,推荐使用专门的迁移工具配合 GORM:
|
||||
|
||||
### 方案一:Goose
|
||||
#### 方案一:Goose
|
||||
|
||||
```bash
|
||||
# 安装
|
||||
go install github.com/pressly/goose/v3/cmd/goose@latest
|
||||
|
||||
# 创建迁移文件
|
||||
# 创建迁移文件(按时间戳命名)
|
||||
goose create add_user_email sql
|
||||
```
|
||||
|
||||
# 生成文件:20260101_001_add_user_email.sql
|
||||
生成的文件类似 `20260101_001_add_user_email.sql`,内容结构如下:
|
||||
|
||||
```sql
|
||||
-- +goose Up
|
||||
ALTER TABLE users ADD COLUMN email VARCHAR(128) UNIQUE;
|
||||
ALTER TABLE users ADD COLUMN email_verified BOOLEAN DEFAULT FALSE;
|
||||
@@ -147,58 +164,109 @@ ALTER TABLE users DROP COLUMN email;
|
||||
ALTER TABLE users DROP COLUMN email_verified;
|
||||
```
|
||||
|
||||
与 GORM 集成:
|
||||
> [!note] Goose 的注释指令
|
||||
> `-- +goose Up` 和 `-- +goose Down` 是 Goose 识别迁移方向的特殊注释标记。Goose 通过解析这些标记来执行对应方向的迁移。
|
||||
|
||||
与 GORM 集成(从 GORM 实例中获取底层 `*sql.DB`,交给 goose 执行版本化迁移):
|
||||
|
||||
```go
|
||||
func migrateDB(db *gorm.DB) {
|
||||
// 先让 GORM 做必要的表存在性检查和基础初始化
|
||||
db.AutoMigrate(&User{})
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/pressly/goose/v3"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func migrateDB(db *gorm.DB) error {
|
||||
// 从 GORM DB 中取出底层的 *sql.DB 交给 goose 管理
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 然后用 goose 执行版本化迁移脚本
|
||||
goose.Run("up", dsn)
|
||||
goose.SetBaseDir("./migrations") // 指定迁移文件目录
|
||||
goose.SetLogger(log.New(os.Stdout, "", 0))
|
||||
return goose.Up(sqlDB) // 按版本号依次执行未运行的 Up 脚本
|
||||
}
|
||||
```
|
||||
|
||||
### 方案二:golang-migrate
|
||||
> [!note] Goose 如何保证幂等?
|
||||
> Goose 内部维护了一张 `goose_db_version` 表记录已执行的迁移版本。每次运行 `goose Up` 时只会执行版本号大于当前记录的脚本——因此重复运行不会报错。这与 AutoMigrate 的行为互补:AutoMigrate 负责确保 struct 对应的表存在,Goose 负责应用后续的版本化变更。
|
||||
|
||||
#### 方案二: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
|
||||
生成文件结构如下:
|
||||
|
||||
```
|
||||
migrations/
|
||||
├── 001_add_user_email.up.sql
|
||||
└── 001_add_user_email.down.sql
|
||||
```
|
||||
|
||||
`up` 脚本内容示例:
|
||||
|
||||
```sql
|
||||
-- migrations/001_add_user_email.up.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);
|
||||
```
|
||||
|
||||
-- <向下迁移>
|
||||
`down` 脚本内容示例:
|
||||
|
||||
```sql
|
||||
-- migrations/001_add_user_email.down.sql
|
||||
ALTER TABLE users DROP CONSTRAINT IF EXISTS unique_email;
|
||||
ALTER TABLE users DROP COLUMN IF EXISTS email;
|
||||
```
|
||||
|
||||
与 GORM 集成——启动时先执行迁移再连接数据库:
|
||||
|
||||
```go
|
||||
// 启动时执行迁移
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
_ "github.com/golang-migrate/migrate/v4/database/mysql"
|
||||
_ "github.com/golang-migrate/migrate/v4/source/file"
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
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{})
|
||||
m, err := migrate.New(
|
||||
"migrations/file://./migrations",
|
||||
dsn,
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// ErrNoChange 表示当前已经是最新状态,不算错误
|
||||
if err := m.Up(); err != nil && err != migrate.ErrNoChange {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// 迁移成功后才连接 GORM
|
||||
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
||||
}
|
||||
```
|
||||
|
||||
### 方案三:自建轻量级迁移表
|
||||
> [!note] golang-migrate vs Goose
|
||||
> `golang-migrate` 的优势在于支持**命名迁移文件**(如 `001_add_user_email.up.sql`),文件名可读性更强。而 Goose 的默认行为是按时间戳生成序列号。两者都能与 GORM 配合使用,选择取决于团队偏好。
|
||||
|
||||
#### 方案三:自建轻量级迁移表
|
||||
|
||||
适合小型项目——用一个表记录当前迁移版本号:
|
||||
|
||||
@@ -240,6 +308,8 @@ func ApplyMigrations(tx *gorm.DB) error {
|
||||
```
|
||||
|
||||
> [!question] 为什么生产环境不推荐只用 AutoMigrate?
|
||||
>
|
||||
> 核心原因是**不可回滚**。当你的代码从 `git` checkout 了一个旧版本,AutoMigrate 无法"撤销"之前添加的列或索引——它只会跳过已存在的部分。而版本化迁移方案通过 Down 脚本可以轻松应对回退场景:
|
||||
|
||||
| 维度 | AutoMigrate | 版本化迁移 |
|
||||
|------|------------|-----------|
|
||||
@@ -250,7 +320,10 @@ func ApplyMigrations(tx *gorm.DB) error {
|
||||
| CI/CD 集成 | 难判断是否有未执行的变更 | migration_version 表可做 gate |
|
||||
| 审计追踪 | ❌ | ✅ 谁在什么时候改了什么 |
|
||||
|
||||
## 迁移策略对比
|
||||
> [!tip] 实际开发中的常见做法
|
||||
> 很多团队会选择 **AutoMigrate + DryRun** 作为本地开发的默认方式,同时在 CI/CD pipeline 中引入专门的迁移工具来做生产部署。这样既享受了开发时的便利,又保证了生产环境的可控性。
|
||||
|
||||
### 迁移策略对比
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
@@ -265,9 +338,11 @@ flowchart TD
|
||||
style Large fill:#4FC08D,color:#fff
|
||||
```
|
||||
|
||||
## 迁移最佳实践
|
||||
### 迁移最佳实践
|
||||
|
||||
### 1. 幂等性设计
|
||||
在编写任何迁移脚本之前,建立一个核心理念:**每一次数据库变更都应该是可追踪、可回滚的**。下面从三个维度展开:
|
||||
|
||||
#### 1. 幂等性设计
|
||||
|
||||
迁移脚本应该能够重复执行而不报错:
|
||||
|
||||
@@ -280,7 +355,7 @@ CREATE INDEX IF NOT EXISTS idx_users_phone ON users(phone);
|
||||
ALTER TABLE users ADD COLUMN phone VARCHAR(20);
|
||||
```
|
||||
|
||||
### 2. 大表加列的最佳方式
|
||||
#### 2. 大表加列的最佳方式
|
||||
|
||||
对线上大表(百万级以上)直接 `ALTER TABLE ADD COLUMN` 会锁表导致服务不可用:
|
||||
|
||||
@@ -298,7 +373,9 @@ 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 中的迁移检查
|
||||
#### 3. CI Pipeline 中的迁移检查
|
||||
|
||||
在持续集成流程中加入迁移检查,可以防止模型变更未经审核就部署到生产环境:
|
||||
|
||||
```yaml
|
||||
# .github/workflows/db-check.yml
|
||||
@@ -315,13 +392,42 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Check AutoMigrate consistency
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
|
||||
- name: Run dependency download
|
||||
run: go mod download
|
||||
|
||||
- name: DryRun 校验新模型变更
|
||||
run: |
|
||||
# 创建一个空的临时 DB,运行 AutoMigrate,然后比较 schema
|
||||
go run ./cmd/check-schema ${{ secrets.DB_DSN }}
|
||||
# DryRun 模式下 AutoMigrate 只构建 SQL 不执行
|
||||
# 如果 struct tag 存在语法错误或类型不兼容,此处会报错
|
||||
go run ./cmd/dryrun-check ${{ secrets.DB_DSN }}
|
||||
```
|
||||
|
||||
## 常见坑点速查
|
||||
配套的 Go 实现思路如下:
|
||||
|
||||
```go
|
||||
// cmd/dryrun-check/main.go
|
||||
func main() {
|
||||
dsn := os.Args[1]
|
||||
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
||||
|
||||
// DryRun 模式:构建但不执行任何 SQL
|
||||
err := db.Session(&gorm.Session{DryRun: true}).AutoMigrate(
|
||||
&User{}, &Order{}, &Product{},
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatalf("⚠️ 模型变更无法应用: %v\n", err)
|
||||
}
|
||||
fmt.Println("✅ 模型变更可以通过 AutoMigrate")
|
||||
}
|
||||
```
|
||||
|
||||
> [!tip] 进阶方案:schema 快照对比
|
||||
> 可以结合 `godbcompare` 或手动导出目标库的 `SHOW CREATE TABLE` 结果,与 AutoMigrate 生成的结构做 diff——这样可以检测出 **AutoMigrate 不会执行的破坏性变更**(如删除列)。
|
||||
|
||||
### 常见坑点速查
|
||||
|
||||
| 问题 | 原因 | 解决方案 |
|
||||
|------|------|---------|
|
||||
|
||||
Reference in New Issue
Block a user