db.raw() 是唯一入口,但参数传法决定安全性和可维护性:? 占位最通用,命名参数更清晰(实为语法糖),严禁字符串拼接;scan() 要求字段名、顺序、类型严格匹配;exec() 用于增删改,queryrow() 适合单值查询;事务中必须用 tx.raw() 并显式 scanrows。

db.Raw() 是唯一入口,但参数传法决定安全性和可维护性
直接写 SQL 时,db.Raw() 是绕不开的起点,但它本身不执行、不扫描、不校验——它只拼接 SQL 字符串和参数。传参方式错了,轻则查不到数据,重则 SQL 注入。
- 用
?占位(如db.Raw("SELECT * FROM users WHERE id = ?", 123))最通用,兼容所有驱动,但参数顺序必须严格对应,多个?容易错位 - 用命名参数(
sql.Named("name", "张三")或 map)更清晰,尤其在 OR/AND 混合条件或子查询中;但注意:MySQL 驱动不原生支持@name语法,GORM 内部做了转换,实际仍转为?+ 顺序绑定,所以@name只是写法糖,不是 MySQL 原生预处理变量 - 别拼字符串:
"WHERE name = '" + name + "'"是高危操作,GORM 的参数绑定机制在此完全失效
Scan() 不是万能接收器,结构体字段必须严格对齐 SELECT 列
Scan() 看似简单,但它是“硬匹配”:SELECT 出几个字段、什么顺序、什么类型,Scan() 的目标变量就必须一模一样。错一个字段名或类型,要么零值,要么 panic。
- 查单行用指针指向单个结构体:
var u User; db.Raw("...").Scan(&u) - 查多行必须用切片:
var users []User; db.Raw("...").Scan(&users)(注意是&users,不是users) - SELECT 别名必须和结构体 tag 对应,比如
SELECT name AS username要求结构体有Username string `gorm:"column:username"`或直接字段名Username(GORM 默认蛇形转驼峰) - 聚合函数如
COUNT(*)、SUM(age)返回基础类型,直接 Scan 到int64、float64等,别塞进结构体里硬套
Exec() 和 QueryRow() 场景分明:改数据用 Exec,取单值用 QueryRow
db.Raw().Exec() 用于 INSERT/UPDATE/DELETE 等无结果集语句;而 db.Raw().Scan() 本质是包装了 Query(),适合 SELECT。但如果你只想要一个标量值(比如最新插入 ID、统计总数),QueryRow() 更轻量、更明确。
-
Exec()返回sql.Result,可调RowsAffected()或LastInsertId()(后者在 MySQL 中需开启parseTime=true且驱动支持) -
QueryRow().Scan(&id)比Raw().Scan(&struct{ID int}{})少一层结构体定义,适合“SELECT LAST_INSERT_ID()”这类场景 - UPDATE 带
RETURNING(PostgreSQL)可用Scan()直接取回更新后的字段;MySQL 不支持,得拆成两步:先Exec(),再单独查
事务里用 Raw 必须显式 ScanRows,不能依赖全局 db
在 db.Transaction() 回调里执行原生 SQL,不能直接用外部 db,否则会脱离事务上下文——尤其是 UPDATE/DELETE 可能作用到事务外的连接,导致数据不一致。
- 务必用传入的
tx *gorm.DB调用Raw() - 查多行时,
tx.Raw("...").Rows()得到*sql.Rows,再用tx.ScanRows(rows, &u)才能正确绑定模型(自动处理 time.Time、JSON 等 GORM 类型) - 别在事务里混用
db.Raw()和tx.Raw(),连接池和事务状态会错乱 - 子查询嵌套命名参数时,外层
Raw()的参数不会透传到内层Raw(),得手动展开或用fmt.Sprintf拼接(仅限已知可信值,如固定表名)
最常被忽略的是命名参数的“假象”——看着像 MySQL 原生变量,其实仍是顺序绑定,调试时打印出最终 SQL(用 logger.Default.LogMode(logger.Info))才能确认参数是否真按预期填充。还有就是 Scan 对字段名的零容忍,少一个 tag 或拼错大小写,静默失败比报错更难排查。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











