GORM:Go 的 ORM 利器

GORM 是 Go 最流行的 ORM 框架,提供 Hook、关联预加载和事务支持。本文详解复合主键、乐观锁和读写分离,对比 GORM 与 sqlx、ent 的性能差异

GORM:Go 的 ORM 利器

在之前的文章中,我们学习了如何使用 database/sql 直接操作数据库。虽然这种方式灵活且性能高,但在实际项目中,大量的 CRUD 操作写起来非常繁琐,而且容易出错。

这时候,ORM(Object-Relational Mapping,对象关系映射)就派上用场了。ORM 把数据库的表映射成代码中的结构体,把 SQL 操作映射成方法调用,让数据库操作变得简单而安全。GORM 是 Go 语言中最流行的 ORM 框架,它功能强大、API 友好、性能优异。本文将从基础到进阶,全面讲解 GORM 的使用。

安装 GORM

go get -u gorm.io/gorm
go get -u gorm.io/driver/mysql  # 或 sqlite, postgres, sqlserver

连接数据库

package main

import (
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

func main() {
	dsn := "root:password@tcp(localhost:3306)/myapp?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		panic("连接数据库失败")
	}

	sqlDB, _ := db.DB()
	sqlDB.Ping()
	println("数据库连接成功!")
}

定义模型

GORM 使用结构体来定义模型。通过标签(tags)可以配置表名、字段名、约束等。

package main

import (
	"time"
	"gorm.io/gorm"
)

type User struct {
	ID        uint           `gorm:"primarykey"`
	CreatedAt time.Time
	UpdatedAt time.Time
	DeletedAt gorm.DeletedAt `gorm:"index"`

	Name     string `gorm:"type:varchar(100);not null;comment:用户名"`
	Email    string `gorm:"uniqueIndex;not null;comment:邮箱"`
	Age      int    `gorm:"default:0;comment:年龄"`
	IsActive bool   `gorm:"default:true;comment:是否激活"`
}

func (User) TableName() string {
	return "users"
}

常用标签

标签说明例子
column列名gorm:"column:user_name"
type数据类型gorm:"type:varchar(100)"
size大小gorm:"size:255"
primaryKey主键gorm:"primaryKey"
unique唯一索引gorm:"unique"
default默认值gorm:"default:0"
not null非空gorm:"not null"
autoIncrement自增gorm:"autoIncrement"
comment注释gorm:"comment:用户邮箱"

自动迁移

// 自动迁移(只会创建表、添加缺失的列和索引,不会删除或修改已有列)
db.AutoMigrate(&User{})

// 检查表是否存在
db.Migrator().HasTable(&User{})

// 删除表
db.Migrator().DropTable(&User{})

注意:AutoMigrate 只会在开发环境使用。生产环境应该使用专业的迁移工具(如 golang-migrate)。

CRUD 操作

Create(创建)

// 创建单条记录
user := User{Name: "张三", Email: "zhangsan@example.com", Age: 25}
db.Create(&user)
fmt.Println("用户ID:", user.ID)

// 批量创建
users := []User{
	{Name: "李四", Email: "lisi@example.com", Age: 30},
	{Name: "王五", Email: "wangwu@example.com", Age: 28},
}
db.Create(&users)

// 指定字段创建
db.Select("Name", "Email").Create(&user)

// 忽略某些字段
db.Omit("Age").Create(&user)

// 使用 Map 创建
db.Model(&User{}).Create(map[string]interface{}{
	"Name":  "赵六",
	"Email": "zhaoliu@example.com",
	"Age":   35,
})

Read(查询)

// 查询单条记录
var user User
db.First(&user)
db.Last(&user)
db.Take(&user)
db.First(&user, 1)
db.First(&user, "name = ?", "张三")

// 查询多条记录
var users []User
db.Find(&users)
db.Find(&users, []int{1, 2, 3})
db.Find(&users, "age > ?", 20)

// 条件查询
db.Where("name = ?", "张三").First(&user)
db.Where("name LIKE ?", "%张%").Find(&users)
db.Where("age BETWEEN ? AND ?", 20, 30).Find(&users)

// 链式查询
db.Where("age > ?", 20).
	Where("is_active = ?", true).
	Order("age desc").
	Limit(10).
	Offset(0).
	Find(&users)

