クラウド経費 の開発を担当している野田 (@quanon_jp) と申します。趣味はヨーヨーで、プログラムを書いてる最中も頭の中はヨーヨーのことでいっぱいです。
同じチームで働くみやむー (@miyamura.koyo) が 2024/10 に以下の記事を執筆しました。
今回はその続編です。Ruby on Rails (以下 Rails) のバージョンを 7.0 から 7.1 に更新しました。前回の記事と重複する内容もありますが、改めてその記録を残しておきます。
- Rails 7.0 から 7.1 に更新する動機
- 進め方
- 実際にやったこと
- コードの修正
- to_s を to_fs に置き換えた
- Model.table_name からデータベース名を取り除いた
- alias_attribute を alias_method に置き換えた
- slim を更新した
- 数値以外の配列で sum を使用していた箇所を別のメソッドに置き換えた
- ActiveSupport::MessageEncryptor の内部実装に依存したコードを修正した
- permit されていない params で to_h を呼び出さないようにした
- after_commit を after_save に置き換えた
- serialize メソッドで type を指定するようにした
- controller.request=, controller.response= を controller.set_request!, controller.set_response! に置き換えた
- response.parsed_body の返り値の型が変わったので修正した
- MemCacheStore と RedisCacheStore がコネクションプールを使用しないようにした
- その他
- コードの修正
- まとめ
Rails 7.0 から 7.1 に更新する動機
クラウド経費・クラウド債務支払の Web アプリケーションは Rails で構築されています。2025/01 時点で使用していた Rails のバージョンは 7.0 でした。このバージョンはサポート終了日である 2025/04/01 (火) が迫っており、早急な更新が求められています。
Ruby on Rails — Maintenance policy より
List of currently supported releases
8.0.x - Supported until November 7, 2026
7.2.x - Supported until August 9, 2026
7.1.x - Supported until October 1, 2025
7.0.x - Supported until April 1, 2025
前回の 6.1 から 7.0 へのメジャーバージョン更新とは異なり、今回は 7.0 から 7.1 へのマイナーバージョン更新です。そのため前回よりは楽だろうと高を括っていました。しかし Rails は巨大なフレームワークであり、マイナーバージョン更新でも数々の破壊的変更が行われます。今回も時間と労力を掛けて更新を行いました。
進め方
更新内容を調査する
Rails ガイドを読み、どのような更新が行われたのか事前に調べておきましょう。
また、ruby-jp(Ruby プログラマ同士の交流を目的とした Slack ワークスペース) の Scrapbox に Rails 7.1 の更新内容や更新事例がまとめられたページがあるので、それを参考にするのもよいでしょう。
Rails を更新する
まず Gemfile を書き換え、Rails のバージョンを 7.1 の最新バージョンに更新します。
# Gemfile gem 'rails', '7.1.5.1'
$ bundle update --conservative rails
そして app:update
というコマンドを実行し、新しい Rails のバージョンで新しく必要になるファイルや設定ファイルの生成と更新を行います。
$ bin/rails app:update
全件テストを実行する
単体テストを全件実行し、失敗するテストケースを洗い出します。クラウド経費・クラウド債務支払と合計して約 41,000 件のテストケースのうち 4,300 件が失敗しました。失敗の原因を地道に取り除いていきます。
非推奨警告 (DEPRECATION WARNING) を検知する
先ほどの単体テストでテストの失敗と同時に非推奨警告も洗い出して取り除きます。警告を全て取り除いたら、本番のアプリケーションで以下のコードを動かし、対応が漏れている警告を検知し、取り除きます。
# config/initializers/deprecation_log.rb # Rails 7.0 if Rails.env.production? ActiveSupport::Notifications.subscribe('deprecation.rails') do |_name, _start, _finish, _id, payload| next if payload[:message].blank? body = <<~BODY.chomp #{payload[:message]} (Gem: #{payload[:gem_name] || '--'}, Removed in: #{payload[:deprecation_horizon] || '--'}) BODY body << ("\n" + payload[:callstack].first(5).join("\n")) if payload[:callstack].is_a?(Array) Rails.logger.warn(body) end end
実際にやったこと
コードの修正
to_s を to_fs に置き換えた
https://railsguides.jp/7_0_release_notes.html#active-support-%E9%9D%9E%E6%8E%A8%E5%A5%A8%E5%8C%96
フォーマットを #to_s に渡すことが非推奨化された。今後 Array, Range, Date, DateTime, Time, BigDecimal, Float, Integer では #to_fs を使うこと。
to_s
にフォーマットを渡すことが非推奨になっていたため、すべて to_fs
に置き換えました。単純な呼び出しだけでなく Object#try
を使用して呼び出している場合もあるので注意です。
count.to_s(:delimited) # ↓ count.to_fs(:delimited) user.created_at.try(:to_s, :short_jp) # ↓ user.created_at.try(:to_fs, :short_jp)
Model.table_name からデータベース名を取り除いた
モデルの table_name
にデータベース名が含まれている箇所をテーブル名のみにしました。
class User < ApplicationRecord self.table_name = 'hoge_production.fuga_users' end # ↓ class User < ApplicationRecord self.table_name = 'fuga_users' end
データベースには MySQL を使用しているのですが、table_name
にデータベース名を含めていると、insert_all
あるいは insert_all!
を呼び出した際の INSERT 文がシンタックスエラーになるためです。
-- Rails 7.0 INSERT INTO `hoge_production`.`fuga_users` (...) VALUES (...) ON DUPLICATE KEY UPDATE `foo_id`=`foo_id` -- ↓ -- Rails 7.1 INSERT INTO `hoge_production`.`fuga_users` (...) VALUES (...) AS `hoge_production`.`fuga_users_values` ON DUPLICATE KEY UPDATE `foo_id`=`hoge_production`.`fuga_users_values`.`foo_id`
ただし、モデルのテーブルのデータベース名がデフォルトのものでない場合は、依然としてデータベース名を含める必要があります。そのようなモデルでは苦肉の策として、以下のように insert_all
あるいは insert_all!
を override する対応を入れ、シンタックスエラーを回避しました。
# Rails 7.1 class Member < ApplicationRecord self.table_name = 'piyo_production.members' # @override https://github.com/rails/rails/blob/v7.1.5.1/activerecord/lib/active_record/persistence.rb#L242-L244 def self.insert_all!(attributes, returning: nil, record_timestamps: nil) InsertAllInLogDB .new(self, attributes, on_duplicate: :raise, returning: returning, record_timestamps: record_timestamps) .execute end end if ActiveSupport.version >= Gem::Version.new('7.2') raise('This class implementation is for Rails 7.1.x. If you update the Rails version, please check that this override is correct.') end class InsertAllInLogDB < ActiveRecord::InsertAll private # @override https://github.com/rails/rails/blob/v7.1.5.1/activerecord/lib/active_record/insert_all.rb#L189-L191 def to_sql # Replace "AS `piyo_production`.`members_values`" with "AS `piyo_production_members_values`" # to prevent MySQL syntax error. super.gsub(/(AS `\w+)(`\.`)(\w+`)/) { "#{Regexp.last_match(1)}_#{Regexp.last_match(3)}" } end end
alias_attribute を alias_method に置き換えた
Deprecate aliasing non-attributes with alias_attribute.
(alias_attribute を使った属性以外のエイリアスを廃止します。)
という警告に従い、alias_attribute
を alias_method
に置き換えました。
アソシエーションの別名を alias_attribute
で定義していましたが、それにより nested attributes を使用している箇所の挙動が意図せず変わっていることに気づきました。alias_method
に置き換えることでその問題も解消しました。
# Rails 7.0 alias_attribute :user, :fuga_user # ↓ # Rails 7.1 alias_method :user, :fuga_user alias_method :user=, :fuga_user=
slim を更新した
slim のバージョンを 5.1.1 から 5.2.1 に更新しました。
ビューとして使用している slim ファイルで、以下のように class の定義が重複している箇所が軒並みエラーになりました。
/ Rails 7.0 - hoge = "foo" button class="#{hoge}" class="fuga"
ActionView::Template::Error: undefined method `+' for #<ActionView::OutputBuffer:0x00007f93252e2a60 @raw_buffer="foo">
このエラーは slim のバージョンを更新することで解消しました。
ただしこのバージョンアップの影響で、以下のように slim ファイルで scss:
という記述を使用している場合に .sass-cache
というディレクトリを生成するようになりました。
https://github.com/slim-template/slim/pull/931/commits/3a31cef50b6c67e649f6cfc625c961728bd20dbf
この挙動によりファイルシステム上の問題が起きたので、scss:
をすべて css:
に置き換えました。
/ Rails 7.0 scss: .hoge { .fuga { z-index: calc(infinity) } } / ↓ / Rails 7.1 css: .hoge .fuga { z-index: calc(infinity) }
数値以外の配列で sum を使用していた箇所を別のメソッドに置き換えた
Rails 7.0 has deprecated Enumerable.sum in favor of Ruby's native implementation available since 2.4.
Sum of non-numeric elements requires an initial argument.
(Rails 7.0 では Enumerable.sum が非推奨となり、Ruby 2.4 以降で利用可能な Ruby ネイティブの実装が推奨されています。
数値以外の要素を合計するには第 1 引数が必要です。)
という警告に従い、数値以外の配列で sum
を使用していた箇所を別のメソッドに置き換えました。例えば、配列を要素とする配列の場合、sum
の代わりに flat_map
を使用するようにしました。さもないと Rails 7.1 では TypeError が発生します。
# Rails 7.0 [[1], [2]].sum { _1 } # DEPRECATION WARNING: Rails 7.0 has deprecated Enumerable.sum in favor of Ruby's native implementation available since 2.4. # Sum of non-numeric elements requires an initial argument. #=> [1, 2] # ↓ # Rails 7.1 [[1], [2]].sum { _1 } # TypeError: Array can't be coerced into Integer [[1], [2]].flat_map { _1 } #=> [1, 2]
ActiveSupport::MessageEncryptor の内部実装に依存したコードを修正した
ActiveSupport::MessageEncryptor
の内部実装が変わり、それを継承したクラスが動作しなくなっていたので修正しました。
permit されていない params で to_h を呼び出さないようにした
params.to_h
が ActionController::UnfilteredParameters を発生させていました。そこで、適切に permit を呼び出すに修正しました。
# Rails 7.0 params.to_h.symbolize_keys.slice(:hoge, :fuga, :foo) # ActionController::UnfilteredParameters: unable to convert unpermitted parameters to hash # ↓ # Rails 7.1 params.permit(:hoge, :fuga, :foo)
after_commit を after_save に置き換えた
1 箇所 after_commit を after_save に置き換えました。after_commit でジョブをエンキューしている処理が Rails 7.0 では 1 回だけ呼ばれていたにもかかわらず、Rails 7.1 では 2 回呼ばれるようになってしまったためです。
どうやら以下の差分が関係しているようです。after_commit を after_save に置き換えるとコールバックが実行されるタイミングがトランザクションの外から中に変わってしまいます。動作上問題ないことを確認した上で置き換え、解決しました。
https://github.com/rails/rails/pull/45280/commits/936a862f3c7d472b8df0450205b6bc2672744e16
serialize メソッドで type を指定するようにした
DEPRECATION WARNING: Passing the class as positional argument is deprecated and will be removed in Rails 7.2. Please pass the class as a keyword argument: serialize :body, type: Hash (クラスを位置引数として渡すことは非推奨であり、Rails 7.2 で削除されます。クラスはキーワード引数として渡してください。)
という警告に従い、type: Hash
を指定するようにしました。
# Rails 7.0 class Message < ApplicationRecord serialize :body, Hash end # ↓ # Rails 7.1 class Message < ApplicationRecord serialize :body, type: Hash end
controller.request=, controller.response= を controller.set_request!, controller.set_response! に置き換えた
Rspec を使用したテストコードで controller.request=
, controller.response=
と書いていた箇所を controller.set_request!
, controller.set_response!
に置き換えました。ダミーのリクエストとレスポンスを設定する目的で controller.request=
, controller.response=
を利用していましたが、controller.response_body の挙動が変わってしまい問題が起きました。
ActionController::Metal#dispatch の実装を参考に set_request!
, set_response!
を使用すると正しく動作するようになりました。
# Rails 7.0 before do dummy_request = ActionController::TestRequest.create(controller) dummy_response = controller.class.make_response!(controller.request) controller.request = dummy_request controller.response = dummy_response end # ↓ # Rails 7.1 before do # 略 controller.set_request!(dummy_request) controller.set_response!(dummy_response) end
response.parsed_body の返り値の型が変わったので修正した
ActionDispatch::TestResponse#parsed_body が Rails 7.0 では String オブジェクトを返していましたが、Rails 7.1 では Nokogiri::HTML4::Document オブジェクトを返すようになりました。それに合わせてテストコードを修正しました。例えば Nokogiri::HTML4::Document#css メソッドが使えるようになったので、レスポンスボディの HTML が特定の要素を含んでいるかという、より詳細なテストが可能になりました。
# Rails 7.0 body = response.parsed_body expect(body).to include(target_id) # ↓ # Rails 7.1 body = response.parsed_body expect(body.css("option[value=#{target_id}]")).to be_present
MemCacheStore と RedisCacheStore がコネクションプールを使用しないようにした
MemCacheStore と RedisCacheStore がデフォルトでコネクションプールを使用するようになりましたが、従来の挙動を変えないように pool: false
を指定するようにしました。
# Rails 7.0 config.cache_store = :mem_cache_store, ENV.fetch('MEMCACHED_HOST') # ↓ # Rails 7.1 config.cache_store = :mem_cache_store, ENV.fetch('MEMCACHED_HOST'), { pool: false }
その他
Rails 7.0 の時点でリリース可能な差分をリリースする
Rails 7.0 のアプリケーションに含めることが可能な差分は事前にマージし、リリースしておきます。そうすることで、Rails 7.1 に更新するリリースを行う際の差分が最小になり、変更内容を把握しやすくしたり、リリースの影響を最小限にしたりすることができます。
E2E テストを実施する
- 全ての単体テストが成功する
- 全ての非推奨警告を取り除く
以上が完了した後、アプリケーションを検証環境にデプロイし、E2E テストを実施してバグが発生していないかを確認しました。
まとめ
今回は Rails を 7.0 から 7.1 に更新する際に行ったことをご紹介しました。Rails は巨大なフレームワークであり、メジャーバージョンアップだけでなくマイナーバージョンアップでも多くの変更が行われるため、慎重に更新を行う必要があります。世の中には Rails 7.1 より前のバージョンで動作しているアプリケーションがまだまだ多く存在していると思います。この記事が更新の一助になれば幸いです。
マネーフォワード福岡開発拠点ではエンジニアを募集しています!多くのユーザーに利用されている Rails アプリケーションに携わりたい方はぜひ!
福岡開発拠点のサイトもあります。もしご関心があれば、ぜひともご覧ください!