← [ BACK_TO_NOTES ]

BodySense 数据库第 4 课:Go 数据访问——database/sql、GORM、sqlc 与 Repository Boundary

理解 Go 数据访问栈的职责:database/sql 连接池、GORM ORM、sqlc 的静态 SQL 路线与 BodySense Repository 边界,并能追踪 ORM 最终 SQL。

#course #growing #backend #database #postgresql #bodysense

[!info] related notes

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]

  1. *sql.DB 是否等于“一条已经连接好的 PostgreSQL connection”?
  2. 使用 GORM 是否意味着不需要理解 SQL?
  3. 跨两个 repository 的业务动作,transaction boundary 更适合放 Repository 还是 Application Service?
  4. Repository 的一个方法每次自行 .Transaction(...),会有什么潜在问题?
  5. 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 边界。

创建于 2026/9/18 更新于 2026/9/18
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
关闭浮层 / 取消命令;输入框内清空文本