Laravel: суммирование атрибута во вложенном отношении

Я хочу суммировать вложенную коллекцию.

У меня есть следующие таблицы:

  • Места
  • Предложения
  • Заказы

У venue может быть много offers, а у offer может быть много orders.

Примерные данные могут быть:

Места

id        |  name
==========================
5         |  Pizza 4 Less
10        |  Poundland

Предложения

id | venue_id | name
==================================
17 | 5        | Buy 1 get one free 
24 | 5        | 30% off pizza
32 | 10       | 50% off
50 | 10       | 20% off

Заказы

id | offer_id | bill | paid
===========================
85 | 17       | 15   | true
86 | 17       | 20   | true
87 | 17       | 90   | true
88 | 24       | 14   | true
89 | 32       | 15   | true
90 | 32       | 65   | true
91 | 50       | 24   | true
92 | 50       | 1000 | false

Я хочу использовать модель Laravel Elqouent, чтобы получить общую сумму, уплаченную за каждое место. Итак, для приведенных выше данных я хочу получить следующий результат:

id | name          | total_paid
===============================
5  | Pizza 4 Less  | 139
10 | Poundland     | 104

Обратите внимание, что итоговые суммы не включают неоплаченные заказы (например, заказ 92).

В настоящее время я делаю это следующим образом:

$venues = Venue::with(['offers.orders' => function ($query) {
        $query->where('paid', '=', true);
    }])
    ->get();

$totals = [];
foreach ($venues as $venue) {
    $totalPaid = 0;
    foreach ($venue->offers as $offer) {
        $totalPaid += $offer->orders->sum('bill');
    }
    $totals[$venue->name] = $totalPaid;
}

Как видите, код выше неэффективный и длинный.

Есть лучший способ сделать это?


person Yahya Uddin    schedule 02.06.2017    source источник


Ответы (1)


Чистая версия, но неэффективная

// In your Venue Model
public function getTotalPaidAttribute()
{
    return $this->offers->sum('TotalPaid');
}

// In your Offer Model
protected $appends = ['totalPaid'];

public function getTotalPaidAttribute()
{
    return $this->orders->sum('paid');
}

// use it : 
foreach($venues as $venue){
     //do what you want with it
     $venue->totalPaid;
}

(отредактировано)

Как сказано в комментариях, этот метод может быть чище, но неэффективен:

Эффективный способ:

// In your Venue Model
public function orders(){
    return $this->hasManyThrough(Order::class, Offer::class)
        ->selectRaw('sum(paid) as aggregate, venue_id')
        ->groupBy('venue_id');
}

public function totalPaid(){
    if ( ! array_key_exists('orders', $this->relations)) $this->load('orders');
    $relation = $this->getRelation('orders');

     return ($relation) ? $relation->aggregate : 0;
}

public function getTotalPaidAttribute()
{
    return $this->totalPaid();
}

возможно, я перепутал ваши ключи, возможно, вам придется использовать полное объявление отношения:

return $this->hasManyThrough(
        Order::class, Offer::class
        'venue_id', 'offer_id', 'id'
    );

но я только что сделал со своим проектом, и он работает как шарм

person Mathieu Ferre    schedule 02.06.2017
comment
Чистый и организованный - person Sérgio Reis; 02.06.2017
comment
Это может быть чистым, но не эффективным. Попробуйте запустить тест с парой тысяч строк, и вы все поймете. - person Sandeesh; 02.06.2017
comment
да, но если вы знаете, как сделать этот очиститель, я был бы рад, я пытаюсь получить то же самое, но для avg(), это решение может быть неэффективным, но даже самое грязное я не могу это сделать :( - person Mathieu Ferre; 02.06.2017
comment
Хотя это намного чище, кажется, что он выполняет то же количество запросов, что и моя версия, поэтому все еще очень неэффективен. - person Yahya Uddin; 02.06.2017