Railsのバージョンを6.1に上げました

株式会社リゾーム 企画・開発部第3グループの廣江(@buta_botti)です。
今年から弊社のテックブログがスタートします。どうぞよろしくお願いします。

弊社サービスについて

弊社ではショッピングセンター向けの製品の自社開発を行っています。その中の一つにRuby on Railsで開発しているBOND GATE*1というサービスがあります。僕はこの製品の開発を担当しています。

BOND GATEのFirst commit を見てみると2011年の1月末となっており、ちょうど11年になります。

% git log --reverse
commit 927e662f96154982d887d7b1f48c483935a0404d
Author: ************
Date:   Mon Jan 31 20:09:17 2011 +0900
    First commit BOND GATE

2011年ということは、Rubyは1.9系、Railsは3系くらいでしょうか。*2
現在ではRubyは3.1、Railsは7系が最新となっていますが、BOND GATEはまだまだ追いつけていないというのが現状です。

そんなBOND GATEですが、ここ数年はライブラリのバージョンアップに力を入れて取り組んでおり、先日でなんとかRuby2.7、Rails6.1までバージョンアップするプルリクエストがマージできるところまで来ました。 今日はRails6.1に上げる際にやったことをいくつかピックアップしてみます。

Rails6.1に上げる前に

バージョンアップ作業と言っても、いきなりbundle update railsなんてするわけにもいかないので、まずは変更点の調査とコードの事前修正をします。
リリースノートを見ると変更点はいくつかありましたが、影響がありそうな変更点は主に以下の4つでした。

  • ActionDispatch::Http::ParameterFilterの削除
  • ActiveRecord::Base#update_attributesおよびActiveRecord::Base#update_attributes!の削除
  • where.notがNORではなくNANDを述部で生成するようになる
  • ActiveRecord::Relationメソッドでの安全でない生SQLの利用を削除

これらの修正を行います。

古いコードの置き換え

  • ActionDispatch::Http::ParameterFilterの削除
  • ActiveRecord::Base#update_attributesおよびActiveRecord::Base#update_attributes!の削除

年季の入ったコードなので古いクラスやメソッドを利用した部分が多く、そういった箇所が影響を受けていますね。
これらは新しいものに置き換えてやれば良さそうです。エディタの置換機能を使ってサクッと置き換えてしまいます。

ActionDispatch::Http::ParameterFilter -> ActiveSupport::ParameterFilter
update_attributes -> update

where.notの挙動変更

  • where.notがNORではなくNANDを述部で生成するようになる

こちらはRails6.1からの挙動の変更になります。where.notの引数に複数のキーワードを渡している場合に影響を受けます。
例えばUser.where.not(country: 'Japan', age: '20')とした場合、Rails < 6.1では「日本人ではない"もしくは"20歳ではない」ユーザーを抽出できますが、 これがRails >= 6.1では「日本人ではない"かつ"20歳ではない」ユーザーを抽出します。

元々where.notをNORの挙動を期待して使うことはなく、意図しない挙動を招く原因となるとして今回挙動が修正されることとなったようです。
書いてて混乱してきそうな話ですが、要はAかBではないのような抽出では

Model.where.not(column_a: ...).where.not(column_b: ...)

のように分けて書き、AでもなくBでもないのような抽出では

Model.where.not(column_a: ..., column_b: ...)

のように書きなさいよという話です。

Rails < 6.1 ではこれら2つが同じ挙動をしていたのですが、6.1からは挙動が分かれるため分けて書くように修正します。

安全でない生SQL

これが今回のバージョンアップで一番よくわからないところでした。
ActiveRecord::Relationメソッドでの安全でない生SQL」が何を指すのかがよくわかりません。
とりあえずRails6.1でrails newして、6.0では動いて6.1ではエラーの出る生SQLを探してみると、以下のようなコードでエラーが出ました。

Model.group(:parent_id).pluck(:parent_id, 'SUM(CASE WHEN column_1 = "something" THEN column_2 ELSE 0 END)')
# ActiveRecord::UnknownAttributeReference: Query method called with non-attribute argument(s): "SUM(CASE WHEN column_1 = \"something\" THEN column_2 ELSE 0 END)" 

