Laravel 8 如何按父模型布尔开关条件加载关联模型
问题原因
- 误用
whereHas方法:该方法作用是过滤主表记录,仅返回存在匹配关联的Domain数据,会直接把certificate_checks_enabled=false的Domain从结果中剔除,和「返回所有符合条件的Domain、仅部分记录挂载关联」的需求完全冲突。 - 关联条件逻辑错误:
certificate_checks_enabled是Domain表的字段,原有代码将其写在Certificate表的查询条件中,永远无法生效;同时whereHas闭包内多余的->get()调用会破坏查询构造器的链式逻辑。 - 搜索条件分组存在语法隐患:闭包内第一个条件使用
orWhere,会生成带前导OR的分组SQL,触发语法错误,需要将第一个orWhere改为where。
正确实现方案
使用条件预加载替换原有的whereHas逻辑,通过关联反向约束筛选需要加载的Certificate数据,全程仅产生2条SQL,无N+1性能问题:
$domains = Domain::where('user_id', Auth::id()) ->where(function ($sql) use ($search) { // 第一个条件改为where,避免SQL语法错误 $sql->where('domain', 'like', "$search%") ->orWhere('status', 'like', "$search%") ->orWhere('registrar', 'like', "$search%"); }) // 条件预加载关联,仅给开启了证书检查的Domain挂载Certificate ->with(['certificate' => function ($query) { // 前提:Certificate模型中需要定义反向关联domain(),返回$this->belongsTo(Domain::class) $query->whereHas('domain', function ($subQuery) { $subQuery->where('certificate_checks_enabled', true); }); }]) ->orderBy($sort[0] ?? 'expires_on', $sort[1] ?? 'desc') ->paginate((int) $perPage);
如果不想定义Certificate的反向关联,也可以用原生SQL实现相同的预加载约束:
->with(['certificate' => function ($query) { $query->whereRaw('EXISTS ( SELECT 1 FROM domains WHERE domains.id = certificates.domain_id AND domains.certificate_checks_enabled = 1 )'); }])
实现效果
修改后主查询会返回所有符合用户ID、搜索条件的Domain分页记录,预加载阶段仅查询对应Domain开启了证书检查的Certificate数据,Laravel会自动将匹配的Certificate模型挂载到对应Domain实例上,未开启证书检查的Domain实例的certificate关联值为null,完全符合预期。
内容的提问来源于stack exchange,提问作者Ryan H
相关产品推荐
相关产品推荐

