用raw()执行查询必须配scan()或rows(),否则sql不发送;增删改和ddl必须用exec();传参只允许占位符,表名列名需白名单校验;事务中须用tx.raw();字段映射须严格匹配。

用 Raw() 执行查询必须配 Scan() 或 Rows()
很多人调用 db.Raw("SELECT * FROM users WHERE id = ?", 1) 后就以为执行完了,其实它只返回一个 *gorm.DB 对象,什么都没发生。不接 .Scan(&u) 或 .Rows(),SQL 根本不会发给数据库。
- 查单行单列(比如
COUNT(*)):用var count int64; db.Raw("SELECT COUNT(*) FROM users").Row().Scan(&count) - 查多行结构体:必须传切片地址,
var users []User; db.Raw("SELECT * FROM users").Scan(&users)(注意是&users,不是users) - 结果集太大或字段类型不固定:改用
rows, _ := db.Raw("SELECT ...").Rows(),然后手动rows.Next()+rows.Scan(),绕过 GORM 反射开销
Exec() 是增删改和 DDL 的唯一正解
别用 Raw().Scan() 去执行 UPDATE 或 CREATE INDEX——它不会报错,但可能静默失败,且拿不到影响行数。
-
Exec()返回sql.Result,可立刻检查result.RowsAffected()是否符合预期,比如更新 0 行往往意味着 WHERE 条件没匹配到 - MySQL 插入后取自增 ID:调
result.LastInsertId();PostgreSQL 想一次拿到新数据,得用Raw().Scan()配RETURNING *,Exec()不支持返回结果集 - 建表、加索引、
TRUNCATE这类 DDL 语句,统一走Exec(),GORM 不会拦截或重写
参数只能用占位符,表名/列名不能动态传参
? 或命名参数(如 sql.Named("id", 123))是唯一安全的传参方式。字符串拼接 "WHERE name = '" + name + "'" 等同于敞开 SQL 注入大门。
- MySQL/SQLite 用
?,PostgreSQL 实际也转成?处理,别写$1——Raw()不识别原生驱动语法 - 表名、列名、排序字段(如
ORDER BY ?)不能用占位符,必须拼接;但必须严格校验来源,比如白名单过滤或正则匹配^[a-zA-Z_][a-zA-Z0-9_]*$ - 传
nil会导致参数数量错位,GORM 静默跳过;需要空值时,显式用sql.NullString等类型包装
事务里必须用 tx.Raw(),不能用全局 db
在 db.Transaction(func(tx *gorm.DB) error { ... }) 回调里,如果还用外部的 db.Raw(),那这条 SQL 完全脱离事务控制——Commit 成功,但它早已提交;Rollback 触发,它却不受影响。
- 正确写法:所有原生 SQL 都基于传入的
tx对象,tx.Raw("UPDATE ...").Exec()或tx.Raw("SELECT ...").Scan(&u) - 验证是否真在事务中:打印
tx.Statement.ConnPool == tx.ConnPool,应为true;若为false,说明用了错的实例 - PostgreSQL 对临时表或序列操作更敏感,裸 SQL 若含
CREATE TEMP TABLE,可能被事务隔离机制拒绝,需提前测试
原生 SQL 最容易被忽略的其实是「字段映射的硬匹配」:SELECT 出的列名、顺序、类型,必须和 Scan() 目标变量完全一致,GORM 不做任何容错或转换。别指望蛇形字段自动驼峰,也别以为少查一列它会帮你填零值——漏掉的字段就是零值,错一位类型就 panic。











