【Rails5】今更ながらRails4からRails5へのアップグレードをどうやっていくのが良いか調べてみた


## はじめに

みなさん、こんにちは。今回はRailsアップグレードをどうやって行っていくのか、その手順や調べた文献を共有したいと思います。

## Rails4からRails5にアップグレードします

現在、会社で運用しているRails4のシステムを今回Rails5にアップグレードします。
以前、先輩が行ったRailsアップグレードですが何らかの原因があってRevert・断念した失敗談があります。その後は、Rails4で新たな機能を作ってきた訳ですが、段々Rails4ではサポートしきれない問題が発生してきました。
特に問題だったのがフロントエンドの開発です。ここ最近は、弊社でもフロントエンドで行うこと、バックエンドで行うことをそれぞれ独立させて新機能を開発してきました。
特にフロントエンドの開発は、jQueryやcoffeeスクリプトをそろそろ止めて生JSで書くことが多くなってきました。
しかし、フロントエンドの開発においては、Railsのgemに依存するものが依然として残っており、このままRails4のまま開発していくのは辛みしかなかったので、アップグレードを急ぐこととなりました。
また、フロントエンドのテストシナリオには[Request Spec](https://relishapp.com/rspec/rspec-rails/docs/request-specs/request-spec)や[System Spec](https://relishapp.com/rspec/rspec-rails/docs/system-specs/system-spec)を利用する予定なため、今後の開発スピードを上げていくためにアップグレードを行うこととしました。

## 今回のアップデート

Rails 4.2.0 -> 5.0.7
ruby 2.3.6 -> 2.4.5

~~今回はrubyのversionがRails５でも必須バージョンよりも上なため、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://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](https://techracho.bpsinc.jp/hachi8833/2018_11_14/64279)

基本的には、前回RevertされたPRから、秘伝のタレのような修正がありますので、今回はほぼそれを参考にしています。上二つはRailsアップグレードを行うためにはどうすればいいのか、大まかな所を調べるために読みました。

## アップグレード手順

1. [bundle updateによるコンフリクトを解消する](#bundle)
2. [app updateによるアップデートタスクを叩く](#update)
3. [ActiveRecord::Base継承の全モデルをApplicationRecord継承に置き換える](#application_record)
4. [DEPRECATION WARNINGの解消](#deprecation)
5. [その他](#other)
6. [rubyの更新](#ruby)

## <a name="bundle">bundle updateによるコンフリクトを解消する </a>

まずは単純にRailsのversionをあげてみましょう。

```ruby:Gemfile
- gem 'rails', '4.2.0'
+ gem 'rails', '~> 5.0.0'
```

```bash
% 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](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`をかけます。

```bash
% bundle update
```

## <a name="#update">app updateによるアップデートタスクを叩く</a>

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タスクにはそのような機能はありませんでした...)

## <a name="application_record">ActiveRecord::Base継承の全モデルをApplicationRecord継承に置き換える</a>

Rails5.0以降では全モデルがApplicationRecordをスーパークラスとして使うようになりました。という事で、、全モデルから継承されるスーパークラスのモデルとなるapp/models/application_record.rb を作成します。

```ruby:app/models/application_record.rb
class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true
end
```

あとは他のモデルをApplicationRecordから継承されるように正規表現で置き換えを行います。

`./app/models`配下のrubyファイル(ただし、スーパークラスとなるapplication_record.rbは覗く)を全てApplicationrecordから継承されるように置き換えを行います。

```bash
% find ./app/models/ -name "*.rb" ! -name "application_record.rb" -type f -print | xargs sed -i "s/ActiveRecord::Base/ApplicationRecord/g
```

## <a name="deprecation">DEPRECATION WARNINGの解消</a>

Rails5および5.1以降でDEPRECATIONになるコードや設定を正規表現で置き換えを行い、修正をかけます。

### FactoryBotへの置き換え

```bash
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](https://github.com/thoughtbot/factory_bot/blob/v4.9.0/UPGRADE_FROM_FACTORY_GIRL.md)

```bash
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](https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/FactoryBot/AttributeDefinedStatically)

```bash
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ファイルを修正します。

```bash:Gemfile
gem "rubocop-rspec"
```

```bash
% 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されるので、今のうちに削除します。

```bash
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を返すようにしてあげましょう。

```ruby
before_action :authenticate_user!, raise: false
```

### skip_callbackの置き換え

[https://github.com/thoughtbot/factory_bot/issues/931#issuecomment-307542965](https://github.com/thoughtbot/factory_bot/issues/931#issuecomment-307542965)

Rails４で使えたFoo.skip_callback(:create, :before, :sample_check)がRails5では使えません。
そこで、以下のように置き換えます。

```ruby
  after(:build) do
    Sample.skip_callback(:create, :before, :example_check, raise: false)
  end
```

raise: falseのオプションにより、skip_callbackの機能を維持しながらエラーメッセージを回避することができます(というかRails4の時点でもfactoryのプログラムの中にあるskip_callbackが機能していなかったぽいですね)。

### Rails4のスタイルをRails5のスタイルに変更する

```bash
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](https://github.com/tjgrathwell/rails5-spec-converter)

```ruby:Gemfile
gem "rails5-spec-converter"
```

```bash
% bundle exec rails5-spec-converter
```

### xhrの記述を修正

```bash
DEPRECATION WARNING: `xhr` and `xml_http_request` are deprecated and will be removed in Rails 5.1.
```

xhrの記述も変更になったので、以下のように変更します。こちらは、 `rails5-spec-converterで機械的に修正できないため手動で修正します。`

```ruby
- subject { xhr :get, :favorite }
+ subject { get :favorite, xhr: true }
```

### uniqをdistinctに変更

```bash
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](https://github.com/plataformatec/devise/issues/4171)

上記のエラーに引っかかったため、deviseの更新を行います。

#### deviseのparameter展開を修正

```bash
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対策を防止することができます。

```ruby:app/controllers/example/sessions_controller.rb

  protect_from_forgery prepend: true
```

だんだん疲れてきました。

### redirect_to :backを置き換え

redirect_to :backもRails 5.1以降使えなくなるため、以下のように修正します。

こちらも同じように修正します。

```ruby
- redirect_to(:back)
+ redirect_back(fallback_location: root_path)
```

### レスポンスボディが空なものはhead

```bash
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メソッドを使うようにします。

```ruby
- render nothing: true
+ head :no_content
```

### findを使うときは必ずidを指定してあげる

```bash
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になるため、これも修正します。

```ruby
- ***** = Sample.find(tmp_*****)
+ ***** = Sample.find(tmp_*****.id)
```

### enumの返り値がキーになったので修正

https://qiita.com/yusuke-matsuda/items/df05c8165e2f084023b0

```bash
 social_type
=> "social_type_sns"
```

enumでマッピングの値の方ではなく、キー名が返ってくるようになったので、これをRails4の時のように値を返すようにメソッド名を変更します。

```bash
social_type_before_type_cast
=> 1234
```

### belongs_toで関連付けしている時はnullなデータが入るのを許可しないようになったので、optional: trueで回避

https://github.com/rails/rails/issues/18233

そもそも、belongs_toで紐づけられている子エンティティが親エンティティを参照できていないのが問題な気がしますが、ひとまず、`optional: true`で回避します。

### RailsでArrayのメソッドが直接使えなくなったので、配列変換

[http://kamina-dev.hatenablog.com/entry/2018/01/17/175830](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経由で取得したレコードを配列変換して解決します。

```ruby
Admin.where(id: 100).to_a.unshift(current_user)
```

### render_templateなどのSpecテストはひとまずコメントアウト

Rails5になったに伴い、controller_specで書いていたものはrequest_specに書くように推奨されています。
今後のcontroller_specでは、assignsとassert_templateの使用が非推奨となってる模様です。
直ぐに用意できないのであればひとまずコメントアウトで凌ぎましょう。

## <a name="other">その他</a>

その他はプロジェクト毎に必要・不必要なものがありますので、Optionalとして呼んでください。

### XML シリアライズ

今後もXMLシリアライズを使うには、Gemfileに`gem 'activemodel-serializers-xml'`を追加します。

### Railsテストのヘルパーメソッド用のgemを追加

[https://railsguides.jp/upgrading_ruby_on_rails.html#rails-%E3%82%B3%E3%83%B3%E3%83%88%E3%83%AD%E3%83%BC%E3%83%A9%E3%81%AE%E3%83%86%E3%82%B9%E3%83%88](https://railsguides.jp/upgrading_ruby_on_rails.html#rails-%E3%82%B3%E3%83%B3%E3%83%88%E3%83%AD%E3%83%BC%E3%83%A9%E3%81%AE%E3%83%86%E3%82%B9%E3%83%88)

assignsメソッドとassert_templateメソッドがrails-controller-testing gemに移転したため、これらを今後も使う場合はGemfileに以下を追加します。

```ruby: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](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](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](https://blog.bigbinary.com/2016/05/30/rails-5-adds-or-support-in-active-record.html)

### Rails5のproduction環境でのAutoloadが無効になったので、再度有効化する

* [https://qiita.com/joooee0000/items/3ab0f3d791e0d0beb639](https://qiita.com/joooee0000/items/3ab0f3d791e0d0beb639)

Rails５ではデフォルトでAutoLoadがdisabledになったため、config/application.rbで有効にしておきます。

```ruby:config/application.rb
config.enable_dependency_loading = true
```

### websocket_railsがRails5で動くようにする

Rails5以降において、websocketによる双方向通信を実現するためには、`ActionCable` が簡単です。しかし、本システムでは既にwebsocket_railsによってwebsocketによる双方向通信の機能を実装しているため、ActionCableによる実装を行うまでは、websocket_railsが動くようにします。

[ActionCableの概要](https://railsguides.jp/action_cable_overview.html)

[websocket_rails](https://github.com/websocket-rails/websocket-rails)

基本的にGemのversion関連による不具合が多かったため、下記の記事を参考にgemのversionを固定しておきます。

[第34回　あのGemは今 WebSocket編 (松永紘)](https://www.school.ctc-g.co.jp/columns/masuidrive/masuidrive34.html)

### Sprockets 3以降、コンパイルしていないCSS、画像などのリソースファイルがapplication.assetsで読み込めなくなった

[https://github.com/rails/sprockets-rails/issues/425](https://github.com/rails/sprockets-rails/issues/425)

最初は、`compass-rails`による参照で読み込めていたのですが、読み込んだCSSファイルが改行されず、タグが連結されて設定されるようになっていました。苦肉の策として今回直接参照しているファイルもprecompile対象とすることにしました。

## <a name="ruby">rubyの更新</a>

rubyを2.3から2.4にアップグレードします。今回はdeprecationが出ていた部分が外部アプリケーションのAPIレベルでしかなかったので、Rails側で特に何かすることはありませんでした。
しかし、2.4以降`xmlrpc`が標準ライブラリから外れてしまったので、こちらを追加しておきます。

```bash:Gemfile
gem 'xmlrpc'
```

[https://stackoverflow.com/questions/41784012/ruby-fog-gem-causing-server-not-to-run-cannot-load-such-file-xmlrpc-client](https://stackoverflow.com/questions/41784012/ruby-fog-gem-causing-server-not-to-run-cannot-load-such-file-xmlrpc-client)

また、FixnumがIntegerのエイリアスになったため、関係するGemも一緒に更新します。

[https://stackoverflow.com/questions/41463999/warning-constant-fixnum-is-deprecated-when-generating-new-model](https://stackoverflow.com/questions/41463999/warning-constant-fixnum-is-deprecated-when-generating-new-model)

### key must by 32 bytes エラーの解消

ruby 2.4以降openssl周りので変更があり、暗号、復号化に利用するキー文字列はきっちり32byteでないといけません。
https://qiita.com/rentalname@github/items/05eaf0afd9b8acf8f26d

Rails側で文字列を丸めてあげましょう。

```ruby
ActiveSupport::MessageEncryptor.new(SECURE[0..31], CIPHER)
```

## まとめ

ということで、Railsアップグレードの際にやったことを殴り書きですが、まとめてみました。実際にアップグレードを行った感想ですが、正直言って二度とやりたくないです←
何故なら、このようなフレームワークのアップグレードで必要なスキルはほぼリサーチ能力くらいだと思っているからです。また、ここ最近の筆者のモチベーションとして言語やフレームワーク、ツールの勉強をするよりも(RailsエンジニアやAWSやGCPと言ったクラウドエンジニアになりたい訳じゃない)、モノの考え方を重視してアーキテクチャの構造に関する知識やソフトウェアの設計能力を身に着けることが重要だと考えているので、「正直、一回はこういう経験するのは良いけどこれっきりで良いかな」とか思ってたりもします。
とは言え、何か問題が発生した時に落ち着いて対処する能力も重要だと考えているので、そういう意味では一回こういう経験をした良かったかなとは思っています。

上手くまとまっていない記事になっているかもしれませんが、ちょくちょく修正かけて良い記事にしていきます。それでは。

## 参考文献

* [Rails 5 adds OR support in Active Record](https://blog.bigbinary.com/2016/05/30/rails-5-adds-or-support-in-active-record.html)
* [rails 4.2.10とruby 2.4.3 で `key must be 32 bytes`の例外が発生する](https://qiita.com/rentalname@github/items/05eaf0afd9b8acf8f26d)
* [Rails5でコントローラのテストをController specからRequest specに移行する](https://qiita.com/t2kojima/items/ad7a8ade9e7a99fb4384)