Laravel 中的数据库查询与汇总逻辑应放在 Service 还是 Query 类中?
建议先把这段代码放进专门的报表查询类,例如 CarsByPurchaseDateQuery,由 Controller 调用。这个类负责按筛选条件读取车辆数据,返回按购买日期分组的明细和汇总。仅凭当前代码,暂时没有必要增加 Service 或 Repository。
这段代码实际在哪里做汇总?
执行位置以 get() 为界:
get()之前的筛选、字段选择和排序由数据库执行。get()之后的groupBy()、map()和sum()在 PHP 的 Collection 上执行。
这段代码先查询数据库,再在内存中汇总。由于 groupBy('purchase_date') 出现在 get() 之后,它不会生成 SQL 的 GROUP BY。
Query 类、Service 和 Repository 怎么选?
可以根据各层的职责判断:
| 位置 | 适用情况 | 对当前代码的建议 |
|---|---|---|
| Query 类 | 封装某个读取场景的筛选、排序、分组和结果组装 | 适合当前报表 |
| Service | 编排多个操作,或承载需要复用的业务规则 | 出现这些需求后再引入 |
| Repository | 集中提供数据访问接口,或遵循项目已有的数据访问约定 | 当前代码本身不足以证明需要这一层 |
app/Queries 可以作为项目自己的目录约定,框架并不强制使用这个位置。如果项目已经统一把报表读取放在 Service 中,继续沿用也合理。让相关逻辑集中起来,可以避免 Controller 越来越长。
Eloquent 的存在也不排除 Repository 的价值。引入时可以考虑,新增的层是否提供了明确的接口或职责,还是只转发了一次 Eloquent 调用。
如何提取成查询类?
下面的示例假设 filterQuery($request) 返回 Eloquent Builder,只负责筛选,没有提前限制查询字段;同时,模型能够正确提供 net_profit 属性。
namespace App\Queries;
use Illuminate\Database\Eloquent\Builder;
class CarsByPurchaseDateQuery
{
public function get(Builder $query): array
{
$carsByDate = (clone $query)
->orderBy('purchase_date', 'asc')
->orderBy('purchase_time', 'asc')
->get()
->groupBy('purchase_date');
$summary = $carsByDate->map(function ($cars) {
return [
'volume_total' => $cars->sum('quantity'),
'gross_profit_total' => $cars->sum('gross_profit'),
'tax_total' => $cars->sum('tax'),
'handling_fees_total' => $cars->sum('handling_fee'),
'delivery_fees_total' => $cars->sum('delivery_fee'),
'net_profit_total' => $cars->sum('net_profit'),
];
});
return [
'cars' => $carsByDate,
'summary' => $summary,
];
}
}
Controller 获得筛选后的 Builder 后即可调用。假设 $reportQuery 是注入的 CarsByPurchaseDateQuery 实例:
$query = $this->cars->filterQuery($request);
return $reportQuery->get($query);
这样保留了原有的请求筛选入口,由报表类负责后续读取和汇总。如果 filterQuery() 返回的不是 Eloquent Builder,需要按实际返回类型调整方法参数。
示例中的 get() 没有指定字段,是为了先展示职责划分。实际使用时可以恢复明确的字段列表,但要包含返回明细和计算汇总所需的全部字段。
原代码需要检查 net_profit 的来源
原来的 select() 没有包含 net_profit,后面却执行了:
$cars->sum('net_profit');
需要区分两种情况:
- 如果
net_profit是数据库字段,应将它加入查询字段列表。 - 如果它是模型的 accessor,应确认 accessor 所依赖的字段都已查询出来。
现有代码无法说明它属于哪一种情况,也不能据此推导净利润公式。字段缺失可能导致汇总结果不符合预期,具体表现取决于模型实现及缺失属性的处理配置。
如果净利润涉及需要在多个场景复用的业务规则,可以单独提取计算逻辑,再由报表查询或 Service 调用。
什么时候改用数据库聚合?
当前返回结果同时包含全部车辆明细和每日汇总。如果筛选后的记录量可控,读取一次后使用 Collection 汇总,是一种直接的实现方式。
如果接口只需要每日汇总,可以考虑在数据库中使用 SUM() 和 GROUP BY,减少传输到 PHP 的记录数。调整时需要注意:
- 把
groupBy()移到get()之前,会改变为数据库分组语义,不能继续照搬原来的明细字段列表。 - 如果
net_profit只存在于 PHP accessor 中,不能直接对它执行 SQLSUM(net_profit)。 - 如果明细分页,当前页的 Collection 汇总只代表当前页;全量汇总需要使用相同筛选条件另行计算。
- 当前 Collection 按
purchase_date的实际键值分组。如果它包含具体时间,按自然日统计就需要先统一日期粒度。
可以先完成查询类提取,再根据实际数据量、内存占用和响应时间决定是否改用数据库聚合。代码放在哪一层,与汇总在哪里执行,可以分别决定。
备注:内容仅供参考。
