Одна из самых распространенных проблем с производительностью, которую я видел в Laravel - это использование методов Eloquent и отношений из шаблонов Blade, создание ненужных дополнительных циклов и запросов. В этой статье я покажу различные сценарии и способы их эффективного использования. ### Сценарий 1. Загрузка отношения belongsTo(): не забудьте про «жадную загрузку» Типичный случай — вы перебираете записи через @foreach, и, в каком-то столбце, вам нужно показать родительскую запись с определенным полем. ```PHP @foreach ($sessions as $session) {{ $session->created_at }} {{ $session->user->name }} @endforeach ``` И, конечно, Session принадлежит User, в app/Session.php : ```PHP public function user() { return $this->belongsTo(User::class); } ``` Код выглядит правильным и безобидным, но в зависимости от того, что будет в вашем контроллере, могут возникнуть огромные проблемы с производительностью. Неправильный Контроллер: ```PHP public function index() { $sessions = Session::all(); return view('sessions.index', compact('sessions'); } ``` Правильный: ```PHP public function index() { $sessions = Session::with('user')->get(); return view('sessions.index', compact('sessions'); } ``` Видите разницу? Мы загружаем отношения с основным запросом Eloquent. Это и называется «жадная загрузка». Если мы этого не сделаем, то в нашем шаблоне Blade в цикле foreach будет делаться SQL запрос для каждого сессии, запрашивая пользователя непосредственно из базы данных. И, если у вас есть таблица со 100 сессиями, то получится 101 запрос — 1 для списка сессий и 100 для получения пользователей. Так что, не забывайте про «Жадную Загрузку»! ### Сценарий 2. Загрузка отношений hasMany() Другая типичная ситуация — вам нужно вывести все дочерние записи в родительском цикле ```PHP @foreach ($posts as $post) {{ $post->title }} @foreach ($post->tags as $tag) {{ $tag->name }} @endforeach @endforeach ``` Догадайтесь, что будем применять здесь? Правильно, «жадную загрузку»! Без неё, для каждого Post, будет отдельный запрос к базе данных. Поэтому в контроллере делаем так: ```PHP public function index() { $posts = Post::with('tags')->get(); // а не просто Post::all()! return view('posts.index', compact('posts')); } ``` ### Сценарий 3. НЕ использовать скобки в отношениях hasMany() Представим, что у вас есть голосование и нужно показать его пункты с количеством голосов. Разумеется вы делаете «жадную загрузку» в контроллере: ```PHP public function index() { $polls = Poll::with('votes')->get(); return view('polls', compact('polls')); } ``` А затем, в файле Blade, выводите: ```PHP @foreach ($polls as $poll) {{ $poll->question }} ({{ $poll->votes()->count() }})
@endforeach ``` Выглядит вроде неплохо, да? Но, обратите внимание на скобки в ->votes(). Если вы оставите это так, то для каждого пункта голосования по прежнему будет делаться отдельный запрос к базе. Потому что это не получение уже загруженных данных отношения, а вызов метода Eloquent. Лучше сделайте так: {{ $poll->votes->count() }}. Без скобок. И, кстати, это же относится и к отношениям belongsTo. Не используйте скобки при загрузке отношений в Blade. Оффтоп : просматривая StackOverflow, я видел примеры и похуже. Например: ```PHP {{ $poll->votes()->get()->count() }} //или @foreach ($poll->votes()->get() as $vote)... ``` Протестируйте это через Laravel Debugbar и посмотрите количество SQL запросов. ### Сценарий 4. А если отношения пустые? Одна из самых распространенных ошибок в Laravel - «trying to get property of non-object», получали такую в своих проектах? (да ладно, не обманывайте) Обычно это происходит примерно так: ```HTML {{ $payment->user->name }} ``` Нет никакой гарантии, что User этого Payment все еще существует. Быть может он был подвергнут «мягкому удалению». Или в базе данных отсутствует внешний ключ, что позволило кому-то удалить пользователя безвозвратно. Решение этой проблемы зависит от вашей версии Laravel. До Laravel 5.7 показ дефолтного значения был таким: ```HTML {{ $payment->user->name or 'Anonymous' }} ``` Начиная с Laravel 5.7 синтаксис пришёл в соответствие с оператором PHP 7: ```HTML {{ $payment->user->name ?? 'Anonymous' }} ``` А вы знаете, что можно назначить дефолтное значение на уровне Eloquent? ```PHP public function user() { return $this->belongsTo(User::class)->withDefault(); } ``` Метод withDefault() вернет пустую модель класса User, если отношение не существует. Более того, вы можете заполнить эту дефолтную модель нужными значениями! ```PHP public function user() { return $this->belongsTo(User::class) ->withDefault(['name' => 'Anonymous']); } ``` ### Сценарий 5. Как избежать запросов с дополнительными отношениями в шаблонах Blade Знаком такой код в Blade? ```PHP @foreach ($posts as $post) @foreach ($post->comments->where('approved', 1) as $comment) {{ $comment->comment_text }} @endforeach @endforeach ``` Итак, вы фильтруете комментарии (конечно «жадная загрузка», так ведь? так?) через условие where(‘approved’, 1). И это работает и даже не вызывает проблем с производительностью, но мои личные принципы (а также принципы MVC) говорят о том, что логика должна быть вне шаблонов, а где-то в «логическом» слое. Это может быть сама модель Eloquent, где вы можете указать отдельное отношение для утвержденных комментариев в app/Post.php. ```PHP public function comments() { return $this->hasMany(Comment::class); } public function approved_comments() { return $this->hasMany(Comment::class)->where('approved', 1); } ``` И уже после этого вы загружаете эти конкретные отношения в Контроллере/Blade: ```PHP $posts = Post::with(‘approved_comments’)->get(); ``` ### Сценарий 6. Как избежать сложных условий с помощью метода Читателя (Accessor) Недавно в одном проекте у меня была задача: перечислить вакансии, со значком конверта для сообщений и с указание оплаты, которую следует взять из ПОСЛЕДНЕГО сообщения, содержащего эту сумму. Звучит сложно, и это так есть. Вообще жизнь штука сложная! Для начала я сделал так: ```PHP @foreach ($jobs as $job) ... @if ($job->messages->where('price is not null')->count()) {{ $job->messages->where('price is not null')->sortByDesc('id')->first()->price }} @endif @endforeach ``` Ужас! Нужно проверить, существует ли оплата, затем взять последнее сообщение с этой оплатой, но ... Чёрт, этому не место в шаблонах. В итоге я использовал метод Читателя (Accessor) в Eloquent и сделал это в app/Job.php: ```PHP public function getPriceAttribute() { $price = $this->messages ->where('price is not null') ->sortByDesc('id') ->first(); if (!$price) return 0; return $price->price; } ``` Конечно, в таких сложных ситуациях легче сразу перейти к проблеме запроса N+1 или просто делать запросы несколько раз. Поэтому не забывайте использовать Laravel Debugbar для поиска проблем. Также могу порекомендовать пакет [Laravel N+1 Query Detector](https://github.com/beyondcode/laravel-query-detector). ### Бонус Напоследок покажу вам вероятно самый худший образец кода, который я нашёл в Laracasts, когда исследовал эту тему. Кто-то попросил советов по нему. К сожалению, такой код часто встречается в живых проектах. Ну ведь работает... (не пытайтесь повторить дома) ```HTML @foreach($user->payments()->get() as $payment) {{$payment->type}} {{$payment->amount}}$ {{$payment->created_at}} @if($payment->method()->first()->type == 'PayPal')
Paypal: {{ $payment->method()->first()->paypal_email }}
@else
Card: {{ $payment->payment_method()->first()->card_brand }} **** **** **** {{ $payment->payment_method()->first()->card_last_four }}
@endif @foreach ```