
本文详解如何使用 Laravel Eloquent 的 whereHas 和 withCount 实现「仅获取拥有至少一个活跃任务的客户」,并支持搜索、排序与分页,避免 N+1 查询和前端过滤导致的分页失效问题。
本文详解如何使用 laravel eloquent 的 `wherehas` 和 `withcount` 实现「仅获取拥有至少一个活跃任务的客户」,并支持搜索、排序与分页,避免 n+1 查询和前端过滤导致的分页失效问题。
在构建客户管理后台时,常需展示“有活跃任务的客户”列表——这不仅涉及数据筛选,更关乎性能与分页正确性。原始代码中,foreach 循环内逐个查询 Job::where('client_id', $client->id)->where('is_active', 1) 导致严重 N+1 问题,且因过滤逻辑后置,$recordsTotal 统计的是全部客户,造成分页总数与实际显示数据不一致。
✅ 正确解法是将筛选条件下推至数据库层,利用 Eloquent 关系方法实现高效、可分页的查询:
1. 使用 whereHas 筛选拥有活跃任务的客户
whereHas 允许你基于关联模型的约束条件进行主模型筛选。假设 Client 模型已正确定义 jobs 关系(如 hasMany(Job::class)),则:
$query = Client::select('clients.*')
->whereHas('jobs', function ($q) {
$q->where('is_active', 1);
});
该语句生成 SQL 类似:
SELECT clients.* FROM clients
WHERE EXISTS (
SELECT * FROM jobs
WHERE jobs.client_id = clients.id AND jobs.is_active = 1
);
确保只返回至少拥有一条 is_active = 1 任务的客户,且该条件参与 COUNT(*) 和分页 LIMIT/OFFSET,保障 recordsTotal 与实际结果一致。
2. 使用 withCount 预加载活跃任务数量
为避免循环中重复查询计数,用 withCount 一次性获取每个客户的活跃任务数,并支持条件计数:
$query = Client::select('clients.*')
->whereHas('jobs', function ($q) {
$q->where('is_active', 1);
})
->withCount(['jobs as active_jobs' => function ($q) {
$q->where('is_active', 1);
}]);
此时 $client->active_jobs 即为该客户活跃任务数量(整型),可直接用于视图渲染,无需额外查询。
3. 整合进 dataSource 函数(修正版)
以下是优化后的完整 dataSourcejobs 方法,关键改进包括:
- ✅
whereHas保证数据库级筛选 - ✅
withCount预加载计数字段 - ✅
orderBy支持按active_jobs排序(需加入$sortColumns) - ✅ 移除循环内查询,提升性能
public function dataSourcejobs(Request $request)
{
$search = $request->query('search', ['value' => '', 'regex' => false]);
$draw = $request->query('draw', 0);
$start = $request->query('start', 0);
$length = $request->query('length', 25);
$order = $request->query('order', [0, 'desc']);
$filter = $search['value'];
$sortColumns = [
0 => 'clients.id',
1 => 'clients.title',
2 => 'active_jobs', // 注意:此字段由 withCount 生成,需用表别名或 select 显式指定
3 => 'clients.is_enabled',
4 => 'actions'
];
// 构建主查询:筛选有活跃任务的客户 + 预加载活跃任务数
$query = Client::select('clients.*')
->whereHas('jobs', fn ($q) => $q->where('is_active', 1))
->withCount(['jobs as active_jobs' => fn ($q) => $q->where('is_active', 1)]);
// 全局搜索(仅对 clients 表字段)
if (!empty($filter)) {
$query->where('clients.title', 'like', "%{$filter}%");
}
// 总记录数(已受 whereHas 影响,准确!)
$recordsTotal = $query->count();
// 排序(注意:active_jobs 是聚合字段,需确保数据库支持 ORDER BY alias)
$sortColumnName = $sortColumns[$order[0]['column']];
$query->orderBy($sortColumnName, $order[0]['dir'])
->take($length)
->skip($start);
$clients = $query->get();
$json = [
'draw' => $draw,
'recordsTotal' => $recordsTotal,
'recordsFiltered' => $recordsTotal, // 因搜索也在主查询中,二者一致
'data' => [],
];
foreach ($clients as $client) {
$json['data'][] = [
$client->id,
e($client->title), // 防 XSS
'<button class="jobs">' . $client->active_jobs . ' Jobs</button>',
$client->is_enabled ? 'Yes' : 'No',
'<a href="/client/'%20.%20%24client->id%20.%20'/edit">' . config('ecl.EDIT') . '</a>
<a href="/client-workload/'%20.%20%24client->id%20.%20'">' . config('ecl.WORK') . '</a>'
];
}
return $json;
}
⚠️ 注意事项
-
关系定义验证:确保
Client模型中存在public function jobs() { return $this->hasMany(Job::class); }。 -
排序兼容性:若数据库不支持直接
ORDER BY active_jobs(如旧版 MySQL),可改用orderByRaw('jobs_count DESC')并在select()中显式添加DB::raw('COUNT(jobs.id) as jobs_count')及对应leftJoin。 -
索引优化:为
jobs.client_id和jobs.is_active字段建立联合索引(ALTER TABLE jobs ADD INDEX idx_client_active (client_id, is_active);),大幅提升whereHas性能。 -
空值处理:
withCount对无关联记录返回0,无需额外判空。
通过 whereHas + withCount 组合,你既实现了业务所需的精准筛选,又保持了分页完整性与查询高性能,是 Laravel 关系查询的最佳实践之一。











