@[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 невероятно мощны, и я настоятельно рекомендую прочитать более подробный список их возможностей.
В нашем контексте на практике это означает, что мы можем заставить логику DI Athena внедрять экземпляр этого типа везде, где нам может понадобиться сохранить объект, например контроллер или другой сервис. Основное преимущество этого заключается в том, что это упрощает тестирование типов, которые его используют, поскольку мы можем внедрить макетную реализацию в наши модульные тесты, чтобы гарантировать, что мы не тестируем слишком много. Это также помогает обеспечить централизацию и возможность повторного использования кода.
Теперь, когда у нас есть все необходимые условия, мы можем, наконец, настроить постоянство статей, причем первым шагом будет предоставление нашему менеджеру объектов доступа к ArticleController. Для этого мы можем сделать контроллер службой и определить инициализатор, который создаст переменную экземпляра типа Blog::Services::EntityManager, например:
@[ADI::Register(public: true)]
class Blog::Controllers::ArticleController < ATH::Controller
def initialize(@entity_manager : Blog::Services::
EntityManager);
end
# ...
end
По причинам реализации служба должна быть общедоступной, следовательно, поле public: true в аннотации. Разрешено извлекать общедоступную службу непосредственно по типу или имени из контейнера, а не только через конструктор DI.. Это может измениться в будущем. Как только мы это сделаем, мы сможем ссылаться на нашего менеджера сущностей, как и на любую другую переменную экземпляра.
На данный момент нам действительно нужно добавить только одну строку, чтобы сохранить наши статьи. Метод #create_article теперь должен выглядеть так: