ハトネコエ Web がくしゅうちょう

プログラミングやサーバー・Web制作、チームマネジメントなど得た技術のまとめ

Rails 4 から Rails 5 へのアップデートでおこなった10個以上の対応

関わっている会社の Rails アプリを 4 から 5 に上げましたので、おこなったことを書いておきます。

1. なぜ上げたかったか

Rails 5 は多くの便利メソッドが足されていて開発がいっそう効率的になる、だとか、
世の中で書かれている記事の多くが Rails 5 を対象にしているものになってきた、だとか、
Rails 6 が安定してきて、Rails 4 の対応を外す gem がそろそろ増えていきそう、などの理由があります。

そして一番大きかった理由が Ruby 2.6 にした場合に deprecation warning が出てしまったこと、ここにありました。

/bundle/gems/activesupport-4.2.11.3/lib/active_support/core_ext/object/duplicable.rb:111: warning: BigDecimal.new is deprecated; use BigDecimal() method instead.

実は Rails 4 は v4.2.11.3 が 2020/05/15 にリリースされるなど、
今でもしっかりセキュリティメンテナンスがおこなわれています。

しかし Ruby 2.5 については来年2021年3月にはサポート終了することが
Ruby のリリースサイクル上決まっています。

deprecation warning を無視して Ruby 2.6 に上げる手段も取れますが、
この警告が出ているということは Ruby 2.7 ではメソッドが削除されることを示唆されており、*1
そのような警告を無視したまま Ruby のバージョンを上げてしまうのは危険です。

deprecation warning が確認できない状態に周辺を揃えてから Ruby のバージョンは上げるべきです。

……というわけで、セキュリティサポートの対象から外れないよう
Rails 4 を2021年3月までに卒業しなくては、という意識でいました。
(最悪警告を無視して Ruby 2.6 に上げるとしても、それのサポート終了は 2022年3月)

2. 下準備

Rails 5 に上げることは頭の中にあったので、着々と準備を進めていました。

関わり始めた2019年12月の時点では、

でしたので、リリースノートの確認とステージングでの入念な確認をおこないつつ、

  • Ruby 2.4.9 へのバージョンアップ
  • Rails 4.2.11.1 へのバージョンアップ
  • Ruby 2.5.7 へのバージョンアップ
  • Rails 4.2.11.3 へのバージョンアップ

を順々におこなっていきました。

Ruby のバージョンアップや Rails のパッチバージョンアップはわりと破壊的変更が少ないので、期待していたとおり特に新たな対応をおこなう必要なくバージョンアップをおこなえました。
Ruby 2.5 系で CSV.generate のバグがあるのでそこだけ注意ですかね。

また、不必要な gem の削除や、gem のバージョンアップを可能な限りおこない、
依存関係上 Rails 5 に上げる妨げとなる refile からの脱却もするなど(単純に refile で出来ないことがあったのが一番の理由だけど)、
着々と準備を進めてきました。

おかげで、 Gemfile 内を

gem 'rails', '5.0.0'

と書き換えた上で bundle update rails をおこなってもエラーが起こらない状態にまではなっていました。

テストについては、gem の機能そのものを試しているような無駄なテストがある一方で、
モデルやビューなどのテストがほぼ皆無でしたので、機会を見つけて少しずつ(ほんの少しですが)足していました。

3. Rails 5 に上げた際の作業内容

主にこちらの記事を参考にしました。

3-1. Rails 5.2.4.3 を選択

最初は Rails 5.0.0 にアップデートして少しずつ上げていく予定でしたが、
v5.0.0 では bin/rails app:update をおこなうと

