Gorm学习文档
GORM 学习文档
版本: GORM v2.x | 适用场景: Go 语言 MySQL/PostgreSQL/SQLite 开发
文档定位: 从入门到企业级实践的完整学习指南
目录
- 一、GORM 概述
- 二、快速开始
- 三、模型定义
- 四、日志管理
- 五、CRUD 操作
- 六、钩子函数 (Hooks)
- 七、高级查询
- 八、关联关系
- 九、自定义类型
- 十、事务
- 十一、性能优化与最佳实践
- 附录:快速参考表
一、GORM 概述
1.1 什么是 ORM
ORM(Object Relational Mapping,对象关系映射) 解决了面向对象编程语言与关系型数据库之间的数据交互问题。核心思想如下:
| 面向对象概念 | 关系型数据库概念 |
|---|---|
| 类(Class) | 表(Table) |
| 属性 / 字段 | 列(Column) |
| 实例化对象 | 一行记录(Row) |
简单来说:使用一个 struct 表示一张表,struct 中的属性表示表的字段,struct 的实例表示一条记录。
1.2 GORM 简介
GORM 是 Go 语言中最流行的 ORM 库,具有以下特点:
- 全功能 ORM,支持关联(Has One、Has Many、Belongs To、Many To Many)
- 支持 MySQL、PostgreSQL、SQLite、SQL Server 等主流数据库
- 链式 API,代码简洁优雅
- 预加载(Preload)、钩子函数(Callbacks)、事务支持
- 自动迁移(AutoMigrate)、软删除、自定义日志
- 开发者友好,社区活跃
1.3 GORM 特性
- 全功能 ORM
- 关联关系(包含一对一、一对多、多对多)
- 预加载(Eager Loading / Preload)
- 事务(嵌套事务、Save Point、Rollback To)
- Context 支持
- 数据库迁移(Migration)
- SQL 构建器(原生 SQL、子查询)
- 日志集成(可自定义 Logger)
- 可扩展的插件 API
- 每个特性都有完善的测试
1.4 ORM 的优缺点
优点:
- 提高开发效率,减少重复的 SQL 编写
- 代码可读性强,面向对象思维更直观
- 数据库无关性,切换数据库只需更换驱动
- 内置防 SQL 注入机制(参数化查询)
- 自动处理数据类型映射
缺点:
- 需要学习和掌握 ORM 框架的使用方式
- 自动生成 SQL 会消耗一定的计算资源,对性能有一定影响
- 对于极其复杂的查询场景,ORM 可能不如手写 SQL 灵活
- 学习成本较高,调试复杂问题时需要理解底层 SQL
二、快速开始
2.1 安装
go get gorm.io/gorm
go get gorm.io/driver/mysql # MySQL 驱动
# go get gorm.io/driver/postgres # PostgreSQL 驱动
# go get gorm.io/driver/sqlite # SQLite 驱动
2.2 连接数据库
基础连接方式
package main
import (
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
var DB *gorm.DB
func InitDB() {
username := "root"
password := "123456"
host := "127.0.0.1"
port := 3306
dbname := "testdb"
timeout := "10s"
// 构建 DSN(Data Source Name)
dsn := fmt.Sprintf(
"%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local&timeout=%s",
username, password, host, port, dbname, timeout,
)
// 连接 MySQL,获取 *gorm.DB 实例
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
panic("连接数据库失败, error=" + err.Error())
}
DB = db
}
DSN 参数说明:
| 参数 | 说明 |
|---|---|
charset=utf8mb4 |
字符集,支持 emoji 等 4 字节字符 |
parseTime=True |
将 time.Time 类型正确解析 |
loc=Local |
使用本地时区 |
timeout=10s |
连接超时时间 |
2.3 高级配置
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
SkipDefaultTransaction: true, // 跳过默认事务,提高写入性能(约 30% 提升)
NamingStrategy: schema.NamingStrategy{
TablePrefix: "t_", // 表名前缀
SingularTable: true, // 使用单数表名(默认复数 users -> user)
NoLowerCase: false, // 是否关闭小写转换
},
// DisableForeignKeyConstraintWhenMigrating: true, // 禁用自动创建外键约束
// Logger: customLogger, // 自定义日志
})
NamingStrategy 配置项:
| 配置项 | 默认值 | 说明 |
|---|---|---|
| TablePrefix | “” | 表名前缀 |
| SingularTable | false | true 使用单数表名,false 使用复数 |
| NoLowerCase | false | true 关闭自动小写转换 |
2.4 连接池配置
GORM 底层使用 database/sql 的连接池,可通过 DB.Set() 配置:
sqlDB, err := db.DB()
if err != nil {
panic(err)
}
// 设置连接池参数
sqlDB.SetMaxIdleConns(10) // 最大空闲连接数
sqlDB.SetMaxOpenConns(100) // 最大打开连接数
sqlDB.SetConnMaxLifetime(time.Hour) // 连接最大存活时间
sqlDB.SetConnMaxIdleTime(10 * time.Minute) // 连接最大空闲时间
推荐配置参考:
| 环境 | MaxIdleConns | MaxOpenConns | ConnMaxLifetime |
|---|---|---|---|
| 开发环境 | 5 | 20 | 30 分钟 |
| 测试环境 | 10 | 50 | 1 小时 |
| 生产环境 | 20 | 100 | 2 小时 |
三、模型定义
3.1 模型基础
GORM 模型是标准的 Go struct,由 Go 基本数据类型、实现了 Scanner 和 Valuer 接口的自定义类型及其指针或别名组成。
type Student struct {
ID uint `gorm:"primaryKey;comment:主键ID"`
Name string `gorm:"type:varchar(16);not null;comment:姓名"`
Age int `gorm:"type:tinyint;not null;comment:年龄"`
Email *string `gorm:"type:varchar(128);unique;comment:邮箱"` // 指针类型用于存 NULL 值
Phone string `gorm:"type:varchar(11);index;comment:手机号"`
Status int `gorm:"default:1;comment:状态 1-正常 0-禁用"`
CreatedAt time.Time // 自动记录创建时间
UpdatedAt time.Time // 自动记录更新时间
DeletedAt gorm.DeletedAt `gorm:"index"` // 软删除字段
}
GORM 约定的模型字段:
| 字段名 | 类型 | 说明 |
|---|---|---|
ID |
uint/int/uint64/int64 |
默认主键 |
CreatedAt |
time.Time |
记录创建时间 |
UpdatedAt |
time.Time |
记录更新时间 |
DeletedAt |
gorm.DeletedAt |
软删除标记,非空则已被删除 |
3.2 AutoMigrate 表迁移
AutoMigrate 是 GORM 提供的自动迁移工具,会根据模型自动创建/修改表结构。
基本用法:
// 自动迁移(只新增,不删除已有列;会修改字段大小)
err := DB.AutoMigrate(&Student{})
if err != nil {
fmt.Println("迁移表失败:", err)
return
}
指定存储引擎:
// 指定 InnoDB 引擎进行迁移
err := DB.Set("gorm:table_options", "ENGINE=InnoDB").AutoMigrate(&Student{})
if err != nil {
fmt.Println("迁移表失败")
return
}
批量迁移多个模型:
DB.AutoMigrate(
&Student{},
&Teacher{},
&Course{},
)
AutoMigrate 行为规则:
| 操作 | 支持情况 |
|---|---|
| 创建不存在的表 | ✅ 支持 |
| 缺失的外键 | ✅ 支持 |
| 缺失的列 | ✅ 支持 |
| 缺失的索引 | ✅ 支持 |
| 修改列类型/大小 | ⚠️ 仅支持扩大(varchar(10) → varchar(20)) |
| 删除未使用的列 | ❌ 不支持 |
| 重命名列/表 | ❌ 不支持 |
修改字段默认大小的两种方式:
Name string `gorm:"type:varchar(12)"` // 方式一:直接指定类型和长度
Name string `gorm:"size:12"` // 方式二:使用 size 标签
3.3 模型标签大全
| 标签 | 说明 | 示例 |
|---|---|---|
column |
指定列名 | gorm:"column:user_name" |
type |
指定字段类型 | gorm:"type:varchar(32)" |
size |
字段长度 | gorm:"size:255" |
primaryKey |
主键 | gorm:"primaryKey" |
autoIncrement |
自增 | gorm:"autoIncrement" |
unique |
唯一索引 | gorm:"unique" |
index |
普通索引 | gorm:"index" |
uniqueIndex |
唯一索引(可命名) | gorm:"uniqueIndex:idx_name" |
default |
默认值 | gorm:"default:1" |
not null |
非空约束 | gorm:"not null" |
comment |
字段注释 | gorm:"comment:用户名" |
embedded |
嵌套结构体 | gorm:"embedded" |
embeddedPrefix |
嵌套字段前缀 | gorm:"embeddedPrefix:info_" |
- |
忽略该字段 | gorm:"-" |
serializer |
序列化方式 | gorm:"serializer:json" |
autoCreateTime |
自动创建时间 | gorm:"autoCreateTime" |
autoUpdateTime |
自动更新时间 | gorm:"autoUpdateTime" |
foreignKey |
外键指定 | gorm:"foreignKey:UID" |
references |
外键引用 | gorm:"references:Name" |
many2many |
多对多中间表 | gorm:"many2many:article_tags" |
check |
约束检查 | gorm:"check:age > 18" |
constraint |
外键约束 | gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL" |
3.4 嵌套结构体
对于公共字段,可以使用嵌套结构体来避免重复定义:
// 公共基础字段
type BaseModel struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index"`
}
// 使用 embedded 嵌入
type Student struct {
BaseModel // 嵌入公共字段
Name string `gorm:"type:varchar(16);not null;comment:姓名"`
Age int `gorm:"type:tinyint;not null;comment:年龄"`
}
// 使用 embeddedPrefix 为嵌入字段添加前缀
type InfoModel struct {
Status int `gorm:"default:1"`
Addr string `gorm:"type:varchar(255)"`
}
type User struct {
ID uint
Name string
Info InfoModel `gorm:"embedded;embeddedPrefix:info_"`
// 生成的列为: info_status, info_addr
}
四、日志管理
GORM 默认集成了日志组件,用于记录 SQL 执行语句、耗时、慢 SQL 和错误信息。
4.1 日志等级
GORM 提供四种日志等级:
| 等级 | 常量 | 说明 | 适用场景 |
|---|---|---|---|
| 静默 | logger.Silent |
不输出任何日志 | 生产环境(完全静默) |
| 错误 | logger.Error |
仅输出错误日志 | 生产环境 |
| 警告 | logger.Warn |
输出错误 + 慢 SQL | 测试/预发布环境 |
| 信息 | logger.Info |
输出所有 SQL | 开发环境 |
4.2 全局开启 SQL 日志(开发环境)
适用于开发阶段全局查看所有 SQL 执行情况。
package main
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
func main() {
dsn := "root:123456@tcp(127.0.0.1:3306)/test"
// 设置日志等级为 Info(打印所有 SQL)
mysqlLogger := logger.Default.LogMode(logger.Info)
db, err := gorm.Open(
mysql.Open(dsn),
&gorm.Config{Logger: mysqlLogger},
)
if err != nil {
panic(err)
}
// 执行查询 —— 控制台将输出 SQL 及耗时
var user User
db.First(&user)
// 输出示例:
// [2.341ms] [rows:1] SELECT * FROM `users` ORDER BY `users`.`id` LIMIT 1
}
特点:
- 全局生效,所有操作都会打印日志
- 适合开发环境调试
- 生产环境不建议使用(影响性能且暴露敏感信息)
4.3 Session 方式开启日志
当只需要在某个模块或某次操作中查看日志时,可以使用 Session。
var mysqlLogger logger.Interface
mysqlLogger = logger.Default.LogMode(logger.Info)
// 创建带日志的 Session(局部生效)
db = db.Session(&gorm.Session{Logger: mysqlLogger})
// 例如:仅在表迁移时打印日志
db.Session(&gorm.Session{Logger: mysqlLogger}).AutoMigrate(
&User{},
&Role{},
)
// 输出:
// CREATE TABLE `users` ...
// CREATE TABLE `roles` ...
特点:
- 局部生效,不影响全局设置
- 适合数据迁移、问题排查等临时场景
4.4 Debug() 方法
最常用的快捷调试方式,本质是创建一个临时的 Info 级别 Session。
// 表迁移 + 日志
db.Debug().AutoMigrate(&Student{})
// 输出: CREATE TABLE `students` ...
// 单条查询 + 日志
var user User
db.Debug().First(&user)
// 输出: SELECT * FROM `users` ORDER BY `users`.`id` LIMIT 1
// 条件查询 + 日志
var users []User
db.Debug().Where("age > ?", 18).Find(&users)
// 输出: SELECT * FROM `users` WHERE age > 18
Debug() 原理:
// db.Debug() 内部源码等价于:
db.Session(&gorm.Session{
Logger: db.Logger.LogMode(logger.Info),
})
即:Debug 本质就是创建一个临时 Session,并将日志等级调整为 Info。
4.5 自定义日志器(生产环境推荐)
通过自定义 Logger 可以精细控制日志行为:
package main
import (
"log"
"os"
"time"
"gorm.io/gorm/logger"
)
// 创建自定义日志器
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
logger.Config{
SlowThreshold: time.Second, // 慢 SQL 阈值:超过 1s 视为慢 SQL
LogLevel: logger.Warn, // 日志等级:Warn
IgnoreRecordNotFoundError: true, // 忽略 ErrRecordNotFound 错误
Colorful: false, // 禁用彩色输出(生产环境建议关闭)
},
)
// 应用到 GORM
db, err := gorm.Open(
mysql.Open(dsn),
&gorm.Config{Logger: newLogger},
)
Config 参数说明:
| 参数 | 类型 | 说明 |
|---|---|---|
| SlowThreshold | time.Duration | 慢 SQL 时间阈值 |
| LogLevel | logger.LogLevel | 日志等级 |
| IgnoreRecordNotFoundError | bool | 是否忽略 RecordNotFound 错误 |
| Colorful | bool | 是否启用彩色输出 |
4.6 Slow SQL 监控
配置慢 SQL 阈值后,超过阈值的 SQL 将会被特殊标记:
// 设置慢 SQL 阈值为 500ms
logger.Config{
SlowThreshold: 500 * time.Millisecond,
}
// 当 SQL 执行超过 500ms 时,输出示例:
// [1200.123ms] [rows:0] SLOW SQL >= 500ms
// SELECT * FROM `users` WHERE ...
4.7 企业项目最佳实践
实际项目中通常根据不同环境配置不同的日志策略:
开发环境 → logger.Info(全部打印)
测试环境 → logger.Warn(只打印错误和慢 SQL)
生产环境 → logger.Error(仅打印错误)
配置文件方式(配合 Viper):
# config.yaml
gorm:
log_level: warn
slow_sql: 500ms
// 结构体映射
type GormConfig struct {
LogLevel string `mapstructure:"log_level"`
SlowSQL time.Duration `mapstructure:"slow_sql"`
}
// 动态选择日志等级
switch conf.Gorm.LogLevel {
case "info":
level = logger.Info
case "warn":
level = logger.Warn
case "error":
level = logger.Error
default:
level = logger.Silent
}
4.8 四种方式对比
| 方式 | 作用范围 | 推荐场景 | 代码量 |
|---|---|---|---|
| 全局 Logger | 整个项目 | 开发环境 | 中等 |
| Session Logger | 某个模块/操作 | 数据迁移、问题排查 | 较多 |
| Debug() | 单条 SQL 语句 | 日常调试 | 最少(一行搞定) |
| 自定义 Logger | 整个项目 | 生产环境 | 较多但灵活 |
五、CRUD 操作
5.1 Create 新增数据
GORM 新增数据的方式包括:单条新增、批量新增、指定字段新增、忽略字段新增等。
单条新增
email := "zhangsan@example.com"
student := Student{
Name: "张三",
Age: 23,
Email: &email,
}
err := DB.Create(&student).Error
if err != nil {
fmt.Println("添加失败:", err)
return
}
fmt.Println("新增成功, ID =", student.ID) // 主键自动回填
生成的 SQL:
INSERT INTO `students` (`name`,`age`,`email`) VALUES ('张三',23,'zhangsan@example.com')
关键要点:
Create()必须传指针(&student),因为 GORM 需要回填自增 ID 到结构体- 新增成功后,
student.ID会自动被赋值为数据库生成的主键值
实际项目写法(Gin 接口中):
var student model.Student
if err := c.ShouldBindJSON(&student); err != nil {
c.JSON(400, gin.H{"error": "参数错误"})
return
}
if err := global.DB.Create(&student).Error; err != nil {
c.JSON(500, gin.H{"error": "创建失败"})
return
}
c.JSON(200, gin.H{"data": student})
批量新增
var students []Student
for i := 0; i < 10; i++ {
students = append(students, Student{
Name: fmt.Sprintf("学生%d", i+1),
Age: 20 + i,
})
}
err := DB.Create(&students).Error
if err != nil {
fmt.Println("批量新增失败:", err)
}
GORM 会生成一条包含多条 VALUES 的 INSERT 语句,而非循环逐条插入,性能更高:
INSERT INTO `students` (`name`,`age`) VALUES ('学生1',20),('学生2',21),('学生3',22),...
指定批次大小(CreateInBatches)
当数据量较大时(如 10000 条),单次 INSERT 可能超出 MySQL 的 max_allowed_packet 限制:
// 每 100 条执行一次 INSERT
err := DB.CreateInBatches(students, 100).Error
Map 方式新增
DB.Model(&Student{}).Create(map[string]interface{}{
"name": "张三",
"age": 18,
})
// INSERT INTO `students` (`name`,`age`) VALUES ('张三',18)
指定字段新增(Select)
student := Student{Name: "张三", Age: 18, Email: &email}
// 只插入 name 和 age 字段
DB.Select("Name", "Age").Create(&student)
// INSERT INTO `students` (`name`,`age`) VALUES ('张三',18)
忽略字段新增(Omit)
// 插入除 email 以外的所有字段
DB.Omit("Email").Create(&student)
// INSERT INTO `students` (`name`,`age`) VALUES ('张三',18)
Create 返回结果
result := DB.Create(&student)
fmt.Println(result.RowsAffected) // 影响行数:1
if result.Error != nil { // 错误信息
fmt.Println(result.Error)
}
指针字段说明(重点)
为什么模型中有些字段使用指针类型?
Email *string // 指针类型
普通类型 vs 指针类型的区别:
| 类型 | Go 零值 | 存入数据库 |
|---|---|---|
string |
"" (空字符串) |
'' |
*string |
nil |
NULL |
使用指针可以区分「没有值」和「值为空字符串」两种状态:
// 不传 Email —— 存入 NULL
student := Student{Name: "张三", Age: 18}
DB.Create(&student)
// INSERT INTO `students` (`name`,`age`,`email`) VALUES ('张三',18,NULL)
// 传 Email —— 存入具体值
email := "test@qq.com"
student := Student{Name: "张三", Age: 18, Email: &email}
DB.Create(&student)
注意:给指针字段赋值时必须传地址(
cannot use string as *string
5.2 Read 查询数据
查询单条记录
GORM 提供三种查询单条记录的方法:
| 方法 | SQL 行为 | 说明 |
|---|---|---|
First() |
... ORDER BY id LIMIT 1 |
按主键升序取第一条 |
Last() |
... ORDER BY id DESC LIMIT 1 |
按主键降序取第一条 |
Take() |
... LIMIT 1 |
不排序,随机取一条(性能略差于 First) |
var student Student
// First - 升序取第一条
DB.First(&student)
// SELECT * FROM `students` ORDER BY `students`.`id` LIMIT 1
// Last - 降序取最后一条
DB.Last(&student)
// SELECT * FROM `students` ORDER BY `students`.`id` DESC LIMIT 1
// Take - 取任意一条(无排序,全表扫描)
DB.Take(&student)
// SELECT * FROM `students` LIMIT 1
警告:
Find()用于查询多条记录。使用First/Take/Last查询单条时,如果无匹配记录会返回ErrRecordNotFound错误。
根据主键或条件查询
var u Student
// 根据主键 ID 查询
DB.Take(&u, 32)
// SELECT * FROM `students` WHERE `students`.`id` = 32 LIMIT 1
// 根据条件查询(使用 ? 占位符防止 SQL 注入)
DB.Take(&u, "name = ?", "张三")
// SELECT * FROM `students` WHERE name = '张三' LIMIT 1
// 通过结构体的主键字段查询
u.ID = 32
DB.Take(&u)
// SELECT * FROM `students` WHERE `students`.`id` = 32 LIMIT 1
查询多条记录
var students []Student
// 查询所有记录
result := DB.Find(&students)
fmt.Println(result.RowsAffected) // 总条数
for _, s := range students {
fmt.Println(s)
}
// 根据 ID 列表查询
DB.Find(&students, []int{1, 2, 3})
// SELECT * FROM `students` WHERE `id` IN (1,2,3)
// 根据其他条件查询多条
DB.Find(&students, "name in ?", []string{"张三", "李四"})
// SELECT * FROM `students` WHERE name IN ('张三','李四')
注意:使用
Find()查询时务必加Where条件或限制数量,避免全表扫描。
获取查询记录数
// 方式一:RowsAffected
count := DB.Take(&student).RowsAffected
// 方式二:Count()
var count int64
DB.Model(&Student{}).Count(&count)
查询错误处理
err := DB.Take(&student, "name = ?", "不存在的人").Error
switch err {
case gorm.ErrRecordNotFound:
fmt.Println("没有找到记录")
case nil:
fmt.Println("查询成功:", student)
default:
fmt.Println("SQL 错误:", err)
}
5.3 Update 更新数据
GORM 更新操作主要分为三类:Save(全字段更新)、Update(单列更新)、Updates(多列更新)。
Save — 全字段更新
Save 会保存所有字段(包括零值),先执行 SELECT 再执行 UPDATE:
var u Student
DB.Take(&u, 32) // 先查询
u.Name = "更新测试"
email := "new@email.com"
u.Email = &email
DB.Save(&u)
// 执行两条 SQL:
// 1. SELECT * FROM `students` WHERE `id` = 32 LIMIT 1
// 2. UPDATE `students` SET `name`='更新测试',`age`=23,`email`='new@email.com' WHERE `id` = 32
指定字段 Save(部分更新):
DB.Select("Name").Save(&u)
// 只更新 name 字段,其他字段不变
// UPDATE `students` SET `name`='更新测试' WHERE `id` = 32
Update — 更新单个字段
// 方式一:先 Find 再 Update(两步操作)
var students []Student
DB.Find(&students, "id IN ?", []int{35, 36, 37}).
Update("name", "批量更新测试")
// 方式二:Model + Where(一步操作,性能更好)
DB.Model(&Student{}).
Where("age = ?", 30).
Update("name", "model更新测试")
// UPDATE `students` SET `name`='model更新测试' WHERE age = 30
Updates — 更新多个字段
使用结构体方式(会忽略零值字段):
// 方式一:先 Find 再 Updates
DB.Find(&students, "id IN ?", []int{38, 39}).
Updates(Student{Name: "test", Age: 25})
// 方式二:Model + Where(推荐,省去 SELECT)
DB.Model(&Student{}).
Where("age = ?", 29).
Updates(Student{Name: "updates测试"})
// 注意:结构体中零值字段不会被更新!
使用 Map 方式(不会忽略零值):
DB.Model(&Student{}).
Where("age = ?", 29).
Updates(map[string]interface{}{
"name": "map方式实现",
"age": 0, // 零值也会被更新!
})
重要区别:结构体方式的 Updates 会忽略零值字段(因为无法区分"不想更新"和"想更新为零值");Map 方式则会更新所有指定字段,包括零值。
5.4 Delete 删除数据
根据主键删除
// 删除单条(软删除)
DB.Delete(&Student{}, 41)
// DELETE FROM `students` WHERE `id` = 41(或 UPDATE ... SET deleted_at = ...)
// 批量删除
DB.Delete(&Student{}, []int{40, 39})
// DELETE FROM `students` WHERE `id` IN (40, 39)
根据条件删除
// Model + Where 删除
DB.Model(&Student{}).Where("id = ?", 38).Delete(nil)
DB.Where("email = ?", "old@email.com").Delete(&Student{})
5.5 软删除详解
GORM 默认启用软删除(Soft Delete)。如果模型中包含 DeletedAt gorm.DeletedAt 字段,调用 Delete() 时不会真正删除记录,而是将该字段的值设置为当前时间。
软删除工作原理:
type Student struct {
ID uint
Name string
DeletedAt gorm.DeletedAt `gorm:"index"` // 软删除标记
}
// 执行删除
DB.Delete(&student, 1)
// 实际执行的 SQL(不是 DELETE,而是 UPDATE):
// UPDATE `students` SET `deleted_at` = '2024-01-01 12:00:00' WHERE `id` = 1 AND `deleted_at` IS NULL
软删除后的查询行为:
// 普通查询自动排除已软删除的记录
DB.Find(&students)
// SELECT * FROM `students` WHERE `deleted_at` IS NULL
// 查询已软删除的记录(使用 Unscoped)
DB.Unscoped().Where("id = ?", 41).Find(&student)
// SELECT * FROM `students` WHERE `id` = 41(包含已删除记录)
// 永久删除(物理删除)
DB.Unscoped().Delete(&student, 41)
// DELETE FROM `students` WHERE `id` = 41(真正的删除)
软删除相关方法汇总:
| 操作 | 方法 | 说明 |
|---|---|---|
| 软删除 | DB.Delete(&user) |
设置 deleted_at |
| 查询(排除已删) | DB.Find(&users) |
自动过滤 deleted_at IS NOT NULL |
| 查询(包含已删) | DB.Unscoped().Find(&users) |
不过滤软删除记录 |
| 永久删除 | DB.Unscoped().Delete(&user) |
物理删除记录 |
| 恢复已删记录 | DB.Unscoped().Model(&user).Update("deleted_at", nil) |
清空 deleted_at |
六、钩子函数 (Hooks)
GORM 钩子允许在执行数据库操作前后插入自定义逻辑。支持的钩子包括:
| 钩子函数 | 触发时机 |
|---|---|
BeforeSave |
保存之前 |
AfterSave |
保存之后 |
BeforeCreate |
创建之前 |
AfterCreate |
创建之后 |
BeforeUpdate |
更新之前 |
AfterUpdate |
更新之后 |
BeforeDelete |
删除之前 |
AfterDelete |
删除之后 |
AfterFind |
查询之后(从数据库读取后) |
示例:创建前自动填充邮箱:
func (stu *Student) BeforeCreate(tx *gorm.DB) error {
// 在插入记录前,如果没有设置邮箱,则设置默认值
if stu.Email == nil {
defaultEmail := "default@example.com"
stu.Email = &defaultEmail
}
return nil
}
// 使用
DB.Create(&Student{
Name: "XX",
Age: 24,
// 此处不需要传 email,BeforeCreate 会自动填充
})
// 更多钩子示例
// 查询后数据处理
func (stu *Student) AfterFind(tx *gorm.DB) error {
// 对查询结果做脱敏处理
if stu.Phone != "" {
stu.Phone = stu.Phone[:3] + "****" + stu.Phone[7:]
}
return nil
}
// 更新前记录日志
func (stu *Student) BeforeUpdate(tx *gorm.DB) error {
log.Printf("即将更新学生 ID=%d 的数据", stu.ID)
return nil
}
注意:如果在钩子中返回 error,整个操作将会中止并回滚。
七、高级查询
7.1 Where 条件查询
GORM 的 Where 方法支持多种查询条件写法,以下演示常见场景:
var students []Students
// 1. 精确匹配
DB.Where("name = ?", "枫枫").Find(&students)
// SELECT * FROM `students` WHERE name = '枫枫'
// 2. Not 取反
DB.Not("name = ?", "枫枫").Find(&students)
// SELECT * FROM `students` WHERE NOT name = '枫枫'
// 3. IN 查询
DB.Where("name IN ?", []string{"如燕", "李元芳"}).Find(&students)
// SELECT * FROM `students` WHERE name IN ('如燕','李元芳')
// 4. LIKE 模糊查询
DB.Where("name LIKE ?", "李%").Find(&students) // 姓李的所有人
DB.Where("name LIKE ?", "李_").Find(&students) // 姓李的两字名称
// 5. AND 多条件
DB.Where("age > ? AND email LIKE ?", 23, "%@qq.com").Find(&students)
// 链式写法(等价):
DB.Where("age > ?", 23).Where("email LIKE ?", "%@qq.com").Find(&students)
// 6. OR 条件
DB.Where("gender = ?", false).Or("email LIKE ?", "%@qq.com").Find(&students)
// 7. 结构体查询(注意:零值字段会被忽略,条件之间为 AND 关系)
DB.Where(&Students{Name: "李元芳", Age: 0}).Find(&students)
// 只会生成 WHERE name = '李元芳'(Age=0 被忽略)
// 8. Map 查询(零值不会被忽略)
DB.Where(map[string]interface{}{"name": "如燕", "age": 32}).Find(&students)
// WHERE name = '如燕' AND age = 32
Struct vs Map 条件查询对比:
| 方式 | 零值处理 | 适用场景 |
|---|---|---|
| Struct | 忽略零值字段 | 动态构建查询条件 |
| Map | 保留零值字段 | 需要精确匹配所有条件 |
7.2 Select 选择字段
// 只查询指定字段(其余字段显示对应类型的零值)
DB.Select("name", "age").Find(&students)
DB.Select([]string{"name", "age"}).Find(&students)
// Scan 到不同的结构体(只经历一次查询)
type UserInfo struct {
Name string
Age int
}
var userList []UserInfo
DB.Model(&Students{}).Select("name", "age").Scan(&userList)
7.3 Order 排序
// 降序(DESC)
DB.Order("age DESC").Find(&students) // 按年龄倒序
// 升序(ASC)
DB.Order("age ASC").Find(&students) // 按年龄升序
// 多字段排序
DB.Order("age DESC, name ASC").Find(&students)
7.4 Distinct 去重
var ageList []int
// 方式一:使用 Distinct 函数
DB.Model(&Students{}).Select("age").Distinct("age").Scan(&ageList)
// 方式二:在 Select 中使用 distinct 关键字
DB.Model(&Students{}).Select("DISTINCT age").Scan(&ageList)
// 查询所有不重复的年龄值
7.5 Limit & Offset 分页
分页的核心公式:Offset = (页码 - 1) × 每页条数
// 原生 SQL 分页原理
// 第一页(每页 2 条):SELECT * FROM student LIMIT 2 OFFSET 0
// 第二页(每页 2 条):SELECT * FROM student LIMIT 2 OFFSET 2
// GORM 分页封装
page := 1 // 当前页码
limit := 10 // 每页条数
var students []Student
result := DB.Limit(limit).Offset((page - 1) * limit).Find(&students)
total := result.RowsAffected // 总条数(可用于计算总页数)
通用分页函数封装示例:
type PageResult struct {
List interface{} `json:"list"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
func Paginate(db *gorm.DB, page, pageSize int, dest interface{}) (*PageResult, error) {
var total int64
db.Model(dest).Count(&total)
offset := (page - 1) * pageSize
if err := db.Limit(pageSize).Offset(offset).Find(dest).Error; err != nil {
return nil, err
}
return &PageResult{
List: dest,
Total: total,
Page: page,
PageSize: pageSize,
}, nil
}
7.6 Group 分组查询
// 统计男生和女生的人数
type GenderGroup struct {
Count int
Gender string
}
var groupList []GenderGroup
DB.Model(&Students{}).
Select("COUNT(id) AS count, gender").
Group("gender").
Scan(&groupList)
// SELECT COUNT(id) as count, gender FROM `students` GROUP BY gender
// 结果: [{5 1}, {3 0}]
分组 + 字符串聚合:
// 统计男女生人数并展示对应的人员名单
type GroupDetail struct {
Count int
Gender string
NameList string `gorm:"column:name_list"`
}
var detailList []GroupDetail
DB.Model(&Students{}).
Select("COUNT(id) AS count, gender, GROUP_CONCAT(name) AS name_list").
Group("gender").
Scan(&detailList)
// SELECT COUNT(id) as count, gender, GROUP_CONCAT(name) as name_list
// FROM `students` GROUP BY gender
// 结果: [{3 0 李琦,晓梅,如燕} {6 1 李元芳,张武,枫枫,刘大,李武,魔灵}]
7.7 原生 SQL 执行
当 GORM 的链式 API 无法满足需求时,可以直接执行原生 SQL:
// Raw + Scan 执行查询类 SQL
type GroupDetail struct {
Count int
Gender string
NameList string
}
var groupList []GroupDetail
DB.Raw(
"SELECT COUNT(id) AS count, gender, GROUP_CONCAT(name) AS name_list FROM `students` GROUP BY gender",
).Scan(&groupList)
// Exec 执行写入类 SQL
DB.Exec("DELETE FROM students WHERE age < 18")
DB.Exec("UPDATE students SET status = 0 WHERE age > 60")
7.8 子查询
GORM 支持使用 *gorm.DB 对象作为子查询条件:
// 查询年龄大于平均年龄的用户
var students []Students
DB.Where("age > (?)", DB.Model(&Students{}).Select("AVG(age)")).Find(&students)
// SELECT * FROM `students` WHERE age > (SELECT AVG(age) FROM `students`)
// 子查询作为条件值
DB.Where("name IN (?)", DB.Model(&Students{}).Select("name").Where("age > ?", 20)).Find(&students)
// SELECT * FROM `students` WHERE name IN (SELECT name FROM `students` WHERE age > 20)
八、关联关系
8.1 Belongs To 一对一(从属)
场景:一篇文章属于一个用户。每篇文章有一个外键指向所属用户。
type User struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"type:varchar(20);comment:用户名"`
}
type Article struct {
ID uint `gorm:"primaryKey"`
Title string `gorm:"type:varchar(100);comment:标题"`
UserID uint `gorm:"index"` // 外键(默认格式:[字段类型]ID)
User User `gorm:"foreignKey:UserID"` // 关联引用
}
外键约定规则:
- 默认外键名:
[关联字段名]ID(如User→UserID) - 默认引用字段:
[关联模型]ID(即主键)
8.2 Has One 一对一(拥有)
场景:一个用户有一份详细资料。用户表持有资料的外键。
type User struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"type:varchar(20)"`
Profile UserProfile `gorm:"foreignKey:UserRef"` // 用户持有一份资料
}
type UserProfile struct {
ID uint `gorm:"primaryKey"`
Address string `gorm:"type:varchar(255)"`
UserRef uint `gorm:"unique;index"` // 反向引用用户 ID
}
8.3 Has Many 一对多
场景:一个用户有多篇文章。
// User 用户表 — 一个用户拥有多篇文章
type User struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"type:varchar(20);comment:用户名"`
Articles []Article `gorm:"foreignKey:UserID"` // 一对多关联
}
// Article 文章表 — 一篇文章属于一个用户
type Article struct {
ID uint `gorm:"primaryKey"`
Title string `gorm:"type:varchar(100);comment:标题"`
UserID uint `gorm:"index;comment:用户ID"` // 外键
User User `gorm:"foreignKey:UserID"` // 反向关联
}
重写外键关联(foreignKey):
type User struct {
ID uint `gorm:"primaryKey"`
Name string
Articles []Article `gorm:"foreignKey:UID"` // 将外键从 UserID 改为 UID
}
type Article struct {
ID uint `gorm:"primaryKey"`
Title string
UID uint // 自定义外键名
User User `gorm:"foreignKey:UID"`
}
重写外键引用(references):
type User struct {
ID uint `gorm:"primaryKey"`
Name string
Articles []Article `gorm:"foreignKey:UserName;references:Name"` // 用 Name 作为关联依据
}
type Article struct {
ID uint `gorm:"primaryKey"`
Title string
UserName string // 外键存储的是 Name 而不是 ID
User User `gorm:"references:Name"`
}
一对多的 CRUD 操作
新增(Association 模式):
// 1. 创建用户时同时创建文章
DB.Create(&User{
Name: "test",
Articles: []Article{
{Title: "Python 入门"},
{Title: "Go 语言实战"},
},
})
// 2. 创建文章,关联已有用户
DB.Create(&Article{
Title: "学习 Python",
UserID: 1, // 直接指定外键
})
// 3. 给已有用户绑定文章(使用 Association)
var user User
var article Article
DB.Take(&user, 1)
DB.Take(&article, 6)
// Append 添加关联
DB.Model(&user).Association("Articles").Append(&article)
// INSERT INTO `articles` (`title`, `user_id`, `id`)
// VALUES ('xxx', 1, 6) ON DUPLICATE KEY UPDATE `user_id` = VALUES(`user_id`)
查询(预加载 Preload):
// 普通查询——无法获取关联数据
var article Article
DB.Take(&article)
// 只能查出文章本身,User 字段为空
// 预加载——同时查询关联的用户
DB.Preload("User").Take(&article)
// 执行两条 SQL:
// SELECT * FROM `articles` LIMIT 1
// SELECT * FROM `users` WHERE `id` = 1
// 嵌套预加载——查询用户及其文章对应的用户信息
var user User
DB.Preload("Articles.User").Take(&user)
// 带条件的预加载
DB.Preload("Articles", "id > ?", 2).Take(&user) // 只加载 ID > 2 的文章
删除(级联删除):
// 场景一:删除用户时,级联删除其文章
DB.Take(&user, 1)
DB.Select("Articles").Delete(&user)
// 删除用户及关联的文章
// 场景二:删除用户时,保留文章(解除关联)
DB.Preload("Articles").Take(&user, 2)
DB.Model(&user).Association("Articles").Delete(user.Articles) // 解除关联
DB.Delete(&user) // 删除用户
8.4 Many to Many 多对多
场景:一篇文章可以有多个标签,一个标签也可以属于多篇文章。
// Tag 标签表
type Tag struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"type:varchar(20);comment:标签名"`
Articles []Article `gorm:"many2many:article_tags"` // 多对多关联
}
// Article 文章表
type Article struct {
ID uint `gorm:"primaryKey"`
Title string `gorm:"type:varchar(100);comment:标题"`
Tags []Tag `gorm:"many2many:article_tags"` // 多对多关联
}
GORM 会自动创建中间表 article_tags,包含两个字段的主键组合。
多对多的 CRUD 操作
新增:
// 1. 创建文章时同时创建标签
DB.Create(&Article{
Title: "Go 入门",
Tags: []Tag{
{Name: "Go"},
{Name: "后端"},
},
})
// 2. 创建文章,关联已有标签
var tags []Tag
DB.Find(&tags, []int{1, 2})
DB.Create(&Article{
Title: "Python 基础",
Tags: tags,
})
查询:
var articles []Article
DB.Preload("Tags").Find(&articles) // 预加载标签
更新(替换关联):
var article Article
DB.Preload("Tags").Take(&article, 1)
// 方式一:先删后加
DB.Model(&article).Association("Tags").Delete(article.Tags)
var newTag Tag
DB.Take(&newTag, 3)
DB.Model(&article).Association("Tags").Append(&newTag)
// 方式二:使用 Replace(推荐,原子操作)
DB.Model(&article).Association("Tags").Replace(&newTag)
8.5 自定义连接表
默认的多对多中间表只有双方的主键 ID,无法存储额外信息(如创建时间)。可以通过自定义连接表来解决:
type Tag struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"type:varchar(20)"`
}
type Article struct {
ID uint `gorm:"primaryKey"`
Title string `gorm:"type:varchar(100)"`
Tags []Tag `gorm:"many2many:article_tags"`
}
// 自定义连接表(可存储更多信息)
type ArticleTag struct {
ArticleID uint `gorm:"primaryKey"`
TagID uint `gorm:"primaryKey"`
CreatedAt time.Time `gorm:"autoCreateTime"` // 额外字段:关联时间
}
func SetupCustomJoinTable(db *gorm.DB) {
db.SetupJoinTable(&Article{}, "Tags", &ArticleTag{}) // 注册自定义连接表
db.AutoMigrate(&Tag{}, &Article{}, &ArticleTag{})
}
自定义连接表主键映射:
type ArticleModel struct {
ID uint `gorm:"primaryKey"`
Title string
Tags []TagModel `gorm:"many2many:article_tags;joinForeignKey:ArticleID;joinReferences:TagID"`
}
type TagModel struct {
ID uint `gorm:"primaryKey"`
Name string
Articles []ArticleModel `gorm:"many2many:article_tags;joinForeignKey:TagID;joinReferences:ArticleID"`
}
type ArticleTagModel struct {
ArticleID uint `gorm:"primaryKey"`
TagID uint `gorm:"primaryKey"`
CreatedAt time.Time `gorm:"autoCreateTime"`
}
查询连接表数据:
// 场景:用户收藏文章(需要知道收藏时间)
type UserModel struct {
ID uint
Name string
Collects []ArticleModel `gorm:"many2many:user_collects;joinForeignKey:UserID;joinReferences:ArticleID"`
}
type UserCollectModel struct {
ArticleID uint `gorm:"primaryKey"`
ArticleModel ArticleModel `gorm:"foreignKey:ArticleID"`
UserID uint `gorm:"primaryKey"`
UserModel UserModel `gorm:"foreignKey:UserID"`
CreatedAt time.Time `gorm:"autoCreateTime"`
}
// 直接查询连接表(获取收藏时间等信息)
var collects []UserCollectModel
DB.Preload("ArticleModel").Preload("UserModel").Find(&collects, "user_id = ?", 1)
8.6 关联模式 (Association Mode)
GORM 的 Association 模式用于处理关联关系的增删改查:
| 方法 | 说明 |
|---|---|
Find |
查找关联记录 |
Append |
添加关联(追加) |
Replace |
替换关联(先删后加) |
Delete |
删除关联(解除关联,不删除记录) |
Clear |
清空所有关联 |
Count |
统计关联数量 |
var user User
DB.Take(&user, 1)
// 查找关联的文章数量
var count int64
DB.Model(&user).Association("Articles").Count(&count)
// 追加关联
var newArticle Article
DB.Take(&newArticle, 10)
DB.Model(&user).Association("Articles").Append(&newArticle)
// 替换所有关联
var articles []Article
DB.Find(&articles, []int{1, 2, 3})
DB.Model(&user).Association("Articles").Replace(&articles)
// 删除某个关联
DB.Model(&user).Association("Articles").Delete(&articles[0])
// 清空所有关联
DB.Model(&user).Association("Articles").Clear()
8.7 预加载 (Preload)
预加载用于解决 N+1 查询问题,在一次查询中加载关联数据。
// 基础预加载
DB.Preload("Orders").Find(&users)
// 嵌套预加载
DB.Preload("Orders.Items").Find(&users)
DB.Preload("Orders.Addresses").Find(&users)
// 带条件预加载
DB.Preload("Orders", "state = ?", "paid").Find(&users)
DB.Preload("Orders", func(db *gorm.DB) *gorm.DB {
return db.Order("orders.created_at DESC").Limit(10)
}).Find(&users)
// 自定义预加载(使用 Joins,只执行一次 SQL)
DB.Joins("Company").Find(&users)
// SELECT users.*, company.* FROM users LEFT JOIN company ON ...
// 预加载全部关联(Preload All)
db.Set("gorm:preload_associations", true).Find(&users)
Preload vs Joins 对比:
| 特性 | Preload | Joins |
|---|---|---|
| SQL 数量 | 2 条(主表 + 关联表) | 1 条(JOIN 查询) |
| 无法匹配关联 | 主表数据仍返回 | 主表数据不返回 |
| 适用场景 | 关联数据可选 | 关联数据必选 |
| 排序/条件 | 支持对关联表单独筛选 | 在 JOIN 中统一筛选 |
九、自定义类型
9.1 JSON 类型存储
很多场景下需要在数据库中存储 JSON 或数组数据。GORM 要求自定义类型必须实现 database/sql 包中的 Scanner 和 Valuer 接口。
Scanner 接口:从数据库读取数据(Scan)
Valuer 接口:向数据库写入数据(Value)
package main
import (
"database/sql/driver"
"encoding/json"
"errors"
"fmt"
)
// Info 自定义 JSON 类型
type Info struct {
Status string `json:"status"`
Addr string `json:"addr"`
Age int `json:"age"`
}
// Scan 从数据库读取(实现 Scanner 接口)
func (i *Info) Scan(value interface{}) error {
bytes, ok := value.([]byte)
if !ok {
return errors.New(fmt.Sprint("Failed to unmarshal JSONB value:", value))
}
return json.Unmarshal(bytes, i)
}
// Value 写入数据库(实现 Valuer 接口)
func (i Info) Value() (driver.Value, error) {
return json.Marshal(i)
}
// 使用 Info 类型的模型
type Users struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"type:varchar(32)"`
Info Info `gorm:"type:json"` // 存储为 JSON 格式
}
func main() {
DB.AutoMigrate(&Users{})
// 写入
DB.Create(&Users{
Name: "张三",
Info: Info{
Status: "active",
Addr: "北京市朝阳区",
Age: 25,
},
})
// 读取(自动反序列化为 Info 结构体)
var user Users
DB.Take(&user)
fmt.Println(user.Info.Status) // active
fmt.Println(user.Info.Addr) // 北京市朝阳区
}
GORM 内置 Serializer(更简洁的替代方案):
从 GORM v2 开始,内置了 serializer 功能,无需手动实现接口:
type UserWithJSON struct {
ID uint `gorm:"primaryKey"`
Name string
Extra map[string]string `gorm:"type:json;serializer:json"` // 自动序列化
}
// 使用
DB.Create(&UserWithJSON{
Name: "test",
Extra: map[string]string{
"hobby": "coding",
"city": "Beijing",
},
})
9.2 枚举类型
Go 语言没有原生的枚举类型,通常使用 const + iota 或自定义类型来实现枚举语义。
基于常量的枚举模式:
package main
import (
"encoding/json"
"fmt"
)
// HostStatus 主机状态枚举
type HostStatus int
const (
Running HostStatus = iota + 1 // 运行中
Except // 异常
OffLine // 离线
)
// MarshalJSON 自定义 JSON 序列化(返回可读名称而非数字)
func (s HostStatus) MarshalJSON() ([]byte, error) {
var str string
switch s {
case Running:
str = "Running"
case Except:
str = "Except"
case OffLine:
str = "OffLine"
default:
str = "Unknown"
}
return json.Marshal(str)
}
// UnmarshalJSON 自定义 JSON 反序列化
func (s *HostStatus) UnmarshalJSON(data []byte) error {
var str string
if err := json.Unmarshal(data, &str); err != nil {
return err
}
switch str {
case "Running":
*s = Running
case "Except":
*s = Except
case "OffLine":
*s = OffLine
default:
*s = Running // 默认值
}
return nil
}
// String 实现 Stringer 接口
func (s HostStatus) String() string {
switch s {
case Running:
return "Running"
case Except:
return "Except"
case OffLine:
return "OffLine"
default:
return "Unknown"
}
}
// Host 主机模型
type Host struct {
ID uint `json:"id" gorm:"primaryKey"`
Name string `json:"name" gorm:"type:varchar(32)"`
Status HostStatus `json:"status" gorm:"type:tinyint;default:1"`
}
func main() {
host := Host{ID: 1, Name: "server-01", Status: Running}
data, _ := json.Marshal(host)
fmt.Println(string(data))
// {"id":1,"name":"server-01","status":"Running"}
// 在 GORM 中使用
DB.AutoMigrate(&Host{})
DB.Create(&host)
var h Host
DB.Take(&h, 1)
fmt.Println(h.Status.String()) // Running
}
十、事务
10.1 事务概述
事务(Transaction)是一系列数据库操作的集合,这些操作作为一个完整的逻辑单元,要么全部成功执行,要么全部回滚(ACID 特性)。
典型场景:转账操作 —— A 向 B 转账 100 元,A 的余额减 100,B 的余额加 100。任何一步失败,整体都应失败。
GORM 事务默认行为:GORM 默认在事务中执行写入操作(Create、Update、Delete)。如无强一致性要求,可以在初始化时禁用以提升约 30% 性能:
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
SkipDefaultTransaction: true, // 全局禁用默认事务
})
注意:MySQL 的 InnoDB 引擎才支持事务,MyISAM 不支持。
10.2 自动事务 (Transaction)
使用 Transaction 方法,GORM 自动处理提交和回滚:
type User struct {
ID uint `gorm:"primaryKey"`
Name string
Money int `json:"money"`
}
// 转账示例:张三给李四转账 100 元
func Transfer(db *gorm.DB) error {
var zhangsan, lisi User
db.Take(&zhangsan, "name = ?", "张三")
db.Take(&lisi, "name = ?", "李四")
err := db.Transaction(func(tx *gorm.DB) error {
// 步骤一:张三扣款 100 元
zhangsan.Money -= 100
if err := tx.Model(&zhangsan).Update("money", zhangsan.Money).Error; err != nil {
return err // 返回 error 会自动 Rollback
}
// 步骤二(模拟异常):如果这里出错,步骤一的操作也会回滚
// if true { return errors.New("模拟异常") }
// 步骤三:李四收款 100 元
lisi.Money += 100
if err := tx.Model(&lisi).Update("money", lisi.Money).Error; err != nil {
return err
}
// 返回 nil 提交事务
return nil
}) // 自动 Commit 或 Rollback
return err
}
Transaction 的工作流程:
- 闭包返回
nil→ 自动 Commit(提交) - 闭包返回
error→ 自动 Rollback(回滚) - 发生 panic → 自动 Rollback(回滚)
10.3 手动事务 (Begin/Commit/Rollback)
需要更细粒度控制时,使用手动事务:
func TransferManual(db *gorm.DB) error {
var zhangsan, lisi User
db.Take(&zhangsan, "name = ?", "张三")
db.Take(&lisi, "name = ?", "李四")
// 开始事务
tx := db.Begin()
// 步骤一:张三扣款
zhangsan.Money -= 100
if err := tx.Model(&zhangsan).Update("money", zhangsan.Money).Error; err != nil {
tx.Rollback() // 出错则回滚
return err
}
// 步骤二:李四收款
lisi.Money += 100
if err := tx.Model(&lisi).Update("money", lisi.Money).Error; err != nil {
tx.Rollback() // 出错则回滚
return err
}
// 全部成功,提交事务
return tx.Commit()
}
10.4 嵌套事务
GORM 支持嵌套事务(Save Point),内层事务失败不会影响外层事务:
db.Transaction(func(tx *gorm.DB) error {
tx.Create(&User{Name: "outer_user"})
tx.Transaction(func(tx2 *gorm.DB) error {
tx2.Create(&User{Name: "inner_user"})
// 内层返回错误,只会回滚内层的操作
return errors.New("inner error")
})
// 外层继续执行
tx.Create(&User{Name: "another_outer"})
return nil
})
// 最终结果:outer_user 和 another_outer 创建成功,inner_user 被回滚
10.5 SavePoint
手动设置保存点,用于部分回滚:
tx := db.Begin()
tx.Create(&User{Name: "user1"})
tx.SavePoint("sp1") // 设置保存点 sp1
tx.Create(&User{Name: "user2"})
tx.RollbackTo("sp1") // 回滚到 sp1(user2 被撤销,user1 保留)
tx.Create(&User{Name: "user3"})
tx.Commit()
// 最终结果:user1 和 user3 创建成功
十一、性能优化与最佳实践
11.1 性能优化建议
| 优化项 | 说明 | 影响 |
|---|---|---|
| 跳过默认事务 | SkipDefaultTransaction: true |
写入性能提升 ~30% |
| 预编译语句 | PrepareStmt: true(后续执行相同 SQL 无需重新编译) |
重复查询性能提升明显 |
| 合理使用缓存 | 对热点数据使用 Redis 等缓存 | 减少数据库压力 |
| 批量操作 | 使用 CreateInBatches |
避免 max_allowed_packet 超限 |
| 选择性查询 | 使用 Select 指定需要的字段 |
减少网络传输和内存占用 |
| 预加载优化 | 复杂关联考虑用 Joins 替代 Preload |
减少 SQL 查询次数 |
| 索引优化 | 为高频查询字段建立合适的索引 | 查询速度显著提升 |
| 连接池调优 | 合理设置 MaxOpenConns / MaxIdleConns |
避免连接耗尽或资源浪费 |
PrepareStmt 预编译配置:
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
SkipDefaultTransaction: true,
PrepareStmt: true, // 预编译 SQL 语句缓存
})
11.2 错误处理规范
// 推荐:统一的错误处理模式
result := DB.Create(&student)
if result.Error != nil {
// 区分错误类型
if errors.Is(result.Error, gorm.ErrDuplicatedKey) {
// 唯一键冲突
return fmt.Errorf("数据已存在: %w", result.Error)
}
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
// 记录不存在
return fmt.Errorf("记录不存在: %w", result.Error)
}
// 其他数据库错误
return fmt.Errorf("数据库操作失败: %w", result.Error)
}
常用 GORM 错误类型:
| 错误 | 含义 |
|---|---|
ErrRecordNotFound |
查询无匹配记录 |
ErrDuplicatedKey |
唯一键/主键冲突 |
ErrForeignKeyViolated |
外键约束违反 |
InvalidTransaction |
无效的事务操作 |
Registered |
模型已注册 |
InvalidField |
无效字段 |
MissingWhereClause |
更新/删除缺少 WHERE 条件(安全保护) |
11.3 常见问题排查
Q1: Create 为什么必须传指针?
GORM 需要通过指针回填自增主键、CreatedAt、UpdatedAt 等字段。传值拷贝无法修改原始变量。
Q2: Updates 使用结构体时零值字段为何不被更新?
GORM 无法区分「字段本身就是零值」和「不想更新此字段」。解决方案:
- 使用
map[string]interface{}进行更新 - 使用
Select明确指定要更新的字段 - 使用
pointer类型(*int)使零值为 nil
Q3: 如何避免全表 Update/Delete?
GORM v2 默认要求更新/删除操作必须有 WHERE 条件(全局模式下可通过 AllowGlobalUpdate 配置关闭此保护):
// 安全操作(有 WHERE)
DB.Model(&User{}).Where("role = ?", "admin").Update("active", false)
// 全局更新(需显式启用)
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{
DisableAutomaticPing: false,
// AllowGlobalUpdate: true, // 允许不带 WHERE 的全局更新(不推荐)
})
Q4: Find 查询结果为空时会报错吗?
不会。Find 查询不到数据时返回空切片,不会报错。而 First/Take/Last 查询不到数据时会返回 ErrRecordNotFound 错误。
Q5: 如何查看 GORM 生成的 SQL 但不执行?
使用 Session(&gorm.Session{DryRun: true}):
stmt := DB.Session(&gorm.Session{DryRun: true}).First(&user, 10)
fmt.Println(stmt.Statement.SQL.String()) // SQL 语句
fmt.Println(stmt.Statement.Vars) // 参数列表
附录:快速参考表
CRUD 快速对照
| 操作 | 方法 | 说明 |
|---|---|---|
| 新增单条 | DB.Create(&obj) |
必须传指针 |
| 新增多条 | DB.Create(&objs) |
批量 INSERT |
| 批次新增 | DB.CreateInBatches(objs, n) |
分批插入 |
| 查询单条 | DB.First(&obj) / DB.Take(&obj) / DB.Last(&obj) |
无记录报错 |
| 查询多条 | DB.Find(&objs) |
无记录返回空切片 |
| 全字段更新 | DB.Save(&obj) |
更新所有字段含零值 |
| 单列更新 | DB.Model(&m).Where(...).Update("field", val) |
更新单个字段 |
| 多列更新 | DB.Model(&m).Where(...).Updates(map/struct) |
更新多个字段 |
| 删除 | DB.Delete(&obj) |
软删除(如有 DeletedAt) |
| 物理删除 | DB.Unscoped().Delete(&obj) |
永久删除 |
| 统计行数 | DB.Model(&m).Count(&n) |
统计总数 |
关联关系速查
| 关系 | 标签 | 说明 |
|---|---|---|
| Belongs To | 默认 | 子表有外键指向父表 |
| Has One | 默认 | 父表有外键指向子表 |
| Has Many | 默认 | 父表一对多子表 |
| Many to Many | many2many:table_name |
通过中间表关联 |
钩子函数执行顺序
Before Save → Before Create/Update → SQL 执行 → After Create/Update → After Save
Before Delete → SQL 执行 → After Delete
After Find → (查询结果可用)
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)