// 统计
var count int64
db.Model(&User{}).Where("age > ?", 20).Count(&count)

// Pluck
db.Model(&User{}).Pluck("name", &names)

Update(更新)

db.Model(&user).Update("name", "新名字")
db.Model(&user).Updates(User{Name: "新名字", Age: 27})
db.Model(&User{}).Where("age < ?", 20).Update("is_active", false)
db.Model(&User{}).Update("age", gorm.Expr("age + 1"))

Delete(删除)

db.Delete(&user)
db.Where("age < ?", 18).Delete(&User{})
db.Unscoped().Find(&users)
db.Unscoped().Delete(&user)

进阶概念:钩子机制

GORM 的钩子(Hooks)允许在特定事件前后执行自定义逻辑。完整的钩子执行顺序如下:创建时依次执行 BeforeCreate、BeforeSave、创建 SQL 操作、AfterCreate、AfterSave;更新时依次执行 BeforeUpdate、BeforeSave、更新 SQL 操作、AfterUpdate、AfterSave;删除时依次执行 BeforeDelete、删除 SQL 操作、AfterDelete;查询时依次执行 AfterFind(GORM v2 中 renamed 为 AfterFind)。

type Product struct {
	ID    uint
	Slug  string
	Name  string
	Price float64
}

func (p *Product) BeforeCreate(tx *gorm.DB) error {
	p.Slug = slugify(p.Name)
	return nil
}

func (p *Product) AfterCreate(tx *gorm.DB) error {
	log.Printf("商品 %s 已创建,价格 %.2f", p.Name, p.Price)
	return nil
}

func (p *Product) BeforeUpdate(tx *gorm.DB) error {
	if p.Price < 0 {
		return errors.New("价格不能为负数")
	}
	return nil
}

func (p *Product) BeforeDelete(tx *gorm.DB) error {
	log.Printf("商品 %s 被删除", p.Name)
	return nil
}

func slugify(name string) string {
	return strings.ToLower(strings.ReplaceAll(name, " ", "-"))
}

钩子中还可以使用 tx 参数进行额外的数据库操作,但要注意避免死循环(比如钩子里又触发同一个钩子)。

进阶概念:关联预加载策略

预加载(Preload)是解决 N+1 查询问题的核心手段。GORM 提供了三种预加载方式:Preload 自动查询关联数据、Joins 使用 JOIN 语句一次性查询、以及关联模式手动控制。

// 基础预加载
db.Preload("Posts").Find(&users)

// 预加载时添加条件
db.Preload("Posts", "status = ?", "published").Find(&users)
db.Preload("Posts", func(db *gorm.DB) *gorm.DB {
	return db.Order("posts.created_at desc")
}).Find(&users)

// 多层预加载
db.Preload("Posts.Comments").Find(&users)
db.Preload("Posts.Comments.User").Find(&users)

// Joins 方式(一次性查询)
db.Joins("JOIN posts ON posts.user_id = users.id").
	Where("posts.status = ?", "published").
	Find(&users)

嵌套预加载与条件控制

db.Preload("Orders.Items.Product", func(db *gorm.DB) *gorm.DB {
	return db.Select("id", "name", "price")
}).Find(&users)

使用 Preload 时,GORM 会执行主查询 + N 个关联查询。使用 Joins 时则只执行一个 JOIN 查询,但要注意 JOIN 会产生重复数据,适合一对一关联或计数统计。

进阶概念:自定义数据类型与复合主键

GORM 支持通过实现 Scanner 和 Valuer 接口来自定义数据类型。

type JSON map[string]interface{}

func (j JSON) Value() (driver.Value, error) {
	if j == nil {
		return nil, nil
	}
	return json.Marshal(j)
}

func (j *JSON) Scan(value interface{}) error {
	if value == nil {
		*j = nil
		return nil
	}
	bytes, ok := value.([]byte)
	if !ok {
		return errors.New("type assertion to []byte failed")
	}
	return json.Unmarshal(bytes, j)
}

type User struct {
	ID       uint
	Name     string
	Settings JSON `gorm:"type:json"`
}

复合主键

type OrderItem struct {
	OrderID   uint `gorm:"primaryKey"`
	ProductID uint `gorm:"primaryKey"`
	Quantity  int
}

