GORM
GORM 在 database/sql 之上提供模型映射、链式 CRUD、关联与事务;适合中等 CRUD,生产迁移、零值更新与 N+1 需显式治理。
[!info] Related notes
- 所属 MOC:数据访问 · 学习路线
- 底层:database/sql
- 迁移:golang-migrate
- 相关:struct tags、context、错误处理、分层
- 实战:User 结构体时间字段、可空时间 *time.Time
GORM
这个概念为什么出现
手写 database/sql 时,CRUD 样板、扫描字段、简单关联会重复。GORM 用结构体描述模型,提供链式 API 与约定(表名、主键、时间戳等),加快业务交付。
它不是 Go 语言本体,也不能替代:
- SQL 与索引知识
- 事务边界设计
- 版本化 schema 迁移
[!abstract] 一句话理解 GORM 是建立在驱动与连接池之上的 ORM:用模型 + 链式方法生成 SQL;便利换取抽象层,关键路径必须能下沉看 SQL、控 N+1 与迁移。
最小可运行示例
package main
import (
"fmt"
"log"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
type Product struct {
gorm.Model
Code string
Price uint
}
func run() error {
// 内存数据库:每次运行独立,不累积演示数据。
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
if err != nil {
return err
}
sqlDB, err := db.DB()
if err != nil {
return err
}
defer sqlDB.Close()
// SQLite :memory: 按连接隔离;本例限制为单连接。
sqlDB.SetMaxOpenConns(1)
if err := db.AutoMigrate(&Product{}); err != nil {
return err
}
if err := db.Create(&Product{Code: "D42", Price: 100}).Error; err != nil {
return err
}
var p Product
if err := db.First(&p, "code = ?", "D42").Error; err != nil {
return err
}
fmt.Println(p.Code, p.Price)
return nil
}
func main() {
if err := run(); err != nil {
log.Fatal(err)
}
}
将代码保存为新目录中的 main.go。以下固定为已验证版本;SQLite 驱动依赖 CGO 和可用的 C 编译器:
go mod init example.com/gorm-demo
go get gorm.io/gorm@v1.31.2 gorm.io/driver/sqlite@v1.6.0
go run . # 输出 D42 100
官方:GORM Docs · Connecting · CRUD
核心概念
在栈中的位置
Handler/Service → Repository(GORM + 方言适配) → database/sql 池 → SQL 驱动 → DB
Repository 对外仍可暴露领域接口,内部用 *gorm.DB,便于测试替换。
模型与约定
- 常嵌
gorm.Model(ID、时间戳、软删除字段) - 默认表名为结构体名的蛇形复数,列名为字段名的蛇形;表名可用 TableName/NamingStrategy,列名用 column tag 覆盖
- tag:
primaryKey、size、index、uniqueIndex等
见官方 Declaring Models / Conventions。
CRUD 心智
| 操作 | 典型 API |
|---|---|
| Create | Create / 批量创建 |
| Read | First Find Where |
| Update | Update Updates / map |
| Delete | Delete(注意软删除) |
传统 API 与 Generics API(GORM v1.30.0 起)并存。这里使用传统 API,通过返回的 .Error 与 .RowsAffected 判断结果;不要忽略写操作错误。GORM v2 使用 gorm.io/gorm 模块,其模块版本仍为 v1.x,不能仅据此误认成旧版 github.com/jinzhu/gorm。
关联
Has One / Has Many / Belongs To / Many2Many / 多态等。
Preload 通常按关联批量执行独立查询:查询一批父记录再用 IN 查询子记录,能避免逐条懒加载造成的 N+1。逐父记录调用 Preload/查询仍会形成 N+1;预加载过多关联则会过度取数。Join Preload 主要用于一对一关系(has one / belongs to),不能与所有 Preload 场景直接互换。
事务
err := db.Transaction(func(tx *gorm.DB) error {
// 使用 tx 而非 db
return nil
})
回调返回错误会回滚,返回 nil 才提交;事务内每次操作也要检查 .Error,否则业务失败可能仍提交。嵌套事务通常借助保存点,不是独立提交的事务;能力取决于数据库/驱动。多表一致性边界应在 service/用例层 决策。
Context
优先 WithContext(ctx),让取消与超时传到驱动;是否及时中断由驱动/数据库实现决定,取消不能单独证明写入未发生。
迁移:AutoMigrate vs 版本化 SQL
| AutoMigrate | golang-migrate 等 | |
|---|---|---|
| 速度 | 原型快 | 需写文件 |
| schema 变更 | 创建缺少的表/列/索引/约束,可调整部分类型或可空性;不删除废弃列 | 显式、按版本执行 SQL |
| 数据回填/回退 | 不提供完整的数据迁移与版本回退计划 | 需自行编写并验证迁移 |
| 审查 | 难 | Git 可审 |
| 生产 | 慎用 | 推荐 |
详见 database-migration-golang-migrate。
规范 / 实现 / 库边界
- 语言:不规定 ORM。
- 标准库:
database/sql抽象连接与查询。 - GORM:第三方库行为随版本变;以当前文档与 release note 为准。
边界与反直觉
1. 结构体 Updates 与零值
Updates(struct) 通常忽略零值字段;要写 0/false/"" 时用 map[string]any 或 Select 指定列。
2. 软删除
gorm.Model 的 DeletedAt 使默认查询带“未删除”条件;未注意会“删了还在/查不到”。
3. ErrRecordNotFound
First、Take、Last 未找到时返回 gorm.ErrRecordNotFound,用 errors.Is 判断;Find 未找到通常为 nil 错误、RowsAffected=0。错误是否映射 404 取决于用例,不要一律 500。
4. 会话与全局配置
可共享 gorm.Open 返回的基础 DB/连接池;传统 API 中不要随意复用已追加 Where 等条件的链实例,否则可能累积条件。需要复用查询模板时使用 Session(&gorm.Session{}) 等新会话方法,并按官方规则处理并发;它不会新建数据库连接池。
5. 安全
拼接字符串当 SQL 条件是注入温床;值用参数化 Where("name = ?", name)。列名、表名、Order/Select 等 SQL 结构不能靠值占位符保护,应从服务端白名单选择;不要把不可信输入直接传给这些 API。
常见误区
[!warning] 用 ORM 跳过 SQL
复杂报表、批量、锁语义仍要懂 SQL 与执行计划。
[!warning] 每请求
gorm.Open
应进程内共享*gorm.DB(底层池)。
[!warning] 生产只靠 AutoMigrate
缺可审历史与可控数据迁移。
[!warning] 无脑 Preload 全部关联
可能产生超大结果集;循环逐条查关联才是典型 N+1。
工程实践
- 仓储接口隔离 GORM,service 单测打 fake。
- 日志:开发
Debug;生产采样慢查询。 - 指标:连接池、回调错误率。
- 读写拆分等高级能力(Resolver)按需,勿过早。
- 错误翻译:
errors.Is到领域错误。 - 批量:
CreateInBatches/FindInBatches控内存。 - 与 database-sql-in-go 对照:能写清 GORM 生成的 SQL。
可验证实验
Create+First走通 SQLite 示例。- 用结构体
Updates设Price: 0,观察是否未写入;再改 map 对比。 AutoMigrate后手工改列,体会与版本迁移差异。- 打开 Debug,观察 Preload 产生的 SQL 条数。
验证记录:Go 1.26.8、GORM v1.31.2、SQLite 驱动 v1.6.0 下,SQLite 实测通过了 struct/map/Select 零值更新、First/Find 无记录、软删除、全表更新保护、事务回滚与批量 Preload 查询条数,并通过 race 检查。此验证不代表 PostgreSQL/MySQL 方言或生产迁移已验证。
本节总结
- GORM = 生产力 ORM,建立在
database/sql之上。 - 收益是 CRUD/关联速度;代价是抽象泄漏与迁移纪律。
- 生产关键:共享 DB、context、零值更新、N+1、版本化迁移。
自测题
概念题
- 为什么说 GORM 不能替代事务隔离知识?
- 结构体零值更新的正确姿势?
- AutoMigrate 与迁移工具如何分工?
代码推理题
db.Model(&Product{}).Updates(Product{Code: "", Price: 0}) 可能发生什么?(Product 如上,嵌入 gorm.Model。)
工程思考题
如何设计 repository,使将来可从 GORM 换回 sqlx/database/sql?
参考答案
展开
- 隔离级别、锁、并发写冲突在 DB 语义层,ORM 只是发起语句。
map或Select明确列。- 原型可用 AutoMigrate;发布用版本化 up/down SQL。
推理:Code/Price 的零值被忽略,但 gorm.Model 的 UpdatedAt 会参与更新;没有 WHERE/主键,默认触发 ErrMissingWhereClause。即使用 map/Select 写零值,也应显式限定目标,并检查 Error/RowsAffected。 工程:repo 接口用领域类型;GORM 仅实现细节。
延伸阅读与资料来源
| 资料 | 类型 | 支撑 |
|---|---|---|
| GORM Documentation | 官方 | 总览与 CRUD |
| Connecting to Database | 官方 | 连接 |
| Preload | 官方 | 批量预加载与 Join Preload |
| Generics · Method Chaining | 官方 | API 版本与会话复用 |
| Transactions | 官方 | 事务 |
| Migration | 官方 | AutoMigrate |
| Performance | 官方 | 性能选项 |
| Security | 官方 | 安全 |
| database/sql | 标准库 | 底层抽象 |
| database-sql-in-go · database-migration-golang-migrate | 本库 | 基础与迁移 |
笔记元信息
- 文件:
gorm.md - 状态:已深化
- 建议下一篇:database-migration-golang-migrate 或 database-sql-in-go