CREATE SCHEMA IF NOT EXISTS "test" AUTHORIZATION "blog_user";
Технически нам это пока не нужно, однако это понадобится позже, в Главе 14 «Тестирование». Далее давайте создадим db/001_users.sql со следующим содержимым:
CREATE TABLE IF NOT EXISTS "articles"
(
"id" BIGINT GENERATED ALWAYS AS IDENTITY NOT NULL
PRIMARY KEY,
"title" TEXT NOT NULL,
"body" TEXT NOT NULL,
"created_at" TIMESTAMP NOT NULL,
"updated_at" TIMESTAMP NOT NULL,
"deleted_at" TIMESTAMP NULL
);
Мы просто храним некоторые стандартные значения вместе с временными метками и целочисленным первичным ключом с автоинкрементом.
Поскольку наш сервер Postgres работает внутри контейнера Docker, нам нужно использовать команду docker для запуска файлов миграции из контейнера:
docker exec -it pg psql blog_user -d postgres -f /migrations/ 000_setup.sql
docker exec -it pg psql blog_user -d postgres -f /migrations /001_articles.sql
Сохраняющиеся статьи
Продолжая с того места, на котором мы остановились в предыдущем разделе, мы работали над сохранением наших статей в базе данных.
Первое, что нам нужно сделать, это включить модуль DB::Serializable в нашу сущность Article. Как упоминалось ранее, этот модуль позволяет нам создать его экземпляр из DB::ResultSet, который представляет собой результат запроса, сделанного к базе данных.
Поскольку у нас есть несколько вещей, которые должны произойти, прежде чем статья будет фактически сохранена, давайте продолжим и создадим несколько абстракций для решения этой проблемы. Конечно, если бы мы использовали ORM, у нас были бы встроенные способы сделать это, но будет полезно увидеть, как это можно сделать довольно легко, а также это станет хорошим переходом к другой функции Athena — DI.
Учитывая, что все, что нам нужно, это запустить некоторую логику перед сохранением чего-либо, мы можем просто создать метод с именем #before_save, который мы можем вызывать. Как вы уже догадались — перед тем, как мы сохраним объект в базу данных. В конечном итоге это будет выглядеть так:
protected def before_save : Nil
if @id.nil?
@created_at = Time.utc
end
@updated_at = Time.utc
end
Я сделал метод защищенным, поскольку он более внутренний и не является частью общедоступного API. В случае новой записи, когда идентификатора еще нет, мы устанавливаем созданную временную метку. Свойство update_at обновляется при каждом сохранении, учитывая, что именно для этого и предназначена эта временная метка.
В некоторых Crystal ORM, а также в Ruby ActiveRecord обычно имеется метод #save непосредственно на объекте, который обрабатывает его сохранение в базе данных. Лично я не являюсь поклонником этого подхода, поскольку считаю, что он нарушает принцип единой ответственности SOLID, поскольку он обрабатывает как моделирование того, что представляет собой статья, так и сохранение ее в базе данных. Вместо этого подхода мы собираемся создать другой тип, который будет обеспечивать сохранение экземпляров DB::Serializable.
Этот тип будет простым, но определенно может быть намного более сложным, поскольку чем больше абстракций вы добавляете, тем больше вы, по сути, создаете свой собственный ORM. Эти дополнительные абстракции не потребуются для нашего блога об одной сущности/таблице, но могут быть очень полезны для более крупных приложений. Однако в этот момент, возможно, стоит рассмотреть возможность использования ORM. В конце концов, все зависит от вашего конкретного контекста, поэтому делайте то, что имеет наибольший смысл.
Суть этого нового типа будет заключаться в предоставлении метода #persist, который принимает экземпляр DB::Serializable. Затем он вызовет метод #before_save, если он определен, и, наконец, вызовет метод #save, где будет внутренняя перегрузка для нашей сущности статьи. Таким образом, все будут счастливы, и мы придерживаемся наших SOLID принципов. Давайте создадим этот тип как src/services/entity_manager.cr. Обязательно добавьте require “./services/*" в src/blog.cr. Реализация этого будет выглядеть так: