PAYATHON 2026

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 中,不能直接对它执行 SQL SUM(net_profit)。
  • 如果明细分页,当前页的 Collection 汇总只代表当前页;全量汇总需要使用相同筛选条件另行计算。
  • 当前 Collection 按 purchase_date 的实际键值分组。如果它包含具体时间,按自然日统计就需要先统一日期粒度。

可以先完成查询类提取,再根据实际数据量、内存占用和响应时间决定是否改用数据库聚合。代码放在哪一层,与汇总在哪里执行,可以分别决定。

备注:内容仅供参考。