Laravel中with()使用offset/limit如何为每个模型分页取关联数据?
问题复现
执行以下代码时,预期为每篇Post返回最多10条分页后的关联评论,实际仅第一篇Post返回了评论数据,其余Post的评论数组为空:
Post::with(['comments' => function($query) { $query->offset(0)->limit(10); }])->get()
实际返回结构:
[ { "...", "comments" : [ "[Comment]", "[Comment]", "[Comment]" ] }, { "...", "comments" : [] } ]
核心需求:在预加载关联时实现单条父模型维度的评论分页,每个Post最多挂载10条关联评论。
失效原因
Laravel的预加载机制只会执行1条关联查询SQL:先查出所有Post记录,提取所有Post的主键ID,再通过WHERE post_id IN (所有PostID)的语句一次性拉取所有关联评论,最后在内存中按post_id将评论分配给对应的Post模型。
你在闭包中添加的offset(0)->limit(10)是全局作用在这条关联查询上的,最终只会返回所有符合条件评论中的前10条,这10条评论只会分配给最先匹配到的Post,其余Post自然拿不到评论数据,因此无法直接通过全局limit实现单Post维度的评论限数。
可行实现方案
方案1:循环预加载(适合Post数量较小的场景)
先查询所有Post记录,再循环为单个Post单独加载10条评论,此时limit是作用在单Post的关联查询上:$posts = Post::get()->each(function (Post $post) { $post->load(['comments' => function ($query) { $query->offset(0)->limit(10); }]); });注意:该方案会产生N+1查询,Post总条数较多时会产生明显的性能损耗,不适合大数据量场景。
方案2:窗口函数分组取数(性能最优,无N+1问题)
如果你的数据库支持窗口函数(MySQL 8.0+、PostgreSQL、SQLite 3.25+等),可以通过ROW_NUMBER()窗口函数按post_id分组排序,直接在SQL层完成每个Post取前10条评论的逻辑,仅需1次关联查询即可实现需求。
第一步:在Post模型中定义自定义关联:// app/Models/Post.php public function limitedComments() { return $this->hasMany(Comment::class) ->fromRaw('( SELECT *, ROW_NUMBER() OVER ( PARTITION BY post_id ORDER BY created_at DESC ) as comment_rank FROM comments ) as sorted_comments') ->where('comment_rank', '<=', 10); }第二步:查询时直接预加载该关联即可:
// 一次性拿到所有Post,每个Post下最多带10条最新评论 $posts = Post::with('limitedComments')->get();如果需要动态调整偏移量、每页条数,可以在预加载闭包里动态修改
where条件的阈值即可。方案3:使用第三方扩展包封装的关联限数能力
不想手写原生窗口函数逻辑的话,可以使用封装了Eloquent关联限数能力的第三方包,底层同样基于窗口函数实现,提供了更简洁的调用语法,无需手动写原生SQL。
内容的提问来源于stack exchange,提问作者dscribbler1221

