GORM

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

#type / resource #status / growing #tech / dev / backend #resource / go

[!info] 关联笔记

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:primaryKeysizeindexuniqueIndex

见官方 Declaring Models / Conventions。

CRUD 心智

操作典型 API
CreateCreate / 批量创建
ReadFirst Find Where
UpdateUpdate Updates / map
DeleteDelete(注意软删除)

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

AutoMigrategolang-migrate 等
速度原型快需写文件
删列/改类型/数据迁移显式 SQL
审查Git 可审
生产慎用推荐

详见 database-migration-golang-migrate

规范 / 实现 / 库边界

  • 语言:不规定 ORM。
  • 标准库database/sql 抽象连接与查询。
  • GORM:第三方库行为随版本变;以当前文档与 release note 为准。

边界与反直觉

1. 结构体 Updates 与零值

Updates(struct) 通常忽略零值字段;要写 0/false/"" 时用 map[string]anySelect 指定列。

2. 软删除

gorm.ModelDeletedAt 使默认查询带“未删除”条件;未注意会“删了还在/查不到”。

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 或超大结果集。

工程实践

  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. 用结构体 UpdatesPrice: 0,观察是否未写入;再改 map 对比。
  3. AutoMigrate 后手工改列,体会与版本迁移差异。
  4. 打开 Debug,观察 Preload 产生的 SQL 条数。

本节总结

  • GORM = 生产力 ORM,建立在 database/sql 之上。
  • 收益是 CRUD/关联速度;代价是抽象泄漏与迁移纪律。
  • 生产关键:共享 DB、context、零值更新、N+1、版本化迁移。

自测题

概念题

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

代码推理题

db.Model(&User{}).Updates(User{Name: "", Age: 0}) 可能发生什么?

工程思考题

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

参考答案

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

笔记元信息

创建于 2026/6/20 更新于 2026/7/15