跳转到正文
zeno's blog
返回

GORM(一):核心用法与设计决策

专题: GORM

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 APIv1.30.0+ 引入,与传统 API 完全兼容,可混用

支持的数据库

数据库Driver 包
MySQLgorm.io/driver/mysql
PostgreSQLgorm.io/driver/postgres(底层用 pgx)
SQLitegorm.io/driver/sqlite
SQL Servergorm.io/driver/sqlserver
ClickHousegorm.io/driver/clickhouse
TiDB兼容 MySQL 协议,用 MySQL driver
GaussDBgorm.io/driver/gaussdb
Oraclegithub.com/oracle-samples/gorm-oracle/oracle

v1 → v2 关键变化


二、模型定义

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
}

约定

自定义表名

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:用户名"
checkCHECK 约束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。要写入零值,用指针类型 *intsql.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 — 关键区别

维度SaveUpdates
更新范围全字段,无论是否有变化struct 只更新非零值,map 更新所有指定字段
Upsert有主键 → Update,无主键 → Create不做 Upsert
关联处理自动级联保存关联对象不处理关联
性能大 struct 时可能浪费(更新不需要改的列)更精确,更高效
Generics API已移除(避免歧义),推荐用 Create/Updates 替代正常使用
典型场景完整替换记录状态部分字段更新

Pitfall:不要把 SaveModel 一起用,会产生未定义行为。

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 将方法分为三类:

  1. Chain MethodWhereSelectJoinsScopes 等 — 修改当前 Statement 上的 Clauses
  2. Finisher MethodCreateFirstFindSaveUpdateDelete — 执行 SQL
  3. New Session MethodSessionWithContextDebug — 创建新的 *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 在三个层面自动注入逻辑:

  1. Delete 时:将 DELETE 改写为 UPDATE ... SET deleted_at = NOW()
  2. Query 时:自动追加 WHERE deleted_at IS NULL
  3. Unscoped:绕过以上两个自动行为

这是通过 GORM 的 Callback 插件系统实现的,不是简单的字符串替换。

为什么默认包装事务

GORM 默认为每个写操作(Create/Update/Delete)开启事务,因为:

代价是每个写操作多一次 BEGIN + COMMIT,约 30% 性能开销。在以下场景可以禁用:


十、连接配置

// 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)

十一、性能优化清单

  1. 禁用默认事务SkipDefaultTransaction: true(约 30% 提升)
  2. Prepared Statement 缓存PrepareStmt: true
  3. 只查需要的字段Select("name", "age") 或用更小的 API struct(Smart Select)
  4. 分批处理FindInBatchesCreateInBatches
  5. Index Hintshints.UseIndex("idx_name")
  6. 读写分离: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 NULLUnscoped()
First 返回 ErrRecordNotFoundFirst/Last/Take 空结果报错Find + 检查 RowsAffected,或 errors.Is
Hook 中修改没生效Hook 内应操作 tx 而非全局 db在 Hook 中用参数 tx 执行操作
AutoMigrate 不删列设计如此,保护数据手动删列或用 Atlas 等迁移工具

延伸方向


分享这篇文章:

上一篇
GORM(二):关联与预加载
下一篇
Go 数据库:sqlc 的 SQL-first 类型安全方案