db.raw() 用于执行原生 sql,需用占位符防注入并链式调用 scan/rows;db.exec() 专用于无返回操作,跳过钩子且返回影响行数;混合查询可用 table().select() 插入原生字段,但注意别名映射和无预加载;事务中必须使用 tx 实例,否则脱离事务控制。

用 DB.Raw() 执行原生 SQL 最直接
想绕过 GORM 的 ORM 层,直接发 SQL 给数据库,DB.Raw() 是第一选择。它不解析、不拦截、不拼接,把字符串原样传给驱动,适合复杂查询、批量更新、存储过程调用等场景。
常见错误是传参方式不对:用字符串拼接变量(如 "WHERE id = " + strconv.Itoa(id))——这会引发 SQL 注入;或者误以为 Raw() 返回的是模型切片,其实它只返回 *gorm.DB,得链式调用 .Scan() 或 .Rows() 才能取数据。
- 参数必须用
?占位符(SQLite/MySQL)或$1, $2(PostgreSQL),GORM 自动做转义 - 如果查单行单列(比如
COUNT(*)),用.Row().Scan(&count);查多行,用.Scan(&results)配合结构体切片 - 执行无返回结果的语句(如
UPDATE、DELETE),记得接.Error检查是否成功,RowsAffected不会自动暴露
// PostgreSQL 示例:批量更新并返回 ID
var ids []int64
db.Raw("UPDATE users SET status = ? WHERE active = ? RETURNING id", "locked", true).Scan(&ids)
<p>// MySQL 示例:防注入的条件查询
var users []User
db.Raw("SELECT * FROM users WHERE name LIKE ? AND age > ?", "%li%", 18).Scan(&users)
</p>
DB.Exec() 专用于不返回结果集的操作
如果你只是执行 INSERT、UPDATE、DELETE 或 DDL(如 CREATE TABLE),别用 Raw().Scan(),直接上 DB.Exec()。它返回 *sql.Result,可安全获取影响行数和最后插入 ID,且跳过 GORM 的钩子(BeforeCreate 等)——这点常被忽略,导致业务逻辑意外跳过。
容易踩的坑是混淆 Exec() 和 Save():前者完全 bypass 模型生命周期,后者走完整 ORM 流程;还有人对 Exec() 的错误处理松懈,以为没报错就一定成功,其实得显式检查 result.RowsAffected() 是否符合预期。
-
Exec()不支持结构体参数,所有值必须扁平传入,按顺序匹配占位符 - PostgreSQL 中执行
INSERT ... RETURNING时,Exec()不会返回结果集,要用Raw().Scan()替代 - 事务内用
Exec()没问题,但注意它不参与 GORM 的嵌套事务管理(比如Session(&gorm.Session{AllowGlobalUpdate: true})对它无效)
用 DB.Table().Select().Where() 混合原生字段和 ORM 查询
不需要全写原生 SQL 时,可以只在关键字段或表达式上“破圈”:比如 SELECT COUNT(*) OVER (PARTITION BY category)、JSON_EXTRACT、ST_Distance 这类数据库特有函数。这时用 DB.Table("users").Select("name, JSON_EXTRACT(profile, '$.age') as age") 更轻量,还能复用 GORM 的连接池、日志、事务上下文。
陷阱在于字段别名和结构体字段映射:GORM 默认按列名(非别名)绑定,所以 as age 要对应结构体里的 Age 字段(首字母大写 + json:"age" 标签无效),否则 Scan 后字段为空;另外,混合写法不支持预加载(Preload),关联数据得另起查询。
- 原生字段必须显式写在
Select()里,不能靠*带出,否则 GORM 会尝试映射全部列,可能类型冲突 - WHERE 条件里也可以塞原生表达式,如
.Where("created_at > NOW() - INTERVAL '7 days'"),但要注意方言差异(MySQL vs PostgreSQL 时间函数不同) - 这种写法生成的 SQL 仍受 GORM 日志开关控制(
logger.LogMode()),方便调试,比纯Raw()更易追踪
事务中执行原生 SQL 必须共用同一个 *gorm.DB 实例
在 db.Transaction(func(tx *gorm.DB) error { ... }) 里,所有原生操作必须用传入的 tx,而不是外层的 db。否则看似在事务里,实际原生语句走的是独立连接,提交/回滚时完全不受控——这是线上数据不一致的高频原因。
另一个盲点是:用 tx.Raw().Exec() 后,如果后续又调用了 tx.Create(),两者共享事务状态没问题;但如果中间混入了 sql.DB.Query()(绕过 GORM),那就彻底脱离事务了。
- 事务内的
Raw()/Exec()报错后,GORM 不会自动 rollback,要靠 defer 或 return error 触发回调里的 rollback 逻辑 - PostgreSQL 的
SAVEPOINT在 GORM 事务中可用,但原生 SQL 里手动写SAVEPOINT sp1后,得用tx.Exec("ROLLBACK TO SAVEPOINT sp1")配合,不能依赖 GORM 的 Savepoint 方法 - SQLite 不支持 savepoint 嵌套,原生 SQL 里乱用会 panic,而 GORM 层对此无防护
原生 SQL 的自由度越高,对数据库方言、事务边界、参数安全的敏感度就越强——写之前先确认你真正需要的是“控制权”,而不是“偷懒绕过 GORM”。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











