Table of contents
Open Table of contents
TL;DR
GORM 是 Go 生态最主流的 ORM,通过 struct tag 映射模型、method chaining 构建查询、自动事务保证写入一致性。核心概念:gorm.Model 提供 ID/时间戳/软删除,Session/Statement 架构解决链式调用的状态污染问题,Save() 更新全字段而 Updates() 只更新非零值字段。v1.30.0+ 引入 Generics API 提供类型安全。
一、版本信息
| 项目 | 说明 |
|---|---|
| 当前稳定版 | v1.30.1(模块路径 gorm.io/gorm) |
| Go 版本要求 | Go 1.18+(Generics API 需要) |
| v2 发布时间 | 2020 年,模块路径从 github.com/jinzhu/gorm 迁移到 gorm.io/gorm |
| Generics API | v1.30.0+ 引入,与传统 API 完全兼容,可混用 |
支持的数据库
| 数据库 | Driver 包 |
|---|---|
| MySQL | gorm.io/driver/mysql |
| PostgreSQL | gorm.io/driver/postgres(底层用 pgx) |
| SQLite | gorm.io/driver/sqlite |
| SQL Server | gorm.io/driver/sqlserver |
| ClickHouse | gorm.io/driver/clickhouse |
| TiDB | 兼容 MySQL 协议,用 MySQL driver |
| GaussDB | gorm.io/driver/gaussdb |
| Oracle | github.com/oracle-samples/gorm-oracle/oracle |
v1 → v2 关键变化
- 模块路径:
github.com/jinzhu/gorm→gorm.io/gorm,driver 拆分为独立模块 - Tag 命名:snake_case → camelCase(
auto_increment不再支持) - 软删除:v1 只要有
DeletedAt字段就自动启用 → v2 必须用gorm.DeletedAt类型 - Hook 签名:
func(*gorm.DB) error(v1 是func(*Scope)) - 错误处理:
RecordNotFound()方法 →errors.Is(err, gorm.ErrRecordNotFound) - 全局更新保护:v2 默认启用
BlockGlobalUpdate - 表操作:
db.CreateTable()→db.Migrator().CreateTable() - 关联 API:
.Related()移除,改用Association().Find() - 新特性:Context 支持、批量操作、Prepared Statement 缓存、嵌套事务/Savepoint、Upsert、行锁、命名参数、Generics API
二、模型定义
gorm.Model
// GORM 预定义的基础模型
type Model struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
}
// 嵌入到你的 struct
type User struct {
gorm.Model // 自动包含 ID, CreatedAt, UpdatedAt, DeletedAt
Name string
Email *string
Age uint8
}
约定:
- 字段名
ID→ 默认主键 - struct 名
User→ 表名users(snake_case + 复数) - 字段名
MemberNumber→ 列名member_number CreatedAt/UpdatedAt自动跟踪时间
自定义表名
func (User) TableName() string {
return "my_users"
}
// 动态表名用 Scopes(v2 中 TableName 结果会被缓存)
func UserTable(u *User) func(*gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
return db.Table("user_" + u.Role)
}
}
db.Scopes(UserTable(&user)).Create(&user)
Struct Tag 完整参考
| Tag | 用途 | 示例 |
|---|---|---|
column | 列名 | gorm:"column:user_name" |
type | 列类型 | gorm:"type:varchar(100)" |
size | 列长度 | gorm:"size:256" |
primaryKey | 主键 | gorm:"primaryKey" |
autoIncrement | 自增 | gorm:"autoIncrement" |
unique | 唯一约束 | gorm:"unique" |
not null | 非空 | gorm:"not null" |
default | 默认值 | gorm:"default:18" |
index | 索引 | gorm:"index" |
uniqueIndex | 唯一索引 | gorm:"uniqueIndex" |
embedded | 嵌入结构体 | gorm:"embedded" |
embeddedPrefix | 嵌入字段前缀 | gorm:"embedded;embeddedPrefix:author_" |
serializer | 序列化格式 | gorm:"serializer:json" |
comment | 列注释 | gorm:"comment:用户名" |
check | CHECK 约束 | gorm:"check:age_check,age > 0" |
autoCreateTime | 创建时间戳 | gorm:"autoCreateTime:milli" |
autoUpdateTime | 更新时间戳 | gorm:"autoUpdateTime:nano" |
- | 忽略字段 | gorm:"-" |
-:all | 忽略所有操作 | gorm:"-:all" |
-:migration | 忽略迁移 | gorm:"-:migration" |
字段权限控制
type User struct {
Name string `gorm:"<-:create"` // 只允许创建时写入
Age int `gorm:"<-:update"` // 只允许更新时写入
Role string `gorm:"<-"` // 允许创建和更新
Pass string `gorm:"<-:false"` // 只读(禁止写入)
Bio string `gorm:"->"` // 只读
Key string `gorm:"->:false;<-:create"` // 禁止读,允许创建
}
嵌入结构体
// 匿名嵌入:字段直接合并
type Blog struct {
Author // Author 的字段直接成为 Blog 的列
ID int
Upvotes int32
}
// 命名嵌入 + 前缀
type Blog struct {
ID int
Author Author `gorm:"embedded;embeddedPrefix:author_"`
// Author.Name → author_name 列
}
时间跟踪变体
type User struct {
CreatedAt time.Time // 默认用 time.Time
Updated int64 `gorm:"autoUpdateTime"` // unix 秒
Updated int64 `gorm:"autoUpdateTime:milli"` // unix 毫秒
Updated int64 `gorm:"autoUpdateTime:nano"` // unix 纳秒
}
三、CRUD 操作
Create
// 创建单条
user := User{Name: "Jinzhu", Age: 18}
result := db.Create(&user) // 必须传指针
user.ID // 自动回填主键
result.Error // 错误
result.RowsAffected // 插入行数
// 批量创建
users := []User{{Name: "a"}, {Name: "b"}, {Name: "c"}}
db.Create(&users)
// 分批插入(每批 100 条)
db.CreateInBatches(users, 100)
// 选择/忽略字段
db.Select("Name", "Age", "CreatedAt").Create(&user)
db.Omit("Name", "Age").Create(&user)
// 从 map 创建(不触发 Hook,不回填主键)
db.Model(&User{}).Create(map[string]interface{}{
"Name": "jinzhu", "Age": 18,
})
// 创建时附带关联
db.Create(&User{
Name: "jinzhu",
CreditCard: CreditCard{Number: "411111111111"},
})
// 跳过关联
db.Omit(clause.Associations).Create(&user)
Upsert(插入或更新)
// 冲突时不做任何事
db.Clauses(clause.OnConflict{DoNothing: true}).Create(&user)
// 冲突时更新指定字段
db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "id"}},
DoUpdates: clause.Assignments(map[string]interface{}{"role": "user"}),
}).Create(&users)
// 冲突时更新所有字段
db.Clauses(clause.OnConflict{UpdateAll: true}).Create(&users)
// 冲突时用 SQL 表达式更新
db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "id"}},
DoUpdates: clause.Assignments(map[string]interface{}{
"count": gorm.Expr("GREATEST(count, VALUES(count))"),
}),
}).Create(&users)
默认值与零值
type User struct {
Name string `gorm:"default:galeone"`
Age *int `gorm:"default:18"` // 指针类型区分零值和未设置
Active sql.NullBool `gorm:"default:true"` // sql.Null* 类型也行
}
Pitfall:struct 中零值(0, "", false)不会写入数据库,会使用 default。要写入零值,用指针类型 *int 或 sql.NullXxx。
Read
// 单条查询
db.First(&user) // ORDER BY id LIMIT 1
db.Take(&user) // LIMIT 1(无排序)
db.Last(&user) // ORDER BY id DESC LIMIT 1
// 以上三个方法找不到记录时返回 gorm.ErrRecordNotFound
// 按主键
db.First(&user, 10) // WHERE id = 10
db.First(&user, "id = ?", "uuid-string") // UUID 主键
db.Find(&users, []int{1, 2, 3}) // WHERE id IN (1,2,3)
// 全量查询
db.Find(&users)
// result.RowsAffected == len(users)
// Where 条件
db.Where("name = ?", "jinzhu").First(&user)
db.Where("name IN ?", []string{"a", "b"}).Find(&users)
db.Where("name LIKE ?", "%jin%").Find(&users)
db.Where("created_at BETWEEN ? AND ?", start, end).Find(&users)
// Struct 条件(零值字段被忽略!)
db.Where(&User{Name: "jinzhu", Age: 20}).First(&user)
// SELECT * FROM users WHERE name = "jinzhu" AND age = 20
// Map 条件(包含零值)
db.Where(map[string]interface{}{"name": "jinzhu", "age": 0}).Find(&users)
// SELECT * FROM users WHERE name = "jinzhu" AND age = 0
// 指定 Struct 查询字段(解决零值被忽略的问题)
db.Where(&User{Name: "jinzhu"}, "name", "Age").Find(&users)
// SELECT * FROM users WHERE name = "jinzhu" AND age = 0
// Not / Or
db.Not("name = ?", "jinzhu").First(&user)
db.Where("role = ?", "admin").Or("role = ?", "super_admin").Find(&users)
// 选择字段
db.Select("name", "age").Find(&users)
// 排序、分页
db.Order("age desc, name").Limit(10).Offset(5).Find(&users)
db.Limit(-1) // 取消 limit
db.Offset(-1) // 取消 offset
// Group / Having
db.Model(&User{}).Select("name, sum(age) as total").
Group("name").Having("name = ?", "group").Find(&result)
// Distinct
db.Distinct("name", "age").Order("name, age desc").Find(&results)
// Pluck(提取单列到 slice)
var ages []int64
db.Model(&User{}).Pluck("age", &ages)
// Scan(扫描到自定义 struct)
var result Result
db.Table("users").Select("name", "age").Scan(&result)
// Count
var count int64
db.Model(&User{}).Where("name = ?", "jinzhu").Count(&count)
高级查询
// SubQuery
db.Where("amount > (?)", db.Table("orders").Select("AVG(amount)")).Find(&orders)
// Group Conditions(复杂条件组合)
db.Where(
db.Where("pizza = ?", "pepperoni").Where(
db.Where("size = ?", "small").Or("size = ?", "medium"),
),
).Or(
db.Where("pizza = ?", "hawaiian").Where("size = ?", "xlarge"),
).Find(&Pizza{})
// Named Arguments
db.Where("name1 = @name OR name2 = @name", sql.Named("name", "jinzhu")).Find(&user)
// Find To Map
result := map[string]interface{}{}
db.Model(&User{}).First(&result, "id = ?", 1)
// FirstOrInit(查不到就初始化 struct,不写数据库)
db.Where(User{Name: "non_existing"}).Attrs(User{Age: 20}).FirstOrInit(&user)
// FirstOrCreate(查不到就创建)
db.Where(User{Name: "non_existing"}).Attrs(User{Age: 20}).FirstOrCreate(&user)
// FindInBatches(分批处理大数据集)
db.Where("processed = ?", false).FindInBatches(&results, 100, func(tx *gorm.DB, batch int) error {
// 每批 100 条
tx.Save(&results)
return nil
})
// Scopes(可复用的查询条件)
func AmountGreaterThan1000(db *gorm.DB) *gorm.DB {
return db.Where("amount > ?", 1000)
}
db.Scopes(AmountGreaterThan1000).Find(&orders)
// 行锁
db.Clauses(clause.Locking{Strength: "UPDATE"}).Find(&users) // FOR UPDATE
db.Clauses(clause.Locking{Strength: "UPDATE", Options: "NOWAIT"}).Find(&users) // FOR UPDATE NOWAIT
db.Clauses(clause.Locking{Strength: "UPDATE", Options: "SKIP LOCKED"}).Find(&users) // SKIP LOCKED
// Optimizer/Index Hints
import "gorm.io/hints"
db.Clauses(hints.UseIndex("idx_user_name")).Find(&User{})
db.Clauses(hints.ForceIndex("idx_user_name").ForJoin()).Find(&User{})
Update
// Save — 更新所有字段(Upsert 行为)
db.Save(&user) // 有主键 → UPDATE 全字段;无主键 → INSERT
// Update 单列(必须有条件,否则报 ErrMissingWhereClause)
db.Model(&user).Update("name", "hello")
// UPDATE users SET name='hello', updated_at='...' WHERE id = 111
// Updates 多列
// struct(零值字段被跳过!)
db.Model(&user).Updates(User{Name: "hello", Age: 18})
// map(零值字段也更新)
db.Model(&user).Updates(map[string]interface{}{"name": "hello", "age": 0})
// 选择/忽略字段
db.Model(&user).Select("name").Updates(map[string]interface{}{"name": "hello", "age": 18})
// 只更新 name
db.Model(&user).Select("*").Updates(User{Name: "jinzhu", Age: 0})
// 更新所有字段,包括零值
// 批量更新
db.Model(User{}).Where("role = ?", "admin").Updates(User{Name: "hello"})
// SQL 表达式
db.Model(&product).Update("price", gorm.Expr("price * ? + ?", 2, 100))
// SubQuery 更新
db.Model(&user).Update("company_name", db.Model(&Company{}).Select("name").Where("companies.id = users.company_id"))
// 跳过 Hook 和时间戳更新
db.Model(&user).UpdateColumn("name", "hello")
db.Model(&user).UpdateColumns(User{Name: "hello", Age: 18})
// 返回更新后的数据(需数据库支持 RETURNING)
db.Model(&users).Clauses(clause.Returning{}).Where("role = ?", "admin").Update("salary", gorm.Expr("salary * ?", 2))
// 全局更新保护 — 默认禁止无条件更新
db.Session(&gorm.Session{AllowGlobalUpdate: true}).Model(&User{}).Update("name", "jinzhu")
Save vs Updates — 关键区别
| 维度 | Save | Updates |
|---|---|---|
| 更新范围 | 全字段,无论是否有变化 | struct 只更新非零值,map 更新所有指定字段 |
| Upsert | 有主键 → Update,无主键 → Create | 不做 Upsert |
| 关联处理 | 自动级联保存关联对象 | 不处理关联 |
| 性能 | 大 struct 时可能浪费(更新不需要改的列) | 更精确,更高效 |
| Generics API | 已移除(避免歧义),推荐用 Create/Updates 替代 | 正常使用 |
| 典型场景 | 完整替换记录状态 | 部分字段更新 |
Pitfall:不要把 Save 和 Model 一起用,会产生未定义行为。
Delete
// 软删除(有 gorm.DeletedAt 字段时自动启用)
db.Delete(&user)
// UPDATE users SET deleted_at='2024-01-01 10:00:00' WHERE id = 111
// 后续查询自动加 WHERE deleted_at IS NULL
// 查询被软删除的记录
db.Unscoped().Where("age = 20").Find(&users)
// SELECT * FROM users WHERE age = 20(不加 deleted_at IS NULL)
// 永久删除
db.Unscoped().Delete(&user)
// DELETE FROM users WHERE id = 111
// 按主键删除
db.Delete(&User{}, 10)
db.Delete(&User{}, []int{1, 2, 3})
// 条件删除
db.Where("email LIKE ?", "%jinzhu%").Delete(&Email{})
// 全局删除保护 — 默认禁止无条件删除
db.Where("1 = 1").Delete(&User{}) // OK
db.Session(&gorm.Session{AllowGlobalUpdate: true}).Delete(&User{}) // OK
// 返回删除的数据
db.Clauses(clause.Returning{}).Where("role = ?", "admin").Delete(&users)
软删除进阶
// 默认:gorm.DeletedAt(sql.NullTime,存时间戳)
type User struct {
gorm.Model
Name string
}
// 用 unix 秒存储(plugin: gorm.io/plugin/soft_delete)
type User struct {
ID uint
DeletedAt soft_delete.DeletedAt // WHERE deleted_at = 0
}
// 用 flag 模式(0/1)
type User struct {
ID uint
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"` // WHERE is_del = 0
}
// 混合模式
type User struct {
ID uint
DeletedAt time.Time
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag,DeletedAtField:DeletedAt"`
}
四、Hooks
执行顺序
Create:
BeforeSave → BeforeCreate → [保存关联] → [INSERT] → AfterCreate → AfterSave → [commit/rollback]
Update:
BeforeSave → BeforeUpdate → [保存关联] → [UPDATE] → AfterUpdate → AfterSave → [commit/rollback]
Delete:
BeforeDelete → [DELETE] → AfterDelete → [commit/rollback]
Query:
[加载数据] → AfterFind(preloading 之后)
签名与用法
所有 Hook 签名统一为 func(*gorm.DB) error:
func (u *User) BeforeCreate(tx *gorm.DB) (err error) {
u.UUID = uuid.New()
if !u.IsValid() {
return errors.New("invalid data") // 返回 error → 回滚事务
}
return
}
func (u *User) AfterFind(tx *gorm.DB) (err error) {
if u.MemberShip == "" {
u.MemberShip = "user"
}
return
}
Hook 中修改当前操作
func (u *User) BeforeCreate(tx *gorm.DB) error {
// 只插入指定字段
tx.Statement.Select("Name", "Age")
// 添加 ON CONFLICT
tx.Statement.AddClause(clause.OnConflict{DoNothing: true})
return nil
}
Hook 中检测字段变化
func (u *User) BeforeUpdate(tx *gorm.DB) error {
if tx.Statement.Changed("Role") {
return errors.New("role changes not allowed")
}
return nil
}
跳过 Hook
db.Session(&gorm.Session{SkipHooks: true}).Create(&user)
db.Session(&gorm.Session{SkipHooks: true}).Find(&users)
注意:从 map 创建、UpdateColumn/UpdateColumns 都不触发 Hook。
五、事务
默认事务
GORM 默认将每个写操作(Create/Update/Delete)包装在事务中,保证数据一致性。代价是约 30% 的性能开销。
// 全局禁用(适用于不需要事务保证的场景)
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
SkipDefaultTransaction: true,
})
// 单次禁用
tx := db.Session(&gorm.Session{SkipDefaultTransaction: true})
自动事务(推荐)
err := db.Transaction(func(tx *gorm.DB) error {
if err := tx.Create(&Animal{Name: "Giraffe"}).Error; err != nil {
return err // 返回 error → 自动回滚
}
if err := tx.Create(&Animal{Name: "Lion"}).Error; err != nil {
return err
}
return nil // 返回 nil → 自动提交
})
手动事务
tx := db.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
if err := tx.Create(&Animal{Name: "Giraffe"}).Error; err != nil {
tx.Rollback()
return err
}
return tx.Commit().Error
嵌套事务与 Savepoint
db.Transaction(func(tx *gorm.DB) error {
tx.Create(&user1)
tx.Transaction(func(tx2 *gorm.DB) error {
tx2.Create(&user2)
return errors.New("rollback user2") // 只回滚 user2
})
tx.Transaction(func(tx3 *gorm.DB) error {
tx3.Create(&user3)
return nil
})
return nil
})
// 结果:user1 和 user3 被提交,user2 被回滚
// 手动 Savepoint
tx := db.Begin()
tx.Create(&user1)
tx.SavePoint("sp1")
tx.Create(&user2)
tx.RollbackTo("sp1") // 回滚 user2
tx.Commit() // 提交 user1
六、关联
四种关联类型
// Belongs To — User 属于 Company
type User struct {
gorm.Model
Name string
CompanyID int // 外键在自己这边
Company Company
}
// Has One — User 有一个 CreditCard
type User struct {
gorm.Model
CreditCard CreditCard // 外键在对方(CreditCard.UserID)
}
// Has Many — User 有多个 CreditCard
type User struct {
gorm.Model
CreditCards []CreditCard
}
// Many to Many — User 和 Language 多对多
type User struct {
gorm.Model
Languages []Language `gorm:"many2many:user_languages;"`
}
自定义外键与引用
type User struct {
gorm.Model
CompanyRefer int
Company Company `gorm:"foreignKey:CompanyRefer"` // 自定义外键字段
}
type User struct {
gorm.Model
CompanyID string
Company Company `gorm:"references:Code"` // 引用非主键字段
}
type User struct {
CreditCards []CreditCard `gorm:"foreignKey:UserRefer"` // Has Many 自定义外键
}
type User struct {
MemberNumber string
CreditCards []CreditCard `gorm:"foreignKey:UserNumber;references:MemberNumber"`
}
外键约束
type User struct {
gorm.Model
CreditCard CreditCard `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
}
Preload(预加载)
// 分离查询(N+1 → 2 查询)
db.Preload("Orders").Find(&users)
// SELECT * FROM users;
// SELECT * FROM orders WHERE user_id IN (1,2,3,4);
// 带条件的 Preload
db.Preload("Orders", "state NOT IN (?)", "cancelled").Find(&users)
// 自定义 Preload SQL
db.Preload("Orders", func(db *gorm.DB) *gorm.DB {
return db.Order("orders.amount DESC")
}).Find(&users)
// 嵌套 Preload
db.Preload("Orders.OrderItems.Product").Preload("CreditCard").Find(&users)
// Preload 所有关联
db.Preload(clause.Associations).Find(&users)
// Joins 预加载(单次 JOIN 查询,仅限 has one / belongs to)
db.Joins("Company").Joins("Manager").First(&user, 1)
db.Joins("Company", db.Where(&Company{Alive: true})).Find(&users)
// 嵌套 Joins
db.Joins("Manager").Joins("Manager.Company").Find(&users)
七、Migration
AutoMigrate
db.AutoMigrate(&User{})
db.AutoMigrate(&User{}, &Product{}, &Order{})
// 设置表引擎
db.Set("gorm:table_options", "ENGINE=InnoDB").AutoMigrate(&User{})
// 禁用外键约束创建
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
DisableForeignKeyConstraintWhenMigrating: true,
})
AutoMigrate 会做的事:创建表、添加缺失的列/外键/约束/索引、修改列类型(size/precision 变化时)。
AutoMigrate 不会做的事:不会删除未使用的列(保护数据)、不会删除表、不会做数据迁移。
Migrator 接口
// 表操作
db.Migrator().CreateTable(&User{})
db.Migrator().HasTable(&User{})
db.Migrator().DropTable(&User{})
db.Migrator().RenameTable(&User{}, &UserInfo{})
db.Migrator().GetTables()
// 列操作
db.Migrator().AddColumn(&User{}, "Name")
db.Migrator().DropColumn(&User{}, "Name")
db.Migrator().AlterColumn(&User{}, "Name")
db.Migrator().HasColumn(&User{}, "Name")
db.Migrator().RenameColumn(&User{}, "Name", "NewName")
db.Migrator().ColumnTypes(&User{}) // 返回 []ColumnType
// 索引操作
db.Migrator().CreateIndex(&User{}, "Name")
db.Migrator().DropIndex(&User{}, "idx_name")
db.Migrator().HasIndex(&User{}, "idx_name")
db.Migrator().RenameIndex(&User{}, "idx_name", "idx_name_2")
// 约束操作
db.Migrator().CreateConstraint(&User{}, "CreditCards")
db.Migrator().HasConstraint(&User{}, "CreditCards")
db.Migrator().DropConstraint(&User{}, "CreditCards")
// 视图操作
query := db.Model(&User{}).Where("age > ?", 20)
db.Migrator().CreateView("active_users", gorm.ViewOption{Query: query})
db.Migrator().DropView("active_users")
生产环境建议:AutoMigrate 适合开发和简单场景。生产环境推荐使用 Atlas(atlas migrate diff --env gorm)或 goose/golang-migrate 等版本化迁移工具。
八、索引定义
type User struct {
// 基础索引
Name string `gorm:"index"`
Email string `gorm:"uniqueIndex"`
// 复合索引(同名 index 自动合并)
Name2 string `gorm:"index:idx_member"`
Age int `gorm:"index:idx_member"`
// 带选项的索引
Name3 string `gorm:"index:,sort:desc,collate:utf8,type:btree,length:10,where:name3 != 'jinzhu'"`
Bio string `gorm:"index:,class:FULLTEXT,comment:全文索引"`
Score int64 `gorm:"index:,expression:ABS(score)"`
// 一个字段多个索引
Data string `gorm:"index:idx_a;index:idx_b,unique"`
}
九、设计决策深入解析
为什么用 Method Chaining + Session/Statement 架构
GORM 将方法分为三类:
- Chain Method:
Where、Select、Joins、Scopes等 — 修改当前Statement上的Clauses - Finisher Method:
Create、First、Find、Save、Update、Delete— 执行 SQL - New Session Method:
Session、WithContext、Debug— 创建新的*gorm.DB,隔离 Statement
核心问题:Chain Method 会修改 *gorm.DB 内部的 Statement 状态,如果复用同一个 *gorm.DB,条件会累积:
// 错误用法 — 条件污染
queryDB := db.Where("name = ?", "jinzhu")
queryDB.Where("age > ?", 10).First(&user1)
queryDB.Where("age > ?", 20).First(&user2)
// user2 的查询实际是:WHERE name = "jinzhu" AND age > 10 AND age > 20
// ^^^^^^^^ 上一次查询的条件残留了
// 正确用法 — 用 Session 隔离
queryDB := db.Where("name = ?", "jinzhu").Session(&gorm.Session{})
queryDB.Where("age > ?", 10).First(&user1)
// WHERE name = "jinzhu" AND age > 10
queryDB.Where("age > ?", 20).First(&user2)
// WHERE name = "jinzhu" AND age > 20 ✓ 正确
Generics API 从设计上消除了这个问题:每次 Finisher 调用返回独立结果,不修改原始 builder。
软删除的内部机制
gorm.DeletedAt 本质是 sql.NullTime 的包装(有效时间 = 已删除,NULL = 未删除)。GORM 在三个层面自动注入逻辑:
- Delete 时:将
DELETE改写为UPDATE ... SET deleted_at = NOW() - Query 时:自动追加
WHERE deleted_at IS NULL - Unscoped:绕过以上两个自动行为
这是通过 GORM 的 Callback 插件系统实现的,不是简单的字符串替换。
为什么默认包装事务
GORM 默认为每个写操作(Create/Update/Delete)开启事务,因为:
- Hook 在事务内执行,返回 error 可以回滚整个操作
- 关联保存(如
Create同时写入关联表)需要原子性 - 防止 Hook 中的副作用在主操作失败时无法回滚
代价是每个写操作多一次 BEGIN + COMMIT,约 30% 性能开销。在以下场景可以禁用:
- 简单的单表写入,不需要 Hook 的原子性保证
- 已经在外层手动管理事务
- 高吞吐写入场景,愿意用一致性换性能
十、连接配置
// MySQL
dsn := "user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
// PostgreSQL
dsn := "host=localhost user=gorm password=gorm dbname=gorm port=5432 sslmode=disable TimeZone=Asia/Shanghai"
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
// SQLite
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{})
// 内存数据库
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
// SQL Server
dsn := "sqlserver://gorm:LoremIpsum86@localhost:9930?database=gorm"
db, err := gorm.Open(sqlserver.Open(dsn), &gorm.Config{})
// 连接池配置
sqlDB, err := db.DB()
sqlDB.SetMaxIdleConns(10)
sqlDB.SetMaxOpenConns(100)
sqlDB.SetConnMaxLifetime(time.Hour)
十一、性能优化清单
- 禁用默认事务:
SkipDefaultTransaction: true(约 30% 提升) - Prepared Statement 缓存:
PrepareStmt: true - 只查需要的字段:
Select("name", "age")或用更小的 API struct(Smart Select) - 分批处理:
FindInBatches、CreateInBatches - Index Hints:
hints.UseIndex("idx_name") - 读写分离:Database Resolver 插件
十二、常见 Pitfall 总结
| 问题 | 原因 | 解法 |
|---|---|---|
| struct 零值不写入/不更新 | GORM 跳过零值字段 | 用 *int/sql.NullXxx/map/Select("*") |
| Where struct 条件丢失 | 零值字段被忽略 | 用 map 或 Where(&User{}, "field1", "field2") |
| 条件污染(链式调用残留) | Statement 状态共享 | 用 Session(&gorm.Session{}) 或 Generics API |
| Save 更新了不该改的字段 | Save 更新全字段 | 用 Updates 精确更新 |
| 查询不到软删除的记录 | 自动 WHERE deleted_at IS NULL | 用 Unscoped() |
| First 返回 ErrRecordNotFound | First/Last/Take 空结果报错 | 用 Find + 检查 RowsAffected,或 errors.Is |
| Hook 中修改没生效 | Hook 内应操作 tx 而非全局 db | 在 Hook 中用参数 tx 执行操作 |
| AutoMigrate 不删列 | 设计如此,保护数据 | 手动删列或用 Atlas 等迁移工具 |
延伸方向
- GORM Gen:基于 GORM 的代码生成工具,编译期生成类型安全的查询代码
- sqlc vs GORM:sqlc 是 SQL-first(写 SQL 生成 Go 代码),GORM 是 code-first(写 Go struct 生成 SQL),各有适用场景
- Database Resolver:多数据库、读写分离插件
- Sharding 插件:
gorm.io/sharding,分表支持