Представляю вам вольный перевод статьи "Tweaking Eloquent relations – how to get hasMany relation count efficiently?" с сайта softonsofa.com.

Работая с отношениями моделей, вы вероятнее всего хотели бы подсчитать количество полученных элементов (например, комментарии или лайки). Очевидно существуют способы для этого, но не всегда эффективные, особенно когда вы загружаете коллекцию моделей и их отношения.

Что ж, позвольте мне рассказать вам что мы можем с этим сделать.

hasMany relation

public function comments()
{
  return $this->hasMany('Comment');
}

Наиболее прямолинейным способом может быть использование одного из двух методов ниже (возможно, обернутый в метод типа getCommentsCount в Post модели):

[1] > $post = Post::first();
// object(Post)(
//   'incrementing' => true,
//   'timestamps' => true,
//   'exists' => true
// )
[2] > $post->comments->count();// 4
[3] > $post->comments()->count();// 4

Однако это все еще не лучшее решение.

  • $post->comments->count(); загружает коллекцию и возвращает количество ее элементов, не подходит если вам нужно получить только количество;
  • $post->comments()->count(); лучше, не загружает коллекцию, но отправляет запрос каждый раз, когда мы запускаем метод.

Когда вам нужно получить коллекцию постов и посчитать комментарии к ним, то вы конечно можете использовать join, groupBy и все такое, но кого это волнует? Почему мы должны писать что вроде этого?

$posts = Post::leftJoin('comments', 'comments.post_id', '=', 'posts.id')
  ->select('posts.*', 'count(*) as commentsCount')
  ->groupBy('posts.id')
  ->get();

Если мы предпочитаем элегантный синтаксис в стиле Eloquent.

$posts = Post::with('commentsCount')->get();

Именно это нам и нужно, давайте наконец сделаем это. Чтобы заставить Eloquent предзагрузить колличество наших комментариев, мы должны создать в модели метод, возвращяющий Relation обьект.

public function comments()
{
  return $this->hasMany('Comment');
}
 
public function commentsCount()
{
  return $this->comments()
    ->selectRaw('post_id, count(*) as aggregate')
    ->groupBy('post_id');
}

Это конечно работает, но я не был бы самим собой, если бы оставил все как есть.

$post = Post::with('commentsCount')->first();
 
$post->commentsCount; 
$post->commentsCount->first(); 
$post->commentsCount->first()->aggregate; 

Давайте немного улучшим на код.

  • Будем использовать hasOne вместо hasMany, для того что бы избежать возврата коллекции с одним элементом
  • Будем использовать метод доступа, для того что бы просто посчитать комментарии


public function commentsCount()
{
  return $this->hasOne('Comment')
    ->selectRaw('post_id, count(*) as aggregate')
    ->groupBy('post_id');
}
 
public function getCommentsCountAttribute()
{
 
  if ( ! array_key_exists('commentsCount', $this->relations)) 
    $this->load('commentsCount');
 
  $related = $this->getRelation('commentsCount');
 
  
  return ($related) ? (int) $related->aggregate : 0;
}

Теперь намного проще и красивее работать с этим.


$post = Post::first();
$post->commentsCount; // 4

$posts = Post::with('commentsCount')->get();
$posts->first()->commentsCount;
Поделиться с друзьями
-->

Комментарии (16)


  1. AmdY
    27.05.2016 18:03
    +2

    В новой версии добавят готовый метод. Убегаю, пруф позже сброшу.


  1. zenn
    27.05.2016 20:41
    +2

    Годно, но можно пойти дальше. Взять стандартную модель, которую мы наследуем — Illuminate\Database\Eloquent\Model и расширить (extend'нуть) ее ровно на аналогичный метод универсально для всех моделей. Но как говорит комментатор выше — это уже сделано в новой версии.


  1. OldEr
    27.05.2016 21:26
    +2

    Начиная с версии Laravel 5.2.32 метод withCount() доступен «из коробки».
    laravel.com/docs/5.2/eloquent-relationships#querying-relations


  1. Apelsinka223
    27.05.2016 21:26
    -2

    Можно было остановиться на $post->comments->count(); Лишних запросов не делает, не требует добавления n строк кода каждый раз как нужно что-то посчитать, выглядит понятно и красиво.


  1. gultaj
    27.05.2016 21:26
    +1

    Вместо
    array_key_exists('commentsCount', $this->relations)
    можно писать
    $this->relationLoaded('commentsCount')


  1. mulligan
    28.05.2016 17:37

    А что если хранить count отдельно и при добавлении поста его инкрементировать? У меня сейчас сделано именно так. Какие подводные камни?


    1. OldEr
      28.05.2016 18:41
      +1

      Как минимум избыточность. Также в зависимости от приложения необходимо будет инкрементировать/декрементировать счётчик при других действиях над постом: модерация, удаление и так далее.


      1. franzose
        02.06.2016 16:33

        Эта избыточность поможет избежать лишних запросов в БД, потому что *_count будет просто еще одной колонкой в таблице. А обновления счетчика можно сделать через события :)


        1. OldEr
          03.06.2016 14:22

          Безусловно, но это «палка о двух концах». У обоих подходов есть как плюсы, так и минусы. Выбирать нужно исходя из специфики разрабатываемого приложения.


  1. M-A-XG
    28.05.2016 21:33
    -5

    Вся эта работа с фреймворками напоминает барахтанье в дерьме :)
    О чудо, тривиальные вещи делаются через тройные выверты.

    Трезвый взгляд на фреймворки:
    http://blog.kpitv.net/article/frameworks-1/


    1. imgen
      29.05.2016 23:15
      -2

      классный блог кстати, мне нравится ваш минимализм


    1. Helldar
      30.05.2016 03:58
      +1

      Дочитал по линке до строки «Плохая документация.» и сразу захотел «дать в рыло» ответным линком.
      Где здесь «плохая документация»?


      1. M-A-XG
        31.05.2016 19:26
        -3

        К Ларе статье относиться меньше всего.
        С ней я и меньше всего работал. :)
        Там дальше расшифровывается, что имеется под «Плохой документацией».


        1. Helldar
          02.06.2016 03:22

          И, тем не менее, Лара есть фреймворк ;)
          Другими словами, ты пишешь «все фреймворки отстой» и при этом говоришь, что «фреймворк Laravel меньше всего относится к статье»…
          Где логика, чувак?


          1. M-A-XG
            02.06.2016 09:14
            -2

            Там написано не совсем «все фреймворки отстой».
            Мне не нравиться, что для многих вещей нужно делать тройные выверты.


            1. Helldar
              02.06.2016 17:11

              M-A-XG написал:
              Вся эта работа с фреймворками напоминает барахтанье в дерьме :)
              О чудо, тривиальные вещи делаются через тройные выверты.

              Трезвый взгляд на фреймворки:
              http://blog.kpitv.net/article/frameworks-1/


              Просто оставлю цитату здесь.