- PVSM.RU - https://www.pvsm.ru -
Жил-был поставщик облачных сервисов и захотелось ему не отставать от прогресса. И решил он обновиться с Rails 4.2.8 до Rails 5.0.2. А как это было, что по пути отвалилось, что по лбу вдарило с ускорением и какой опыт из этого вынесли — читайте под катом.
Повествовать буду разрозненно, так как большинство особенностей и проблем никак не связаны друг с другом. Поэтому если встретили что-то унылое или известно — смело двигайтесь к следующему пункту, там, быть может, будет поинтереснее.
Цель повествования: никакого универсального алгоритма перехода не будет, просто хочется поделиться найденной информацией и подробностями разной степени интимности о том, почему нельзя просто так взять и перейти на Rails 5 подравив версии в Gemfile.
Приложение у нас среднестатистическое: Ruby 2.2.3, три с хвостиком сотни гемов, ~1500 экшенов, постгрес, эластик, редиска. В общем, ничего особенного. Разве что тестами покрыто на 90%+. Это, кажись, не очень характерно для некоторых современных приложений.
Хоть на горизонте уже маячит Rails 5.2, но никто не спешит обновлять мажорную версию. Поэтому в интернете не так уж много информации об особенностях перехода, как хотелось бы. Есть более-менее информативное описание вот тут [1]. Есть релизноты для 5.0 [2] и 5.1 [3] (впрочем, кто их читет пока не припрёт?). Есть офф.гайд [4] с основными шагами по обновлению до Rails 5. Он хороший, но хочется больше информации, особенно касаемо гемов, их совместимости и прочего хозяйства, которое обычно отваливается при обновлениях. Хотя, справедливости ради, если бы мы его изучили более подробно, то не прошлись бы по паре знатных грабелек (про них будет ниже). Поэтому, далее я буду упоминать некоторые пункты из гайда (то есть некоторые базовые изменения), которые, однако, для нас оказались довольно важными.
Чтобы что-то упало нужно что-то обновить. Начать, естественно, нужно с гема rails и его зависимостей. Тут можно зависнуть на день-другой и никаких рецептов нет. Просто много плясок с бубном вокруг Gemfile, Gemfile.lock и постепенное понимание того, что обновить понадобится ещё добрую половину всего, что есть в проекте, дабы оно хотя бы сбандлилось. Этапы получаются следующие:
Теперь вразнобой обо всём.
Для тех кто ещё не в курсе: теперь нужно делать prepend
. Если коротко: эта штука добавляет модуль после класса, а не перед, как всем известный include
. Как бонус получаем super
вместо with/without методов. Подробности и примеры смотреть тут [5].
Генерятся они через rails app:update
. Но задачка эта очень тупенькая и просто создаёт файлы, а если у вас уже был какой-то из них — мержите сами и да прибудет с вами сила.
Для новых версий device он не нужен и заменяется парой строчек кода по офф.манулу [6]. Справедливости ради, это работает начиная с Rails 4.2, но в 5-х рельсах гем окончательно перестаёт работать и таки нужно всё переписать на ActiveJob.
Нужно заменить на CanCanCan [7]. Так как у CanCan кончается поддержка и сыпется туча депрекейшенов, которые станут проблемой в Rails 5.1. В общем случае больших проблем с заменой быть не должно. У нас были свои допилы CanCan'а под inherited_resources [8], поэтому мы страдали немного больше.
У нас был расширенный пропатченый вариант. Он умел подставлять API-key в запрос. Но перепил в последней мажорной версии сваггера случился знатный. Поэтому после перехода на 3.0 мы не смогли найти никаких знакомых ориентиров и старый патч было тупо некуда вставлять. В итоге оказалось что там теперь есть документированная фича для кастомных http-заголовков, но она не взлетела и пришлось немножко контрибьютить [9]. Теперь всё работает.
При вставке в has_many ассоциацию невалидного объекта, в Rails 5 всё падает (объект persisted и невалидным мы его сделали только в памяти). Раньше происходило сохранение и в ассоциацию попадал исходный объект из БД, а изменения из памяти либо скипались (или скипались валидации), либо хз что с ним было, глубоко не копали.
Его куда-то дели и больше он не работает.
Uniq таки заменили на distinct и в 5.1 окончательно выпилят uniq для Reilation. Как я понимаю, наконец-то пришло осознание, что Relation, мимикрирующий под Array — это не фича, а большая хрень.
Нужно решительно выпилить. Всё что он умеет [10] делать довольно просто заменяется при помощи 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
У нас, почему-то, с ним почти ничего не произошло. Он просто продолжил работать. Просто отвалилась возможность делать одновременно include_blank
и require
для поля.
# теперь так делать не надо
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 [11] (подробности можно посмотреть в дифах этого метода в 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
second_model = Model.second
second_model.reload_has_one_relation_name
before_
колбэков при возврате false
— выпиливается. Теперь нужно явно райзить throw(:abort)
в колбэках. Вот тут [12] 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 [13].LEFT JOIN
без сайд-эффектов. До Rails 5 OUTER JOIN
можно было написать явно через joins [14], либо взять includes [15] + references [16], либо им эквивалентный eager_load [17]. В качестве довеска мы получаем загрузку всех указанных реляций, так как основное назначение всего вышеуказанного (за исключением joins
) — это помощь в решении N+1. Теперь есть ActiveRecord::Relation#left_outer_joins
, который просто делает левый джоин и ничего лишнего. За подробностями можно сходить в MR [18].Теперь это не HashWithIndifferentAccess
, а самостоятельный класс ActionController::Parameters
, который ради обратной совместимости мимикрирует под хэш, но сильно ругается, если им пользуются как хэшом, то есть если делают merge
, update
и т.д. То есть, если я правильно понял идею, у params
две цели: хранить то, что пришло от клиента и фильтровать содержимое, используя Strong Parameters [19]. Никаких иных модификаций над ним производить не нужно. Иными словами, модифицировать данные прямо в params
, ровно как и добавлять туда свои — это дурной тон. Делайте отдельный объект для этих действий, либо вызывайте params.to_h
или даже params.to_unsafe_hash
и после этого уже работайте с хэшем, как раньше.
ActiveModel::Errors#[]=
выпиливается в Rails 5.1, в конце-то концов нужно использовать model.errors.add(:name, "can't be blank")
. Сообщения в логах по этому поводу, естественно, присутствуют.
Если в 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(:create, :after, :my_method)
в 4-х рельсах позволяет передавать какие-угодно параметры и не падает даже если такого метода нет в цепочке колбэков, в 5-х рельсах аналогичный метод рэйзит эксепшн, если не нашёл метода, который нужно скипнуть. Поэтому могут случиться странные падения и прийдётся углубляться в промышленную археологию, дабы понять "а был ли мальчик?".
Меняют :text
и прочие форматы на явное указание mime_type
. Заменяют :nothing
на :head
и тд. То есть возвращаются к истокам и делают названия приближенными к сути, а не доступными для понимания любой домохозйкой.
Теперь дефолтный порядок прогона :random
. Если у вас тесты зависят от последовательности выполнения — это не очень хорошо, но вернуть старое поведение довольно просто:
Rails.application.configure do
config.active_support.test_order = :sorted
end
Их в 5-х рельсах пока (или уже) никто не поддерживает. Исходные обсерверы [20] остановились на 4-х рельсах. В мастере у них заявлена поддержка 5-х, но ничего не работает. Есть какой-то японец с альтернативой обсерверов [21] для 5-х рельс (которая очень похожа на копипаст и ребрендинг), но и эта штука не работает. Лучший выход — использовать родные колбэки из AR.
Нашли парочку:
uniq
на ActiveRecord::Associations::CollectionProxy
, который оказался известным и уже поправлен в 5.1;Обновили его до 3.1.1 и повылазило много разной мелочи:
email
на case sensitive;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
. И немного поменяли [24] дефолтные настройки генератора: теперь он по умолчанию делает 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 [25].csrf
токена: одна до девайса и одна после. Сие совсем не способствовало упрощению поиска проблемы.ActiveRecord::Type
, который изрядно перекопали в новых рельсах: было [26], стало [27] и которые перенесли [28] в ActiveModel
.К чему я всё это: core_ext
быть не должно. Совсем. И переход на новые рельсы — это отличный повод начать с них и постараться выпилить столько, сколько возможно, а на всё остальное посмотреть очень придирчивым взглядом и вспомнить историю (несомненно очень пёструю) предшествующую появлению этого расширения. Так будет гораздо проще разбираться, когда оно отвалится в процессе перехода.
Честно говоря, мы неосилили. Бодро воткнув 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
Источник [29]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/ruby/254823
Ссылки в тексте:
[1] тут: http://mensfeld.pl/2015/12/upgrading-to-ruby-on-rails-5-0-from-rails-4-2-application-use-case/
[2] 5.0: http://edgeguides.rubyonrails.org/5_0_release_notes.html
[3] 5.1: http://edgeguides.rubyonrails.org/5_1_release_notes.html
[4] офф.гайд: http://edgeguides.rubyonrails.org/upgrading_ruby_on_rails.html#upgrading-from-rails-4-2-to-rails-5-0
[5] тут: http://www.justinweiss.com/articles/rails-5-module-number-prepend-and-the-end-of-alias-method-chain/
[6] офф.манулу: https://github.com/plataformatec/devise#activejob-integration
[7] CanCanCan: https://github.com/CanCanCommunity/cancancan
[8] inherited_resources: https://github.com/activeadmin/inherited_resources
[9] контрибьютить: https://github.com/ruby-grape/grape-swagger/commit/95675e33b100555e8cc153e4831ac4a66a50ccdc
[10] умеет: https://github.com/DockYard/postgres_ext/blob/master/docs/querying.md
[11] attribute: https://apidock.com/rails/ActiveRecord/Attributes/ClassMethods/attribute
[12] тут: https://github.com/rails/rails/pull/17227
[13] MR: https://github.com/rails/rails/pull/11898/files
[14] joins: https://apidock.com/rails/ActiveRecord/QueryMethods/joins
[15] includes: https://apidock.com/rails/ActiveRecord/QueryMethods/includes
[16] references: https://apidock.com/rails/ActiveRecord/QueryMethods/references
[17] eager_load: https://apidock.com/rails/ActiveRecord/QueryMethods/eager_load
[18] MR: https://github.com/rails/rails/pull/21762
[19] Strong Parameters: https://github.com/rails/strong_parameters
[20] Исходные обсерверы: https://github.com/rails/rails-observers
[21] альтернативой обсерверов: https://github.com/yasaichi/everett
[22] первый: https://github.com/rails/rails/issues/28717
[23] второй: https://github.com/rails/rails/issues/28718
[24] поменяли: https://github.com/rails/rails/pull/21762/files
[25] RequestStore: https://github.com/steveklabnik/request_store
[26] было: https://github.com/rails/rails/tree/4-2-stable/activerecord/lib/active_record/type
[27] стало: https://github.com/rails/rails/tree/5-0-stable/activerecord/lib/active_record/type
[28] перенесли: https://github.com/rails/rails/tree/5-0-stable/activemodel/lib/active_model/type
[29] Источник: https://habrahabr.ru/post/326706/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best
Нажмите здесь для печати.