gorm.raw()最稳妥用于动态where、复杂join或数据库特有函数调用,必须用?占位防注入;返回结果宜用[]map[string]interface{}接收,scan不触发钩子且需字段名严格匹配。

直接用 GORM.Raw() 执行原生 SQL 最稳妥
当 WHERE 条件动态拼接、多表 JOIN 逻辑复杂、或需要调用数据库特有函数(比如 PostgreSQL 的 jsonb_extract_path_text)时,GORM.Raw() 是最可控的选择。它绕过 GORM 的模型映射层,直接把 SQL 交给数据库执行,避免 ORM 自动加表前缀、字段别名或隐式类型转换带来的意外行为。
注意:参数必须用 ? 占位符,不能字符串拼接,否则会引发 SQL 注入;GORM 会自动处理参数绑定和类型转换:
var users []map[string]interface{}
db.Raw("SELECT u.name, COUNT(o.id) as order_count FROM users u LEFT JOIN orders o ON u.id = o.user_id WHERE u.status = ? GROUP BY u.id HAVING COUNT(o.id) > ?", "active", 0).Scan(&users)
- 返回结果用
[]map[string]interface{}接收最灵活,字段名就是 SELECT 中的别名(如order_count) - 若要映射到结构体,结构体字段名必须和 SELECT 列名完全一致(大小写敏感),且字段需为导出字段(首字母大写)
-
Scan()不会触发 GORM 的钩子(AfterFind等),也不走缓存
用 GORM.Session().Model().Select().Joins().Where() 组合构建半原生查询
适合“主体是 ORM 风格,但局部需要突破限制”的场景,比如想复用 GORM 的软删除过滤、租户字段自动注入,又得在 SELECT 中写表达式或子查询。这时不要硬套 Find(),改用 Session() + Model() 显式指定目标表,并用 Select() 控制字段列表:
type UserOrderStat struct {
Name string `gorm:"column:name"`
TotalAmount float64 `gorm:"column:total_amount"`
}
var stats []UserOrderStat
db.Session(&gorm.Session{NewDB: true}).Model(&User{}).
Select("users.name, COALESCE(SUM(orders.amount), 0) as total_amount").
Joins("LEFT JOIN orders ON orders.user_id = users.id").
Where("users.deleted_at IS NULL").
Group("users.id, users.name").
Scan(&stats)
-
Session(&gorm.Session{NewDB: true})防止上层 DB 实例的全局钩子干扰(比如自动添加tenant_id) -
Model(&User{})只是用来指定主表,不强制要求结构体字段和 SELECT 完全对应 -
Select()中的字段名必须和结构体 tag 中的column:一致,否则扫描失败 - 不能在
Where()里写子查询表达式(如"id IN (SELECT ...)"),GORM 会尝试解析而报错;此时应回退到Raw()
带命名参数的原生查询要用 GORM.NamedExec()
当 SQL 很长、参数多且易混淆时,用命名参数比问号占位更可读、更不易错。GORM 支持 NamedExec() 和 NamedQuery(),底层依赖 sqlx 风格语法(:name):
sql := `INSERT INTO logs (level, message, created_at) VALUES (:level, :message, :now)`
db.NamedExec(sql, map[string]interface{}{
"level": "error",
"message": "timeout occurred",
"now": time.Now(),
})
- 所有参数名必须用英文冒号前缀(
:level),不能用@level或$1 - 只支持
map[string]interface{}或结构体传参,不支持混合;结构体字段需导出且 tag 可选(GORM 会按字段名匹配) -
NamedQuery()返回*gorm.Rows,需手动Close(),适用于流式读取超大数据集
事务中混用原生 SQL 和 GORM 操作要显式控制连接
在 db.Transaction() 内部,GORM 默认复用同一个 *sql.Tx。但如果先执行了 Raw().Scan(),再调用 Create(),可能因连接状态不一致导致事务失效或 panic。根本原因是 Scan() 可能提前关闭 Rows,影响后续操作。
- 统一用
tx := db.Begin()显式开启事务,所有操作都基于tx对象(tx.Raw()、tx.Create()) - 避免在事务中使用
db.Raw().Scan(),改用tx.Raw().Scan() - 如果必须用
Rows流式处理,务必在defer rows.Close()后再执行其他写操作 - PostgreSQL 下尤其要注意:某些原生语句(如
LISTEN)不支持在事务块内执行,会直接报错ERROR: LISTEN cannot be called in a transaction block
原生 SQL 的自由度高,代价是失去 GORM 的安全网。每次写完都该反问一句:这个字段名/表名/参数顺序,有没有可能被未来重构悄悄破坏?











