はじめに
みなさん、こんにちは。今回はRailsアップグレードをどうやって行っていくのか、その手順や調べた文献を共有したいと思います。
Rails4からRails5にアップグレードします
現在、会社で運用しているRails4のシステムを今回Rails5にアップグレードします。
以前、先輩が行ったRailsアップグレードですが何らかの原因があってRevert・断念した失敗談があります。その後は、Rails4で新たな機能を作ってきた訳ですが、段々Rails4ではサポートしきれない問題が発生してきました。
特に問題だったのがフロントエンドの開発です。ここ最近は、弊社でもフロントエンドで行うこと、バックエンドで行うことをそれぞれ独立させて新機能を開発してきました。
特にフロントエンドの開発は、jQueryやcoffeeスクリプトをそろそろ止めて生JSで書くことが多くなってきました。
しかし、フロントエンドの開発においては、Railsのgemに依存するものが依然として残っており、このままRails4のまま開発していくのは辛みしかなかったので、アップグレードを急ぐこととなりました。
また、フロントエンドのテストシナリオにはRequest SpecやSystem Specを利用する予定なため、今後の開発スピードを上げていくためにアップグレードを行うこととしました。
今回のアップデート
Rails 4.2.0 -> 5.0.7
ruby 2.3.6 -> 2.4.5
今回はrubyのversionがRails5でも必須バージョンよりも上なため、Railsだけをアップグレードします。
すみません。rubyも2.4系にアップグレードします。
アップグレードに参考にした文献
https://railsguides.jp/upgrading_ruby_on_rails.html#rails-4-2%E3%81%8B%E3%82%89rails-5-0%E3%81%B8%E3%81%AE%E3%82%A2%E3%83%83%E3%83%97%E3%82%B0%E3%83%AC%E3%83%BC%E3%83%89
https://techracho.bpsinc.jp/hachi8833/2018_11_14/64279
基本的には、前回RevertされたPRから、秘伝のタレのような修正がありますので、今回はほぼそれを参考にしています。上二つはRailsアップグレードを行うためにはどうすればいいのか、大まかな所を調べるために読みました。
アップグレード手順
- bundle updateによるコンフリクトを解消する
- app updateによるアップデートタスクを叩く
- ActiveRecord::Base継承の全モデルをApplicationRecord継承に置き換える
- DEPRECATION WARNINGの解消
- その他
- rubyの更新
bundle updateによるコンフリクトを解消する
まずは単純にRailsのversionをあげてみましょう。
- gem 'rails', '4.2.0'
+ gem 'rails', '~> 5.0.0'
% bundle update rails
Bundler could not find compatible versions for gem "rails":
In Gemfile:
rails (~> 5.0.0)
******* was resolved to *****, which depends on
rails (~> 4.0)
railsをupdateしようと試みると、他のgemのrailsのバージョンとぶつかってしまいました。
~>によるバージョン指定は4.xの更新は上がってもいいが、メジャーバージョンが上がると利用不可
という指定になります。
そのため、rails ~> 4.0を指定しているgemも更新が必要となります。このように、地道にRailsアプリで利用している全てのgemが競合しないように更新をかけていく
のが最初の登竜門です。
Gemfileのバージョンの記述方法とその内容は以下の通りです。
http://railsdoc.com/references/gemfile
バージョン | 説明 |
---|---|
x.x.x | バージョン固定 |
>= x.x.x | x.x.x以上のバージョンが必要 |
>= x.x.x, < y.y.y | x.x.x以上、y.y.y以下のバージョンが必要 |
~> x.0 | x.1からx.9は良いが、メジャーバージョンが上がると不可 |
この辺りは以前のPRで大体差分は分かっているのですが、実際やっている事は地道にアップデートを繰り返す泥臭い苦行に入るので、説明はこれくらいにしておきます。
コンフリクトの解消が解決できたら、bundle update
をかけます。
% bundle update
app updateによるアップデートタスクを叩く
Railsのバージョン更新後はタスクとして提供されているrails app:update
を叩きます。これにより、新しいバージョンでのファイル作成や既存ファイルの変更を対話形式で行うことができます。
% rails app:update
conflict config/boot.rb
Overwrite /usr/local/rails/config/boot.rb? (enter "h" for help) [Ynaqdh]
コンフリクトが発生した時、上書きする場合はYを、更新せず既存の設定を保つ時はnを入力します。diffを確認したい時は、dを入力します。
差分を柔軟に設定したいときはqコマンドでrails app:update
で一時中断し、手動で該当ファイルを修正します(本当は、FreeBSDのmergemasterのように、エディタを開いて該当ファイルを直接修正するようなやり方があればいいのですが、app:updateタスクにはそのような機能はありませんでした...)
ActiveRecord::Base継承の全モデルをApplicationRecord継承に置き換える
Rails5.0以降では全モデルがApplicationRecordをスーパークラスとして使うようになりました。という事で、、全モデルから継承されるスーパークラスのモデルとなるapp/models/application_record.rb を作成します。
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
end
あとは他のモデルをApplicationRecordから継承されるように正規表現で置き換えを行います。
./app/models
配下のrubyファイル(ただし、スーパークラスとなるapplication_record.rbは覗く)を全てApplicationrecordから継承されるように置き換えを行います。
% find ./app/models/ -name "*.rb" ! -name "application_record.rb" -type f -print | xargs sed -i "s/ActiveRecord::Base/ApplicationRecord/g
DEPRECATION WARNINGの解消
Rails5および5.1以降でDEPRECATIONになるコードや設定を正規表現で置き換えを行い、修正をかけます。
FactoryBotへの置き換え
DEPRECATION WARNING: The factory_girl gem is deprecated. Please upgrade to factory_bot. See https://github.com/thoughtbot/factory_bot/blob/v4.9.0/UPGRADE_FROM_FACTORY_GIRL.md for further instructions. (called from require at /usr/local/lib/ruby/site_ruby/2.3.0/bundler/runtime.rb:81)
no_proxy is unsupported
下記のURLでFactoryGirl、factory_girlをFactoryBot、factory_botに置き換えるための正規表現のサンプルが載っているのでこちらを使います。
https://github.com/thoughtbot/factory_bot/blob/v4.9.0/UPGRADE_FROM_FACTORY_GIRL.md
grep -e FactoryGirl **/*.rake **/*.rb -l | xargs sed -i "s|FactoryGirl|FactoryBot|"
grep -e factory_girl **/*.rake **/*.rb -l | xargs sed -i "s|factory_girl|factory_bot|"
RSpecの修正
FactoryBotの静的属性の定義がdeprecateになりました。こちらはFactoryBot 5で削除される予定です。これからは、動的属性になるようにRSpecの記述をブロック形式にする必要があります。
https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/FactoryBot/AttributeDefinedStatically
To automatically update from static attributes to dynamic ones,
install rubocop-rspec and run:
rubocop \
--require rubocop-rspec \
--only FactoryBot/AttributeDefinedStatically \
--auto-correct
Gemfileにrubocop-rspecを追加して全RSpecファイルを修正します。
gem "rubocop-rspec"
% bundle exec rubocop \
--require rubocop-rspec \
--only FactoryBot/AttributeDefinedStatically \
--auto-correct
before_skip_filter, before_filterをbefore_actionに変更
Rails5.1以降skip_before_filter,before_filterがdeprecateされるので、今のうちに削除します。
DEPRECATION WARNING: skip_before_filter is deprecated and will be removed in Rails 5.1. Use skip_before_action instead.
before_action has not been definedの解消
Rails 5.0以降、Deviseをインストールした事により使えるヘルパーメソッドなど、コントローラには直接定義されていないbefore_actionを呼び出そうとするとArgumentError
を発生させてしまいます。
raiseによる例外処理でfalseを返すようにしてあげましょう。
before_action :authenticate_user!, raise: false
skip_callbackの置き換え
https://github.com/thoughtbot/factory_bot/issues/931#issuecomment-307542965
Rails4で使えたFoo.skip_callback(:create, :before, :sample_check)がRails5では使えません。
そこで、以下のように置き換えます。
after(:build) do
Sample.skip_callback(:create, :before, :example_check, raise: false)
end
raise: falseのオプションにより、skip_callbackの機能を維持しながらエラーメッセージを回避することができます(というかRails4の時点でもfactoryのプログラムの中にあるskip_callbackが機能していなかったぽいですね)。
Rails4のスタイルをRails5のスタイルに変更する
Deprecated style:
get :show, { id: 1 }, nil, { notice: "This is a flash message" }
New keyword style:
get :show, params: { id: 1 }, flash: { notice: "This is a flash message" },
こちらのgetやpostリクエストのparams指定、flash指定もRails5から明示的に名前を入れるようなスタイルに変更になりました。
数が多いのでgemに頼ります。
https://github.com/tjgrathwell/rails5-spec-converter
gem "rails5-spec-converter"
% bundle exec rails5-spec-converter
xhrの記述を修正
DEPRECATION WARNING: `xhr` and `xml_http_request` are deprecated and will be removed in Rails 5.1.
xhrの記述も変更になったので、以下のように変更します。こちらは、 rails5-spec-converterで機械的に修正できないため手動で修正します。
- subject { xhr :get, :favorite }
+ subject { get :favorite, xhr: true }
uniqをdistinctに変更
DEPRECATION WARNING: uniq is deprecated and will be removed from Rails 5.1 (use distinct instead)
Railsのuniq
メソッドがdeprecatedになったので、distinct
に置き換えます。
deviseの更新
https://github.com/plataformatec/devise/issues/4171
上記のエラーに引っかかったため、deviseの更新を行います。
deviseのparameter展開を修正
DEPRECATION WARNING: [Devise] Changing the sanitized parameters through "Devise::ParameterSanitizer#for(sign_up) is deprecated and it will be removed from Devise 4.1.
Please use the `permit` method:
deviseのdeprecation warningも上述の通りに修正します。
また、deviseが更新されるにあたってDeviseコントローラ側でもCSRFの対策が必須となりました。
が、当社ではapplication_controller側で既にCSRFトークンを発行する設定を行なっているので、deviseライブラリの方でまたCSRF対策する必要がなかったため、 protect_from_forgery prepend: true
をdeviseコントローラをオーバライドしているコントローラに追記します。これにより、protect_form_forgeryコールバックがtrueを返せるため、deviseコントローラ側ではCSRF対策を防止することができます。
protect_from_forgery prepend: true
だんだん疲れてきました。
redirect_to :backを置き換え
redirect_to :backもRails 5.1以降使えなくなるため、以下のように修正します。
こちらも同じように修正します。
- redirect_to(:back)
+ redirect_back(fallback_location: root_path)
レスポンスボディが空なものはhead
DEPRECATION WARNING: `:nothing` option is deprecated and will be removed in Rails 5.1. Use `head` method to respond with empty response body.
renderのnothing: trueが5.1以降使えなくなるので、代わりにheadメソッドを使うようにします。
- render nothing: true
+ head :no_content
findを使うときは必ずidを指定してあげる
DEPRECATION WARNING: You are passing an instance of ActiveRecord::Base to `find`. Please pass the id of the object by calling `.id`.
Rails5以降ではなく4.2から出る警告になりますが、5.1からArgumentErrorになるため、これも修正します。
- ***** = Sample.find(tmp_*****)
+ ***** = Sample.find(tmp_*****.id)
enumの返り値がキーになったので修正
social_type
=> "social_type_sns"
enumでマッピングの値の方ではなく、キー名が返ってくるようになったので、これをRails4の時のように値を返すようにメソッド名を変更します。
social_type_before_type_cast
=> 1234
belongs_toで関連付けしている時はnullなデータが入るのを許可しないようになったので、optional: trueで回避
そもそも、belongs_toで紐づけられている子エンティティが親エンティティを参照できていないのが問題な気がしますが、ひとまず、optional: true
で回避します。
RailsでArrayのメソッドが直接使えなくなったので、配列変換
http://kamina-dev.hatenablog.com/entry/2018/01/17/175830
4.2 までは ActiveRecord::Relation の #method_missing で一部メソッドを Arrayに delegate していた
5.0 からはその処理が削除され、その代わりに Enumerable を include するように修正された
そのため、ほとんどのメソッドはそのまま使えるが、 #slice は Enumerable に存在しないため、使えなくなった
unshiftもsliceと同じく、使えなくなったため、コード的にどうかと思いますが、一先ずActiveRecord経由で取得したレコードを配列変換して解決します。
Admin.where(id: 100).to_a.unshift(current_user)
render_templateなどのSpecテストはひとまずコメントアウト
Rails5になったに伴い、controller_specで書いていたものはrequest_specに書くように推奨されています。
今後のcontroller_specでは、assignsとassert_templateの使用が非推奨となってる模様です。
直ぐに用意できないのであればひとまずコメントアウトで凌ぎましょう。
その他
その他はプロジェクト毎に必要・不必要なものがありますので、Optionalとして呼んでください。
XML シリアライズ
今後もXMLシリアライズを使うには、Gemfileにgem 'activemodel-serializers-xml'
を追加します。
Railsテストのヘルパーメソッド用のgemを追加
assignsメソッドとassert_templateメソッドがrails-controller-testing gemに移転したため、これらを今後も使う場合はGemfileに以下を追加します。
gem "rails-controller-testing"
draperの更新
Draperはrails/test_unit/sub_test_task
のライブラリをrequireしていますが、Rails5以降はrailties/lib/rails/test_unit
にいくつかのライブラリが移動しました。Draperが新しいライブラリをサポートできるようにDraperも3以上に更新します。
https://github.com/drapergem/draper/issues/681
image_tagがnilを許容できなくなったのでpresent?メソッドで回避
https://stackoverflow.com/questions/37530024/rails-5-nil-is-not-a-valid-asset-source
present?メソッドでActionView::Template::Error:を回避します。
where_valuesをorメソッドで置き換え
where_valuesメソッドが廃止となりました。
代わりにRails5から追加されたorメソッドを使います。
https://blog.bigbinary.com/2016/05/30/rails-5-adds-or-support-in-active-record.html
Rails5のproduction環境でのAutoloadが無効になったので、再度有効化する
Rails5ではデフォルトでAutoLoadがdisabledになったため、config/application.rbで有効にしておきます。
config.enable_dependency_loading = true
websocket_railsがRails5で動くようにする
Rails5以降において、websocketによる双方向通信を実現するためには、ActionCable
が簡単です。しかし、本システムでは既にwebsocket_railsによってwebsocketによる双方向通信の機能を実装しているため、ActionCableによる実装を行うまでは、websocket_railsが動くようにします。
基本的にGemのversion関連による不具合が多かったため、下記の記事を参考にgemのversionを固定しておきます。
Sprockets 3以降、コンパイルしていないCSS、画像などのリソースファイルがapplication.assetsで読み込めなくなった
https://github.com/rails/sprockets-rails/issues/425
最初は、compass-rails
による参照で読み込めていたのですが、読み込んだCSSファイルが改行されず、タグが連結されて設定されるようになっていました。苦肉の策として今回直接参照しているファイルもprecompile対象とすることにしました。
rubyの更新
rubyを2.3から2.4にアップグレードします。今回はdeprecationが出ていた部分が外部アプリケーションのAPIレベルでしかなかったので、Rails側で特に何かすることはありませんでした。
しかし、2.4以降xmlrpc
が標準ライブラリから外れてしまったので、こちらを追加しておきます。
gem 'xmlrpc'
また、FixnumがIntegerのエイリアスになったため、関係するGemも一緒に更新します。
key must by 32 bytes エラーの解消
ruby 2.4以降openssl周りので変更があり、暗号、復号化に利用するキー文字列はきっちり32byteでないといけません。
https://qiita.com/rentalname@github/items/05eaf0afd9b8acf8f26d
Rails側で文字列を丸めてあげましょう。
ActiveSupport::MessageEncryptor.new(SECURE[0..31], CIPHER)
まとめ
ということで、Railsアップグレードの際にやったことを殴り書きですが、まとめてみました。実際にアップグレードを行った感想ですが、正直言って二度とやりたくないです←
何故なら、このようなフレームワークのアップグレードで必要なスキルはほぼリサーチ能力くらいだと思っているからです。また、ここ最近の筆者のモチベーションとして言語やフレームワーク、ツールの勉強をするよりも(RailsエンジニアやAWSやGCPと言ったクラウドエンジニアになりたい訳じゃない)、モノの考え方を重視してアーキテクチャの構造に関する知識やソフトウェアの設計能力を身に着けることが重要だと考えているので、「正直、一回はこういう経験するのは良いけどこれっきりで良いかな」とか思ってたりもします。
とは言え、何か問題が発生した時に落ち着いて対処する能力も重要だと考えているので、そういう意味では一回こういう経験をした良かったかなとは思っています。
上手くまとまっていない記事になっているかもしれませんが、ちょくちょく修正かけて良い記事にしていきます。それでは。