undefined method `instance' for ActiveSupport::Deprecation:Class (NoMethodError)

とエラーが出てしまい、これを解決するには v5.0.6 を使うという話を見て、
「そういう Rails 5 の初期の方のバグを踏むくらいなら、はじめから Rails 5 の最新版を使ったほうがいいのでは」という気持ちになってきて、
5系の最新である v5.2.4.3 にアップデートすることに決めました。

……ただし、これはサービス利用者があまり多くなく、ふだんのDB書き込み量から考えて、
ある程度の問題が起きても対応できると考えての判断なので、
通常は Rails 5.0 → 5.1 → 5.2 と上げていくことをおすすめします。

Rails 5.0 で deprecated warning、Rails 5.1 でメソッド削除、などの変更が、
特に Rails 5.0 と 5.1 間では多いですから、慎重を取るに越したことはありません。

Gemfile に

gem 'rails', '5.2.4.3'

と記載したら

bundle update rails

です。

3-2. bin/rails app:update

bin/rails app:update

で config ファイルの更新などがおこなえます。

上書きするか聞かれる時に「d」を押せば diff が見られますので、
それを見て上書きするか考えると良いでしょう。

diff を見ることで、「あれ? もしかしてこの config ってなくなってる?」などに気付けます。
例えば config.raise_in_transactional_callbacks なんかは Rails 5.0 で deprecated、Rails 5.1 で削除されています。

3-3. まずは web サーバーが起動するようになるまで

私の場合は routing に問題があって引っかかりました。

resources :user_feedbacks, only: :index do
  get "/:year/:month", on: :collection, to: :monthly_index, as: :monthly
end

と書かれている場所が「controller 指定しないとダメよ〜」ってエラーが出ていたので、

resources :user_feedbacks, only: :index do
  get "/:year/:month", on: :collection, to: "user_feedbacks#monthly_index", as: :monthly
end

と直しました。

Web サーバーが起動しない系の問題は、明確なエラーが出るのでここはなんとかなることでしょう。

3-4. Single Table Inheritance

STI (Single Table Inheritance) の仕組みを使っている箇所があったのですが、これが通常の Rails way に乗っかっていない書き方であったため少し苦労しました。 親のモデルに self.find_sti_class(type) メソッドと self.sti_name メソッドをいい感じに定義してあげることでなんとかしました。

どんな感じかと言うと、Music モデルから派生する Jazz モデル、Rock モデルがあるとして、
通常は Music.find(123).is_a?(Jazz) みたく判定させればいいのですが、
Music.find(123).jazz? で判定させたかったようで、

class Music < ActiveRecord::Base
  enum type: { jazz: "Jazz", rock: "Rock" }
end

このように type カラムについての定義がありました。
しかしこれだと、 jazz モデルは存在しないので
SubclassNotFound エラーを吐いてしまいます。

そういうわけで、 find_sti_class, sti_name メソッドを上書きするように

class Music < ActiveRecord::Base
  enum type: { jazz: "Jazz", rock: "Rock" }

  class << self
    def find_sti_class(type)
      type.camelize.constantize
    end

    def sti_name
      types[name.demodulize.underscore]
    end
  end
end

と書いたわけです。本来のコードとは離れるので、動くけど微妙感もあります。

今思いついたんですけど、enum 的な使用が jazz?, rock? メソッドだけなのであれば、

以下のような対処でよかったですね。
不必要に override しちゃうのは今後問題を起こす可能性があるので、これでもいけるか後で見ておきます。

class Music < ActiveRecord::Base
  def jazz?
    is_a?(Jazz)
  end

  def rock?
    is_a?(Rock)
  end
end

余談ですが、Single Table Inheritance のような機能は、
サービス運営を続けていくうちに共通部分がほとんどなくなる、というケースが往々にしてあるので、できれば使わないほうがいいと思います。

3-5. belongs_to にデフォルトでバリデーションが働くようになったことへの対応

Rails 5 への変更で一番大きかったのはここだと思います。

class Song < ActiveRecord::Base
  belongs_to :vocal
end

のような定義がある場合、Song 作成時・更新時に vocal_id が nil だと
バリデーションエラーが出るようになりました。

vocal_id が nil の場合がありえるのであれば、

class Song < ActiveRecord::Base
  belongs_to :vocal, optional: true
end

と書かなくてはいけません。optional: true を付ける必要があります。

逆に言えば、今まで

class Song < ActiveRecord::Base
  belongs_to :vocal
  validates :vocal_id, presence: true
end

となっていたのであれば、 validates の行を消しても問題ないわけですね。

この対応はしっかりおこなわないと、
アプリケーションエラーが起こらないぶん、ユーザーが困っているのに開発者が気付かない、ということが起こりえますので充分に確認しましょう。
自信がなければすべてに optional: true を付けてしまうのも、無くはない選択だと思います。

3-6. skip_before_filter を skip_before_action に

xxxxx_filter コールバックは無くなり xxxxx_action に統一されましたので、 skip_before_filterskip_before_action に、 before_filterbefore_action に変更しましょう。

3-7. create.js → create.js.erb に

Rails非同期通信処理をおこないたい場合に、
create.js というファイル名でも ERB 記法を埋め込むことが出来ましたが、
.erb の拡張子でないとおこなえないように変更されていますので修正しました。

3-8. Ridgepole に --allow-pk-change オプションを付ける

Rails 5 では、id カラムのデフォルトの型が integer から bigint に変更されました。

id の型を bigint にしたい場合は、ridgepole の実行に --allow-pk-change オプションを付与することをお勧めします。
このオプションを使用するには、ridgepole が v0.7.1 以上である必要があります。

オプションを付けていないと rspec 実行時にこんな warning を吐いたりします。

[WARNING] Primary key definition of `versions` differ but `allow_pk_change` option is false
  from: {:id=&gt;:serial}
    to: {}

もしくは、id の型を integer のままにする選択もあります。
テーブル作成のオプションとして MySQL の場合は id: :integerPostgreSQL の場合は id: :serial を追加します。
参考: https://kamihikouki.hatenablog.com/entry/2017/04/11/141447

私は integer のままにする選択を取りました。
vocal_id などの外部キーと関連するカラムも bigint に変更する必要がありますし、
bigint は integer の2倍の 8 Byte ですから、多少データベースサイズが大きくなってしまうことが予想されます。

integer で足りないということは21億行を超えるデータ数となるわけですが、
将来的にそうなりそうなテーブルはほとんどなかったため、 bigint に変えない選択を取りました。
必要に応じて bigint に変更していけばいいと思います。

3-9. autoload 廃止への対策

Rails 5 では autoload が development 環境以外では効かなくなりました

この変更はけっこう悪魔的で、
development 環境では autoload が効くので、
本番(ステージング)にリリースした時に初めて動かないことに気付く、ということが起こりえます。

詳しくはこちらをお読みください ↓
https://qiita.com/joooee0000/items/3ab0f3d791e0d0beb639

いくつかの解決方法が述べられていますが、Rails 4 からのバージョンアップの場合であれば、

config.enable_dependency_loading = true

config/application.rb に足して autoload を本番環境でも有効にする、というやり方が一番安全だと思います。
autoload を廃止しようという動きからこの変更がなされたことを考えると邪道に思いますが、
開発環境で動くのに本番環境で動かない、という恐ろしい罠を避けるには安全を取りたいかなと……。

autoload_paths に追加しているのが lib だけならば、

config.paths.add "lib", eager_load: true

と書くやり方でいいと思います。

3-10. time 型の挙動変更への対応

Rails 5.1 から time 型はデフォルトでタイムゾーンに対応しました。
参考: https://blog.willnet.in/entry/2015/06/12/063731

これにより、データベースのタイムゾーンUTC、アプリケーションのタイムゾーンが日本時間の場合、
Rails 4 では日本時間 19:00 を示す場合、データベースにも 19:00 と保存されていましたが、
Rails 5.1 ではデータベースに 10:00 と保存されるようになります。

つまり!

Rails 4 の時代にデータベースに 19:00 と保存されているデータが、
Rails 5.1 では日本時間 04:00 と解釈されます! 時刻がズレます!!

これを防ぐためには、 config/application.rb

config.active_record.time_zone_aware_types = [:datetime]

と記述しましょう。

私はリリース後にこの問題に気付いてしまったので、 config を変えるのではなく
保存されてるデータを 9 時間前にして保存し直すスクリプトを書いて対応しましたが、
ユーザーから見える時間がズレることが1秒たりとも許されない場合は、
上で書いた config を上書きする対応をするしかないかと思います。

3-11. その他いろいろ

autoprefixer に「display: box; じゃなくて display: flex; を使うようにして」って警告出されたので対応したり、

validates :office_email, format: { with: VALID_EMAIL_REGEXP }, if: "office_email.present?"

のように validates の if 内で String を使う方法は廃止されたので、

validates :office_email, format: { with: VALID_EMAIL_REGEXP }, if: Proc.new { |vocal| vocal.office_email.present? }

というふうに直したり、 enum の扱いの微妙な違いなど、
細かい修正をいくらかおこないました。

モデルについての仕様変更が多いように感じました。
主要なモデルのテストを作っておいたために気付けたことが多く、テストの偉大さを再認識しました。

3-12. あとでやればいいこと

  • ActiveRecord::Base を継承した ApplicationRecord を作成し、model はそれを継承するようにする
  • form_for, form_tag から form_with への切り替え
  • jquery_ujs を rails-ujs に変える

これらも Rails 5 においての大きな変化ですが、急いでおこなう必要はないです
Rails 5 対応は大きなプルリクエストになりますから、必要以上に diff を増やしてレビュアーの負担を増やす必要もないと思います。

class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true
end

ApplicationRecord クラスを作ってそれを継承するようにすることは、
文字列置換をすればササッとできることなのでやってもいいと思いますが、
Rails 5 対応リリース後の別のプルリクで良いでしょう。

form_with に切り替えるのは、失敗した場合の影響が大きいので、
remote: true がデフォルトになっていることなどの理解を深めつつ、
Rails 5 対応リリース後に少しずつ進めていきましょう。

jquery_ujs を rails-ujs に切り替えるのも注意が必要です。
例えば SweetAlert2 のような Rails の操作に関係する JavaScript ライブラリを使っている場合、実装方法を大きく変える必要があります

rails-ujs は jQuery 依存をなくすために新たに1から書き直されてる別のライブラリだという認識を持ち、
現在 jQuery への依存があるなら「別に変えなくてもいいか」という気持ちも大切です。

フロントエンドにおけるミスは、開発者が気付かずにユーザーが困って初めて気付く、ということが起こりがちですので。

4. Rails 5 対応をリリースしての感想

ステージングでの1週間程度の様子見のあと、本番リリースしました。

作業前にRailsガイドで触れてる変更点なんかを眺める限りだと、
「関係ありそうな箇所はそんなに多くないかな」と思ってしまうんですけれど、
実際に作業してみるとけっこう対応しないといけない点ありましたね……。

3-10. で書いたように、time 型の挙動の違いについてはリリース後に気付くという失敗をしでかしたのであせりましたが、
データベースに影響する問題はそれくらいでしたのでよかったです。
リリースしてから2日程度はそれらバグ対応をおこないましたが、その後は安定しています。

特にリリースしてサイト速度が上がったなどの実感はありませんが、
Rails 5 に上げられたことで Rails 6 へのバージョンアップの道もだいぶ見えてきました。
今年12月にリリースされる予定の Ruby 3.0 にも対応できるよう、着々と準備を進めていきたいですね!

*1:実際エラーになるようです https://tech.recruit-mp.co.jp/server-side/post-19932/