直接用 db.Raw() 查数据、db.Exec() 改数据最简洁安全;查数据必须链式调用 Scan() 或 Rows(),否则 SQL 不执行;Exec() 跳过钩子且无返回值,取 RETURNING 需 Raw().Scan();参数须用占位符防注入;事务中必须用 tx.Raw()/tx.Exec()。

直接用 db.Raw() 查数据,db.Exec() 改数据——其他方式要么绕弯子,要么埋坑。
查数据必须链式调用 Scan() 或 Rows()
db.Raw() 本身不执行查询,也不返回结果,它只返回一个 *gorm.DB 实例。漏掉 .Scan() 或 .Rows(),SQL 就压根没发出去。
- 查单行单值(比如
COUNT(*)):用var count int64; db.Raw("SELECT COUNT(*) FROM users").Scan(&count) - 查单行结构体:定义公开字段的 struct,
var u User; db.Raw("SELECT * FROM users WHERE id = ?", 1).Scan(&u) - 查多行:必须传切片地址,
var users []User; db.Raw("SELECT * FROM users LIMIT 10").Scan(&users)(注意是&users,不是users) - 字段名不匹配时,要么让 SELECT 别名符合驼峰规则(
user_name → UserName),要么在 struct 字段加gorm:"column:user_name"tag
Exec() 不返回结果集,但必须检查 RowsAffected
db.Exec() 专用于 INSERT/UPDATE/DELETE/DDL,它跳过 GORM 钩子(BeforeUpdate 等全失效),也拿不到查询结果——想取自增 ID 或 RETURNING 字段,得换 Raw().Scan()。
- 执行后务必检查错误:
result, err := db.Exec("UPDATE users SET status = ? WHERE id = ?", "active", 1); if err != nil { ... } - 影响行数要显式读:
n := result.RowsAffected(),不能默认“没报错就成功” - PostgreSQL 的
INSERT ... RETURNING id必须用Raw().Scan(),Exec()完全忽略返回值 - 批量操作传 slice 要扁平展开:
db.Exec("DELETE FROM logs WHERE id IN ?", []int64{1,2,3})—— GORM 会自动展开成IN (?, ?, ?)
参数只能用占位符,字符串拼接等于开后门
GORM 的参数绑定只对 ?(MySQL/SQLite)或命名参数(sql.Named("name", "x"))生效。一旦用 "WHERE name = '" + name + "'",转义机制彻底失效,SQL 注入风险拉满。
-
?最通用,但顺序必须严丝合缝:db.Raw("WHERE a = ? AND b = ? AND c = ?", v1, v2, v3) - 命名参数是语法糖,MySQL 实际仍转为
?绑定,写@name看着清楚,但别当它是 MySQL 原生变量 - 结构体字段名、map key 名、
sql.Named名,三者大小写敏感,拼错就收不到值 - 空 slice 传参(如
IN ?)会被 GORM 自动展开;但nilslice 会直接报错,需提前判空
事务里必须用 tx.Raw() 和 tx.Exec()
原生 SQL 在事务中不自动挂载——用 db.Raw() 会脱离当前事务,变成独立连接执行,导致原子性崩溃。
- 正确姿势:
tx := db.Begin(); defer tx.Commit(); tx.Raw("UPDATE accounts SET balance = balance - ? WHERE id = ?", 100, 1).Exec() -
tx.Exec()同样跳过钩子,这点和普通Exec()一致,但保证在事务上下文内 - 别指望
Preload或软删除作用于Raw()查询——它完全绕过 GORM 元信息层,字段、条件、关联全部失能 - 大结果集流式读取用
tx.Rows(),手动rows.Next()+rows.Scan(),避免内存爆掉
最常被忽略的是:原生 SQL 查询不触发任何 GORM 钩子、不走软删除逻辑、不支持预加载,甚至连字段映射都只认名字和类型——它就是一条裸 SQL 直达数据库。用之前先想清楚,你到底是要绕过 ORM,还是只是懒得写链式调用。











