Joins预加载适用于一对一或Belongs To场景,本质是LEFT JOIN单SQL查询,适合User→Company等关系;一对多时会导致主表记录重复、count失真,应优先用Preload。

Joins预加载适用于一对一或 Belongs To 场景
Joins 预加载本质是用 LEFT JOIN 把主表和关联表一次查出来,只发一条 SQL。它天然适合一对一(如 User → Company)、Belongs To(如 Order → User)这类关系。一对多(如 User → Orders)强行用 Joins 会导致主表数据膨胀(1 个用户 + 5 个订单 = 5 行结果),后续还得去重或手动聚合,得不偿失。
常见误用:对 HasMany 关联调用 Joins("Orders"),结果返回重复用户、难以反序列化。此时应优先选 Preload。
- 正确场景示例:
db.Joins("Company").First(&user)(User 属于 Company) - 错误信号:日志里看到
JOIN ... ON users.id = orders.user_id且你预期返回 1 个用户,却拿到多行 —— 这就是膨胀了 - 若必须关联一对多并单 SQL 返回,需手动写
Joins("JOIN orders ...")+Select+ 自定义 struct,不再走 GORM 自动映射
Joins 支持链式调用和条件过滤
Joins 可叠加多个关联,也能在 JOIN 条件里加限制,避免拉回无用数据。但它不支持像 Preload 那样对关联表字段做 WHERE 筛选(那是 ON 条件的职责)。
- 基础链式:
db.Joins("Company").Joins("Manager").First(&user) - 带 ON 条件(注意是
ON,不是WHERE):db.Joins("JOIN companies ON users.company_id = companies.id AND companies.status = ?", "active") - 不能这样写:
db.Joins("Company").Where("companies.name LIKE ?", "%tech%")—— 这会变成WHERE子句,可能过滤掉主表记录,逻辑易错 - 真正想“只查活跃公司的用户”,应把条件放在
ON侧,或拆成两步:先查公司 ID 列表,再查用户
Joins 和 Preload 的性能差异关键在 SQL 次数与数据形态
很多人以为 “Joins 一定比 Preload 快”,其实取决于数据分布和使用方式。Preload 发 N+1 条语句但每条干净;Joins 发 1 条但可能带宽和内存压力更大。
- 当关联表数据量小、且是一对一时,
Joins几乎总是更快:省网络往返、数据库可优化 JOIN 顺序 - 当主表返回 100 行,每个关联需 JOIN 一张有 10k 行的宽表,MySQL 可能因临时表过大而降级为磁盘排序,反而更慢
-
Preload的 IN 查询在 ID 数量超 1000 时容易触碰max_allowed_packet,而Joins没这问题 —— 但它对 JOIN 字段索引要求更高,没索引时全表扫描代价翻倍 - 实测建议:在 MySQL 8.0+ 上,对 Belongs To 关系,优先用
Joins;对 HasMany,用Preload并配合Limit和Select
别忽略 Joins 的 struct 映射限制
GORM 不会自动把 JOIN 结果塞进嵌套 struct 字段里。比如 User{Company: Company{}},用 Joins("Company") 后,user.Company 仍是零值 —— 它只填充查询结果集的平铺字段,不构造嵌套对象。
- 想让 Company 数据进 struct,必须显式声明字段并匹配列名:
type UserWithCompany struct { User; CompanyName string `gorm:"column:companies.name"` } - 或者用
Scan到自定义 struct:var results []UserWithCompany; db.Joins("Company").Select("users.*, companies.name as company_name").Find(&results) - 如果你依赖
user.Company.ID这种写法,Joins无法满足,只能退回到Preload - 这也是最容易被忽略的一点:语法跑通 ≠ 业务可用。运行时字段为空,往往是因为忘了映射逻辑



















