Laravel Eloquent深度嵌套关系中withSum的使用问题
Laravel中统计Sprint关联任务总时长的最优方案
我在Laravel聚合关系文档里没找到合适的方案,之前已经用withSum实现了按条件统计用户关联的taskTimeSessions时长,代码如下:
private function refreshUsers() { $this->users = User::withSum(['taskTimeSessions'=> function ($query) { $query->whereMonth('created_at',$this->month) ->where('is_reconciled',1); }],'session_duration_in_seconds') ->get(); }
需求场景
现在需要统计Sprint的总时长,或者至少获取Sprint内各Task的时长以便求和,模型关系如下:
- Sprint 关联多个 SprintTasks(中间表)
- SprintTask 属于一个 Task
- Task 关联多个 TaskTimeSessions
尝试过的方法
- 嵌套
withSum查询,执行无报错但无法找到统计结果:
Sprint::with([ 'sprintTasks.task'=> function ($query) { $query->withSum('taskTimeSessions','session_duration_in_seconds'); }]) ->get();
- 尝试多级关联直接调用
withSum,触发错误:
->withSum('sprintTasks.task.taskTimeSessions', 'session_duration_in_seconds')
错误信息:
Call to undefined method App\Models\Sprint::sprintTasks.task()
相关模型代码
Sprint模型
public function sprintTasks() { return $this->hasMany(SprintTask::class, 'sprint_id'); }
SprintTask模型
protected $fillable = [ 'sprint_id', 'task_id', 'is_completed' ]; public function task() { return $this->belongsTo(Task::class,'task_id'); } public function sprint() { return $this->belongsTo(Task::class,'sprint_id'); }
注意:此处sprint()关联存在错误,应指向Sprint::class而非Task::class,后续方案会修正该问题
Task模型
public function taskTimeSessions() { return $this->hasMany(TaskTimeSession::class, 'task_id'); }
TaskTimeSession模型
protected $fillable = [ 'task_id', 'session_duration_in_seconds' ]; public function task() { return $this->belongsTo(Task::class,'task_id'); }
当前临时方案
目前只能在Blade模板中通过循环求和,实现不够优雅:
@php $timeTracked = 0; foreach ($sprint->sprintTasks as $sprintTask) { $timeTracked += $sprintTask->task->time_tracked_in_seconds; } @endphp
最优方案推荐
方案一:修正关联后封装模型统计方法
首先修正SprintTask模型的关联错误:
public function sprint() { return $this->belongsTo(Sprint::class,'sprint_id'); }
然后在Sprint模型中封装统计方法,直接通过关联查询求和:
public function totalTaskTime() { return $this->sprintTasks() ->join('tasks', 'sprint_tasks.task_id', '=', 'tasks.id') ->join('task_time_sessions', 'tasks.id', '=', 'task_time_sessions.task_id') ->sum('task_time_sessions.session_duration_in_seconds') ?? 0; }
使用时直接调用$sprint->totalTaskTime()即可获取总时长。
方案二:查询时直接添加统计字段(高效推荐)
在查询Sprint时,通过子查询直接统计总时长,避免后续循环计算:
$sprints = Sprint::addSelect(['total_task_time' => function ($query) { $query->selectRaw('SUM(session_duration_in_seconds)') ->from('task_time_sessions') ->join('tasks', 'tasks.id', '=', 'task_time_sessions.task_id') ->join('sprint_tasks', 'sprint_tasks.task_id', '=', 'tasks.id') ->whereColumn('sprint_tasks.sprint_id', 'sprints.id'); }])->get();
每个Sprint实例会直接携带total_task_time属性,直接访问即可。
方案三:修正嵌套withSum的访问方式
如果需要保留各Task的单独时长统计,可使用嵌套withSum,但需注意正确访问统计字段:
$sprints = Sprint::with(['sprintTasks.task' => function ($query) { $query->withSum('taskTimeSessions', 'session_duration_in_seconds'); }])->get();
访问总时长时:
foreach ($sprints as $sprint) { $total = 0; foreach ($sprint->sprintTasks as $st) { $total += $st->task->task_time_sessions_sum_session_duration_in_seconds ?? 0; } // $total即为当前Sprint的总时长 }
内容的提问来源于stack exchange,提问作者Jakub
相关产品推荐
相关产品推荐

