GORM
GORM 在 database/sql 之上提供模型映射、链式 CRUD、关联与事务;适合中等 CRUD,生产迁移、零值更新与 N+1 需显式治理。
[!info] 关联笔记
- 所属 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"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
type Product struct {
gorm.Model
Code string
Price uint
}
func main() {
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
if err != nil {
panic(err)
}
// 原型可用;生产 schema 变更优先版本化迁移
if err := db.AutoMigrate(&Product{}); err != nil {
panic(err)
}
db.Create(&Product{Code: "D42", Price: 100})
var p Product
if err := db.First(&p, "code = ?", "D42").Error; err != nil {
panic(err)
}
fmt.Println(p.Code, p.Price)
}
安装(版本以文档为准):
go get -u gorm.io/gorm
go get -u gorm.io/driver/sqlite # 或其他驱动
官方:GORM Docs · Connecting · CRUD
核心概念
在栈中的位置
Handler/Service → Repository(可用 GORM) → driver → database/sql 池 → DB
Repository 对外仍可暴露领域接口,内部用 *gorm.DB,便于测试替换。
模型与约定
- 常嵌
gorm.Model(ID、时间戳、软删除字段) - 表名、列名有默认复数/蛇形约定,可用 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)并存;读文档时认准你用的主版本。
关联
Has One / Has Many / Belongs To / Many2Many / 多态等。
Preload / Joins 控制加载;滥用 Preload 是 N+1 或过度取数的温床。
事务
err := db.Transaction(func(tx *gorm.DB) error {
// 使用 tx 而非 db
return nil
})
支持嵌套事务、SavePoint(见官方 Transactions)。多表一致性边界应在 service/用例层 决策。
Context
优先 WithContext(ctx),让取消与超时传到驱动。
迁移:AutoMigrate vs 版本化 SQL
| AutoMigrate | golang-migrate 等 | |
|---|---|---|
| 速度 | 原型快 | 需写文件 |
| 删列/改类型/数据迁移 | 弱 | 显式 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 找不到记录时的错误处理要与业务 404 映射,不要一律 500。
4. 会话与全局配置
链式方法可能修改会话状态;注意 Session、预编译、Logger 配置,避免并发误用同一会话假设。
5. 安全
拼接字符串当 SQL 条件是注入温床;用参数化 Where("name = ?", name)。官方有 Security 章节。
常见误区
[!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 条数。
本节总结
- GORM = 生产力 ORM,建立在
database/sql之上。 - 收益是 CRUD/关联速度;代价是抽象泄漏与迁移纪律。
- 生产关键:共享 DB、context、零值更新、N+1、版本化迁移。
自测题
概念题
- 为什么说 GORM 不能替代事务隔离知识?
- 结构体零值更新的正确姿势?
- AutoMigrate 与迁移工具如何分工?
代码推理题
db.Model(&User{}).Updates(User{Name: "", Age: 0}) 可能发生什么?
工程思考题
如何设计 repository,使将来可从 GORM 换回 sqlx/database/sql?
参考答案
展开
- 隔离级别、锁、并发写冲突在 DB 语义层,ORM 只是发起语句。
map或Select明确列。- 原型可用 AutoMigrate;发布用版本化 up/down SQL。
推理:Name/Age 零值常被忽略,可能什么都没更新。
工程:repo 接口用领域类型;GORM 仅实现细节。
延伸阅读与资料来源
| 资料 | 类型 | 支撑 |
|---|---|---|
| GORM Documentation | 官方 | 总览与 CRUD |
| Connecting to Database | 官方 | 连接 |
| Associations / Preload | 官方 | 关联加载 |
| 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