Жил-был поставщик облачных сервисов и захотелось ему не отставать от прогресса. И решил он обновиться с Rails 4.2.8 до Rails 5.0.2. А как это было, что по пути отвалилось, что по лбу вдарило с ускорением и какой опыт из этого вынесли — читайте под катом.
Повествовать буду разрозненно, так как большинство особенностей и проблем никак не связаны друг с другом. Поэтому если встретили что-то унылое или известное — смело двигайтесь к следующему пункту, там, быть может, будет поинтереснее.
Цель повествования: никакого универсального алгоритма перехода не будет, просто хочется поделиться найденной информацией и подробностями разной степени интимности о том, почему нельзя просто так взять и перейти на Rails 5 подправив версии в Gemfile.
Анамнез
Приложение у нас среднестатистическое: Ruby 2.2.3, три с хвостиком сотни гемов, ~1500 экшенов, постгрес, эластик, редиска. В общем, ничего особенного. Разве что тестами покрыто на 90%+. Это, кажись, не очень характерно для некоторых современных приложений.
Начнём с начала
Хоть на горизонте уже маячит Rails 5.2, но никто не спешит обновлять мажорную версию. Поэтому в интернете не так уж много информации об особенностях перехода, как хотелось бы. Есть более-менее информативное описание вот тут. Есть релизноты для 5.0 и 5.1 (впрочем, кто их читет пока не припрёт?). Есть офф.гайд с основными шагами по обновлению до Rails 5. Он хороший, но хочется больше информации, особенно касаемо гемов, их совместимости и прочего хозяйства, которое обычно отваливается при обновлениях. Хотя, справедливости ради, если бы мы его изучили более подробно, то не прошлись бы по паре знатных грабелек (про них будет ниже). Поэтому, далее я буду упоминать некоторые пункты из гайда (то есть некоторые базовые изменения), которые, однако, для нас оказались довольно важными.
Причины основных проблем или вместо TL;DR
Некоторые используемые гемы либо плохо совместимы, либо вообще не совместимы с Rails 5, из-за чего приходится их либо выпиливать и заменять на родные возможности рельсы, либо допиливать гемы до рабочего состояния. В любом случае нужно ковырять их и отвечать на вопросы: "как оно работало?", "будет ли оно работать?", "что же делать, чтобы оно заработало?". После чего начинать форкать/контрибьютить/патчить.
Обновления мажорных версий гемов вслед за мажорной версией Rails и, следовательно, мажорные изменения в интерфейсах, API и прочих загогулинах, которые гемы предоставляют. Сие влечёт массовые замены и/или перепил того, что раньше работало и кушать не просило.
- Всеми любимый и всеми ненавистный core_ext: манкипатчи ядра и гемов для личных нужд. Самые злостные косяки были именно из-за них. Возникает куча вопросов вида: "а зачем это сделали и как оно работало?", "а почему сделали именно так?", "а в новой версии гема всё к чертям переписали, что же делать?".
Обновление гемов
Чтобы что-то упало — нужно что-то обновить. Начать, естественно, нужно с гема rails и его зависимостей. Тут можно зависнуть на день-другой и никаких рецептов нет. Просто много плясок с бубном вокруг Gemfile, Gemfile.lock и постепенное понимание того, что обновить понадобится ещё добрую половину всего, что есть в проекте, дабы оно хотя бы сбандлилось. Этапы получаются следующие:
- бандлится,
- запускается консоль,
- запускается сервер,
- работает хотя бы 3 (три) экшена/запроса подряд.
Жесть, как она есть
Теперь вразнобой обо всём.
alias_method_chain выпиливается
Для тех, кто ещё не в курсе: теперь нужно делать prepend
. Если коротко: эта штука добавляет модуль после класса, а не перед, как всем известный include
. Как бонус — получаем super
вместо with/without методов. Подробности и примеры смотреть тут.
Много новых дефолтных конфигов
Генерятся они через rails app:update
. Но задачка эта очень тупенькая и просто создаёт файлы, а если у вас уже был какой-то из них — мержите сами и да прибудет с вами сила.
Гем device_async
Для новых версий device он не нужен и заменяется парой строчек кода по офф.манулу. Справедливости ради, это работает начиная с Rails 4.2, но в 5-х рельсах гем окончательно перестаёт работать и таки нужно всё переписать на ActiveJob.
Гем CanCan
Нужно заменить на CanCanCan. Так как у CanCan кончается поддержка и сыпется туча депрекейшенов, которые станут проблемой в Rails 5.1. В общем случае больших проблем с заменой быть не должно. У нас были свои допилы CanCan'а под inherited_resources, поэтому мы страдали немного больше.
Гем Grape-Swagger
У нас был расширенный пропатченый вариант. Он умел подставлять API-key в запрос. Но перепил в последней мажорной версии сваггера случился знатный. Поэтому после перехода на 3.0 мы не смогли найти никаких знакомых ориентиров и старый патч было тупо некуда вставлять. В итоге оказалось, что там теперь есть документированная фича для кастомных http-заголовков, но она не взлетела и пришлось немножко контрибьютить. Теперь всё работает.
Вставка невалидных объектов в has_many ассоциацию
При вставке в has_many ассоциацию невалидного объекта, в Rails 5 всё падает (объект persisted и невалидным мы его сделали только в памяти). Раньше происходило сохранение и в ассоциацию попадал исходный объект из БД, а изменения из памяти либо скипались (или скипались валидации), либо хз что с ним было, глубоко не копали.
concat для Relation
Его куда-то дели и больше он не работает.
Не баг, но deprecation про uniq
Uniq таки заменили на distinct и в 5.1 окончательно выпилят uniq для Reilation. Как я понимаю, наконец-то пришло осознание, что Relation, мимикрирующий под Array — это не фича, а большая хрень.
Гем postgres_ext
Нужно решительно выпилить. Всё что он умеет делать довольно просто заменяется при помощи raw-sql и/или Arel. Сам гем, на текущий момент, не совместим с Rails 5. При попытках его использования в новых рельсах получаем ошибки в самых неожиданных местах, например, при вызове count
на STI-классе:
ArgumentError:
wrong number of arguments (given 1, expected 2)
# /Users/username/.rvm/gems/ruby-2.3.3@gemset/gems/arel-7.1.4/lib/arel/visitors/reduce.rb:12:in `visit'
# /Users/username/.rvm/gems/ruby-2.3.3@gemset/gems/postgres_ext-3.0.0/lib/postgres_ext/arel/4.1/visitors/postgresql.rb:22:in `block in
Гем simple_form
У нас, почему-то, с ним почти ничего не произошло. Он просто продолжил работать. Просто отвалилась возможность делать одновременно include_blank
и require
для поля.
Разное вокруг ActiveRecord'а
- AR ругается на передачу в условия констант (в частности классов), говорит передавайте строки, а не константы. Актуально, например, в случае поиска в таблице c STI:
# теперь так делать не надо Model.where(content_type: SharedFile) # надо делать так Model.where(content_type: SharedFile.name ) # или так Model.where(content_type: 'SharedFile')
- Все модели теперь нужно наследовать от
ApplicationRecord
вместоActiveRecord::Base
. То есть появляется промежуточный абстрактный класс:
class ApplicationRecord < ActiveRecord::Base self.abstract_class = true end
Значительно изменён принцип работы маппинга столбцов БД в модель. В частности это касается метода
column_names
. Он больше не возвращает атрибуты, задефайненные пользователем, например, через attribute (подробности можно посмотреть в дифах этого метода в AR между 4 и 5 рельсами). Теперь появилось понятиеattributes_to_define_after_schema_loads
. И теперь, если у вас есть логика, основанная на "виртуальных" атрибутах, и хочется чтобыcolumn_names
по прежнему возвращал всё, то нужно делать как-то так:
def self.column_names super + attributes_to_define_after_schema_loads.keys end
Выпилена константа
ActiveRecord::ConnectionAdapters::Column::TRUE_VALUES
. На сколько я знаю, многие ей пользовались и теперь есть два варианта: вернуть её обратно руками, либо переходить на использованиеFALSE_VALUES
, которую оставили в живых.
- Передача аргумента в
reload
(для force перезагрузки) выпиливается. Теперь нужно вызывать reload на конкретной реляции. Например:
first_model = Model.first first_model.has_many_relation_name.reload
Аналогичная штука для has_one ассоциации:
second_model = Model.second second_model.reload_has_one_relation_name
Остановка
before_
колбэков при возвратеfalse
— выпиливается. Теперь нужно явно райзитьthrow(:abort)
в колбэках. Вот тут MR со знатным холиваром по этому поводу.
Выпиливается параметр
raise_in_transactional_callbacks=
, отвечающий за выброс эксепшенов изafter_commit
/after_rollback
в случае возврата false из них (ранее он тихо писал сообщение в лог и мог не выбрасывать эксепшн). Штуку запилили ещё в Rails 4, но, кажись, мало кто озаботился переписыванием. Теперь пора, а то скоро выпилят старое поведение.
Все реляции
belong_to
стали обязательными по-умолчанию. Чтобы вернуть всё на место, нужно потыкать параметрconfig.active_record.belongs_to_required_by_default
.
Появился метод
ActiveRecord::Relation#update
, который позволяет обновлятьRelation
, вызывая колбэки и валидации. По сути своей это сахар: будет много запросов к БД и внутри это просто map. Подробности в MR.
- Появилась возможность делать
LEFT JOIN
без сайд-эффектов. До Rails 5OUTER JOIN
можно было написать явно через joins, либо взять includes + references, либо им эквивалентный eager_load. В качестве довеска мы получаем загрузку всех указанных реляций, так как основное назначение всего вышеуказанного (за исключениемjoins
) — это помощь в решении N+1. Теперь естьActiveRecord::Relation#left_outer_joins
, который просто делает левый джоин и ничего лишнего. За подробностями можно сходить в MR.
params в контроллерах
Теперь это не HashWithIndifferentAccess
, а самостоятельный класс ActionController::Parameters
, который ради обратной совместимости мимикрирует под хэш, но сильно ругается, если им пользуются как хэшом, то есть если делают merge
, update
и т.д. То есть, если я правильно понял идею, у params
две цели: хранить то, что пришло от клиента и фильтровать содержимое, используя Strong Parameters. Никаких иных модификаций над ним производить не нужно. Иными словами, модифицировать данные прямо в params
, ровно как и добавлять туда свои — это дурной тон. Делайте отдельный объект для этих действий, либо вызывайте params.to_h
или даже params.to_unsafe_hash
и после этого уже работайте с хэшем, как раньше.
Добавление ошибок в модель через errors[]=
ActiveModel::Errors#[]=
выпиливается в Rails 5.1, в конце-то концов нужно использовать model.errors.add(:name, "can't be blank")
. Сообщения в логах по этому поводу, естественно, присутствуют.
Немного про Arel
Если в eq
(да и, скорее всего, в любой аналогичный предикат) из Arel для поля id
(с другими полями всё ок) передать сам объект, то в 4-х рельсах из этого объекта вытаскивал айдишник (pk) и подставлялся в запрос, а в 5-х рельсах такого не происходит. Вместо попытки вытащить pk у объекта просто подставляется NULL и получается что-то типа "account_id = NULL". Пример:
# в Rails 4
MyModel.arel_table[:my_field].eq(MyModel.first).to_sql
=> "my_models"."my_field" = 1
MyModel.arel_table[:id].eq(MyModel.first).to_sql
=> "my_models"."id" = 1
# в Rails 5
MyModel.arel_table[:my_field].eq(MyModel.first).to_sql
=> "my_models"."my_field" = 1
MyModel.arel_table[:id].eq(MyModel.first).to_sql
=> "my_models"."id" = NULL
Посему: старайтесь всегда явно указывать значение.
Про skip_callback
skip_callback(:create, :after, :my_method)
в 4-х рельсах позволяет передавать какие-угодно параметры и не падает даже если такого метода нет в цепочке колбэков, в 5-х рельсах аналогичный метод рэйзит эксепшн, если не нашёл метода, который нужно скипнуть. Поэтому могут случиться странные падения и прийдётся углубляться в промышленную археологию, дабы понять "а был ли мальчик?".
Маленькая революция в render
Меняют :text
и прочие форматы на явное указание mime_type
. Заменяют :nothing
на :head
и тд. То есть возвращаются к истокам и делают названия приближенными к сути, а не доступными для понимания любой домохозйкой.
Изменение порядка прогона тестов
Теперь дефолтный порядок прогона :random
. Если у вас тесты зависят от последовательности выполнения — это не очень хорошо, но вернуть старое поведение довольно просто:
Rails.application.configure do
config.active_support.test_order = :sorted
end
Observer'ы
Их в 5-х рельсах пока (или уже) никто не поддерживает. Исходные обсерверы остановились на 4-х рельсах. В мастере у них заявлена поддержка 5-х, но ничего не работает. Есть какой-то японец с альтернативой обсерверов для 5-х рельс (которая очень похожа на копипаст и ребрендинг), но и эта штука не работает. Лучший выход — использовать родные колбэки из AR.
Баги в ActiveRecord
Нашли парочку:
- первый — старый про
uniq
наActiveRecord::Associations::CollectionProxy
, который оказался известным и уже поправлен в 5.1; - второй — новый и поинтереснее: как можно неявно зафризить атрибут(ы) у AR-модели.
Гем shoulda-matchers
Обновили его до 3.1.1 и повылазило много разной мелочи:
- появилась дефолтная проверка
email
на case sensitive; - ужесточилась проверка на scope: если раньше
should validate_uniqueness_of(:name)
проходило даже в случае, когда в модели указанscope
дляuniqueness
, то теперь тест падает и нужно явно указыватьshould validate_uniqueness_of(:name).scoped_to(:vendor_id)
; - косячно проверяет сериализованные атрибуты и
should serialize(:metadata).as(Hash)
падает сcast_type
; - раньше метод
allow_value('foo', 'bar', 'baz')
брал только первое значение из передаваемых аргументов, а теперь исправился и начал брать все.
Миграции
Изменили способ формирования имён индексов и ещё несколько вещей, которые полностью сломали обратную совместимость. Поэтому нужно явно всем миграциям указать в какой версии они были созданы. Указывать через параметр класса:
class OldMigrationName < ActiveRecord::Migration[4.2]
...
end
class NewMigrationName < ActiveRecord::Migration[5.0]
...
end
Так же дали возможность указывать foreign_key
в опциях для references
. И немного поменяли дефолтные настройки генератора: теперь он по умолчанию делает pk (который id) не integer'ом, а uuid'ом.
Ещё у нас, почему-то, возникли проблемы определением моделей внутри миграций. То есть в штуке вида:
class SomeMigration < ActiveRecord::Migration
class StubForModel < ActiveRecord::Base
has_one :something
end
def up
StubForModel.destroy_all
end
def down
# do nothing
end
end
кто-то сильно ругается на то, что не может найти Base
. Пока не разобрались с причиной, поэтому будте бдительны. Как разберёмся — обновим описание.
Последовательность колбэков в контроллере
Проверка csrf
токена (protect from forgery) в Rails 4 всегда поднималась вверх и выполнялась первым колбэком в контроллере. В Rails 5 она никуда не поднимается и выполняется согласно тому, в каком месте задефайнена и если нужно поднять её, то нужно явно указывать prepend: true
.
Всё бы хорошо, но мы, кажись, не внимательно читали гайды, ссылки на которые привели в начале. У нас просто отвалилась аутентификация. Мы перекопали весь девайс и чуть не уверовали в магию. А оказалось, что сначала девайс успешно проводит аутентификацию и выпиливает из парамсов токен, а затем приходит protect from forgery и сильно негодует по поводу кривого (на самом деле отсутствующего) токена.
Манкипатчи
"Его пример другим наука..."
- В тестах контроллеров
get
падал со stack level too deep в районе cookies. Сломали пару светлых голов и во второй раз чуть было в магию не уверовали, пока не раскопали манкипатч, связанный с RequestStore. - Вышеприведённый кейс с csrf-токеном был немного осложнён своими
допиламиманкипатчами аутентификации. На самом деле, у нас была две проверкиcsrf
токена: одна до девайса и одна после. Сие совсем не способствовало упрощению поиска проблемы. - Ещё раньше упомянутый CanCan тоже был попатчен и приятного было мало.
- Были у нас свои допили
ActiveRecord::Type
, который изрядно перекопали в новых рельсах: было, стало и которые перенесли вActiveModel
. - Есть ещё немало вещей, о которых можно было бы рассказать, но они в переходе на новые рельсы почти не участвовали, поэтому оставим на потом.
К чему я всё это: core_ext
быть не должно. Совсем. И переход на новые рельсы — это отличный повод начать с них и постараться выпилить столько, сколько возможно, а на всё остальное посмотреть очень придирчивым взглядом и вспомнить историю (несомненно очень пёструю) предшествующую появлению этого расширения. Так будет гораздо проще разбираться, когда оно отвалится в процессе перехода.
Обновление версии Ruby
Честно говоря, мы неосилили. Бодро воткнув 2.4.0 после 2.2.3 мы огребли кучу неведомой фигни и решили не выёживаться и переходить в два этапа: сначала Ruby 2.3.3, в котором нет ничего шибко революционного, плюс переход на новые рельсы. А уж затем — обновление руби. Посему, пока что ничего не можем сказать про новый руби. Разве что есть мнение опытных товарищей о том, что если у вас скомпилились native extension для гемов, то большую часть проблем перехода вы обошли и серьёзных препятствий быть не должно.
Магия (задачка на подумать, если есть время и желание)
Безотносительно перехода на новые рельсы, просто странный код, который раскопали в ходе перехода и который работал, хотя не должен (краткое содержание трёх файлов AR-классов):
class Base
TYPES = {first: First, second: Second}
end
class First < Base; end
class Second < Base; end
Эта штука по всем законам жанра работать не может (да в общем-то и не работает на чистых проектах под Rails 4/5). Так как при обращении к кому-либо из наследников автолоад уходит в циклическую загрузку классов и падает. Если же сначала обращаться к базовому классу, а потом к наследникам, то всё хорошо. Но у нас почему-то работало в Rails 4, но отвалилось в Rails 5, хотя явного обращения к Base
для его загрузки ранее, нежели все остальные, мы не нашли, да и вообще не трогали ничего, связанного с автолоадом. Если кто-то знает ещё способы как оно может работать или что интересного случилось с автолоадом в Rails 5 — сообщите, пожалуйста.
Конец
Спасибо, что дочитали. Надеюсь хоть что-то вам пригодится (или уже пригодилось) и мы хоть чуть-чуть сэкономили вам время на переход.
Пожалуйста, не стесняйтесь делиться в комментариях своими знаниями и опытом по теме и не очень.