Насколько хорошо вы знаете Eloquent?

Eloquent ORM от Laravel  —  это мощный инструмент с выразительным и элегантным синтаксисом для взаимодействия разработчиков с базами данных. Многие знают методы вроде find(), where(), first(), get(), save(). Изучим малоизвестные из Eloquent ORM, которыми значительно совершенствуется рабочий процесс.

Начнем с обычных, затем перейдем к оптимальным. Это красивые методы, при помощи которых скорость и эффективность приложения повышаются по-настоящему.

1. tap()

Для чего: применить изменения к модели и вернуть саму модель для цепочки методов.

Когда: нужно изменить объект и сразу использовать его в другой операции.

User::find(1)->tap(function ($user) {
$user->name = 'Updated Name';
})->save();

2. firstOrFail()

Для чего: получить допустимый результат или обработать случай, где результат не найден.

Когда: нужно извлечь конкретную запись и, если ее нет, выбросить ошибку.

$user = User::where('email', '[email protected]')->firstOrFail();
// Обрабатываются сведения о пользователе

3. updateOrCreate()

Для чего: избавиться от дублированных записей, обновив имеющуюся или создав новую.

Когда: нужно убедиться, что запись создана, если ее нет, или обновлена, если она имеется.

User::updateOrCreate(
['email' => '[email protected]'],
['name' => 'John Doe']
);

4. increment()/decrement()

Этот метод прост и красив. Когда его использовать?
Когда: нужно увеличить или уменьшить числовой столбец на один или больше.

Для чего: эффективно обновить значение числового столбца.

User::where('id', 1)->increment('points'); // если «points» равен 7, станет 8
User::where('id', 1)->decrement('points', 5); // если «points» равен 7, станет 2

5. withTrashed()/onlyTrashed()/restore()

В Laravel этой группой методов управляется функционал Soft Deletes.

Для чего: управлять мягко удаленными записями. 
Когда: нужно включить, только включить или восстановить мягко удаленные записи.

$users = User::withTrashed()->get();
$trashedUsers = User::onlyTrashed()->get();
User::withTrashed()->where('id', 1)->restore();

6. withoutEvents()

Для чего: предотвратить запуск прослушивателей событий.
Когда: при выполнении действий, которыми не активируются события вроде пакетного импорта.

Допустим, вы импортируете из внешней системы большое количество пользователей и не хотите запускать событие UserCreated для каждого импортированного пользователя, чтобы не отправлять приветственные сообщения или не регистрировать каждое создание:

User::withoutEvents(function () {
User::create([
'name' => 'John Doe',
'email' => '[email protected]'
]);
User::create([
'name' => 'Jane Doe',
'email' => '[email protected]'
]);
});

7. withoutGlobalScopes()

Для чего: обойти глобальные ограничения запросов. 
Когда: для извлечения всех записей, игнорируя глобальные области, например is_published.

Рассмотрим сценарий, где в приложении имеется модель Post с глобальной областью, в которую включаются только опубликованные статьи. Для эффективного управления контентом администратору нужно видеть все статьи, в том числе неопубликованные и черновики.

Извлечение всех статей с игнорированием глобальных областей:

$allPosts = Post::withoutGlobalScopes()->get();
foreach ($allPosts as $post) {
echo $post->title . ($post->is_published ? ' (Published)' : ' (Draft)') . "\n";
}

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

withoutGlobalScopes() особенно полезен в административных задачах, где требуется полный доступ к данным, или при отладке и тестировании, когда необходимо убедиться, что глобальные ограничения не сказываются на запросах.

10. is()/isNot()

Очень прост и полезен для сравнений и условий.

Для чего: сравнить два экземпляра модели. 
Когда: при проверке, не одного ли экземпляра две модели.

$user1 = User::find(1);
$user2 = User::find(2);

if ($user1->is($user2)) {
// Тот же пользователь
}

if ($user1->isNot($user2)) {
// Не тот же пользователь
}

11. loadMissing()

Пример: допустим, имеется модель User с отношением posts. Нужно загрузить пользователя вместе с его статьями, но нет уверенности, загружено это отношение или еще нет.

Для чего: условно-безотложной загрузки отношений, которые еще не загружены, с оптимизированием запросов к базе данных и предотвращением проблемы запроса N+1.

Когда: нужно загрузить отношения в экземпляре модели, но только если они еще не загружены. Это особенно полезно, когда имеются условные отношения, которые нужно загружать динамически на основе условий, или при загрузке отношений в цикле, где они уже, возможно, загружены.

