← [ BACK_TO_NOTES ]

GORM

GORM 在 database/sql 之上提供模型映射、链式 CRUD、关联与事务;适合中等 CRUD,生产迁移、零值更新与 N+1 需显式治理。

#resource #growing #backend #go

[!info] Related notes

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
CreateCreate / 批量创建
ReadFirst Find Where
UpdateUpdate Updates / map
DeleteDelete(注意软删除)

传统 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

AutoMigrategolang-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。

工程实践

  1. 仓储接口隔离 GORM,service 单测打 fake。
  2. 日志:开发 Debug;生产采样慢查询。
  3. 指标:连接池、回调错误率。
  4. 读写拆分等高级能力(Resolver)按需,勿过早。
  5. 错误翻译:errors.Is 到领域错误。
  6. 批量:CreateInBatches / FindInBatches 控内存。
  7. 与 database-sql-in-go 对照:能写清 GORM 生成的 SQL。

可验证实验

  1. Create + First 走通 SQLite 示例。
  2. 用结构体 Updates 设 Price: 0,观察是否未写入;再改 map 对比。
  3. AutoMigrate 后手工改列,体会与版本迁移差异。
  4. 打开 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、版本化迁移。

自测题

概念题

  1. 为什么说 GORM 不能替代事务隔离知识?
  2. 结构体零值更新的正确姿势?
  3. AutoMigrate 与迁移工具如何分工?

代码推理题

db.Model(&Product{}).Updates(Product{Code: "", Price: 0}) 可能发生什么?(Product 如上,嵌入 gorm.Model。)

工程思考题

如何设计 repository,使将来可从 GORM 换回 sqlx/database/sql?

参考答案

展开
  1. 隔离级别、锁、并发写冲突在 DB 语义层,ORM 只是发起语句。
  2. map 或 Select 明确列。
  3. 原型可用 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本库基础与迁移

笔记元信息

创建于 2026/6/20 更新于 2026/10/7
Digital Biome
Access

键盘导航

j / k / ↑ / ↓
选择对象
Enter / o
打开选择
/ 或 Ctrl+K
全站搜索
i / f / 1–4
筛选当前列表
g → h / l / p / t / a
首页 / 笔记 / 项目 / 工具 / About
r → 1–9
打开所选对象的关联
u
返回上一页
Esc
关闭浮层 / 取消命令;输入框内清空文本