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

旧时模样 提交于 2021-01-04 06:55:06

问题


How to get SUM on related model using eager loading, without loading whole relation data?

In my project there are two models, Account and Transaction. Account model has many transactions.

My requirement is : Get accounts and eager load only the sum on the related table.

My current code is provided : In this code transactions are eager loaded and sum is calculated using php. But I would prefer not to load the whole transactions. The only requirement is sum('amount').

table : accounts

| id | name | address | ...

table : transactions

| 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');
}

The following code gives each accounts and its transactions.

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

SUM is calculated using :

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

I have tried something like this, but didn't work.

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

回答1:


You need sub query to do that. I'll show you some solution:

  • Solution 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;
    }
    
  • Solution 2

    Create a withSum macro to the EloquentBuilder.

    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;
    });
    

    Then, you can use it just like when you're using withCount, except you need to add column that need to sum after the relationships (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();
    



回答2:


Whoops, I completely replaced SUM with COUNT in my head. While this will not directly solve your issue, the underlying code for whereCount() may provide some insight.


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;
}



回答3:


If Account hasMany Transactions, you could use the following query to get the amount

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

You need to make sure that account_id is selected in the Closure otherwise the relationship would not work.

Alternatively, you could also define another relationship like transactionSums as below in Account Model:

public function transactionSums() {

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

Then your controller code will be cleaner as below:

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

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


来源:https://stackoverflow.com/questions/58666676/laravel-how-to-get-sum-of-a-relation-column-using-eloquent

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!