发布于2026-07-06 阅读(0)
扫一扫,手机访问
多态关联的 N+1 问题,比普通关联要隐蔽得多。很多开发者习惯性地用 with('commentable') 去预加载,结果发现查询日志里依然蹦出一堆单条 SQL——这才意识到事情没那么简单。

先说一个核心判断:with() 直接传多态关系名,基本是无效的。必须用 withCount()、withSum() 或者手动 leftJoin,否则就是妥妥的 N+1 查询,只不过 Lara vel 不会报错,而是静默退化成懒加载。
这问题其实出在 Eloquent 对多态关系的底层处理上。当你写 Comment::with('commentable') 的时候,Lara vel 并不知道你加载的到底是 Post、Video 还是其他什么模型。因为没有固定的外键表和表名,Eloquent 生成不了有效的预加载 SQL。它不是报错,而是默默放弃预加载,每次在循环里访问 $comment->commentable->name 时,老老实实发一次新查询。
DB::getQueryLog(),发现一堆结构一模一样但参数不一样的 SELECT * FROM users WHERE id = ? 或 SELECT * FROM posts WHERE id = ?,来源混杂,很难排查。commentable_type 和 commentable_id 的组合,无法被普通的 IN 或 JOIN 统一处理,Eloquent 索性放弃预加载。dd($comment->relationLoaded('commentable')),结果大概率是 false,哪怕你明确写了 with('commentable')。如果你的业务场景只需要聚合信息——比如“这条评论对应的文章有多少阅读量”或者“这条评论作者有多少粉丝”——那就别硬拉整个模型。这些聚合方法走的是子查询,不依赖 JOIN,天然适配多态场景。
Comment::withCount(['commentable' => function ($q) { $q->where('published', true); }])->get() 会生成带条件的 COUNT 子查询,并且会自动根据 commentable_type 分表执行,不需要你操心。withSum('commentable', 'views') 同理,但它只对数字字段有效,而且不支持跨模型字段映射——比如 users.followers_count 和 posts.views 不能共用一个 withSum。withCount(['commentable as post_views' => fn($q) => $q->when($q->getModel() instanceof Post, fn($q) => $q->select('views'))])。这种写法需要手动分模型分支,稍显繁琐,但可控性高。如果你明确知道 commentable_type 只会有 'App\Models\Post' 和 'App\Models\Video' 两种,那可以拆成两个 leftJoin,用 COALESCE 把字段合并起来。
Comment::select('comments.*', \DB::raw('COALESCE(posts.title, videos.title) as commentable_title'))
->leftJoin('posts', function ($join) {
$join->on('comments.commentable_id', '=', 'posts.id')
->where('comments.commentable_type', '=', 'App\Models\Post');
})
->leftJoin('videos', function ($join) {
$join->on('comments.commentable_id', '=', 'videos.id')
->where('comments.commentable_type', '=', 'App\Models\Video');
})
->get()commentable_type,对应行的 commentable_title 会直接变成 null。而且这种写法无法复用 Eloquent 的模型实例和访问器逻辑。这个方法不是链式调用的,而是针对已经存在的集合,按类型分组后批量加载。但前提是必须手动指定所有可能的模型类,而且不能加条件闭包。
$comments = Comment::with('user')->get();
$comments->loadMorph('commentable', [Post::class, Video::class]);Post ID 对应的记录,再查所有 Video ID 对应的记录,最后挂载到各自的 commentable 属性上。load() 那样传闭包过滤(比如只加载已发布的 Post),也不能用于 Builder 实例——Comment::query()->loadMorph() 会直接报错。loadMorph() 的类数组和数据库里实际存的 commentable_type 字符串完全一致,包括命名空间。否则某类数据永远为空,排查起来很头大。最后补充一点:多态 N+1 最容易被忽略的地方,往往不是查询本身,而是模型的 appends 或访问器里偷偷调用了 commentable。比如 getCommentableNameAttribute() 方法里写了 $this->commentable->name。这种隐式调用不会被 loadMorph() 覆盖,依然会触发懒加载,值得特别留意。
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
正版软件
正版软件
正版软件
正版软件
正版软件
1
2
3
7
8