$user = User::find(1);

// Проверяется, не загружено ли уже отношение «posts»
if (!$user->relationLoaded('posts')) {
// Отношение «posts» загружается, только если оно еще не загружено
$user->loadMissing('posts');
}

// Теперь доступ к отношению «posts» получается без опасений дублированных запросов
foreach ($user->posts as $post) {
echo $post->title . "\n";
}

12. makeHidden()/makeVisible()

Для чего: контролировать видимость атрибутов модели. 
Когда: при временном скрытии или отображении атрибутов, например, в ответах API.

$user = User::find(1);
$user->makeHidden('email');
$user->makeVisible('email');

13. touch()

Для чего: обновить временну́ю метку updated_at
Когда: нужно пометить запись как обновленную, не меняя других атрибутов.

$user = User::find(1);
$user->touch();

14. append()

Для чего: добавления пользовательских атрибутов в массив модели или форму JSON. 
Когда: нужно включить в представление модели дополнительные, вычисляемые атрибуты.

$user = User::find(1);
$user->append('custom_attribute');

15. replicate()

Для чего: дублирования экземпляра модели. 
Когда: при создании нового экземпляра с теми же атрибутами, например, для клонирования шаблона.

$user = User::find(1);
$newUser = $user->replicate(); // «$newUser» сопоставляется с «$user»
$newUser->save();

16. chunkById()

Допустим, имеется таблица с 20 000 000 записей, и с каждой записью нужно выполнить действие. Как это сделать без…

Для чего: эффективной обработки больших наборов данных большими кусками. 
Когда: для обработки больших наборов данных, эффективной работы с памятью при оптимальной производительности в больших таблицах.

Допустим, имеется таблица базы данных с 20 000 000 записей, и с каждой записью нужно выполнить конкретное действие.

use App\Models\YourModel;

YourModel::orderBy('id')->chunkById(1000, function ($records) {
foreach ($records as $record) {
// Обрабатывается каждая запись
}
});

Примечание: имеется аналогичный метод chunk(). Им выполняются похожие задачи, но имеются и различия.

При помощи chunk() и chunkById() выполняется эффективная пакетная обработка больших наборов данных, предотвращается исчерпание памяти, оптимизируется производительность. И с тем, и с другим проходятся по большому набору данных, не загружая в память всего набора сразу. Но они различаются в том, как ими определяются пакеты данных.

chunk():

  • Методом chunk() набор данных разделяется на крупные куски в зависимости от количества записей в каждом куске, которое указывается в первом параметре.
  • Записи извлекаются им из таблицы базы данных последовательно, без учета конкретного порядка.
  • Записи в каждом куске извлекаются по порядку, в котором извлекались из базы данных, но не обязательно по первичному ключу.
  • Этот метод полезен, когда порядок обработки не важен или просто нужно обрабатывать данные небольшими, более управляемыми кусками.

chunkById():

  • Методом chunkById() набор данных разделяется на крупные куски по первичному ключу записей  —  обычно id.
  • Записи извлекаются им из таблицы базы данных последовательно по их первичным ключам.
  • В каждом блоке содержатся записи с первичными ключами в указанном диапазоне, записи обрабатываются по первичным ключам.
  • Этот метод особенно полезен, когда важен порядок обработки, например при выполнении миграций данных или обновлений, которым требуется последовательная обработка по первичным ключам.

17. existsOr()

Для чего: для выполнения обратного вызова, если модель имеется, или возвращения значения по умолчанию.
Когда: нужно провести проверки наличия записи с пользовательской логикой.

$exists = User::where('email', '[email protected]')->existsOr(function () {
return 'User does not exist';
});

18. firstOrCreate()

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

$user = User::firstOrCreate(['email' => '[email protected]'], ['name' => 'John Doe']);

19. firstOrNew()

Для чего: чтобы извлечь запись или создать экземпляр новой записи без сохранения. 
Когда: нужно получить имеющуюся запись или создать новый экземпляр, не сохраняя его.

$user = User::firstOrNew(['email' => '[email protected]'], ['name' => 'John Doe']);

20. sole()

Для чего: для извлечения только одной записи или выбрасывания исключения.
Когда: ожидается единственный, уникальный результат и нужно обрабатывать дубли как ошибки.

https://www.youtube.com/watch?v=FWl1P2nd5mw

$user = User::where('email', '[email protected]')->sole();

21. findMany()

