Laravel:如何使用 Eloquent 获取关系列的 SUM

Laravel: How to get SUM of a relation column using Eloquent

如何在不加载整个关系数据的情况下使用预先加载在相关模型上获取 SUM?

在我的项目中有两个模型,AccountTransaction。账户模型 has many 笔交易。

我的要求是获取帐户并立即加载相关 table 上的总和.

提供了我当前的代码:在此代码中,transactions 是预先加载的,并且使用 php 计算总和。但我宁愿不加载整个交易。唯一的要求是 sum('amount').

table : 帐户

| id | name | address | ...

table : 交易

| id | account_id | amount | ...

Account.php

/**
 * Get the transaction records associated with the account.
 */
public function transactions()
{
    return $this->hasMany('App\Models\Transaction', 'account_id');
}

以下代码给出了每个账户及其交易。

$account = Account::with(['transactions'])->get();

SUM 的计算方法是:

foreach ($accounts as $key => $value) {
    echo $value->transactions->sum('amount'). " <br />";
}

我试过类似的方法,但没有成功。

public function transactions()
{
    return $this->hasMany('App\Models\Transaction', 'account_id')->sum('amount;
}

哎呀,我脑子里把 SUM 完全替换成了 COUNT 虽然这不能直接解决你的问题,但是 [= 的底层代码13=] 可能会提供一些见解。


Counting Related Models

If you want to count the number of results from a relationship without actually loading them you may use the withCount method, which will place a {relation}_count column on your resulting models.

$accounts = Account::withCount('transactions')->get();

foreach ($accounts as $account) {
    $transactionCount = $account->transactions_count;
}

你需要子查询来做到这一点。我会告诉你一些解决方案:

  • 解决方案 1

    $amountSum = Transaction::selectRaw('sum(amount)')
        ->whereColumn('account_id', 'accounts.id')
        ->getQuery();
    
    $accounts = Account::select('accounts.*')
        ->selectSub($amountSum, 'amount_sum')
        ->get();
    
    foreach($accounts as $account) {
        echo $account->amount_sum;
    }
    
  • 解决方案 2

    为 EloquentBuilder 创建一个 withSum 宏。

    use Illuminate\Support\Str;
    use Illuminate\Database\Eloquent\Builder;
    use Illuminate\Database\Query\Expression;
    
    Builder::macro('withSum', function ($columns) {
        if (empty($columns)) {
            return $this;
        }
    
        if (is_null($this->query->columns)) {
            $this->query->select([$this->query->from.'.*']);
        }
    
        $columns = is_array($columns) ? $columns : func_get_args();
        $columnAndConstraints = [];
    
        foreach ($columns as $name => $constraints) {
            // If the "name" value is a numeric key, we can assume that no
            // constraints have been specified. We'll just put an empty
            // Closure there, so that we can treat them all the same.
            if (is_numeric($name)) {
                $name = $constraints;
                $constraints = static function () {
                    //
                };
            }
    
            $columnAndConstraints[$name] = $constraints;
        }
    
        foreach ($columnAndConstraints as $name => $constraints) {
            $segments = explode(' ', $name);
    
            unset($alias);
    
            if (count($segments) === 3 && Str::lower($segments[1]) === 'as') {
                [$name, $alias] = [$segments[0], $segments[2]];
            }
    
            // Here we'll extract the relation name and the actual column name that's need to sum.
            $segments = explode('.', $name);
    
            $relationName = $segments[0];
            $column = $segments[1];
    
            $relation = $this->getRelationWithoutConstraints($relationName);
    
            $query = $relation->getRelationExistenceQuery(
                $relation->getRelated()->newQuery(),
                $this,
                new Expression("sum(`$column`)")
            )->setBindings([], 'select');
    
            $query->callScope($constraints);
    
            $query = $query->mergeConstraintsFrom($relation->getQuery())->toBase();
    
            if (count($query->columns) > 1) {
                $query->columns = [$query->columns[0]];
            }
    
            // Finally we will add the proper result column alias to the query and run the subselect
            // statement against the query builder. Then we will return the builder instance back
            // to the developer for further constraint chaining that needs to take place on it.
            $column = $alias ?? Str::snake(Str::replaceFirst('.', ' ', $name.'_sum'));
    
            $this->selectSub($query, $column);
        }
    
        return $this;
    });
    

    然后,你可以像使用withCount一样使用它,只是你需要在关系(relation.column)之后添加需要求和的列。

    $accounts = Account::withSum('transactions.amount')->get();
    
    foreach($accounts as $account) {
        // You can access the sum result using format `relation_column_sum`
        echo $account->transactions_amount_sum;
    }
    
    $accounts = Account::withSum(['transactions.amount' => function (Builder $query) {
        $query->where('status', 'APPROVED');
    })->get();
    

如果Account hasMany Transactions,您可以使用以下查询来获取金额

Account::with(['transactions' =>  function( $q) {
    $q->selectRaw('sum(amount) as sum_amount, account_id')->groupBy('account_id');
}

您需要确保在闭包中选择了 account_id,否则关系将无法工作。

或者,您也可以在帐户模型中定义另一个关系,例如 transactionSums,如下所示:

public function transactionSums() {

    return $this->hasMany(Transaction::class)->selectRaw('sum(amount) as sum_amount, account_id')->groupBy('account_id');
}

那么您的控制器代码将更清晰,如下所示:

$accounts = Account::with(['transactionSums' ]);

foreach($accounts as $account)
{
    echo $account->transactionSums[0]->sum_amount;
}