事务内查询强制走主库是硬规则,因事务需强一致性,GORM 忽略 dbresolver.Read 等配置;防误写须结合数据库只读权限与 GORM 路由配置。

事务内查询走主库,不是“可配置项”而是硬规则
GORM 的事务对象(tx *gorm.DB)一旦开启,后续所有操作——包括 First、Find、Raw ——都强制绑定到该事务持有的主库连接。你无法通过 db.Clauses(dbresolver.Read) 或其他方式让它切到从库。这不是 bug,是设计前提:事务要求强一致性,从库延迟不可控,GORM 不做妥协。
常见误判是以为“只读事务 = 可走从库”,但 GORM 没有“只读事务”概念。只要调用了 db.Begin() 或进了 Transaction() 回调,就已锁定主库连接。
- 想查刚写入的数据?必须用事务内的
tx.First(),别指望从库能立刻看到 - 想绕过事务查从库?得另起一个非事务的
db实例,但要注意数据可能不一致 -
tx.Session(&gorm.Session{Read: true})无效,Read字段在事务中被忽略
从库写入报错 ERROR: cannot execute INSERT in a read-only transaction 是正确拦截
这个错误不是配置失败,恰恰说明从库连接已正确设为只读(MySQL 层面 SET SESSION TRANSACTION READ ONLY 或用户权限限制),GORM 尝试往从库发 INSERT 时被数据库直接拒绝。这是你想要的安全兜底。
触发它的典型场景:
- 忘了加
dbresolver.Write,直接对从库模型调用Create() - 在非事务上下文中用了
db.Clauses(dbresolver.Read).Create()——Read不会阻止写方法路由,只是提示意图,实际仍按方法语义走主库;但若你手动把Replicas配成了可写连接,那就真会写进去 - 误将主库 DSN 填进
Replicas列表,导致“从库”实际可写,此时不会报错,但破坏读写分离语义
防写入必须靠两层:数据库权限 + GORM 路由配置
单靠 GORM 的 dbresolver 不足以防止误写。它只负责路由,不校验目标库是否真的只读。真正防住的关键在 MySQL 侧:
- 从库账号必须显式
REVOKE INSERT, UPDATE, DELETE ON *.* FROM 'replica_user'@'%' - 避免使用 root 或高权限账号连从库,哪怕 GORM 配置正确,权限失控也会失效
- GORM 中
Replicas的每个Dialector应指向只读账号的 DSN,而非复用主库凭证 - 上线前用
dbresolver.Register()后执行一句db.Raw("INSERT INTO test VALUES (?)", 1).Error测试从库是否真拦截
Raw 查询默认走从库,但含写操作就崩
db.Raw("SELECT * FROM users").Scan(&u) 默认走从库,没问题;但 db.Raw("INSERT INTO logs (...) VALUES (...)").Exec() 如果没加 Clauses(dbresolver.Write),就会落到从库并触发只读错误。
根本原因:GORM 不解析 SQL 文本,Raw 的路由只看调用链上下文(事务 / Clauses / 方法名)。Exec 是写操作语义,所以默认走主库;但如果你在 db.Clauses(dbresolver.Read).Raw(...).Exec() 里强行指定读路由,GORM 会照做——然后数据库报错。
- 原则:写 SQL 就用
db.Exec(),读 SQL 就用db.Raw().Scan(),别混用 - 绝不把
INSERT/UPDATE/DELETE丢进db.Clauses(dbresolver.Read),那等于主动绕过防护 - 复杂 SQL 难以归类读写?统一走主库,或拆成明确的读+写两步
最易被忽略的一点:GORM 的读写分离是“静态语义路由”,不是“动态 SQL 分析”。它不管你的 Raw 里写了什么,只看你调的是 Create 还是 First、有没有开事务、有没有显式 Clauses。防写入的最后一道防线,永远在数据库权限配置上,不在 Go 代码里。