Для чего: для извлечения записей по их первичным ключам. 
Когда: нужно по массиву идентификаторов извлечь несколько записей одним запросом.

$users = User::findMany([1, 2, 3]);

22. update()

Для чего: чтобы обновить сразу несколько записей. 
Когда: нужно эффективно выполнить массовое обновление.

User::where('status', 'active')->update(['status' => 'inactive']);

23. forceDelete()

Для чего: чтобы безвозвратно удалить мягко удаленную модель. 
Когда: нужно удалить запись полностью, минуя мягкие удаления.

$user = User::withTrashed()->find(1);
$user->forceDelete();

24. getDirty()

Этим методом узнают все, что изменилось в модели, прежде чем изменения сохранятся в базе данных.

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

$user = User::find(1);
$user->name = 'New Name';
$dirty = $user->getDirty();

25. getOriginal()

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

$user = User::find(1);
$original = $user->getOriginal('name');

26. setRelation()

Для чего: чтобы задать в модели конкретное отношение. 
Когда: нужно вручную определить отношения для экземпляра модели.

$user = User::find(1);
$user->setRelation('posts', $posts);

27. without()

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

$user = User::with('posts', 'comments')->without('comments')->find(1);

28. preventLazyLoading()

Для чего: чтобы предотвратить отложенную загрузку отношений. 
Когда: нужно перехватить непредусмотренную отложенную загрузку в разработке.

Model::preventLazyLoading(!app()->isProduction());

29. withoutTimestamps()

Для чего: чтобы отключить обновление временны́х меток created_at и updated_at
Когда: нужны действия, которыми не активируются обновления временны́х меток, таких как импорт.

User::withoutTimestamps(function () {
User::create(['name' => 'John Doe']);
});

30. withCasts()

В Laravel правила приведения применяются к атрибутам модели динамически. Это полезно, когда нужно на лету поменять способ приведения типов атрибутов, исходя из сценариев времени выполнения или определенных условий. Например, привести атрибут к другому типу на основе пользовательского ввода или значений базы данных, обеспечивая в приложении согласованность данных и гибкость.

Для чего: чтобы применить правила приведения динамически. 
Когда: нужно на лету поменять способ приведения типов атрибутов.

$user = User::withCasts(['is_admin' => 'boolean'])->find(1);

31. upsert()

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

Допустим, имеется таблица users со столбцом email в качестве уникального идентификатора. Нужно вставить нового пользователя, если его почты еще нет в таблице, или обновить его имя, если почта уже имеется.

use App\Models\User;

User::upsert([
['email' => '[email protected]', 'name' => 'John Doe'],
['email' => '[email protected]', 'name' => 'Jane Doe']
], ['email'], ['name']);

32. scope

Для чего: чтобы определить области переиспользуемых запросов. 
Когда: нужно применить в запросах общие их ограничения.

// В модели «User»
public function scopeActive($query)
{
return $query->where('status', 'active');
}

// Использование
$activeUsers = User::active()->get();

33. macro()

Этим методом создаются пользовательские методы под какие угодно задачи.

Для чего: чтобы определить пользовательские методы в конструкторе запросов Eloquent.
Когда: нужно расширить конструктор запросов собственными методами.

Допустим, чтобы упростить фильтрование пользователей по их роли в приложении, в конструкторе запросов определяется пользовательский макрос role():

use Illuminate\Database\Eloquent\Builder;

// Определяется макрос «role»
Builder::macro('role', function ($role) {
return $this->where('role', $role);
});

// Использование
$admins = User::role('admin')->get();
$customers = User::role('customer')->get();

34. filter()

Для чего: чтобы применить динамические фильтры запросов. 
Когда: нужно применить фильтры по параметрам запроса.

// В модели «User»
public function scopeFilter($query, $filters)
{
return $filters->apply($query);
}

// Использование
$filters = new UserFilters(['status' => 'active']);
$filteredUsers = User::filter($filters)->get();

В этом примере в модели User определяется область filter(), которой принимается набор фильтров. Эти фильтры применяются к запросу методом apply() объекта UserFilters. Так пользователи динамически фильтруются по различным критериям, указанным в переменной $filters.

Методом filter() запросы к базе данных делаются более адаптивными к меняющимся требованиям и пользовательскому вводу, в итоге данные в приложениях Laravel извлекаются гибче и динамичнее.

35. whereJsonContains()

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

$users = User::whereJsonContains('options->languages', 'en')->get();

36. findOr()

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

