Из Rails 4 в Rails 5: как это было

в 6:57, , рубрики: AC&AP, rails 4, rails 5, Rails4 to Rails 5, ruby, ruby on rails, миграция на Rails 5, миграция с Rails 4 на Rails 5, передний край технологий, переход c Rails 4 на Rails 5, переход на Rails 5, Разработка веб-сайтов, регулярные обновления, метки: , , , ,

Из Rails 4 в Rails 5: как это было - 1 Жил-был поставщик облачных сервисов и захотелось ему не отставать от прогресса. И решил он обновиться с 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 5 OUTER 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 — сообщите, пожалуйста.

Конец

Спасибо, что дочитали. Надеюсь хоть что-то вам пригодится (или уже пригодилось) и мы хоть чуть-чуть сэкономили вам время на переход.

Пожалуйста, не стесняйтесь делиться в комментариях своими знаниями и опытом по теме и не очень.

Автор: Loriowar

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js