gorm preload默认生成两条select语句而非join,先查主表再用in批量查关联表,适用于主表数据少且需完整关联结构的场景。

用 Preload 加载关联表数据,但不是 SQL JOIN
GORM 的 Preload 默认生成的是额外的 SELECT 查询(N+1 优化后为 2 次查询),不是一条带 JOIN 的 SQL。它适合“主表少量数据 + 关联字段需完整结构”的场景,比如查几个用户及其全部订单:
var users []User
db.Preload("Orders").Find(&users)注意 User 结构体里必须定义 Orders 字段并打上 gorm:"foreignKey:UserID" 这类标签,否则 Preload 不生效。
真要生成 LEFT JOIN,得用 Joins + Select 手动拼字段
Joins 会生成原生 JOIN 语句,但 GORM 不自动映射关联字段到嵌套结构——它只把结果塞进目标 struct 的同名字段里。所以必须显式 Select 并确保字段别名和 struct 字段匹配:
var results []struct {
UserID uint
UserName string
OrderID uint
Amount float64
}
db.Table("users").
Select("users.id as UserID, users.name as UserName, orders.id as OrderID, orders.amount as Amount").
Joins("left join orders on orders.user_id = users.id").
Find(&results)漏掉 as 别名,struct 字段就收不到值;用 Joins("orders") 简写时,GORM 会按约定推导 ON 条件,但一旦外键名不标准(比如叫 owner_id 而非 user_id),就得写完整 ON。
关联字段太多时,Scan 比 Find 更可控
当 SELECT 的字段数动态变化、或要混入聚合函数(如 COUNT)、或目标 struct 字段名和数据库列名差异大,Scan 是更稳的选择:
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
type UserOrderSummary struct {
Name string
OrderCnt int
TotalAmt float64
}
var summaries []UserOrderSummary
db.Table("users").
Select("users.name, COUNT(orders.id), SUM(orders.amount)").
Joins("left join orders on orders.user_id = users.id").
Group("users.id, users.name").
Scan(&summaries)
Scan 不走 GORM 的字段映射逻辑,直接按顺序绑定,所以字段顺序和 struct 字段顺序必须严格一致;另外 Group 必须包含所有非聚合字段,否则 MySQL 会报错。
WHERE 条件跨表时,Joins 后必须用 Where 而非 Preload
想查 “订单金额大于 100 的用户”,不能写 Preload("Orders", db.Where("amount > ?", 100))——这只会过滤预加载的 Orders,主查询仍返回所有用户。正确做法是把条件放进 JOIN 后的 Where:
db.Table("users").
Joins("inner join orders on orders.user_id = users.id AND orders.amount > ?", 100).
Select("users.*").
Group("users.id").
Find(&users)这里用 inner join 是因为 WHERE 实质依赖订单存在;如果还要包含没订单的用户,就得改用 left join 并把条件移到 Where 子句(注意 NULL 安全):Where("orders.amount > ? OR orders.amount IS NULL", 100)。
多表查询最易被忽略的其实是 NULL 处理和 GROUP BY 字段完整性——GORM 不校验 SQL 语义,错写一个字段名或漏个 GROUP,运行时才暴露,而且错误信息往往指向最后一行而非真实问题点。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










