BodySense 数据库第 4 课:Go 数据访问——database/sql、GORM、sqlc 与 Repository Boundary
理解 Go 数据访问栈的职责:database/sql 连接池、GORM ORM、sqlc 的静态 SQL 路线与 BodySense Repository 边界,并能追踪 ORM 最终 SQL。
[!info] related notes
- 课程 MOC:BodySense 数据库工程深化课程 MOC
- 前置:第 3 课
- 知识:GORM、Go 数据访问 MOC
BodySense 数据库第 4 课:Go 数据访问——database/sql、GORM、sqlc 与 Repository Boundary
[!abstract] 本课目标 ORM 的目标不是让你不学 SQL,而是让常见映射更高效;数据库工程师必须能从 Go 调用追到最终 SQL 和 transaction。
从零建立概念:Go 代码到底怎样到达 PostgreSQL
一条 API 数据访问链路可以粗略画成:
HTTP Handler
→ Service / Use Case
→ Repository
→ GORM
→ database/sql
→ PostgreSQL driver
→ network
→ PostgreSQL Server
每一层职责不同。
如果不区分这些层,很容易产生:
Repository = ORM
GORM = 数据库
sql.DB = 一条连接
Transaction = 一个函数
这些都不准确。
Driver 是什么
database/sql 是 Go 标准库提供的数据库抽象,但它本身不知道 PostgreSQL wire protocol。
真正和某个 DBMS 通信需要 Driver。
BodySense 使用:
gorm.io/driver/postgres
GORM 的 postgres driver 再落到 Go SQL/driver 生态,与 PostgreSQL 建立真实连接。
可以把职责理解成:
database/sql
→ 通用连接池 / Tx / Query API
driver
→ 某个 DBMS 的协议和具体连接实现
sql.DB 为什么不是“DB Connection”
Go 官方文档明确:sql.DB 是零个或多个底层连接的 pool handle,可以被多个 goroutine 并发使用。
所以:
db.QueryContext(...)
背后可能是:
从 pool 借 connection #3
→ 执行 SQL
→ rows close / operation completes
→ connection 回 pool
下一次调用可能用另一条物理连接。
为什么这件事和 Transaction 直接相关
Transaction 不能:
BEGIN on connection A
UPDATE on random connection B
COMMIT on connection C
所以 sql.Tx 会绑定一条连接,直到 Commit/Rollback 后再归还 pool。
GORM 的 transaction abstraction 最终也必须遵守这个数据库连接现实。
Context 为什么进入数据库层
Go context.Context 可以携带:
deadline
cancellation
request-scoped metadata
数据库调用使用 WithContext(ctx) / QueryContext 的意义是:
HTTP client 已断开
或
request deadline 已到
→ DB operation 有机会被取消
→ 不继续无意义地占 connection/resource
这和后面的 Capacity / Timeout 直接相连。
ORM 到底解决什么,不解决什么
ORM = Object-Relational Mapping。
它帮助:
Go struct
↔
relational row
并提供:
- CRUD builder;
- association/preload;
- transaction helper;
- scan/mapping;
- hooks/conventions。
它不替代:
SQL semantics
Schema design
Index design
Transaction/isolation
Query plan
Database constraints
Migration strategy
所以“用了 GORM 就不用学 SQL”相当于“用了 React 就不用学 JavaScript”。
sqlc 为什么是另一种路线
sqlc 的哲学几乎相反:
Developer writes SQL
↓
sqlc analyzes SQL + schema
↓
generate typed Go methods
例如你明确写:
-- name: GetUser :one
SELECT id, email
FROM users
WHERE id = $1;
生成:
GetUser(ctx, id) (...)
它的价值是:
SQL 完全显式
+
Go 调用仍然类型化
不是“sqlc 天生比 GORM 更专业”,而是 abstraction boundary 不同。
Repository 为什么不是数据库的同义词
Repository 是应用架构边界。
理想上上层说:
FindCurrentBodyState(userID)
ClaimPendingJob(workerID)
AppendRuntimeEvent(runID, ...)
而不是:
SELECT ...
JOIN ...
gorm.Clauses(...)
Repository 把数据访问细节封装在 domain-oriented API 后面。
但 Repository 也不能把 application transaction 全部私有化。
如果一个 Use Case 要:
Repository A write
+ Repository B write
必须一起 commit
transaction boundary 应由知道完整业务动作的 Service/Application 层协调。
BodySense TransactionManager 的设计动机
当前:
Service
→ WithinTransaction(ctx, fn)
→ *gorm.DB tx 放入 context
Repository
→ database.FromContext(ctx, fallbackDB)
→ 如果有 tx 就参与它
→ 否则普通 DB
它解决的是:
Repository 不需要知道谁开启事务,但同一个 Application Use Case 中的多个 Repository 可以共享同一个事务。
这是一个非常值得在后端面试中解释的真实工程设计。
1. database/sql 是什么
Go 的 database/sql 更像通用数据库连接与执行抽象:
sql.DB
= connection pool handle
!= 单个物理连接
它负责连接复用、并发借还、超时上下文等基础能力。
2. GORM 增加了什么
GORM 在 database/sql 上提供:
- struct mapping;
- query builder;
- association/preload;
- transaction helper;
- hooks / conventions。
代价是如果不了解生成 SQL,容易出现 N+1、过度 preload、零值更新和隐式行为误判。
3. sqlc 为什么值得比较
sqlc 路线:
handwritten SQL
→ generate typed Go methods
优点是 SQL 显式、类型生成;代价是查询和 schema 工作流与 ORM 不同。课程把它作为 compare,不要求 BodySense production 从 GORM 迁移。
4. Repository Boundary
BodySense repository 应负责:
domain-oriented query/update
↓
GORM / SQL details
↓
PostgreSQL
Service 不应该到处拼 SQL;Repository 也不应该偷偷拥有跨多个业务步骤的 application transaction boundary。
5. 当前 BodySense
database.Connect → GORM postgres driver
sqlDB.SetMaxOpenConns(25)
sqlDB.SetMaxIdleConns(5)
sqlDB.SetConnMaxLifetime(5m)
TransactionManager 通过 context 把同一个 *gorm.DB transaction 传给多个 Repository,使 service 层能协调一个业务事务。
Prediction
[!question]
*sql.DB是否等于“一条已经连接好的 PostgreSQL connection”?- 使用 GORM 是否意味着不需要理解 SQL?
- 跨两个 repository 的业务动作,transaction boundary 更适合放 Repository 还是 Application Service?
- Repository 的一个方法每次自行
.Transaction(...),会有什么潜在问题?- sqlc 与 GORM 的主要差异应该如何理解?
[!success]- Answer / 解析 1. 不是。
sql.DB是并发安全的连接池/句柄,会按需管理多个底层连接。2. 不是。 ORM 只是 SQL 构造和映射抽象;性能、锁、索引、事务最终都发生在数据库层。
3. Application Service。 它最清楚哪些 repository 操作共同构成一个业务原子动作。
4. 多个 repository 可能各自 commit,导致本应原子的业务动作被拆成多个事务;BodySense 的
FromContext正是在解决事务参与问题。5. GORM 偏对象/链式 query abstraction;sqlc 偏 SQL-first + typed codegen。不是简单的“哪个更快”,而是 ownership、显式性和维护成本取舍。
Lab
选择 BodyStateRepository 或 JobRepository 一个方法:
Go method
→ GORM clauses
→ 推导 SQL
→ 找 transaction 来源
→ 标出可能触发的索引/锁
L4 Gate
看到一段 GORM 代码,能写出等价 SQL 的大致结构,并说明它使用哪个 transaction / connection pool 边界。