乐观锁

package main

import (
	"errors"
	"gorm.io/gorm"
)

type Inventory struct {
	ID      uint
	ItemID  uint `gorm:"uniqueIndex"`
	Stock   int
	Version int  `gorm:"default:1"`
}

func DeductStock(db *gorm.DB, itemID uint, quantity int) error {
	for i := 0; i < 5; i++ {
		var inv Inventory
		if err := db.Where("item_id = ?", itemID).First(&inv).Error; err != nil {
			return err
		}
		if inv.Stock < quantity {
			return errors.New("库存不足")
		}
		result := db.Model(&Inventory{}).
			Where("item_id = ? AND version = ?", itemID, inv.Version).
			Updates(map[string]interface{}{
				"stock":   inv.Stock - quantity,
				"version": inv.Version + 1,
			})
		if result.RowsAffected > 0 {
			return nil
		}
	}
	return errors.New("乐观锁重试失败")
}

进阶概念:SQL Builder 底层实现

GORM 底层的 SQL Builder 提供了强大的查询构造能力。通过 Session 配置可以控制全局行为。

// 使用 DryRun 模式预览 SQL
stmt := db.Session(&gorm.Session{DryRun: true}).
	Model(&User{}).
	Where("age > ?", 18).
	Find(&[]User{}).Statement
fmt.Println(stmt.SQL.String())

// 使用命名参数
db.Where("name = @name AND age = @age", map[string]interface{}{
	"name": "张三",
	"age":  25,
}).Find(&users)

// 原生 SQL 查询
db.Raw("SELECT * FROM users WHERE age > ?", 18).Scan(&users)

// 原生 SQL 执行
db.Exec("UPDATE users SET age = age + 1 WHERE name = ?", "张三")

// 子查询
subQuery := db.Model(&Post{}).Select("user_id").Where("status = ?", "published")
db.Where("id IN (?)", subQuery).Find(&users)

ToSQL 方法可以获取生成的 SQL,方便调试和记录日志。

实战案例:完整的 CRUD 与搜索组合

package main

import (
	"fmt"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type User struct {
	ID       uint   `gorm:"primarykey"`
	Name     string `gorm:"index"`
	Email    string `gorm:"uniqueIndex"`
	Age      int
	IsActive bool
}

func main() {
	db, _ := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
	db.AutoMigrate(&User{})

	// 批量创建测试数据
	for i := 1; i <= 20; i++ {
		db.Create(&User{Name: fmt.Sprintf("User%d", i), Email: fmt.Sprintf("u%d@test.com", i), Age: 20 + i, IsActive: i%2 == 0})
	}

	// 分页 + 排序 + 搜索组合
	var users []User
	var total int64
	page, pageSize := 2, 5

	keyword := "User1"
	db.Model(&User{}).Where("name LIKE ?", "%"+keyword+"%").Count(&total)
	db.Where("name LIKE ?", "%"+keyword+"%").Order("age desc").Limit(pageSize).Offset((page - 1) * pageSize).Find(&users)

	fmt.Printf("总记录数: %d\n", total)
	for _, u := range users {
		fmt.Printf("ID=%d Name=%s Age=%d\n", u.ID, u.Name, u.Age)
	}
}

实战案例:事务嵌套与读写分离

package main

import (
	"errors"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"gorm.io/plugin/dbresolver"
)

func main() {
	dsn := "user:pass@tcp(127.0.0.1:3306)/db?charset=utf8mb4&parseTime=True&loc=Local"
	db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})

	// 配置读写分离
	db.Use(dbresolver.Register(dbresolver.Config{
		Replicas: []gorm.Dialector{mysql.Open(dsn)},
	}).SetConnMaxIdleTime(100))

	// 事务嵌套
	err := db.Transaction(func(tx *gorm.DB) error {
		// 外层操作
		if err := tx.Create(&User{Name: "外层用户"}).Error; err != nil {
			return err
		}
		// 嵌套事务
		if err := tx.Transaction(func(ntx *gorm.DB) error {
			return ntx.Create(&Order{UserID: 1, Amount: 100}).Error
		}); err != nil {
			return err
		}
		return nil
	})
	if err != nil {
		panic(err)
	}
}

