• Время чтения ~8 мин
  • 03.10.2022

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, если он работает за вас! Важно делать то, что вам удобно, то, что сделает вас эффективным, а не то, что другие говорят, что вы должны делать в сообществе.

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

Как вы подходите к записи в базу данных? Как далеко вы бы зашли в этом путешествии, а когда слишком далеко? Дайте нам знать ваши мысли в Твиттере!

Comments

No comments yet
Yurij Finiv

Yurij Finiv

Full stack

Про мене

Professional Fullstack Developer with extensive experience in website and desktop application development. Proficient in a wide range of tools and technologies, including Bootstrap, Tailwind, HTML5, CSS3, PUG, JavaScript, Alpine.js, jQuery, PHP, MODX, and Node.js. Skilled in website development using Symfony, MODX, and Laravel. Experience: Contributed to the development and translation of MODX3 i...

Об авторе CrazyBoy49z
WORK EXPERIENCE
Контакты
Ukraine, Lutsk
+380979856297