Опыт переноса приложения с Rails на Elixir/Phoenix
Прошло уже немало времени с тех пор, как я с головой ушёл в Elixir. Для нашей команды в компании Made by Many язык Ruby, многообещающая продуктивность которого не наносит ущерба чистой производительности и масштабируемости, становится победителем в номинации «лучший серверный язык программирования».
Я всячески пытался ускорить процесс изучения Elixir: читал книгу Дэйва Томаса и выполнял приведенные в ней задания, посетил конференцию в Остине, посвященную Elixir, а ещё я попробовал перенести Rails-приложение моего коллеги на Phoenix.
Речь пойдёт о нестандартном приложении для электронной коммерции его довольно прибыльного чулочно-носочного бизнеса на сайте Form &Thread. В процессе переноса я отметил не только кое-какие любопытные факты, но и несколько подводных камней. Конечно, если кто-то ещё проделает ту же работу, у него, вероятно, сложится совершенно иное мнение и возникнут свои трудности. Прежде чем перейти к деталям, рассмотрим три основных момента.
Примечание: я не собираюсь расписывать азы Elixir, процесс установки Phoenix и тому подобные вещи. Предполагается, что вы уже на должном уровне знакомы с этим языком и его фреймворком.
1. Необходимо выкинуть из головы основы объектно-ориентированного подхода, особенно применительно к моделям.
Для Ruby-разработчика изменить эту психологическую установку — то, что в обязательном порядке нужно сделать в первую очередь. Принцип «толстых» моделей и «тонких» контроллеров настолько прочно сидит у рубистов в голове, что облегчить модели не получается даже при условии использования паттернов декоратора/презентера или сервисных объектов.
Вместо этого старайтесь мыслить отдельными функциями, объединяя их в единое целое. Осуществив перенос приложения, я осознал, что не так уж всё и сложно. Волшебство тут почти ни при чём.
2. Кое-какие модули интеграции нужно будет написать самому.
Экосистема Elixir пока не располагает интеграционными библиотеками для каждого из используемых сервисов. Возможно, придётся засучить рукава и проделать эту работу самостоятельно и тем самым внести свой вклад в развитие экосистемы. Именно с этой целью я и создал для Elixir
Для Phoenix также существует намного меньше расширений и вспомогательных библиотек, но я выяснил, что возможности языка Elixir способны это компенсировать, правда, нужно будет дописать пару лишних строчек кода.
3. Кое-что в Elixir/Phoenix устроено совсем по-другому.
Следует отметить, что при программировании на Elixir некоторые шаблоны проектирования должны быть реализованы иным образом. Возьмём, к примеру, асинхронный подход. В Rails-приложениях обычно используются очереди Sidekiq или DelayedJob, которые исполняют задачи в фоновых процессах. Конкурентная модель Elixir позволяет этого избежать. Как правило, достаточно просто обернуть функцию в Task.async
или создать своё OTP-приложение.
«Волшебство тут почти ни при чём».
Процесс переноса сборки фронтенда на brunch
Первым шагом после создания нового приложения стал перенос сборки фронтенда на
Первой проблемой, с которой я столкнулся, стало отсутствие в Brunch обработчика SASS, который в Rails идёт по умолчанию. Однако SAAS можно легко подключить при помощи модуля sass-brunch. Я добавил его вместе с другими необходимыми модулями Node в файл
package.json
.
Необходимо произвести кое-какие модификации в brunch.config
— настроить модуль sass-brunch
так, чтобы тот включал путь к директории
node_modules
. Таким образом, мы подключим библиотеку bourbon, файл
normalize.css
и css-шаблоны компании Mady by Many.
Затем импортируем в application.scss
пути, соответствующие директории модулей
node_modules
.
Также нужно выполнить следующие действия: переместить файлы с расширением js
и css
из директории
assets
в директорию web/static
, в то время как изображения и шрифты — в директорию
web/static/assets
.
Ещё одно важное отличие в области фронтенда — это то, что в Phoenix не существует гема sass-rails, который волшебным образом предоставлял бы
asset_path
или asset_url
. Придётся вычистить из кода всё, что к нему относится, и прописать необходимые пути вручную.
Превращается в...
Перевод моделей с ActiveRecord на Ecto
Примечание: хотя в Phoenix подобные компоненты называются моделями, в Ecto используется название «schema», и я хотел бы, чтобы Phoenix перенял именно этот термин. Это помогает не думать о них, как о моделях, и избавиться от устоев ООП. Но на данный момент, говоря о Phoenix, я всё-таки буду называть их моделями.
Задание оказалось относительно простым: взять файл schema.rb и создать соответствующие файлы модели в Phoenix при помощи команды mix phoenix.gen.model. Я отловил несколько исключений, но в вашем случае их может и не быть.
В Ecto отсутствует функция
has_and_belongs_to_many
, поэтому существует необходимость создавать свою промежуточную модель. Затем её можно использовать простым вызовомhas_many :through
.Расширение HSTORE не реализовано для Ecto.
Существующая модель данных использует Postgres HSTORE для хранения пар ключ-значение у некоторых моделей. Ecto пока не поддерживает HSTORE, а решение этого вопроса заняло бы слишком много времени. Вместо этого я перевёл данные в формат jsonb, имеющий внутреннее представление в виде словаря Elixir. Подробнее
На этом этапе важно проверить, чтобы расширение JSON было включено в настройках окружения:
Далее проводим перенос всех начальных данных из файла priv/repo/seeds.exs
в Elixir-приложение с помощью выполнения команды:
Использование моделей
Ecto и ActiveRecord — это не одно и то же, и нужно будет обратить внимание на некоторые моменты. Например, вот то, что попалось мне на глаза.
Отсутствие скоупов
Скоупы в Ecto не используются. Вместо этого запросы хранятся в модуле, а разработчику остаётся только составить из них более сложный запрос. Подробнее
Превращается в...
Отсутствие отложенной загрузки ассоциаций
Как вы знаете, ActiveRecord реализует отложенную загрузку ассоциаций. Однако в Ecto во избежание ошибок для этого необходимо использовать функцию
Repo.preload
. Мы используем подход, при котором сохраняем часто используемые связи в отдельной функции, а потом передаём её внутрь
Repo.preload
(обратите внимание, что здесь возможно выполнять глубоко вложенные предзагрузки).
Набор изменений вместо функций обратного вызова
Подход с набором изменений позволяет вам модифицировать модели, обеспечивая иммутабельность. При этом вам нужно использовать различные наборы изменений для разных целей вместо написания функций обратного вызова.
Превращается в...
«Ecto и ActiveRecord — это не одно и то же»
Перенос контроллера
Структуры контроллеров Rails и Phoenix настолько похожи, что я даже не буду вдаваться в систему маршрутов. При всём при том кое-что всё-таки требует изменений.
Выбранное мной Rails-приложение для вызова данных использует в контроллерах множество событий before_action
. В Phoenix можно было бы заменить их на плаги, но я вместо этого объединил в пайплайн различные функции внутри действий. Теперь можно отчётливо видеть, что происходит при каждом рендере пайплайна:
Превращается в...
В некоторых случаях помещаем плаги, чтобы проверять выполнение требований перед осуществлением действий.
Превращается в...
Шаблоны/Представления
Здесь имеет место небольшая терминологическая замена: то, что для Rails является представлением, в Phoenix будет называться шаблоном. Файл шаблона компилируется в функцию внутри модуля представления. Представления в Phoenix также являются местом для функций, аналогов хелперов из Rails. Вот простой пример вычисления общего количества отображаемых позиций заказа:
В представлении возможно получить доступ только к явно объявленным переменным, тогда как в Rails по умолчанию доступны все переменные экземпляра. Rails-приложение, переносом которого я занимался, также использует декораторы из гема Draper для расширения моделей. А мы предоставим эти действия функциям в представлении:
Превращается в...
Функцияproduct_images
находится в FormAndThread.ProductView
.
Сервисы
Ещё одна основная составляющая приложения — это сервисы, вбирающие в себя основную бизнес-логику внутри действий. Чтобы сохранить функциональность, я создал Elixir-модули.
Большим преимуществом Elixir является то, что можно с лёгкостью понять порядок выполнения действий. Здесь реализуется такой паттерн, при котором каждому действию, изменяющему некоторую запись заказа в базе данных, возвращается новое состояние, прежде чем осуществится переход к следующему действию.
Подведение итогов
Я уверен, что со временем появится множество различных схем переноса Rails-приложений, но для себя я отметил, что этот процесс не только оказался «безболезненным», но и позволил получить превосходные результаты. Не говоря уже о том, что Elixir-приложение работает гораздо быстрее (показатель времени отклика менее 100 мс льёт мне бальзам на душу).