Laravel Eloquent — одна из самых мощных и удивительных функций в современном фреймворке на сегодняшний день. От приведения данных к объектам и классам значений, защита базы данных с помощью заполняемых полей, транзакций, областей, глобальных областей и отношений. Eloquent позволяет добиться успеха во всем, что вам нужно сделать с базой данных.
Начало работы с Eloquent иногда может показаться пугающим, поскольку он может сделать так много, что вы никогда не знаете, с чего начать. В этом руководстве я сосредоточусь на том, что считаю одним из важнейших аспектов любого приложения, — на записи в базу данных.
Вы можете писать в базу данных в любой области приложения: контроллер, задание, промежуточное ПО, ремесленная команда. Как лучше обрабатывать записи в базу данных?
Начнем с простой модели Eloquent без отношений.
final class Post extends Model
{
protected $fillable = [
'title',
'slug',
'content',
'published',
];
protected $casts = [
'published' => 'boolean',
];
}
У нас есть модель Post
который представляет запись в блоге; у него есть заголовок, слаг, содержание и логический флаг, указывающий, опубликовано ли оно. В этом примере давайте представим, что опубликованное свойство по умолчанию имеет значение true
в базе данных. Теперь, для начала, мы сказали Eloquent, что хотим иметь возможность заполнять свойства или столбцы title
, slug
, content
и published
. Поэтому, если мы передаем что-либо, не зарегистрированное в массиве fillable
, будет выдано исключение, защищающее наше приложение от потенциальных проблем.
Теперь, когда мы знаем, какие поля можно заполнять, мы можем посмотреть на запись данных в базу данных, будь то создание, обновление или удаление. Если ваша модель наследует трейт SoftDeletes
, то удаление записи является действием записи, но для этого примера я буду упрощать; удаление есть удаление.
Вероятнее всего, вы видели, особенно в документации, что-то вроде следующего:
Post::create($request->only('title', 'slug', 'content'));
Это то, что я могу сделать стандартным Eloquent, у вас есть модель, и вы вызываете статический метод для создания нового экземпляра, передавая определенный массив из запроса. У этого подхода есть преимущества; это чисто и просто, и все это понимают. Иногда я могу быть очень самоуверенным разработчиком. Тем не менее, я все равно буду использовать этот подход, особенно если я нахожусь в режиме прототипирования, где речь идет скорее о проверке идеи, чем о создании чего-то долгосрочного.
Мы можем сделать один шаг. далее, запустив новый экземпляр построителя запросов Eloquent в модели, прежде чем запрашивать создание нового экземпляра. Это будет выглядеть следующим образом:
Post::query()->create($request->only('title', 'slug', 'content'));
Как видите, это все еще очень просто и становится все более стандартизированным способом запуска запросов в Laravel. Одним из наиболее значительных преимуществ этого подхода является то, что все после query
соответствует контракту Query Builder, который был недавно введен. Из-за того, как Laravel работает под капотом, ваша IDE не очень хорошо понимает статические вызовы, так как это статический прокси для метода, использующего __callStatic
вместо фактического статического метода. К счастью, это не относится к методу query
, который является статическим методом модели Eloquent, которую вы расширяете.
Существует «старый» метод построения модели для сохранения в базу данных. Тем не менее, я редко вижу, чтобы он использовался очень часто. Однако я упомяну об этом для ясности:
$post = new Post();
$post->title = $request->get('title');
$post->slug = $request->get('slug');
$post->content = $request->get('content');
$post->save();
Здесь мы будем строить модель программно, присваивая значения свойствам, а затем сохраняя их в базе данных. Это было немного многословно, и всегда казалось, что для достижения этого требуется слишком много усилий. Тем не менее, это по-прежнему приемлемый способ создания новой модели, если вы предпочитаете именно его.
На данный момент мы рассмотрели три разных подхода к созданию новых данных в база данных. Мы можем использовать аналогичный подход к обновлению данных в базе данных, статический вызов update
или использование контракта построения запроса query()->where('column', 'value')->update()
или, наконец, программно установить свойство, а затем сохранить
. Я не буду повторяться здесь, так как это почти то же самое, что и выше.
Что нам делать, если мы не уверены, что запись уже существует? Например, мы хотим создать или обновить существующий пост. У нас будет столбец, который мы хотим проверить с точки зрения уникальности, затем мы проходим через массив значений, которые хотим создать или обновить в зависимости от того, существует ли он.
Post::query()->updateOrCreate(
attributes: ['slug' => $request->get('slug'),
values: [
'title' => $request->get('title'),
'content' => $request->get('content'),
],
);
Это имеет некоторые огромные преимущества, если вы не уверены, будет ли запись существовать, и я недавно применил это сам, когда хотел «убедиться», что запись есть в базе данных, несмотря ни на что. Например, для входа через социальную сеть OAuth 2.0 вы можете принять информацию от провайдера и обновить или создать новую запись перед аутентификацией пользователя.
Можем ли мы сделать еще один шаг? Каковы будут преимущества? Вы можете использовать шаблон, подобный шаблону репозитория, чтобы в основном «проксировать» вызовы, которые вы отправляете в красноречивый через другой класс. В этом есть несколько преимуществ, или, по крайней мере, они были до того, как Eloquent стал тем, чем он является сегодня. Давайте рассмотрим пример:
class PostRepository
{
private Model $model;
public function __construct()
{
$this->model = Post::query();
}
public function create(array $attributes): Model
{
return $this->model->create(
attributes: $attributes,
);
}
}
Если бы мы использовали Фасад БД или простой PDO, то, возможно, Шаблон Репозитория дал бы нам довольно много преимуществ в сохранении согласованности. Идем дальше.
В какой-то момент люди решили, что переход от класса Repository к классу Service будет хорошей идеей. Однако это одно и то же... Давайте не будем вдаваться в подробности.
Итак, нам нужен способ взаимодействия с Eloquent, который не был бы таким "встроенным" или процедурным. Несколько лет назад я применил подход, который теперь называется «действия». Он похож на шаблон репозитория. Однако каждое взаимодействие с Eloquent представляет собой отдельный класс, а не метод внутри одного класса.
Давайте рассмотрим этот пример, где у нас есть специальный класс для каждого взаимодействия, называемого «действием»:
final class CreateNewPostAction implements CreateNewPostContract
{
public function handle(array $attributes): Model|Post
{
return Post::query()
->create(
attributes: $attributes,
);
}
}
Наш класс реализует контракт, чтобы красиво связать его с контейнером, что позволяет нам внедрить его в конструктор и при необходимости вызвать метод дескриптора с нашими данными. Это становится все более популярным, и многие люди (а также пакеты) начали применять этот подход, поскольку вы создаете служебные классы, которые хорошо справляются с одной задачей — и для них могут быть легко созданы тестовые двойники. Другое преимущество заключается в том, что мы используем интерфейс; если мы когда-нибудь решим отказаться от Eloquent (не уверен, почему вы этого захотите), мы можем быстро изменить наш код, чтобы отразить это, не выискивая ничего.
Опять же, подход довольно хорош - и в принципе не имеет реальных недостатков. Я упомянул, что я довольно придирчивый разработчик, верно? Что ж...
Моя самая большая проблема с "действиями" после их столь долгого использования заключается в том, что мы помещаем все наши интеграции записи, обновления и удаления под один капот. Действия недостаточно разделяют вещи для меня. Если подумать, у нас есть две разные вещи, которых мы хотим достичь: мы хотим писать и хотим читать. Это частично отражает другой шаблон проектирования под названием CQRS (Command Query Responsibility Segregation), который я немного позаимствовал. В CQRS, как правило, вы должны использовать шину команд и шину запросов для чтения и записи данных, обычно генерируя события для сохранения с использованием источников событий. Однако иногда это намного больше работы, чем вам нужно. Не поймите меня неправильно, для такого подхода определенно есть время и место, но вы должны использовать его только тогда, когда вам это нужно, иначе вы перепроектируете свое решение с самой маленькой части.
Поэтому я разделил свои действия по записи на «Команды», а действия по чтению — на «Запросы», чтобы мои взаимодействия были разделены и сфокусированы. Давайте посмотрим на команду:
final class CreateNewPost implements CreateNewPostContract
{
public function handle(array $attributes): Model|Post
{
return Post::query()
->create(
attributes: $attributes,
);
}
}
Посмотрите на это, кроме названия класса, это то же самое, что и действие. Это по дизайну. Действия — отличный способ записи в базу данных. Мне кажется, они слишком быстро переполняются.
Что еще можно улучшить? Для начала было бы неплохо представить объект передачи домена, поскольку он обеспечивает безопасность типов, контекст и согласованность.
final class CreateNewPost implements CreateNewPostContract
{
public function handle(CreatePostRequest $post): Model|Post
{
return Post::query()
->create(
attributes: $post->toArray(),
);
}
}
Так что теперь мы вводим безопасность типов в массиве, где раньше мы полагались на массивы и надеялись, что все пошло правильно. Да, мы можем проверять сколько угодно, но объекты имеют лучшую согласованность.
Можно ли это как-то улучшить? Всегда есть место для улучшения, но нужно ли нам это? Этот текущий подход надежен, типобезопасен и легко запоминается. Но что нам делать, если таблица базы данных заблокируется до того, как мы сможем выполнить запись, или если у нас возникнет сбой в сетевом подключении, возможно, Cloudflare отключится в самое неподходящее время.
Транзакции базы данных будут спаси наши задницы здесь. Они используются не так часто, как следовало бы, но они представляют собой мощный инструмент, который вам следует рассмотреть в ближайшее время.
final class CreateNewPost implements CreateNewPostContract
{
public function handle(CreatePostRequest $post): Model|Post
{
return DB::transaction(
fn() => Post::query()->create(
attributes: $post->toArray(),
)
);
}
}
В конце концов мы добились своего! Я бы прыгал от радости, если бы увидел такой код в PR или обзоре кода, который должен был сделать. Однако не думайте, что вам нужно писать код таким образом. Помните, что вполне нормально просто встроить static create
, если он работает за вас! Важно делать то, что вам удобно, то, что сделает вас эффективным, а не то, что другие говорят, что вы должны делать в сообществе.
Используя подход, который мы только что рассмотрели, мы могли бы таким же образом подойти к чтению из базы данных. Разбейте проблему, определите шаги и места, где можно сделать улучшения, но всегда сомневайтесь, не зашли ли вы слишком далеко. Если это кажется естественным, вероятно, это хороший знак.
Как вы подходите к записи в базу данных? Как далеко вы бы зашли в этом путешествии, а когда слишком далеко? Дайте нам знать ваши мысли в Твиттере!