Шрифт:
Интервал:
Закладка:
Теперь, когда ваша база данных работает, нам нужно настроить параметры базы данных, а также создать схему для нашей таблицы статей. Существует несколько сегментов для управления миграциями, однако я собираюсь просто сохранить и запустить SQL вручную. Если в вашем проекте будет больше нескольких таблиц, использование инструмента миграции может быть очень полезным, особенно для проектов, которые вы планируете сохранить в течение некоторого времени. Давайте создадим новую папку db/ для хранения наших файлов миграции, создав db/000_setup.sql со следующим содержимым:
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. Реализация этого будет выглядеть так:
@[ADI::Register]
class Blog::Services::EntityManager
@@connection : DB::Database = DB.open ENV["DATABASE_URL"]
def persist(entity : DB::Serializable) : Nil
entity.before_save if entity.responds_to? :before_save
entity.after_save self.save entity
end
private def save(entity : Blog::Entities::Article) : Int64
@@database.scalar(
%(INSERT INTO "articles" ("title", "body", "created_at",
"updated_at", "deleted_at") VALUES ($1, $2, $3, $4, $5)
RETURNING "id";),
entity.title,
entity.body,
entity.created_at,
entity.updated_at,
entity.deleted_at,
).as Int64
end
end
Чтобы упростить запуск нашего кода на разных машинах, мы собираемся использовать переменную среды для URL-адреса соединения. Назовем это DATABASE_URL. Мы можем экспортировать это с помощью следующего:
export DATABASE_URL=postgres://blog_user:mYAw3s0meB
!log@localhost:5432/postgres?currentSchema=public
Поскольку объекту не известен автоматически сгенерированный идентификатор из базы данных, нам нужен способ установить это значение. Метод #save возвращает идентификатор, чтобы мы могли применить его к объекту после сохранения с помощью другого внутреннего метода, называемого #after_save. Этот метод принимает идентификатор сохраняемого объекта и устанавливает его в экземпляре. Реализация этого метода по сути заключается в следующем:
protected def after_save(@id : Int64) : Nil
end
Если бы мы имели дело с большим количеством сущностей, мы, конечно, могли бы создать еще один модуль, включающий DB::Serializable, и добавить некоторые из этих дополнительных вспомогательных методов, но, поскольку у нас есть только один, это не дает особой пользы.
Наконец, что наиболее важно, мы используем аннотацию ADI::Register в самом классе. Как упоминалось в первом разделе, Athena активно использует DI через контейнер сервисов, который уникален для каждого запроса, то есть сервисы внутри него уникальны для каждого запроса. Это предотвращает утечку состояния внутри ваших сервисов между запросами, что может