主题
Go 数据库工具选型
pgx、sqlc 和 GORM 经常出现在同一份选型表里,却不负责同一件事:一个主要负责与 PostgreSQL 通信,一个把 SQL 生成 Go 代码,一个提供对象映射和查询接口。把它们当作只能三选一,容易从一开始就选错问题。
本文先分清驱动、查询层和迁移工具,再逐一说明 pgx、go-sql-driver/mysql、sqlx、sqlc、GORM、ent,以及 golang-migrate、goose、Atlas。最后用两个小实验观察参数绑定和零值更新,让“封装方便在哪里、隐藏了什么”变成可以检查的行为。
资料核对于 2026-10-13。实验固定 Go 1.27.2 / Linux amd64、sqlx v1.4.0、GORM v1.31.1、GORM SQLite 驱动 v1.6.0、实际 SQLite 引擎 3.45.1;完整依赖与校验值见附录。实验范围只包括 sqlx 文本绑定和 GORM 对本地内存 SQLite 的操作,不代表 PostgreSQL、MySQL 的事务、锁或生产性能。其余工具依据官方文档讲解,本次没有运行其生成器、迁移器或真实数据库连接。
1. 先选层,再选工具
| 层次 | 负责什么 | 本文工具 | 不替你解决什么 |
|---|---|---|---|
| 数据库驱动与协议 | 建连、认证、传输 SQL 与参数、解码结果 | pgx、go-sql-driver/mysql | 业务模型、查询质量、迁移计划 |
| SQL 辅助层 | 简化绑定、扫描与样板代码 | sqlx | 自动理解任意关系、编译期检查所有 SQL |
| SQL 代码生成 | 从 schema 和 SQL 生成类型化方法 | sqlc | 自动选最优索引、自动应用 schema |
| ORM / 模型生成 | 从模型构造 CRUD 和关联操作 | GORM、ent | 业务不变量、所有性能问题、无风险改表 |
| schema 迁移 | 记录并推进结构变更 | golang-migrate、goose、Atlas | 保证所有 DDL 都在线、丢失数据都能恢复 |
常见组合可以是“sqlc + pgx”,也可以是“sqlx + database/sql + MySQL 驱动”。迁移工具在部署路径上工作,不需要与每条业务查询绑定。
正在绘制图示…
查看图源代码
flowchart TD
SQL[Schema 与手写 SQL] --> GEN[sqlc 生成 Go 方法]
MODEL[Go 模型或 ent schema] --> ORM[GORM 或 ent 查询层]
APP[业务逻辑] --> GEN
APP --> X[手写 SQL 或 sqlx]
APP --> ORM
GEN --> N[pgx 原生接口]
GEN --> STD[database/sql]
X --> STD
ORM --> STD
N --> PG[PostgreSQL]
STD --> PD[pgx stdlib 适配器]
PD --> PG
STD --> MYD[go-sql-driver/mysql]
MYD --> MY[MySQL]
MIG[独立迁移发布流程] --> PG
MIG --> MY
图中的线是可选组合,不是要求同时接入所有层。database/sql 本身提供连接池和统一接口,但不包含数据库驱动;查询层的抽象也不会让不同数据库的 SQL、类型和隔离行为自动一致。[1]
2. 驱动:先确定连接池由谁管理
pgx:PostgreSQL 原生能力与标准接口之间的选择
pgx 是 PostgreSQL 驱动及工具集。它有原生 API,也提供 stdlib 适配器接入 database/sql。原生接口可以直接使用 PostgreSQL 特有能力,如 COPY、LISTEN / NOTIFY、批量请求及更丰富的类型映射;若上层库要求 *sql.DB,则可以选择适配器。[2]
| 接法 | 池在哪里 | 适合什么情况 | 要留意什么 |
|---|---|---|---|
pgx.Conn | 这是单连接,不是池 | 专用连接、低层协议操作 | 不能把同一连接当作可并发共享的池 |
pgxpool.Pool | pgxpool 管理 | PostgreSQL 专用应用,直接用原生 API | 参数、统计和事务 API 与 sql.DB 不完全相同 |
pgx stdlib + *sql.DB | database/sql 管理 | sqlx、ORM 或已有标准接口代码 | 原生能力不全都能从标准接口直接表达 |
连接池应长期复用,而不是每个请求建一个。pgxpool.New 返回也不表示已经成功建连,应通过带超时的 Ping 或实际获取连接验证。使用原生事务时,还要阅读其 Context 语义:例如 pgxpool 的 Begin 所用 Context 不等于事务在 Context 取消后自动回滚,调用方仍应明确结束事务。不要把 database/sql 的生命周期印象直接套过来。[3][4]
若数据库前面有连接代理,应进一步核实所选查询模式、预备语句缓存和代理池化模式是否兼容。决定采用原生接口,应该因为确实需要它的能力或测得收益,而不是引用一个脱离负载的“必定快几倍”。本文没有做驱动性能对比。
go-sql-driver/mysql:在标准池之下实现 MySQL 协议
go-sql-driver/mysql 是 database/sql 的 MySQL 驱动,业务代码通常通过 *sql.DB 使用。驱动注册名为 mysql;连接数、空闲连接和连接寿命主要在 sql.DB 上配置,不是另外再找一个“mysql 池”。[5]
配置中有几个容易混淆的层次:
| 设置 | 改变什么 | 不能据此推断什么 |
|---|---|---|
parseTime=true | 将 DATE、DATETIME 解码为 time.Time | 不代表数据库会话时区已修改 |
loc | 上述 Go 时间值采用的位置 | 不修改 MySQL 的 time_zone |
timeout / readTimeout / writeTimeout | 建连或 I/O 层超时 | 不等于完整业务请求的统一截止时间 |
| TLS 配置 | 传输加密和证书校验方式 | 开启连接不等于可以省略身份校验 |
| 参数绑定 | 值与 SQL 结构分开传给驱动 | 参数占位符不能替代任意表名、列名或排序方向 |
尤其是时间:数据库里一列表示的是 UTC 时刻、本地日历时间还是日期,需要应用先定协议,再配置解码。parseTime 不能替你决定业务时区。DSN 还可能包含密码,不应完整打印到日志。[5:1]
3. sqlx:保留 SQL,把重复扫描和绑定交给库
sqlx 在 database/sql 之上增加 Get、Select、StructScan、命名参数和 IN 展开等功能。SQL 仍由你写;它不会根据 Go 对象关系自动决定 JOIN,也不是编译期 SQL 检查器。[6]
例如一条查询返回 id 和 display_name,目标结构体可以用 db 标签映射。JOIN 中有两个 id 时,应显式写不同列别名,而不是期望反射推断“这个 id 属于哪张表”。默认找不到目标字段会报错;随手打开 Unsafe 只会掩盖映射问题,不会修复 SQL。[6:1]
一个 slice 不会自动变成一串占位符
假设查询有一个布尔条件和两个用户 ID。实验按顺序执行:命名参数转 ?,展开 slice,再转换成 PostgreSQL 风格的 $n。
go
query, args, err := sqlx.Named(
"SELECT id FROM users WHERE active=:active AND id IN (:ids)",
map[string]any{"active": true, "ids": []int{2, 7}},
)
// 完整程序检查每次调用的 err。
query, args, err = sqlx.In(query, args...)
query = sqlx.Rebind(sqlx.DOLLAR, query)实际输出:
text
sqlx: SELECT id FROM users WHERE active=$1 AND id IN ($2, $3) args=[true 2 7]
sqlx: empty-slice-error=trueSQL 里增加了两个位置,参数仍是独立的值。这一步只生成文本与参数,没有连接 PostgreSQL。空 slice 被 sqlx.In 拒绝,调用方应先决定“空集合表示不返回任何行”还是“忽略此条件”,不能把空集合错误当成数据库异常随意重试。该行为在 v1.4.0 的 bind.go 中也可核对。[7]
Rebind 只改占位符形式,不把 SQL 方言整体翻译成另一个数据库。动态表名、排序列仍需可信白名单,不能把用户文本拼进去。读大量数据时也别把 Select 当成无上限的便利函数:一次装入切片会增加内存占用,需要时使用逐行扫描,并检查关闭与迭代错误。[6:2]
4. sqlc:把手写 SQL 变成有类型的 Go 调用
sqlc 的起点是数据库 schema 和带命名注释的 SQL。生成器分析这些输入,生成参数结构、结果类型和查询方法。应用运行时调用生成后的 Go 代码,仍通过配置的数据库接口执行原来的查询;不是每个请求都启动 sqlc。[8]
可以把它理解为把这条链提前到构建阶段:
正在绘制图示…
查看图源代码
flowchart LR
S[Schema 定义] --> C[sqlc 分析与生成]
Q[命名 SQL 查询] --> C
C --> G[参数类型 结果类型 查询方法]
G --> B[Go 编译与测试]
B --> R[运行时驱动执行 SQL]
D[实际数据库 schema] --> V[集成测试与迁移校验]
R --> V
它解决的是“我愿意写 SQL,但不想手工维护每条查询的 Scan 和参数样板”。复杂 JOIN、CTE 或数据库特有表达式可以保留在 SQL 中,评审者能直接审查询。与此同时,生成器有自己的语法、类型推断和数据库支持边界;遇到自定义类型或特殊查询,要检查配置、类型覆盖及该版本能力,不应承诺任意合法 SQL 都一定可生成。[9]
| 采用 sqlc 后的工作 | 为什么仍然需要 |
|---|---|
| 固定生成器版本和配置 | 同样输入要产生可复核的输出,避免本地和 CI 不同 |
| 修改查询后重新生成、编译并检查差异 | Go 方法和结果类型可能随列变化 |
| 对真实目标数据库做集成测试 | 输入 schema 不等于线上 schema;编译成功不能发现所有漂移 |
| 查看执行计划和行数 | 类型安全不表示查询代价合适 |
| 显式管理事务 | 生成查询不会自动把多次调用合成业务事务 |
sqlc 生成的 WithTx 可以把查询对象绑定到事务。关键是后续调用都使用这个事务对象;如果事务中误用原来的池查询对象,调用可能落在另一条连接上。WithTx 不负责替你选择隔离级别、处理提交失败或重试完整业务操作。[10]
sqlc 更适合查询形状相对明确、团队愿意审 SQL 的项目。高度动态的筛选条件也能设计,但常要选择有限的查询变体、谨慎的可选条件或另一个受控构造层。不要为了“所有查询必须由同一种工具生成”而写出无法维护的大 SQL。
5. GORM:用模型组织 CRUD,也要看它实际写了什么
GORM 提供模型映射、条件构造、关联、回调、事务与迁移接口。它能减少常见 CRUD 的重复代码,但调用的是有约定的 API,并非把一个结构体原封不动地同步到数据库。GORM 也有泛型接口;下面固定使用 v1.31.1 的传统 API,以便清楚观察其更新规则。[11][12]
false 究竟表示“改为假”,还是“没填写”?
实验先插入一名 Active=true 的用户,然后尝试:
go
db.Model(&User{}).Where("id = ?", 1).
Updates(User{Active: false})实际读回:
text
gorm struct: active=true affected=0调用没有报错,但值没变。传统 API 的 Updates(struct) 默认选择非零值字段,false 是 bool 的零值,因此被跳过。程序没有 CreatedAt / UpdatedAt 等额外字段,所以这次没有其他更新。
改成明确的字段 map:
go
db.Model(&User{}).Where("id = ?", 1).
Updates(map[string]any{"active": false})实际读回及 DryRun 输出:
text
gorm map: active=false affected=1
gorm SQL: UPDATE `users` SET `active`=? WHERE id = ? vars=[false 1]也可以显式 Select 要更新的字段。这里更重要的设计问题是:API 输入能否区分“字段没有提供”和“提供了 false / 0 / 空串”?必要时使用指针或专门的变更类型,在应用层保留这个区别,再决定如何调用 ORM。不要只因为代码编译通过,就认定更新语义正确。[12:1]
关联和便利功能也有成本
Preload 通常另发查询批量加载关联,并不等于“一条 JOIN”。这能避免逐父记录查一次的 N+1,但可能返回很多关联行;Join Preload 又有关系类型等适用边界。应看实际 SQL、查询次数和结果规模,而不是按链式调用写了几行估计成本。[13]
GORM 的参数值可以绑定,但 Order、Table 等接受 SQL 结构的入口不能直接接不可信文本。日志里展示的 SQL 也不应未经核实就复制执行。ORM 能帮助构造查询,不能替代输入边界。[14]
AutoMigrate 能创建或调整某些 schema 项,官方也明确它不会为了匹配模型而删除不用的列。但“不删列”不等于变更无锁、无重写或适合每个实例启动时同时执行。本文只在临时 SQLite 中用它准备实验表;生产结构变更应有独立评审和执行流程。[15]
6. ent:先定义 schema,再生成领域数据 API
ent 的核心工作流是用 Go 定义 schema,包括字段、索引和 edges,然后生成类型化的实体与查询构造 API。edges 描述实体间的关系,例如用户拥有多个订单、订单属于一个用户;在 SQL 后端,这些关系最终仍要由表、键和查询实现,不是引入一个可以免费跨关系遍历的内存图。[16][17]
它与 sqlc 的方向不同:sqlc 从你写的 SQL 生成调用;ent 从模型及关系定义生成可组合 API。若领域关系丰富、团队希望字段与关系变更尽早反映到 Go 编译错误中,ent 的方式很有吸引力。它也有 runtime、查询构造和驱动调用,不能因为使用代码生成就称为“没有运行时成本”。
| 实际收益 | 相应取舍 |
|---|---|
| 字段、谓词和关系遍历有生成 API | 要维护生成步骤,评审 schema 与生成差异 |
| 可以围绕模型组织校验、hooks 和查询 | 隐含行为需被团队理解,排查时仍要回到实际 SQL |
与 sql.DB 集成,复用连接管理 | 池参数和底层驱动仍需配置,不会自动消失 |
| 支持事务和版本化迁移集成 | 业务事务边界、锁代价和发布顺序仍需设计 |
生成代码应视为构建产物:修改 schema 后重新生成,不要把手工修补生成文件当成长久方案。使用事务客户端时,确保一个工作单元使用同一事务范围;复杂分析 SQL 或特殊数据库能力是否容易表达,应在选型时做小型验证,而不是等项目已经绑定大量模型后才发现。[18][19][20]
7. 迁移工具:它们管理变更过程,不替数据库改变 DDL 规则
数据库 schema 会随应用演进。迁移工具至少要回答:哪些变更已经执行,下一步执行什么,失败后停在哪儿,多个发布者是否会互相干扰。它们不等于 ORM,也不要求应用一定用 Go。
| 工具 | 核心工作方式 | 合适的需求 | 主要边界 |
|---|---|---|---|
| golang-migrate | 按版本执行成对的 up / down 文件,CLI 或 Go 库 | 团队想直接审 SQL,执行器职责清晰 | 失败 dirty 状态需调查;事务行为取决于数据库及驱动 |
| goose | 有注释分区的 SQL 迁移,也支持 Go 迁移 | SQL 之外确实需要受控程序逻辑 | 自定义程序更难审;事务例外要明确标注 |
| Atlas | 声明式比较目标状态,或生成并执行版本化迁移 | 希望把 schema 差异、检查和发布组织起来 | 生成的差异仍需审查;具体能力与版本、发行及服务方案有关 |
golang-migrate:版本号不能冒充修复
常见文件是一对带相同版本号的 .up.sql 和 .down.sql。工具记录当前版本与 dirty 状态;迁移失败后停下来,避免在不明结构上继续叠加变更。[21]
遇到 dirty,先检查日志、实际 schema 和已生效语句,确定恢复到哪种一致状态,再决定补救和版本标记。force 的意义是设置迁移版本状态,不会替你执行遗漏的 SQL,也不会修复半完成的数据回填。把 dirty 清掉后继续跑,可能让工具记录与真实数据库永久分叉。[22]
事务更不能一概而论。例如 PostgreSQL 驱动文档对多语句执行以及 CREATE INDEX CONCURRENTLY 的事务限制有专门说明;工具名字相同,不代表每个数据库都能把同一份脚本完整回滚。[23]
goose:SQL 注释划边界,Go 迁移承担额外责任
goose 的 SQL 文件通过 -- +goose Up 和可选的 -- +goose Down 区分方向。包含函数体等内部有分号的完整语句时,需要按规则用 StatementBegin / StatementEnd 标注,让解析器知道这是一条语句,而不是按每个分号拆开。[24]
默认一个 SQL 迁移文件内的语句放进事务;需要在事务外运行的操作可以声明 NO TRANSACTION。但数据库 DDL 自身的隐式提交等规则仍然适用,注释不能制造数据库没有的原子性。关闭事务后,更应逐条考虑失败会留下什么状态。[24:1]
Go 迁移可以表达 SQL 难以单独完成的数据转换,却也可能混入不可重复的外部调用。建议把大规模回填拆成可分批、可续跑、可校验的任务,避免在一次发布的 schema 迁移中长时间占锁。是否需要 Go 迁移,应由具体转换需求决定,而不是为了把全部数据工作放进一个工具。[25]
Atlas:目标状态和有序步骤,是两种工作流
声明式迁移描述“希望数据库是什么样”,工具比较实际状态并规划差异;版本化迁移保存“按什么步骤到达那里”,便于评审和部署。Atlas 支持这两条路径,也能把模型提供的 schema 接入规划。生产流程常需要先生成可检查的版本化 SQL,而不是让任意应用启动时临场推断所有变更。[26][27]
atlas.sum 用迁移文件及目录校验信息检测历史被意外修改等问题。它有助于维护迁移目录的一致性,但正确的校验和不证明 SQL 无损、不证明线上没有漂移,也不证明新旧应用兼容。自动 diff 更无法替你决定“删掉一列”到底是可以丢弃,还是一次需要迁移数据的重命名意图。[28]
从哪些入口开始操作
下面是工作流入口,不是可直接复制到生产执行的命令串;连接地址、目标目录、权限和版本必须先确认。
| 工具 | 开始时看什么 | 推进变更 | 故障时先做什么 |
|---|---|---|---|
| golang-migrate | 检查版本和文件顺序 | 按批准的步数执行 up;需要时明确指定版本 | 查看 dirty 与实际 schema,确认恢复方案后才考虑 force |
| goose | status 查看已执行记录 | up 推进,down 只在已评审反向操作时使用 | 分清事务内失败还是已部分执行;不要只盯版本表 |
| Atlas | inspect / diff 查看实际与目标;审查生成文件 | 版本化流程使用 migrate 的检查与 apply 入口 | 检查迁移历史、目录校验和与实际结构;不要盲目重算校验和掩盖改动 |
生产发布的共同底线
把字段从 A 改为 B 时,不要先删 A、再期待所有应用同时升级。可采用扩展再收缩:先增加 B 并允许旧版本继续工作,部署兼容读写,分批回填和校验,切换读取,确认旧版本与回滚窗口结束后再清理 A。这是发布策略,不是某个迁移工具自动保证的功能。
正在绘制图示…
查看图源代码
flowchart LR
A[添加兼容结构] --> B[部署兼容代码]
B --> C[分批回填与校验]
C --> D[切换读取]
D --> E[确认旧版本退出]
E --> F[单独评审破坏性清理]
Down 文件也不等于恢复方案:删掉的列即使重新创建,旧值不会凭空回来。回滚应用、反向迁移、向前修复和从备份恢复,是不同动作。发布前应逐一确认锁等待、预计耗时、重试方式、停止条件和恢复证据。
8. 怎样做最后选择
| 项目主要特征 | 可先验证的组合 | 为什么 |
|---|---|---|
| PostgreSQL 专用,SQL 清晰且较稳定 | pgx + sqlc + 一种迁移工具 | 保留 SQL 控制力,减少参数与结果样板 |
已有大量手写 SQL 和 database/sql 代码 | 原驱动 + sqlx | 渐进减少扫描与绑定重复,改动范围较小 |
| 以常规 CRUD 为主,愿意遵守 ORM 约定 | GORM + 目标驱动 + 受审查迁移 | 开发入口统一;必须验证更新和关联语义 |
| 模型与关系是团队的主要表达方式 | ent + 目标驱动 + 版本化迁移 | 生成类型化 API,schema 变更更早暴露到构建阶段 |
这是试用起点,不是“最佳技术栈”排名。最终验证应拿自己的一条复杂查询、一次部分字段更新、一项跨表事务、一条分页路径和一次真实规模的 schema 变更来做。
至少记录:发出了几条 SQL,绑定了哪些参数,返回多少行,池等了多久,事务在哪里结束,迁移失败留下什么。工具是否适合项目,通常会在这些边界上显现,而不是在最短的 CRUD 演示里。
参考资料
附录:完整实验模块
以下所有文件放在同一目录。实验使用 CGO SQLite 驱动,因此需要 C 编译器;依赖首次下载需联网。本次通过官方 Go module proxy 和 checksum database 下载固定依赖,随后关闭依赖下载执行程序、单元测试、race 与 vet。只创建进程内存数据库,不使用凭据或外部服务。
命令如下;已有 Go 1.27.2 时直接运行:
sh
GOPROXY=https://proxy.golang.org GOSUMDB=sum.golang.org go mod download
CGO_ENABLED=1 GOPROXY=off GOSUMDB=off go run .
CGO_ENABLED=1 GOPROXY=off GOSUMDB=off go test -v ./...
CGO_ENABLED=1 GOPROXY=off GOSUMDB=off go test -race -count=3 ./...
CGO_ENABLED=1 GOPROXY=off GOSUMDB=off go vet ./...sqlx.Rebind 的 PostgreSQL 风格结果只作文本验证;GORM SQL 与行值验证使用 SQLite。没有用 SQLite 推断其他数据库的 RowsAffected、锁、隔离或 DDL 行为。DryRun 只显示构造出的语句,不是执行计划。
本次 go test -v ./... 通过 TestBindingsAndZeroValues;go test -race -count=3 ./... 通过,go vet ./... 退出 0、无输出。它们不是性能基准。下面给出产生正文全部输出的源文件。
go.mod
go
module example.com/storagetools
go 1.27
require (
github.com/jmoiron/sqlx v1.4.0
gorm.io/driver/sqlite v1.6.0
gorm.io/gorm v1.31.1
)
require (
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/mattn/go-sqlite3 v1.14.22 // indirect
golang.org/x/text v0.20.0 // indirect
)go.sum
text
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=main.go
代码块 · 98 行 · 点击收起 / 展开
go
package main
import (
"fmt"
"io"
"os"
"github.com/jmoiron/sqlx"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
type User struct {
ID int `gorm:"primaryKey"`
Name string
Active bool
}
func run(w io.Writer) error {
query, args, err := sqlx.Named("SELECT id FROM users WHERE active=:active AND id IN (:ids)", map[string]any{"active": true, "ids": []int{2, 7}})
if err != nil {
return err
}
query, args, err = sqlx.In(query, args...)
if err != nil {
return err
}
query = sqlx.Rebind(sqlx.DOLLAR, query)
fmt.Fprintf(w, "sqlx: %s args=%v\n", query, args)
_, _, emptyErr := sqlx.In("SELECT id FROM users WHERE id IN (?)", []int{})
fmt.Fprintf(w, "sqlx: empty-slice-error=%t\n", emptyErr != nil)
if emptyErr == nil {
return fmt.Errorf("expected empty slice error")
}
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})
if err != nil {
return err
}
raw, err := db.DB()
if err != nil {
return err
}
defer raw.Close()
raw.SetMaxOpenConns(1)
var version string
if err := raw.QueryRow("SELECT sqlite_version()").Scan(&version); err != nil {
return err
}
fmt.Fprintln(w, "SQLite:", version)
if err := db.AutoMigrate(&User{}); err != nil {
return err
}
if err := db.Create(&User{ID: 1, Name: "Ada", Active: true}).Error; err != nil {
return err
}
result := db.Model(&User{}).Where("id = ?", 1).Updates(User{Active: false})
if result.Error != nil {
return result.Error
}
var got User
if err := db.First(&got, 1).Error; err != nil {
return err
}
fmt.Fprintf(w, "gorm struct: active=%t affected=%d\n", got.Active, result.RowsAffected)
if !got.Active {
return fmt.Errorf("struct unexpectedly updated zero value")
}
result = db.Model(&User{}).Where("id = ?", 1).Updates(map[string]any{"active": false})
if result.Error != nil {
return result.Error
}
got = User{}
if err := db.First(&got, 1).Error; err != nil {
return err
}
fmt.Fprintf(w, "gorm map: active=%t affected=%d\n", got.Active, result.RowsAffected)
if got.Active {
return fmt.Errorf("map did not update false")
}
stmt := db.Session(&gorm.Session{DryRun: true}).Model(&User{}).Where("id = ?", 1).Updates(map[string]any{"active": false})
if stmt.Error != nil {
return stmt.Error
}
fmt.Fprintf(w, "gorm SQL: %s vars=%v\n", stmt.Statement.SQL.String(), stmt.Statement.Vars)
return nil
}
func main() {
if err := run(os.Stdout); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}main_test.go
go
package main
import (
"bytes"
"strings"
"testing"
)
func TestBindingsAndZeroValues(t *testing.T) {
var out bytes.Buffer
if err := run(&out); err != nil {
t.Fatal(err)
}
for _, want := range []string{
"sqlx: SELECT id FROM users WHERE active=$1 AND id IN ($2, $3) args=[true 2 7]",
"sqlx: empty-slice-error=true",
"gorm struct: active=true affected=0",
"gorm map: active=false affected=1",
"vars=[false 1]",
} {
if !strings.Contains(out.String(), want) {
t.Fatalf("missing %q in %s", want, out.String())
}
}
}Go:database/sql。池、驱动与事务接口。 ↩︎
pgxpool API。池构造、获取和事务语义。 ↩︎
pgx v5.11.0 API。单连接并发与事务 Context 边界。 ↩︎
go-sql-driver/mysql 官方 README。database/sql、DSN、parseTime、loc、超时和 TLS。 ↩︎ ↩︎
sqlx v1.4.0:bind.go。IN 展开和空 slice 错误;另见本次实际输出。 ↩︎
sqlc v1.31.1 文档。固定文档版本,不表示本文安装了生成器。 ↩︎
sqlc v1.31.1 配置。数据库、sql_package、生成选项与类型覆盖。 ↩︎
sqlc:Using transactions。WithTx。 ↩︎
GORM:Update;v1.31.1 更新实现。结构体零值、map、Select。 ↩︎ ↩︎
GORM:Preloading。独立查询与 Join Preload。 ↩︎
GORM:Security。值绑定和 SQL 结构入口。 ↩︎
GORM:Migration。AutoMigrate 与版本化流程。 ↩︎
golang-migrate 官方项目。文件、CLI 与库。 ↩︎
golang-migrate FAQ。dirty、版本和锁。 ↩︎
golang-migrate PostgreSQL 驱动说明。多语句与事务限制。 ↩︎
goose 官方文档。SQL 与 Go 迁移。 ↩︎
Atlas:Migration Directory Integrity。atlas.sum 的作用与范围。 ↩︎