
本文介绍如何使用 laravel spatie query builder 实现基于关联模型(如 `examinations`)**最新一条记录**(而非任意记录)的精准过滤,解决 `wherehas` 无法限制“最后一条”的常见误区。
在使用 Spatie Query Builder 进行关系过滤时,一个典型误区是误以为 whereHas(...->orderBy()->limit(1)) 能筛选出“拥有最新检查且患病”的动物——但实际上,whereHas 中的 orderBy 和 limit 在子查询中被忽略(Eloquent 不支持在 whereHas 子查询中使用排序与分页),导致它仍会匹配任意一条满足条件的检查记录,而非严格意义上的“最后一次”。
要真正实现“仅当最新一次检查的 disease_id 非空时才视为生病”,需采用两阶段查询策略:先定位每只动物的最新检查记录 ID,再基于这些 ID 精确过滤。
以下是推荐的、高效且可读性强的实现方式(已适配 Laravel 9+ 及 Spatie Query Builder v5+):
select('animals.*'); // 显式选择主表字段,避免后续 pluck 异常
$animalIdsWithLatestExam = $query
->withMax('examinations', 'id')
->get()
->filter(fn ($animal) => $animal->examinations_max_id !== null)
->pluck('examinations_max_id');
// Step 2: 查询这些最新 examination ID 对应的记录,并筛选 disease_id != null
$sickAnimalIds = Examination::query()
->whereIn('id', $animalIdsWithLatestExam)
->whereNotNull('disease_id')
->pluck('animal_id');
// Step 3: 主查询仅保留符合条件的 animal.id
$query->whereIn('id', $sickAnimalIds);
}
}✅ 关键说明:
- withMax('examinations', 'id') 利用 Eloquent 的聚合关系,为每个 Animal 附加 examinations_max_id 字段(即其最新检查 ID),无需 N+1 查询,性能优秀;
- filter(...->examinations_max_id !== null) 排除从未做过检查的动物,避免空值干扰;
- whereNotNull('disease_id') 比 != null 更语义清晰且兼容 SQL 标准(尤其在 PostgreSQL 中更可靠);
- 整个流程逻辑清晰、可测试、易维护,不依赖原始 SQL,保持 Laravel 生态一致性。
⚠️ 注意事项:
- 确保 examinations 表的 animal_id 字段已建立索引(INDEX animal_id),否则 whereIn + 子查询可能影响性能;
- 若数据量极大(>100k 动物),建议将此逻辑移至数据库视图或使用原生 JOIN 优化(例如通过 ROW_NUMBER() OVER (PARTITION BY animal_id ORDER BY created_at DESC));
- 此过滤器默认启用,需在控制器中显式注册:
$animals = QueryBuilder::for(Animal::class) ->allowedFilters(Filter::custom('sick', SickAnimalsFilter::class)) ->get();
通过该方案,你将精准获得“当前生病”的动物列表——即其最后一次检查明确关联了疾病,彻底规避历史病历造成的误判。










