Skip to content

06-迁移与索引管理

数据库结构随着业务发展不断演进,如何安全、可控地管理这些变更就是数据库迁移要解决的问题。GORM 提供了 AutoMigrate 和 Migrator API 两套机制,但对于生产环境,专业的迁移工具(如 golang-migrate)更为可靠。本篇将系统讲解 GORM 的迁移能力、索引管理以及生产级迁移实践。

AutoMigrate 原理与局限

AutoMigrate 会根据模型结构自动创建表、添加缺失的列和索引。它的设计目标是开发期快速迭代,而非生产环境的版本化管理。

AutoMigrate 的工作原理:

  1. 检查目标表是否存在,不存在则创建
  2. 对比模型字段与数据库列,添加缺失的列
  3. 对比模型索引标签与数据库索引,添加缺失的索引
  4. 部分数据库支持修改列类型
go
package main

import (
	"fmt"
	"log"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type User struct {
	ID     uint   `gorm:"primaryKey"`
	Name   string `gorm:"type:varchar(50);not null"`
	Email  string `gorm:"type:varchar(150);uniqueIndex"`
	Age    int
}

func main() {
	db, err := gorm.Open(sqlite.Open("auto.db"), &gorm.Config{})
	if err != nil {
		log.Fatal(err)
	}

	// 第一次迁移:创建表
	if err := db.AutoMigrate(&User{}); err != nil {
		log.Fatal(err)
	}
	fmt.Println("首次迁移完成")

	// 检查表和列
	m := db.Migrator()
	fmt.Println("表存在:", m.HasTable(&User{}))
	fmt.Println("name 列存在:", m.HasColumn(&User{}, "Name"))
	fmt.Println("email 索引存在:", m.HasIndex(&User{}, "Email"))
}

AutoMigrate 的局限性:

能力是否支持说明
创建表支持表不存在时创建
添加列支持模型新增字段时自动添加
添加索引支持根据标签创建索引
修改列类型部分MySQL/PostgreSQL 支持,SQLite 有限
删除列不支持避免数据丢失
删除索引不支持需手动处理
重命名列不支持需手动 SQL
修改约束不支持如 NOT NULL 变更
go
package main

import (
	"fmt"
	"log"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

// 演示 AutoMigrate 不能删除列
type User struct {
	ID    uint   `gorm:"primaryKey"`
	Name  string `gorm:"type:varchar(50)"`
	Email string `gorm:"type:varchar(150)"`
}

// 后续删除了 Age 字段
// AutoMigrate 不会删除 age 列

func main() {
	db, err := gorm.Open(sqlite.Open("limit.db"), &gorm.Config{})
	if err != nil {
		log.Fatal(err)
	}

	// 先创建包含 Age 的表
	db.Migrator().CreateTable(&struct {
		ID    uint   `gorm:"primaryKey"`
		Name  string
		Email string
		Age   int // 这个字段后续从模型中移除
	}{})

	// 再用不含 Age 的模型 AutoMigrate
	db.AutoMigrate(&User{})

	// 检查 age 列是否还存在
	type ColumnInfo struct {
		Name string
	}
	var columns []ColumnInfo
	db.Raw("PRAGMA table_info(users)").Scan(&columns)
	fmt.Println("users 表的列:")
	for _, c := range columns {
		fmt.Printf("  - %s\n", c.Name)
		// age 列依然存在,AutoMigrate 不会删除它
	}
}

手动迁移:Migrator API

对于 AutoMigrate 无法处理的场景,GORM 提供了完整的 Migrator API。

表操作:CreateTable、DropTable、HasTable

go
package main

import (
	"fmt"
	"log"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type User struct {
	ID    uint   `gorm:"primaryKey"`
	Name  string `gorm:"type:varchar(50)"`
	Email string `gorm:"type:varchar(150);uniqueIndex"`
}

func main() {
	db, err := gorm.Open(sqlite.Open("migrator.db"), &gorm.Config{})
	if err != nil {
		log.Fatal(err)
	}
	m := db.Migrator()

	// 检查表是否存在
	if !m.HasTable(&User{}) {
		// 创建表
		if err := m.CreateTable(&User{}); err != nil {
			log.Fatal(err)
		}
		fmt.Println("表创建成功")
	}

	// 重命名表
	if m.HasTable("users") {
		m.RenameTable(&User{}, "users_backup")
		fmt.Println("表已重命名为 users_backup")
		m.RenameTable("users_backup", &User{})
	}

	// 删除表(谨慎使用)
	// m.DropTable(&User{})
	// m.DropTable("users")

	fmt.Println("迁移操作完成")
}

列操作:AddColumn、DropColumn、AlterColumn

go
package main

import (
	"fmt"
	"log"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type User struct {
	ID       uint   `gorm:"primaryKey"`
	Name     string `gorm:"type:varchar(50)"`
	Email    string `gorm:"type:varchar(150)"`
	Age      int
	Status   int `gorm:"default:1"`
}

func main() {
	db, err := gorm.Open(sqlite.Open("column.db"), &gorm.Config{})
	if err != nil {
		log.Fatal(err)
	}
	db.AutoMigrate(&User{})
	m := db.Migrator()

	// 添加列:先创建一个临时模型只包含新字段
	type AddPhone struct {
		Phone string `gorm:"type:varchar(20)"`
	}
	if !m.HasColumn(&User{}, "Phone") {
		if err := m.AddColumn(&User{}, "Phone"); err != nil {
			// AddColumn 需要模型字段,使用 Migrator 的 AddColumn 接口
			fmt.Println("AddColumn 失败:", err)
		}
	}

	// 直接通过模型字段添加列(推荐方式)
	type UserWithBio struct {
		ID   uint   `gorm:"primaryKey"`
		Name string
		Bio  string `gorm:"type:text"`
	}
	if !m.HasColumn(&UserWithBio{}, "Bio") {
		m.AddColumn(&UserWithBio{}, "Bio")
	}

	// 修改列类型
	type UserNewName struct {
		ID   uint   `gorm:"primaryKey"`
		Name string `gorm:"type:varchar(100)"` // 从 50 改为 100
	}
	if err := m.AlterColumn(&UserNewName{}, "Name"); err != nil {
		fmt.Println("AlterColumn:", err)
	}

	// 删除列(注意:SQLite 对 DropColumn 支持有限,可能需要重建表)
	if m.HasColumn(&User{}, "Age") {
		if err := m.DropColumn(&User{}, "Age"); err != nil {
			fmt.Println("DropColumn:", err)
		}
	}

	// 检查列
	fmt.Println("Name 列存在:", m.HasColumn(&User{}, "Name"))
	fmt.Println("Age 列存在:", m.HasColumn(&User{}, "Age"))

	// 重命名列
	if m.HasColumn(&User{}, "Name") {
		m.RenameColumn(&User{}, "Name", "FullName")
		fmt.Println("Name 重命名为 FullName")
	}
}

索引操作:CreateIndex、DropIndex、HasIndex

go
package main

import (
	"fmt"
	"log"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type User struct {
	ID    uint   `gorm:"primaryKey"`
	Name  string `gorm:"type:varchar(50);index"`
	Email string `gorm:"type:varchar(150);uniqueIndex"`
	City  string `gorm:"type:varchar(50)"`
	Age   int
}

func main() {
	db, err := gorm.Open(sqlite.Open("idx.db"), &gorm.Config{})
	if err != nil {
		log.Fatal(err)
	}
	db.AutoMigrate(&User{})
	m := db.Migrator()

	// 检查索引
	fmt.Println("Name 索引存在:", m.HasIndex(&User{}, "Name"))
	fmt.Println("Email 索引存在:", m.HasIndex(&User{}, "Email"))

	// 创建索引(按字段名)
	type UserWithCityIndex struct {
		City string `gorm:"index:idx_city"`
	}
	if !m.HasIndex(&User{}, "City") {
		m.CreateIndex(&UserWithCityIndex{}, "City")
		fmt.Println("City 索引创建")
	}

	// 创建唯一索引
	type UserWithAgeIndex struct {
		Age int `gorm:"uniqueIndex:idx_age_unique"`
	}
	if !m.HasIndex(&User{}, "Age") {
		m.CreateIndex(&UserWithAgeIndex{}, "Age")
	}

	// 删除索引
	if m.HasIndex(&User{}, "City") {
		m.DropIndex(&User{}, "City")
		fmt.Println("City 索引删除")
	}

	// 重命名索引
	if m.HasIndex(&User{}, "Name") {
		m.RenameIndex(&User{}, "Name", "idx_user_name")
		fmt.Println("Name 索引重命名")
	}
}

索引管理

普通索引、唯一索引、复合索引

go
package main

import (
	"fmt"
	"log"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type User struct {
	ID       uint   `gorm:"primaryKey"`
	Name     string `gorm:"type:varchar(50);index"`                // 普通索引
	Email    string `gorm:"type:varchar(150);uniqueIndex"`         // 唯一索引
	Phone    string `gorm:"type:varchar(20);index:idx_phone"`      // 命名索引

	// 复合索引:city + age
	City     string `gorm:"type:varchar(50);index:idx_city_age,priority:1"`
	Age      int    `gorm:"index:idx_city_age,priority:2"`

	// 复合唯一索引:first_name + last_name
	FirstName string `gorm:"type:varchar(50);uniqueIndex:idx_full_name,priority:1"`
	LastName  string `gorm:"type:varchar(50);uniqueIndex:idx_full_name,priority:2"`

	// 带条件的索引(部分数据库支持)
	Status   int    `gorm:"index:idx_active,priority:1;default:1"`
}

func main() {
	db, err := gorm.Open(sqlite.Open("indexes.db"), &gorm.Config{})
	if err != nil {
		log.Fatal(err)
	}
	db.AutoMigrate(&User{})
	m := db.Migrator()

	// 验证所有索引
	fmt.Println("Name 索引:", m.HasIndex(&User{}, "Name"))
	fmt.Println("Email 唯一索引:", m.HasIndex(&User{}, "Email"))
	fmt.Println("Phone 索引:", m.HasIndex(&User{}, "Phone"))
	fmt.Println("City+Age 复合索引:", m.HasIndex(&User{}, "idx_city_age"))
	fmt.Println("FirstName+LastName 复合唯一索引:", m.HasIndex(&User{}, "idx_full_name"))

	// 插入数据测试唯一索引
	user1 := User{Name: "Tom", Email: "tom@x.com", Phone: "13800000001",
		City: "Beijing", Age: 25, FirstName: "Tom", LastName: "Smith"}
	if err := db.Create(&user1).Error; err != nil {
		log.Fatal(err)
	}

	// 重复 Email 会失败
	user2 := User{Name: "Tom2", Email: "tom@x.com", Phone: "13800000002",
		City: "Shanghai", Age: 30, FirstName: "Tom2", LastName: "Brown"}
	err = db.Create(&user2).Error
	fmt.Printf("重复 Email 创建结果: %v\n", err)

	// 重复复合唯一索引会失败
	user3 := User{Name: "Tom3", Email: "tom3@x.com", Phone: "13800000003",
		City: "Beijing", Age: 30, FirstName: "Tom", LastName: "Smith"}
	err = db.Create(&user3).Error
	fmt.Printf("重复姓名创建结果: %v\n", err)
}

全文索引

MySQL 支持全文索引(FULLTEXT),适合文本搜索:

go
package main

import (
	"fmt"
	"log"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type Article struct {
	ID      uint   `gorm:"primaryKey"`
	Title   string `gorm:"type:varchar(200)"`
	Content string `gorm:"type:text;index:idx_content,full_text"` // MySQL 全文索引
}

func main() {
	db, err := gorm.Open(sqlite.Open("fulltext.db"), &gorm.Config{})
	if err != nil {
		log.Fatal(err)
	}
	db.AutoMigrate(&Article{})

	// 准备数据
	articles := []Article{
		{Title: "Go 入门", Content: "Go 是一门编译型语言,由 Google 开发"},
		{Title: "GORM 教程", Content: "GORM 是 Go 的 ORM 库"},
		{Title: "Python 教程", Content: "Python 是一门解释型语言"},
	}
	db.Create(&articles)

	// MySQL 中使用全文搜索:
	// db.Where("MATCH(content) AGAINST (?)", "Go").Find(&articles)
	// SQLite 中使用 LIKE 模拟
	var results []Article
	db.Where("content LIKE ?", "%Go%").Find(&results)
	fmt.Printf("包含 'Go' 的文章: %d\n", len(results))
	for _, a := range results {
		fmt.Printf("  - %s\n", a.Title)
	}
}

使用 tag 定义索引

通过 tag 可以精细控制索引的各种属性:

go
package main

import (
	"fmt"
	"log"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type Product struct {
	ID    uint    `gorm:"primaryKey"`
	Name  string  `gorm:"type:varchar(200);index:idx_name,class:FULLTEXT"` // 全文索引(MySQL)
	SKU   string  `gorm:"type:varchar(50);uniqueIndex:idx_sku"`            // 命名唯一索引
	Price float64 `gorm:"index:idx_price,sort:desc"`                       // 降序索引

	// 复合索引的多种属性
	CategoryID uint `gorm:"index:idx_cat_status,priority:1;not null"`
	Status     int  `gorm:"index:idx_cat_status,priority:2;default:1"`

	// 函数索引(PostgreSQL 支持)
	// Email string `gorm:"index:idx_lower_email,expression:lower(email)"`
}

func main() {
	db, err := gorm.Open(sqlite.Open("tag_idx.db"), &gorm.Config{})
	if err != nil {
		log.Fatal(err)
	}
	db.AutoMigrate(&Product{})
	m := db.Migrator()

	fmt.Println("idx_name 索引:", m.HasIndex(&Product{}, "idx_name"))
	fmt.Println("idx_sku 索引:", m.HasIndex(&Product{}, "idx_sku"))
	fmt.Println("idx_price 索引:", m.HasIndex(&Product{}, "idx_price"))
	fmt.Println("idx_cat_status 索引:", m.HasIndex(&Product{}, "idx_cat_status"))
}

迁移版本管理:使用 golang-migrate

生产环境推荐使用 golang-migrate 进行版本化迁移管理。

安装 golang-migrate

bash
# 安装 CLI 工具
go install -tags 'sqlite mysql postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest

# 安装 Go 库
go get -u github.com/golang-migrate/migrate/v4
go get -u github.com/golang-migrate/migrate/v4/database/sqlite
go get -u github.com/golang-migrate/migrate/v4/source/file

创建迁移文件

bash
# 创建迁移文件(会生成两个文件:up 和 down)
migrate create -ext sql -dir migrations -seq create_users_table

# 生成的文件:
# migrations/000001_create_users_table.up.sql   # 升级 SQL
# migrations/000001_create_users_table.down.sql # 回滚 SQL

000001_create_users_table.up.sql 内容:

sql
CREATE TABLE users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name VARCHAR(50) NOT NULL,
    email VARCHAR(150) NOT NULL UNIQUE,
    age INTEGER DEFAULT 0,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_users_name ON users(name);

000001_create_users_table.down.sql 内容:

sql
DROP INDEX IF EXISTS idx_users_name;
DROP TABLE IF EXISTS users;

在 Go 中执行迁移

go
package main

import (
	"fmt"
	"log"

	"github.com/golang-migrate/migrate/v4"
	_ "github.com/golang-migrate/migrate/v4/database/sqlite"
	_ "github.com/golang-migrate/migrate/v4/source/file"
	"database/sql"

	_ "modernc.org/sqlite"
)

func main() {
	// 使用 golang-migrate 执行迁移
	m, err := migrate.New(
		"file://migrations",
		"sqlite://migrate.db",
	)
	if err != nil {
		log.Fatal("初始化迁移失败:", err)
	}
	defer m.Close()

	// 执行迁移
	if err := m.Up(); err != nil && err != migrate.ErrNoChange {
		log.Fatal("迁移失败:", err)
	}

	// 查看当前版本
	version, dirty, err := m.Version()
	if err != nil {
		log.Fatal("获取版本失败:", err)
	}
	fmt.Printf("当前迁移版本: %d, dirty: %v\n", version, dirty)

	// 回滚上一次迁移
	// if err := m.Steps(-1); err != nil {
	//     log.Fatal("回滚失败:", err)
	// }

	// 使用 sql.DB 验证
	db, err := sql.Open("sqlite", "migrate.db")
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	var name string
	err = db.QueryRow("SELECT name FROM sqlite_master WHERE type='table' AND name='users'").Scan(&name)
	if err != nil {
		fmt.Println("users 表不存在")
	} else {
		fmt.Printf("users 表已创建: %s\n", name)
	}
}

数据库迁移最佳实践

  1. 每个迁移文件保持原子性:一个迁移只做一件事,便于回滚和审查
  2. 始终提供 down 迁移:确保可以回滚
  3. 大表变更分批进行:避免长时间锁表
  4. 先备份再迁移:生产环境迁移前必须备份
  5. 测试环境验证:迁移先在测试环境跑通
  6. 避免破坏性变更:删除列先重命名,观察一段时间再删除
  7. 不要修改已发布的迁移文件:已应用到生产的迁移文件不能修改
go
package main

import (
	"fmt"
	"log"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

// 演示安全的列删除流程
type User struct {
	ID         uint   `gorm:"primaryKey"`
	Name       string `gorm:"type:varchar(50)"`
	Email      string `gorm:"type:varchar(150)"`
	OldField   string `gorm:"type:varchar(100)"` // 待删除的字段
}

func main() {
	db, err := gorm.Open(sqlite.Open("safe.db"), &gorm.Config{})
	if err != nil {
		log.Fatal(err)
	}
	db.AutoMigrate(&User{})
	m := db.Migrator()

	// 步骤1:先停止使用 OldField 字段(代码层面)
	// 步骤2:重命名为 deprecated_old_field(标记为废弃)
	if m.HasColumn(&User{}, "OldField") {
		m.RenameColumn(&User{}, "OldField", "deprecated_old_field")
		fmt.Println("已重命名为 deprecated_old_field")
	}

	// 步骤3:观察一段时间,确认无影响
	// 步骤4:删除列
	if m.HasColumn(&User{}, "deprecated_old_field") {
		// m.DropColumn(&User{}, "deprecated_old_field")
		fmt.Println("(演示)可安全删除 deprecated_old_field")
	}

	// 大表加列:使用默认值避免锁表
	type UserV2 struct {
		ID     uint   `gorm:"primaryKey"`
		Name   string
		Status int    `gorm:"default:1"` // 有默认值,避免 NOT NULL 锁表
	}
	if !m.HasColumn(&UserV2{}, "Status") {
		m.AddColumn(&UserV2{}, "Status")
		fmt.Println("添加 Status 列(带默认值)")
	}
}

完整示例:博客系统迁移脚本

下面用一个完整的博客系统迁移脚本综合演示迁移管理:

go
package main

import (
	"fmt"
	"log"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

// User 用户模型
type User struct {
	ID        uint   `gorm:"primaryKey"`
	Username  string `gorm:"type:varchar(50);uniqueIndex;not null"`
	Email     string `gorm:"type:varchar(150);uniqueIndex;not null"`
	Password  string `gorm:"type:varchar(100);not null"`
	Nickname  string `gorm:"type:varchar(50)"`
	Status    int    `gorm:"default:1;index"`
	CreatedAt string
	UpdatedAt string
}

// Post 文章模型
type Post struct {
	ID         uint   `gorm:"primaryKey"`
	Title      string `gorm:"type:varchar(200);not null;index"`
	Content    string `gorm:"type:text"`
	ViewCount  int    `gorm:"default:0;index"`
	UserID     uint   `gorm:"index;not null"`
	CategoryID uint   `gorm:"index"`
	Status     int    `gorm:"default:1;index;comment:1发布 0草稿"`
	CreatedAt  string
	UpdatedAt  string
}

// Comment 评论模型
type Comment struct {
	ID        uint   `gorm:"primaryKey"`
	Content   string `gorm:"type:text;not null"`
	PostID    uint   `gorm:"index;not null"`
	UserID    uint   `gorm:"index;not null"`
	ParentID  *uint  `gorm:"index"` // 父评论 ID,可为空(用于回复)
	CreatedAt string
}

// Category 分类模型
type Category struct {
	ID       uint   `gorm:"primaryKey"`
	Name     string `gorm:"type:varchar(100);uniqueIndex;not null"`
	ParentID *uint  `gorm:"index"` // 父分类
	Sort     int    `gorm:"default:0"`
}

// MigrationManager 迁移管理器
type MigrationManager struct {
	db *gorm.DB
}

func NewMigrationManager(db *gorm.DB) *MigrationManager {
	return &MigrationManager{db: db}
}

// RunMigrations 执行所有迁移
func (mm *MigrationManager) RunMigrations() error {
	m := mm.db.Migrator()

	// 迁移1:创建分类表
	if !m.HasTable(&Category{}) {
		fmt.Println("迁移1: 创建 categories 表")
		if err := m.CreateTable(&Category{}); err != nil {
			return fmt.Errorf("创建 categories 表失败: %w", err)
		}
	}

	// 迁移2:创建用户表
	if !m.HasTable(&User{}) {
		fmt.Println("迁移2: 创建 users 表")
		if err := m.CreateTable(&User{}); err != nil {
			return fmt.Errorf("创建 users 表失败: %w", err)
		}
	}

	// 迁移3:创建文章表
	if !m.HasTable(&Post{}) {
		fmt.Println("迁移3: 创建 posts 表")
		if err := m.CreateTable(&Post{}); err != nil {
			return fmt.Errorf("创建 posts 表失败: %w", err)
		}
	}

	// 迁移4:创建评论表
	if !m.HasTable(&Comment{}) {
		fmt.Println("迁移4: 创建 comments 表")
		if err := m.CreateTable(&Comment{}); err != nil {
			return fmt.Errorf("创建 comments 表失败: %w", err)
		}
	}

	// 迁移5:添加索引(如果不存在)
	if !m.HasIndex(&Post{}, "Title") {
		fmt.Println("迁移5: 添加 posts.title 索引")
		m.CreateIndex(&Post{}, "Title")
	}

	// 迁移6:添加复合索引
	type PostComposite struct {
		UserID uint `gorm:"index:idx_user_status,priority:1"`
		Status int  `gorm:"index:idx_user_status,priority:2"`
	}
	if !m.HasIndex(&PostComposite{}, "idx_user_status") {
		fmt.Println("迁移6: 添加 posts 复合索引 idx_user_status")
		m.CreateIndex(&PostComposite{}, "UserID")
	}

	return nil
}

// RollbackMigration 回滚最后一个迁移
func (mm *MigrationManager) RollbackMigration() error {
	m := mm.db.Migrator()
	if m.HasTable(&Comment{}) {
		fmt.Println("回滚: 删除 comments 表")
		return m.DropTable(&Comment{})
	}
	return nil
}

// Status 查看迁移状态
func (mm *MigrationManager) Status() {
	m := mm.db.Migrator()
	tables := []string{"categories", "users", "posts", "comments"}
	fmt.Println("\n=== 迁移状态 ===")
	for _, t := range tables {
		fmt.Printf("表 %s: %v\n", t, m.HasTable(t))
	}
}

func main() {
	db, err := gorm.Open(sqlite.Open("blog_migrate.db"), &gorm.Config{})
	if err != nil {
		log.Fatal(err)
	}

	mm := NewMigrationManager(db)

	// 执行迁移
	if err := mm.RunMigrations(); err != nil {
		log.Fatal("迁移失败:", err)
	}

	// 查看状态
	mm.Status()

	// 验证:插入测试数据
	category := Category{Name: "技术"}
	db.Create(&category)

	user := User{Username: "admin", Email: "admin@blog.com", Password: "hashed"}
	db.Create(&user)

	post := Post{
		Title:      "GORM 迁移指南",
		Content:    "本篇介绍 GORM 迁移...",
		UserID:     user.ID,
		CategoryID: category.ID,
		Status:     1,
	}
	db.Create(&post)

	comment := Comment{
		Content: "写得很好!",
		PostID:  post.ID,
		UserID:  user.ID,
	}
	db.Create(&comment)

	fmt.Println("\n=== 数据验证 ===")
	var postCount, commentCount int64
	db.Model(&Post{}).Count(&postCount)
	db.Model(&Comment{}).Count(&commentCount)
	fmt.Printf("文章数: %d, 评论数: %d\n", postCount, commentCount)
}

小结

本篇系统讲解了 GORM 的迁移与索引管理:

  1. AutoMigrate 原理:自动建表加列加索引,但不会删列、不会重命名
  2. AutoMigrate 局限:不适合生产环境的版本化管理
  3. Migrator API:提供表、列、索引的完整 CRUD 操作
  4. 表操作:CreateTable、DropTable、HasTable、RenameTable
  5. 列操作:AddColumn、DropColumn、AlterColumn、RenameColumn
  6. 索引操作:CreateIndex、DropIndex、HasIndex、RenameIndex
  7. 索引类型:普通索引、唯一索引、复合索引、全文索引
  8. tag 定义索引:priority、composite、class 等属性
  9. golang-migrate:生产级迁移工具,支持版本化和回滚
  10. 最佳实践:原子迁移、提供 down、大表分批、先备份、安全删除列

下一篇我们将学习性能优化与缓存,提升 GORM 应用的执行效率。