type Order struct {
	ID     uint
	UserID uint
	Amount float64
}

func performNestedTransaction(db *gorm.DB) error {
	return db.Transaction(func(tx *gorm.DB) error {
		if err := tx.Create(&User{Name: "交易测试"}).Error; err != nil {
			return err
		}
		if err := tx.Transaction(func(ntx *gorm.DB) error {
			if err := ntx.Create(&Order{UserID: 1, Amount: 200}).Error; err != nil {
				return err
			}
			return nil
		}); err != nil {
			return err
		}
		return nil
	})
}

性能对比:ORM 与原生方案

场景GORM手写 SQLsqlxent
简单查询优秀(自动映射)快(无开销)优秀优秀
复杂关联良好(Preload)手动编写良好优秀
批量插入良好(CreateInBatches)最快良好良好
类型安全编译时检查运行时检查编译时(结构体)编译时生成
学习成本中等低低中等

对于简单查询,手写 SQL 的开销最小,GORM 大约慢 10% 到 20%。但对于复杂业务系统,GORM 的开发效率和维护性明显优于手写 SQL。ent 框架在类型安全方面表现最好,但学习曲线更陡峭。

最佳实践:模型定义与 N+1 避免

// 良好的模型命名和标签规范
type User struct {
	ID        uint           `gorm:"column:id;primaryKey"`
	CreatedAt time.Time      `gorm:"column:created_at;autoCreateTime"`
	UpdatedAt time.Time      `gorm:"column:updated_at;autoUpdateTime"`
	Name      string         `gorm:"column:name;type:varchar(50);not null;index:idx_user_name"`
	Email     string         `gorm:"column:email;type:varchar(100);uniqueIndex:uk_user_email"`
	Age       int            `gorm:"column:age;default:0"`
	IsActive  bool           `gorm:"column:is_active;default:true;comment:用户是否激活"`
	Posts     []Post         `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
}

// 避免 N+1 查询的两种方式
// 方式一:Preload
db.Preload("Posts").Find(&users)

// 方式二:Joins + Preload 组合
db.Joins("Company").Preload("Orders").Find(&users)

最佳实践:连接池优化

package main

import (
	"database/sql"
	"log"
	"time"
	"gorm.io/gorm"
)

func configurePool(sqlDB *sql.DB) {
	sqlDB.SetMaxOpenConns(100)        // 最大打开连接数
	sqlDB.SetMaxIdleConns(10)         // 空闲连接数
	sqlDB.SetConnMaxLifetime(time.Hour) // 连接最大生命周期
	sqlDB.SetConnMaxIdleTime(time.Minute * 10) // 空闲连接最大存活时间

	// 定期打印连接池状态
	sqlDB.Stats()
}

func logPoolStats(sqlDB *sql.DB) {
	go func() {
		ticker := time.NewTicker(30 * time.Second)
		defer ticker.Stop()
		for range ticker.C {
			stats := sqlDB.Stats()
			log.Printf("Pool: open=%d inUse=%d idle=%d wait=%d",
				stats.MaxOpenConnections, stats.InUse, stats.Idle, stats.WaitCount)
		}
	}()
}

Scopes 与分页

func ActiveUsers(db *gorm.DB) *gorm.DB {
	return db.Where("is_active = ?", true)
}

func OlderThan(age int) func(db *gorm.DB) *gorm.DB {
	return func(db *gorm.DB) *gorm.DB {
		return db.Where("age > ?", age)
	}
}

type Pagination struct {
	Page     int
	PageSize int
	Total    int64
}

func Paginate(p *Pagination) func(db *gorm.DB) *gorm.DB {
	return func(db *gorm.DB) *gorm.DB {
		if p.Page <= 0 { p.Page = 1 }
		if p.PageSize <= 0 { p.PageSize = 10 }
		if p.PageSize > 100 { p.PageSize = 100 }
		offset := (p.Page - 1) * p.PageSize
		return db.Offset(offset).Limit(p.PageSize)
	}
}

// 使用
db.Scopes(ActiveUsers, OlderThan(20), Paginate(p)).Find(&users)

实战:完整的博客系统

package main

import (
	"fmt"
	"log"
	"time"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type User struct {
	ID        uint   `gorm:"primarykey"`
	Name      string `gorm:"not null"`
	Email     string `gorm:"uniqueIndex;not null"`
	Posts     []Post
	CreatedAt time.Time
}

type Post struct {
	ID        uint   `gorm:"primarykey"`
	UserID    uint
	User      User
	Title     string `gorm:"not null"`
	Content   string `gorm:"type:text"`
	Tags      []Tag  `gorm:"many2many:post_tags;"`
	CreatedAt time.Time
	UpdatedAt time.Time
}

type Tag struct {
	ID    uint   `gorm:"primarykey"`
	Name  string `gorm:"uniqueIndex;not null"`
	Posts []Post `gorm:"many2many:post_tags;"`
}

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

	db.AutoMigrate(&User{}, &Post{}, &Tag{})

	user := User{Name: "张三", Email: "zhangsan@example.com"}
	db.Create(&user)

	tag1 := Tag{Name: "Go"}
	tag2 := Tag{Name: "教程"}
	db.Create(&tag1)
	db.Create(&tag2)

	post := Post{
		UserID:  user.ID,
		Title:   "GORM 入门",
		Content: "这是一篇关于 GORM 的教程...",
		Tags:    []Tag{tag1, tag2},
	}
	db.Create(&post)

	var userWithPosts User
	db.Preload("Posts.Tags").First(&userWithPosts, user.ID)

	fmt.Printf("用户: %s\n", userWithPosts.Name)
	for _, p := range userWithPosts.Posts {
		fmt.Printf("  文章: %s\n", p.Title)
		for _, t := range p.Tags {
			fmt.Printf("    标签: %s\n", t.Name)
		}
	}

	type UserPostCount struct {
		Name      string
		PostCount int64
	}
	var counts []UserPostCount
	db.Model(&User{}).
		Select("users.name, count(posts.id) as post_count").
		Joins("left join posts on posts.user_id = users.id").
		Group("users.id").
		Scan(&counts)

	for _, c := range counts {
		fmt.Printf("%s: %d 篇文章\n", c.Name, c.PostCount)
	}
}

FAQ

Q1:GORM 的 First、Take 和 Find 有什么区别?
A:First 按主键升序排序后取第一条,找不到会返回 ErrRecordNotFound 错误。Take 不指定排序,直接取一条,不会出错只是返回空。Find 查询所有记录,也不返回错误。

Q2:为什么使用 Preload 后还是有 N+1 查询?
A:检查是否在循环中又访问了关联字段。Preload 只会自动加载顶层关联数据,如果在 for 循环里访问 p.Posts.Author,而 Author 没有预加载,仍然会产生 N+1。

Q3:软删除和物理删除如何使用?
A:默认 Delete 是软删除,会设置 deleted_at 字段。使用 db.Unscoped().Delete() 可以永久删除。在查询时加上 Unscoped 可以查到已删除记录。

Q4:AutoMigrate 可以用于生产环境吗?
A:不建议。AutoMigrate 只会添加缺失的列和索引,不会删除或修改已有列。生产环境推荐使用 golang-migrate 或 Flyway 等专业迁移工具。

Q5:如何实现批量插入并获取自增 ID?
A:使用 db.CreateInBatches(&users, 100) 分批插入,GORM 会自动回填主键到每个结构体的 ID 字段。

Q6:GORM 怎么记录执行的 SQL 日志?
A:通过配置 Logger。可以在 gorm.Config 中设置 Logger 的 LogLevel 为 Info 或更详细级别,也可以自定义 Logger 输出到文件。

Q7:复合主键的关联查询需要注意什么?
A:复合主键需要使用 belongs to 和 has many 等关联标签显式指定外键和引用字段,GORM 可能无法自动推断复合主键的关联关系。

Q8:GORM 连接池参数的最佳设置是什么?
A:MaxOpenConns 通常设为 CPU 核数的两倍到四倍,MaxIdleConns 与 MaxOpenConns 保持一致或略小,ConnMaxLifetime 设为不超过数据库的超时时间。

总结与延伸

本文全面介绍了 GORM 的基础用法和进阶特性。从安装连接到模型定义,从 CRUD 到钩子机制,从预加载到自定义数据类型,GORM 为 Go 开发者提供了强大的 ORM 能力。在性能敏感的场景中,合理使用 GORM 的底层 SQL Builder,结合连接池优化和预加载策略,可以在开发效率和运行性能之间取得平衡。

GORM 极大地简化了数据库操作,让你能专注于业务逻辑。但在复杂查询场景下,仍然可以结合原生 SQL 使用。更多高级用法请参考 GORM 官方文档和相关示例项目。掌握 GORM 的关键在于理解其底层的 SQL 生成原理,避免 N+1 查询,合理配置连接池,以及在合适时机使用原生 SQL。本文覆盖了日常开发中最常用和最重要的场景,配合示例代码可以帮助你快速建立起完整的 GORM 知识体系,在实际项目中游刃有余地进行数据库开发。


进阶概念

钩子机制(Hooks)

GORM 的钩子机制允许在特定数据库操作前后执行自定义逻辑,实现数据校验、自动填充、审计追踪等功能:

type User struct {
    ID        uint   `gorm:"primarykey"`
    Name      string `gorm:"type:varchar(100);not null"`
    Slug      string `gorm:"type:varchar(100);uniqueIndex"`
    Password  string `gorm:"type:varchar(255)"`
    CreatedAt time.Time
    UpdatedAt time.Time
}

// BeforeCreate:创建前生成 slug 和加密密码
func (u *User) BeforeCreate(tx *gorm.DB) error {
    if u.Slug == "" {
        u.Slug = slugify(u.Name)
    }
    if u.Password != "" {
        u.Password = hashPassword(u.Password)
    }
    return nil
}

// BeforeUpdate:更新前验证
func (u *User) BeforeUpdate(tx *gorm.DB) error {
    if u.Name == "" {
        return errors.New("用户名不能为空")
    }
    return nil
}

// AfterCreate:创建后发送通知
func (u *User) AfterCreate(tx *gorm.DB) error {
    log.Printf("新用户 %s 注册成功", u.Name)
    return nil
}

func slugify(name string) string {
    re := regexp.MustCompile(`[^a-zA-Z0-9]+`)
    return strings.ToLower(re.ReplaceAllString(name, "-"))
}

func hashPassword(pwd string) string {
    hash, _ := bcrypt.GenerateFromPassword([]byte(pwd), bcrypt.DefaultCost)
    return string(hash)
}

GORM 支持的钩子有 Before/After Create、Before/After Update、Before/After Delete、Before/After Save、Before/After Find。

关联预加载

预加载避免了 N+1 查询问题。GORM 支持简单预加载和嵌套预加载:

// 简单预加载
db.Preload("Posts").Find(&users)

// 嵌套预加载
db.Preload("Posts.Tags").Preload("Profile").Find(&users)

// 条件预加载
db.Preload("Posts", "status = ?", "published").Find(&users)

// 自定义预加载查询
db.Preload("Posts", func(db *gorm.DB) *gorm.DB {
    return db.Order("created_at DESC").Limit(5)
}).Find(&users)

// Joins 预加载(使用 LEFT JOIN,避免多次查询)
db.Joins("Profile").Joins("Posts.Tags").Find(&users)

自定义数据类型

GORM 允许自定义字段类型,需实现 Scanner 和 Valuer 接口:

// 自定义 JSON 类型
type JSONMap map[string]interface{}

func (j JSONMap) Value() (driver.Value, error) {
    return json.Marshal(j)
}

func (j *JSONMap) Scan(value interface{}) error {
    bytes, ok := value.([]byte)
    if !ok {
        return errors.New(fmt.Sprint("Failed to unmarshal JSON value:", value))
    }
    return json.Unmarshal(bytes, &j)
}

// 使用
type Config struct {
    ID     uint    `gorm:"primarykey"`
    Params JSONMap `gorm:"type:json"`
}

复合主键

type OrderItem struct {
    OrderID   uint `gorm:"primaryKey"`
    ProductID uint `gorm:"primaryKey"`
    Quantity  int
    Price     float64
}

// 自动使用 OrderID + ProductID 作为复合主键

乐观锁

type Inventory struct {
    ID        uint `gorm:"primarykey"`
    ProductID uint `gorm:"uniqueIndex"`
    Quantity  int
    Version   int `gorm:"default:1"`
}

func (i *Inventory) UpdateQuantity(tx *gorm.DB, delta int) error {
    result := tx.Model(&Inventory{}).
        Where("product_id = ? AND version = ?", i.ProductID, i.Version).
        Updates(map[string]interface{}{
            "quantity": gorm.Expr("quantity + ?", delta),
            "version":  gorm.Expr("version + 1"),
        })
    if result.RowsAffected == 0 {
        return errors.New("乐观锁冲突,数据已被修改")
    }
    return nil
}

实战案例

1. 完整 CRUD

type ProductRepository struct {
    db *gorm.DB
}

func (r *ProductRepository) Create(ctx context.Context, p *Product) error {
    return r.db.WithContext(ctx).Create(p).Error
}

func (r *ProductRepository) GetByID(ctx context.Context, id uint) (*Product, error) {
    var p Product
    if err := r.db.WithContext(ctx).First(&p, id).Error; err != nil {
        if errors.Is(err, gorm.ErrRecordNotFound) {
            return nil, nil
        }
        return nil, err
    }
    return &p, nil
}

func (r *ProductRepository) Update(ctx context.Context, id uint, updates map[string]interface{}) error {
    return r.db.WithContext(ctx).Model(&Product{}).Where("id = ?", id).Updates(updates).Error
}

func (r *ProductRepository) Delete(ctx context.Context, id uint) error {
    return r.db.WithContext(ctx).Delete(&Product{}, id).Error
}

func (r *ProductRepository) List(ctx context.Context, filter ProductFilter) ([]Product, int64, error) {
    query := r.db.WithContext(ctx).Model(&Product{})
    if filter.CategoryID > 0 {
        query = query.Where("category_id = ?", filter.CategoryID)
    }
    if filter.MinPrice > 0 {
        query = query.Where("price >= ?", filter.MinPrice)
    }
    if filter.MaxPrice > 0 {
        query = query.Where("price <= ?", filter.MaxPrice)
    }
    if filter.Keyword != "" {
        query = query.Where("name LIKE ?", "%"+filter.Keyword+"%")
    }

    var total int64
    if err := query.Count(&total).Error; err != nil {
        return nil, 0, err
    }

    var products []Product
    err := query.Offset(filter.Offset()).Limit(filter.Limit()).
        Order("created_at DESC").Find(&products).Error
    return products, total, err
}

2. 分页排序搜索

type Pagination struct {
    Page     int    `form:"page"`
    PageSize int    `form:"page_size"`
    Sort     string `form:"sort"`
    Order    string `form:"order"`
}

func (p *Pagination) Offset() int {
    if p.Page <= 0 {
        p.Page = 1
    }
    return (p.Page - 1) * p.Limit()
}

func (p *Pagination) Limit() int {
    if p.PageSize <= 0 {
        p.PageSize = 20
    }
    if p.PageSize > 100 {
        p.PageSize = 100
    }
    return p.PageSize
}

func WithPagination(query *gorm.DB, p *Pagination) *gorm.DB {
    order := "created_at DESC"
    if p.Sort != "" {
        direction := "ASC"
        if strings.ToUpper(p.Order) == "DESC" {
            direction = "DESC"
        }
        order = fmt.Sprintf("%s %s", p.Sort, direction)
    }
    return query.Order(order).Offset(p.Offset()).Limit(p.Limit())
}

3. 事务嵌套

func (r *OrderRepository) CreateOrder(ctx context.Context, userID uint, items []CartItem) error {
    return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
        // 创建订单
        order := Order{UserID: userID, Status: "pending"}
        if err := tx.Create(&order).Error; err != nil {
            return err
        }

        // 创建订单项
        for _, item := range items {
            orderItem := OrderItem{
                OrderID:   order.ID,
                ProductID: item.ProductID,
                Quantity:  item.Quantity,
                Price:     item.Price,
            }
            if err := tx.Create(&orderItem).Error; err != nil {
                return err
            }

            // 扣减库存
            if err := tx.Model(&Product{}).Where("id = ?", item.ProductID).
                UpdateColumn("stock", gorm.Expr("stock - ?", item.Quantity)).Error; err != nil {
                return err
            }
        }

        // 清空购物车(在嵌套事务中)
        if err := tx.Transaction(func(innerTx *gorm.DB) error {
            return innerTx.Where("user_id = ?", userID).Delete(&CartItem{}).Error
        }); err != nil {
            return err
        }

        return nil
    })
}

4. 读写分离

type DBManager struct {
    writeDB *gorm.DB
    readDB  *gorm.DB
}

func (m *DBManager) DB(ctx context.Context) *gorm.DB {
    // 简单策略:GET 请求用读库
    if method, ok := ctx.Value("http_method").(string); ok && method == "GET" {
        return m.readDB.WithContext(ctx)
    }
    return m.writeDB.WithContext(ctx)
}

// 更精细的策略:支持 Hint 强制走主库
func (m *DBManager) ForceWrite(ctx context.Context) *gorm.DB {
    return m.writeDB.WithContext(ctx)
}

func (m *DBManager) ForceRead(ctx context.Context) *gorm.DB {
    return m.readDB.WithContext(ctx)
}

性能对比:GORM vs 手写 SQL vs sqlx vs ent

库类型开发效率性能学习曲线适用场景
GORMORM高中低快速开发、CRUD 为主
手写 SQL原生低高低复杂查询、性能敏感
sqlx轻量封装中高低需要扫描但不想要完整 ORM
ent代码生成高高中GraphQL + 复杂关系
bun现代 ORM高高低追求性能和现代 API
// GORM 批量插入(含蓄水层开销)
db.CreateInBatches(&users, 100)

// sqlx 批量插入(直接 SQL)
query := "INSERT INTO users (name, email) VALUES (:name, :email)"
_, err := sqlx.NamedExec(db, query, users)

// ent 批量插入(代码生成)
client.User.CreateBulk(
    client.User.Create().SetName("张三").SetEmail("a@b.com"),
    client.User.Create().SetName("李四").SetEmail("c@d.com"),
).Save(ctx)

FAQ

Q1:GORM 的 AutoMigrate 会删除数据吗?
A:不会。AutoMigrate 只添加缺失的列和索引,不会删除或修改已有列。生产环境建议使用 golang-migrate 等专业工具。

Q2:如何避免 GORM 的 N+1 查询?
A:使用 Preload 预加载关联数据,或使用 Joins 单查询完成。也可用 FindInBatches 批量查询。

Q3:GORM 怎么执行原生 SQL?
A:使用 db.Raw("SELECT * FROM users WHERE name = ?", name).Scan(&users) 或 db.Exec()。

Q4:Soft Delete 和 Hard Delete 怎么切换?
A:默认是软删除(更新 deleted_at 字段)。用 db.Unscoped().Delete(&user) 执行物理删除。

Q5:GORM 支持数据库迁移吗?
A:AutoMigrate 适合开发环境。生产环境推荐使用 golang-migrate 或 atlas 进行版本化迁移。

Q6:如何处理自定义表名前缀?
A:用 db.Table("my_prefix_users") 或全局配置 NamingStrategy{TablePrefix: "my_prefix_"}。

Q7:GORM 的 Association 和 Preload 有什么区别?
A:Preload 在查询时 JOIN 或二次查询加载关联数据;Association 用于对已加载模型的关联数据进行追加、替换、删除操作。

Q8:GORM Context 方法是线程安全的吗?
A:db.WithContext(ctx) 返回一个新的 *gorm.DB,可以安全地在多个 goroutine 中使用。

最佳实践

  1. 定义 Repository 层:封装数据访问逻辑,避免业务代码直接调用 GORM
  2. 使用 Context:所有查询都应传递 Context,支持超时和取消
  3. 避免 N+1 查询:用 Preload 或 Joins 预加载关联数据
  4. 合理使用钩子:复杂业务逻辑放 Service 层,钩子只用于简单校验和审计
  5. 事务控制:多个写操作必须在事务中执行
  6. 性能敏感用原生 SQL:复杂统计和报表查询可以手写 SQL
  7. 索引优化:根据查询模式在模型标签中声明索引
  8. 连接池配置:通过 db.DB() 获取底层 *sql.DB 配置连接池

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「golang」更多文章

  1. 《Go 语言编程实战》目录
  2. 《Go 语言编程实战》18.3 上线、观测与迭代
  3. 《Go 语言编程实战》18.2 故障演练