Этот тип будет простым, но определенно может быть намного более сложным, поскольку чем больше абстракций вы добавляете, тем больше вы, по сути, создаете свой собственный ORM. Эти дополнительные абстракции не потребуются для нашего блога об одной сущности/таблице, но могут быть очень полезны для более крупных приложений. Однако в этот момент, возможно, стоит рассмотреть возможность использования ORM. В конце концов, все зависит от вашего конкретного контекста, поэтому делайте то, что имеет наибольший смысл.
Суть этого нового типа будет заключаться в предоставлении метода #persist
, который принимает экземпляр DB::Serializable
. Затем он вызовет метод #before_save
, если он определен, и, наконец, вызовет метод #save
, где будет внутренняя перегрузка для нашей сущности статьи. Таким образом, все будут счастливы, и мы придерживаемся наших SOLID принципов. Давайте создадим этот тип как require “./services/*"
в
@[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 через контейнер сервисов, который уникален для каждого запроса, то есть сервисы внутри него уникальны для каждого запроса. Это предотвращает утечку состояния внутри ваших сервисов между запросами, что может произойти, если вы используете такие вещи, как переменные класса. Однако это не означает, что использование переменной класса всегда плохо. Все зависит от контекста. Например, наш менеджер сущностей использует его для хранения ссылки на DB::Database
. В данном случае это нормально, поскольку оно остается закрытым внутри нашего класса и представляет собой пул соединений. Благодаря этому каждый запрос может при необходимости получить собственное соединение с базой данных. Мы также не храним в нем какое-либо состояние, специфичное для запроса, поэтому оно остается чистым.
Аннотация ADI::Register
сообщает контейнеру службы, что этот тип следует рассматривать как службу, чтобы его можно было внедрить в другие службы. Функции DI Athena невероятно мощны, и я настоятельно рекомендую прочитать более подробный список их возможностей.