GORM 学习文档

版本: GORM v2.x | 适用场景: Go 语言 MySQL/PostgreSQL/SQLite 开发
文档定位: 从入门到企业级实践的完整学习指南


目录


一、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 基本数据类型、实现了 ScannerValuer 接口的自定义类型及其指针或别名组成。

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)

注意:给指针字段赋值时必须传地址(&email),直接传字符串会编译报错: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(如 UserUserID
  • 默认引用字段:[关联模型]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 包中的 ScannerValuer 接口。

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 需要通过指针回填自增主键、CreatedAtUpdatedAt 等字段。传值拷贝无法修改原始变量。

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 → (查询结果可用)
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