ThinkPHP 6 中模型关联分页不能直接用 with() + paginate(),因总数统计不包含关联条件导致错位;应改用 JOIN 查询或 whereHas + with 组合。

ThinkPHP 6 中模型关联查询分页不能直接用 with() + paginate(),否则会出现总数不准、关联数据为空、翻页错位等问题。核心原因是 paginate() 默认只对主表 COUNT,而关联条件(如筛选、排序)不参与总数统计,导致分页逻辑与实际展示结果脱节。
避免 with() 直接分页
写法 User::with('posts')->paginate(10) 表面可行,但实际风险高:
- 总数错误:返回的是用户总数,不是「有有效 posts 的用户」数;
- 数据错位:分页切片基于主表 ID,补关联时可能漏掉第 2 页本该出现的记录;
-
闭包失效:
with(['posts' => function($q){ $q->where('status', 1); }])中的条件不影响 COUNT,总数仍按无条件计算。
推荐方案:显式 JOIN 查询
当关联字段需参与筛选、排序或总数必须严格一致时,改用 join 是最稳妥的方式:
- 主表字段加别名(如
alias('u')),避免字段冲突; - JOIN 条件中写入关联过滤(如
AND p.status = 1); - 用
count('u.id')+distinct或group('u.id')防止一对多膨胀导致总数虚高; - 排序字段若来自关联表(如
p.created_at),确保该字段在 JOIN 中存在且已建索引。
示例:
立即学习“PHP免费学习笔记(深入)”;
User::alias('u')->join('posts p', 'u.id = p.user_id AND p.status = 1')
->field('u.id, u.name, p.title as post_title')
->group('u.id')
->order('p.created_at DESC')
->paginate(15);
替代方案:whereHas + with
适用于只需「筛选出有某关联数据的主记录」,不要求关联字段参与排序或展示:
- 用
whereHas('posts', function($q){ $q->where('status', 1); })确保 COUNT 和 SELECT 语义统一; - 再链式
with('posts')补全关联数据; - 不支持深度嵌套(如 User → Order → Item),TP6.0.13+ 仅修复部分两层关联 bug。
示例:
立即学习“PHP免费学习笔记(深入)”;
$query = User::where('status', 1)->whereHas('posts', function($q) {
$q->where('status', 1);
})
->with('posts');
$list = $query->paginate(15);
特殊场景处理
hasManyThrough 关系(如 User → UserGroup → Group)不支持 relation('groups')->paginate(),会报错:
- 正确写法是先构建查询对象,再调用
paginate(); - 筛选关联字段必须用
whereHas('groups', ...),不能用with(['groups' => ...])闭包; - 若需按 Group 字段排序,仍需 JOIN,
with无法满足。
字段精简与响应定制:
- 用
field(['id', 'name'])显式指定输出字段,主键必含; - 取纯数据用
$list->items(),避免toArray()带出大量元信息; - 需当前页码、是否还有下一页,单独调用
$list->currentPage()、$list->hasMore()。



















