必须用Raw而非链式查询的场景包括:子查询、多表UPDATE/DELETE、RETURNING子句、数据库特有函数调用;Raw需配合Exec(无结果)或Scan(有结果)执行,参数须防注入且类型严格匹配。

直接写原生 SQL 在 GORM 里不是“退而求其次”,而是必要手段:复杂 JOIN、聚合计算、RETURNING 子句、数据库特有函数(如 PostgreSQL 的 jsonb_set)等场景,ORM 链式调用根本覆盖不了。
什么时候必须用 Raw 而不是链式查询
链式查询(Where/Joins/Select)本质是构建通用 SQL 模板,它不支持以下几类操作:
- 子查询出现在
SELECT或WHERE右侧(如SELECT (SELECT COUNT(*) FROM logs l2 WHERE l2.user_id = u.id) AS log_count FROM users u) - 多表
UPDATE或DELETE(MySQL 支持UPDATE t1 JOIN t2 ON ... SET t1.x = t2.y,GORM 不生成这种语法) - 需要
RETURNING *获取插入/更新后完整行(PostgreSQL 常用,Create只返回主键,First无法绑定到刚写的行) - 调用数据库函数做字段计算(如
ST_Distance(geom, ST_Point(?, ?))、to_tsvector('english', title))
这些情况硬套链式 API,要么报错,要么生成语义错误的 SQL。此时应立刻切到 Raw。
Raw 的三种参数传入方式及适用场景
Raw 支持位置占位符、命名参数、表达式混用,但容易因类型或顺序错乱导致静默失败或 SQL 注入漏洞:
- 位置参数(
?)最常用,但要求参数顺序和?出现顺序严格一致:db.Raw("SELECT * FROM users WHERE status = ? AND created_at > ?", StatusActive, time.Now().AddDate(0,0,-7)).Scan(&users) - 命名参数(
@name+sql.Named或map[string]interface{})适合动态拼接条件,避免顺序错乱:db.Raw("SELECT * FROM users WHERE role = @role AND age > @min_age", sql.Named("role", "admin"), sql.Named("min_age", 18)).Scan(&users) - 慎用
gorm.Expr拼接字段值——它不转义,仅用于构造 SQL 片段(如gorm.Expr("updated_at = NOW()")),绝不能塞用户输入
注意:Raw 不做任何字段映射校验,Scan 目标结构体字段名必须和 SELECT 列名(或别名)完全一致,大小写敏感。
Exec 和 Scan 的返回值陷阱
Raw 本身不执行,必须跟 Exec(无结果集)或 Scan(有结果集)才能触发数据库调用,且二者行为差异极大:
-
Exec返回sql.Result,可调LastInsertId()(MySQL)或RowsAffected();但 PostgreSQL 的INSERT ... RETURNING必须用Scan,否则 RETURNING 数据丢失 -
Scan要求目标变量能接收整行(*User)、单列(*int64)、多列元组([]interface{})或自定义结构体;若 SELECT 列数与目标字段数不匹配,会 panic - 对
UPDATE/DELETE语句误用Scan会导致 “sql: expected 0 arguments, got X” 错误——因为这类语句没结果集,Scan试图读取却找不到列
典型反例:db.Raw("UPDATE users SET name = ? WHERE id = ?", newName, id).Scan(&user) —— 这里该用 Exec,Scan 会失败。
自定义类型在 Raw 中的序列化问题
即使你为 type UserID int64 实现了 Scanner 和 Valuer,Raw 也不会自动调用它们:
- 作为参数传入时,
Valuer.Value()会被调用(因为底层走database/sql的driver.Valuer协议) - 但
Scan时,GORM 不走模型字段映射逻辑,而是直接按目标变量类型解包;如果 SELECT 出来的是int64,而你 Scan 到UserID类型变量,会因类型不匹配失败 - 解决办法:Scan 到基础类型(
int64),再手动赋值;或定义一个中间结构体,字段用基础类型,再转成业务类型
这个细节常被忽略——自定义类型在链式 API 中工作良好,一到 Raw 就崩,根源在于 Raw 绕过了 GORM 的 Schema 和 Field 映射层,只依赖 database/sql 原生能力。