$user = User::findOr(1, function () {
return 'User not found';
});

37. lockForUpdate()

Методом lockForUpdate() в Eloquent ORM на Laravel блокируются строки базы данных для обновления внутри транзакции. Когда этот метод применяется к запросу, изменение выбранных строк другими транзакциями базы данных приостанавливается до завершения текущей транзакции. Так обеспечивается согласованность данных, предотвращаются конфликты при одновременных попытках в транзакциях обновить одни и те же строки.

Для чего: чтобы применить к запросу блокировку «для обновления». 
Когда: нужно при проведении транзакции предотвратить изменение строк другими транзакциями.

$user = User::where('email', '[email protected]')->lockForUpdate()->first();

38. sharedLock()

Для чего: чтобы применить к запросу общую блокировку. 
Когда: нужно на время транзакции блокировать выбранные строки.

Возьмем финансовое приложение, где пользователи просматривают баланс счета. Нужно обеспечить, чтобы при проверке пользователем баланса отображаемая сумма оставалась неизменной, даже если баланс счета одновременно обновляется другими транзакциями. С помощью sharedLock() заблокируем на время транзакции строки, соответствующие счету пользователя :

use App\Models\Account;

DB::transaction(function () use ($userId) {
$account = Account::where('user_id', $userId)->sharedLock()->first();
// Отображается баланс счета пользователя
});

39. withSum()

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

$users = User::withSum('posts', 'views')->get(); // всего статей

Допустим, имеется модель User, где у каждого пользователя по нескольку заказов orders. Чтобы получить список пользователей с общей суммой их заказов, воспользуемся withSum():

use App\Models\User;

$usersWithTotalOrderAmount = User::withSum('orders', 'amount')->get();

foreach ($usersWithTotalOrderAmount as $user) {
echo "User: {$user->name}, Total Order Amount: {$user->orders_sum_amount}\n";
}

В этом примере с помощью withSum('orders', 'amount') из отношения orders для каждого пользователя извлекается общая сумма столбца amount. Агрегированная сумма доступна как динамически генерируемый атрибут orders_sum_amount каждого пользовательского объекта.

С помощью withSum() из связанных моделей вместе с основным результатом запроса эффективно извлекаются агрегированные данные: код упрощается, повышается производительность.

40. withCount()

Методом withCount() в Eloquent ORM на Laravel извлекаются связанные модели и их количество. Таким образом, без дополнительных запросов или ручного подсчета, получают число связанных записей.

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

use App\Models\User;

$usersWithPostCounts = User::withCount('posts')->get();

foreach ($usersWithPostCounts as $user) {
echo "User: {$user->name}, Post Count: {$user->posts_count}\n";
}

В этом примере с помощью withCount('posts') извлекается число статей, связанных с каждым пользователем. Количество статей доступно как динамически генерируемый атрибут posts_count каждого пользовательского объекта.

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

Заключение

Мы изучили почти 40 малоизвестных методов Eloquent ORM.

В награду за то, что дочитали до конца, вот вам еще 10 методов, которыми совершенствуется рабочий процесс и повышается продуктивность.

  1. oldest(): результаты запроса упорядочиваются в указываемом столбце по возрастанию.
  2. latest(): результаты запроса упорядочиваются в указываемом столбце по убыванию.
  3. has(): в фильтруемый запрос включаются только записи с отношением.
  4. whereHas(): в фильтруемый запрос включаются только записи с отношением, которые соответствуют определенным условиям.
  5. doesntHave(): в фильтруемый запрос включаются только записи, у которых нет отношения.
  6. whereDoesntHave(): в фильтруемый запрос включаются только записи, у которых нет отношения и которые соответствуют определенным условиям.
  7. withPivot(): указываются дополнительные столбцы сводной таблицы, которые извлекаются при запрашивании отношения «многие ко многим» с промежуточными столбцами таблицы.
  8. morphTo(): определяются полиморфные отношения, при которых связанная модель принадлежит нескольким исходным моделям.
  9. morphMany(): определяются полиморфные отношения «один ко многим», где связанная модель принадлежит нескольким исходным моделям.
  10. morphToMany(): определяются полиморфные отношения «многие ко многим», где связанная модель принадлежит нескольким исходным моделям.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Chimeremze Prevail Ejimadu: Deep Dive into Eloquent: 40 Rarely Used Eloquent ORM Methods Every Laravel Developer MUST Know

Предыдущая статьяСтратегии рендеринга, которые должен знать каждый React-разработчик