Preload适用于主表分页(如查前10个用户及其订单),Joins适用于关联记录分页(如查前10条订单及用户信息);前者需额外统计关联总数,后者须去重、用子查询计数并定义匹配结构体。

关联查询时 Preload 和 Joins 怎么选
分页前必须先明确关联加载方式:用 Preload 是 N+1 优化后的「分两步查」,用 Joins 是单 SQL 的「连接查」。二者分页行为完全不同——Preload 分页作用于主表,关联数据自动按主键补全;Joins 若不做去重,会因一对多产生重复主记录,导致分页数量失真。
典型错误是直接 Joins("User").Limit(10).Offset(0),结果返回 8 条(因某用户有 3 个订单,被展开成 3 行),但你以为拿了前 10 个订单。
- 要保主表分页语义(如「查第 1–10 个用户及其订单」)→ 用
Preload - 要查关联组合记录并分页(如「查前 10 条订单及对应用户信息」)→ 用
Joins+Distinct或子查询 -
Preload不支持Order关联字段排序(如Preload("Orders", func(db *gorm.DB) *gorm.DB { return db.Order("created_at DESC") })无效),得靠应用层后处理或改用Joins
Preload 分页下如何正确获取关联总数
GORM 的 Count 不穿透预加载,db.Preload("Orders").Model(&User{}).Count(&total) 只统计用户数,不是「带订单的用户总数」。若需「有多少用户至少有一个订单」,得手动加条件:
db.Model(&User{}).
Where("id IN (?)", db.Table("orders").Select("user_id").Where("user_id IS NOT NULL")).
Count(&total)
更常见的是想知「所有订单总数」用于分页器显示,这时不能依赖主查询,应单独查:
- 用户分页时,额外发一条
db.Model(&Order{}).Count(&orderTotal) - 若需按用户聚合(如每个用户订单数),用
Group+Count,但无法和主分页复用同一查询 - 避免在
Preload回调里写Count,它只影响预加载的 SQL,不改变主Count行为
用 Joins 实现关联记录分页的硬约束
当目标是分页「订单列表+用户姓名」这类扁平结果,Joins 是唯一选择,但必须处理三件事:去重、计数、NULL 关联。
例如:db.Joins("JOIN users ON orders.user_id = users.id").Select("orders.*, users.name as user_name").Order("orders.id DESC").Limit(10).Offset(0) —— 这条语句若存在订单无用户(user_id 为 NULL),该订单会被丢弃(INNER JOIN)。需要保留就得显式写 Joins("LEFT JOIN users ...")。
- 去重必须加
Distinct("orders.id"),否则一对多会膨胀行数 - 总数统计不能直接
Count,得用子查询:db.Table("(SELECT DISTINCT orders.id FROM orders LEFT JOIN users ON orders.user_id = users.id) AS t").Count(&total) - GORM v1.25+ 支持
Scopes封装这类逻辑,但Count子查询仍需手写,没有银弹
分页参数传入与 Scan 结构体怎么配
用 Joins 查出的混合字段,不能直接 Scan(&[]Order{}),因为多了 user_name。必须定义接收结构体:
type OrderWithUser struct {
ID uint `gorm:"primaryKey"`
UserID uint
Amount float64
UserName string `gorm:"column:user_name"`
}
var list []OrderWithUser
db.Joins("LEFT JOIN users ON orders.user_id = users.id").
Select("orders.*, users.name as user_name").
Find(&list)
注意点:
-
Select里必须显式列出字段或用orders.*,不能只写*,否则 GORM 无法映射到嵌套结构 - 如果结构体字段名和数据库列名不一致,用
gorm:"column:xxx"标签,别依赖默认蛇形转驼峰(user_name→UserName是对的,但user_full_name→UserFullName可能失败) - 分页总数和列表必须分开查,GORM 不支持单次查询同时返回分页数据和总计数
最易被忽略的是:LEFT JOIN 后 users.name 可能为 NULL,UserName 字段类型得是 *string 或用 sql.NullString,否则 Scan 会 panic。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











