必须用hasmanythrough定义跨三层关联并配合wherehas筛选深层字段,否则会漏数据或报错;需显式传参、嵌套wherehas与with闭包、注意软删除过滤及join别名规范。

要在 Laravel 中精准获取跨三层模型的数据并按深层字段筛选,比如从国家查出所有酒店且仅限开业三年内的,必须正确组合 hasManyThrough 关联与 whereHas 条件,否则会漏数据或报错。
定义 hasManyThrough 多级穿透关联
在 Country 模型中添加 hotels() 方法,明确指定五参数以避免 Laravel 自动推断错误:
public function hotels()
{
return $this->hasManyThrough(
Hotel::class,
City::class,
'country_id', // City 表外键,指向 Country
'city_id', // Hotel 表外键,指向 City
'id', // Country 主键
'id' // City 主键
);
}
【必须显式传入全部六个参数中的前五个,第六个虽可省略但建议写全,否则在表名不规范时极易因自动推断失败导致查询为空】
用 whereHas 筛选深层关联字段
控制器中不能直接在 Country 查询里写 ->where('hotels.opened_at', '>=', $date),这会报错——Eloquent 不识别点号路径字段。
正确做法是嵌套 whereHas:
$countries = Country::whereHas('hotels', function ($query) use ($threeYearsAgo) {
$query->where('opened_at', '>=', $threeYearsAgo);
with(['hotels' => function ($query) use ($threeYearsAgo) {
$query->where('opened_at', '>=', $threeYearsAgo);
get();
注意:whereHas 控制 Country 是否被选中,with 里的闭包控制最终加载哪些 hotels。两者缺一不可,否则要么漏国家、要么加载了不该显示的旧酒店。
避免 N+1 并防止字段覆盖的预加载策略
方法一:单层 with 预加载(适用于只读展示)
Country::with('hotels')->get();
方法二:带条件的嵌套 with(推荐用于列表页)
Country::with(['hotels' => function ($q) { $q->select('id', 'name', 'city_id', 'opened_at'); }])->get();
方法三:多层级穿透 + 局部约束(当需同时加载 City 名称时)
Country::with(['hotels.city' => function ($q) { $q->select('id', 'name'); }])->get();
这一步操作起来很简单,直接把 with() 里的闭包加进去就行。但切记:如果 hotels 表有软删除字段 deleted_at,必须在闭包中显式加 ->whereNull('deleted_at'),否则已软删的酒店仍会出现在结果里。
用 join 实现字段混排与排序
第一步:用 DB::table 手动拼接三表 JOIN,避免 Eloquent 的模型映射干扰
第二步:为每张表设置唯一别名,users 改成 u,cities 改成 c,hotels 改成 h
第三步:select 字段全部加前缀,如 c.name as city_name, h.name as hotel_name
第四步:on 条件写进 join() 的闭包里,把软删除过滤也塞进去:
->join('cities as c', function ($join) {
$join->on('c.country_id', '=', 'countries.id')
->whereNull('c.deleted_at');
这一步最容易踩坑:如果把 ->whereNull('c.deleted_at') 写在 join() 外面,LEFT JOIN 就会退化成 INNER JOIN,导致没有城市的国家直接消失。











