laravel关联查询核心是wherehas()筛选主模型、with()预加载,但易出错:wherehas()须用闭包且关联名准确,with()必须包含id字段,多层嵌套慎用,务必验证索引与explain。

用 whereHas() 筛选主模型、用 with() 预加载关联数据,是 Laravel 关联查询最核心也最容易出错的组合。盲目套用或混淆执行时机,轻则查不到数据,重则触发 N+1 或全表扫描。
whereHas() 必须配合闭包条件,不能直接写字段
常见错误是把 whereHas() 当成普通 where() 用,比如:
User::whereHas('userAttr', ['job' => 'teacher'])->get(); // ❌ 错误:数组写法不生效,闭包必须显式调用 $query->where()
正确写法要求闭包内操作的是关联表(user_attrs),且必须调用 ->get() 才真正执行:
-
whereHas('userAttr', fn($q) => $q->where('job', 'teacher'))→ 生成 EXISTS 子查询,只过滤users表中存在匹配user_attrs的记录 - 若关联方法名写错(如写成
userattr或UserAttr),Eloquent 找不到关系定义,会静默失败或报Relation not defined - 闭包里不能访问主模型字段(如
$q->where('users.name', ...)),那属于 JOIN 场景,该换join()或whereDoesntHave()
with() 预加载字段要显式指定 ID,否则无效
with('relation:col1,col2') 看似简单,但漏掉主键 ID 就会查不出数据:
Post::with('author:name,email')->get(); // ❌ author 表没查 id,Eloquent 匹配失败,author 始终为 null
必须包含外键字段(通常是 id)才能完成对象映射:
-
with('author:id,name,email')→ 正确,author.id是 Eloquent 关联匹配的锚点 - 多对多中间表字段也要小心:
with('tags:id,name,taggable_id')中taggable_id是中间表字段,不是tags表的,这种写法会报错;应只选tags表自身字段 - 深层嵌套如
with(['comments.author:id,name']),每层都得带 ID,否则链路中断
多层级关联慎用 with(),优先考虑 whereHas + join
像 Company::with('addresses.jobDetails.job') 这种三层预加载,Laravel 默认发 4 条 SQL(主表 + 每级一个 IN 查询),一旦某级没索引(如 job_details.address_id 缺索引),性能断崖下跌。
- 更稳的解法是以目标模型为起点反查:
JobDetail::whereHas('address.company', fn($q) => $q->where('id', $id)),利用 JOIN 一次过滤,再with('job')补充职位信息 - 如果
addresses表里已有company_id字段,就别走address.company三级跳,直接whereHas('address', fn($q) => $q->where('company_id', $id)),少一次 JOIN -
withCount()比with()轻量得多,只要数量不要详情时,无脑换掉
关联查询前先看 EXPLAIN,别猜索引有没有效
whereHas() 和 with() 再规范,底层 SQL 不走索引也是白搭。必须手动验证:
DB::select('EXPLAIN SELECT * FROM users WHERE EXISTS (SELECT * FROM user_attrs WHERE users.id = user_attrs.user_id AND job = ?)', ['teacher']);
重点盯 type 字段:
- 要是
ALL,说明user_attrs.user_id或job缺索引,立刻补:$table->index(['user_id', 'job']) - 复合条件如
where('status', 'active')->where('created_at', '>', now()),联合索引必须等值字段在前(status)、范围字段在后(created_at) -
softDeletes表的deleted_at字段没索引?全表扫描马上来,加($table->index('deleted_at')或覆盖索引($table->index(['deleted_at', 'id']))
真正卡住的从来不是语法,而是你没打开查询日志、没看 EXPLAIN、没确认索引是否存在——这些动作比改代码快,也比任何“优雅写法”管用。