どうやらActiveRecord::UnknownAttributeReferenceがキーワードのようです。
コードを追っていくと、どうやら設定でActiveRecord::Base.allow_unsafe_raw_sqlの値を:deprecated以外の値にすることで例外が起きるようになっていることがわかります。

github.com

とりあえず設定をActiveRecord::Base.allow_unsafe_raw_sql = nilで適当に設定し、Rails6.0で例外を起こすようにしておきます。
あとはテストを実行して例外で落ちる場所を地道に直していくだけです。

修正方法ですが、このキーワードでググるArel.sqlを付けるという解決方法が出てきますが、小手先の解決方法なのであまり好ましくありません。
どういうコードを書いているかにもよるとは思いますが、できるだけきちんとサニタイズしてあげたいですね。
ちなみにBOND GATEでは全ての箇所においてsanitize_sql_arrayを用いることでサニタイズが可能でした。

Model.group(:parent_id).pluck(ActiveRecord::Base.sanitize_sql_array(['parent_id', 'SUM(CASE WHEN column_1 = "something" THEN column_2 ELSE 0 END)'])

どうしてもサニタイズメソッドで対応ができない場合はArel.sqlをつけてあげましょう。

Model.order(Arel.sql('COALESCE(foobar, 0)'))

サニタイズのメソッドは多く用意されているので、きっと利用できるものがあると思います。

安全でない生SQL調査の番外編

テストを実行して例外で落ちる場所を地道に直していくだけ

と簡単そうに書いていますが、これが実は少し大変でした。
弊社サービスのBOND GATEAではActiveJobでdelayed_jobを使っており、該当のコードをdelayed_jobで実行している箇所があります。そこを経由するテストが落ちるには落ちるんですが、 例外が起きた形跡がありません。失敗したというテスト結果だけがログに出ており、テストコード自体は最後まで走っているように見えます。 どうやらdelayed_jobで実行された部分で起きた例外ではRSpecは落ちないようで、delayed_jobで実行されたものが例外で正しい結果を返さないためにテスト結果がたまたま異なって失敗したようです。これではdelayed_jobで例外が起きたけど、たまたま結果は同じだった場合に気づけません。何とかしないと...。

そこでTracePointを使って例外にフックして現在地を書き出すようにしてみました。

RSpec.configure do |config|
  config.around do |e|
    trace = TracePoint.trace(:raise) do |tp|
      File.open('tmp/errors.txt', 'a') { |f| f.puts [tp.inspect, tp.raised_exception, nil] }
    end

    trace.enable
    e.run
    trace.disable
  end
end

テストを回して出てきた箇所を調査し、これで無事全ての修正を終えることができました。

6.1にバージョンアップ!

事前に修正を終えると、やっとバージョンを上げる作業に入れます。Gemfileのrailsのバージョンを指定して...

% bundle update rails
% rails app:update

あとは設定をいい感じに直して終わりです。
とは言ってもこの「設定をいい感じにする」のが実は一番大変な作業ではあるんですが、大体古い設定を使い続けるという選択をするので、バージョンアップ後に少しずつコードを修正しながら設定を切り替えていくという流れになっていくはずです。バージョンを上げるとバージョンアップ作業が終わるのではなく、バージョンを上げてからがバージョンアップ作業の始まりというわけです。

まとめ

今更なんですが、Rails6.1に上げる際の作業をざっくりと綴ってみました。
ここではあまり触れていませんが、バージョンアップする際に一番大事なことはテストが充実しているということです。バージョンアップして影響がないかどうかは最終的にテストを回して担保することになるので、テストが無い内にバージョンアップ作業をするのは無謀とも言えます。バージョンアップに限らずテストを書くということはとても重要なので、日頃からしっかりと書いていきたいですね。

バージョンアップ作業はこれからも続いていき、Rubyは3.1、Rails7.0.1に早く追いつきたいと思っています。
そんな弊社ではRuby on Railsで一緒に開発をしてくれる仲間を募集しています。興味がありましたら以下からご連絡お待ちしています。

www.rhizome-e.com

*1:BOND GATEは直観的で使いやすい、店長支援のためのSC・専門店向けコミュニケーションウェアです。(ホームページ商品説明引用)

*2:その頃僕は入社してないので正直よくわかりません