Rails6.1にアップグレードしたらwice_gridでFrozenErrorが出るようになった話
仕事でRailsのアップグレードを進めていたときに出会ったエラーの内容をゆるくまとめておく。
Rails6.1+wice_gridでFrozenErrorが発生
Rails6.0からRails6.1へのアップグレードを行った際に動作確認をしていると、ソートや検索機能を備えたテーブルを表示するページでエラーが発生した。
エラー内容は
FrozenError (can't modify frozen ActiveRecord::ConnectionAdapters::PostgreSQL::Column ~)
で、バックトレースを確認するとどうやらwice_gridというgemの内部でエラーが発生していることが分かった。
wice_gridはテーブルにソートやフィルタリング機能などを簡単に付けられるgemで、あまり頻繁に更新はされていないのでRails6.1での動作に関しては保証されていなかった。
原因調査
さらにgem内部を見ていくと、wice_gridのtable_column_matrix.rb
の以下のコードで問題が発生していることが分かった。
エラーが発生していた行は4行目のself[model].each_value { |c| c.model = model }
の部分。
エラーメッセージに書かれている通り、freezeによって変更が禁止されているActiveRecord::ConnectionAdapters::PostgreSQL::Column
に対して、新たな属性を追加しようとしてエラーが起きていた。
def init_columns_of_table(model) #:nodoc: self[model] = HashWithIndifferentAccess.new(model.columns.index_by(&:name)) @by_table_names[model.table_name] = self[model] self[model].each_value { |c| c.model = model } end
Rails6.0と6.1で何が変わったのか
上のコードはRails6.0では問題なく動いていたため、Rails6.1でActiveRecordの何かしらの仕様変更があった可能性を疑った。それぞれの環境のRails consoleで確認をしてみると、以下のようにRails6.0ではModel.columnsで返ってくる要素はfreezeされていないのに対して、Rails6.1からは要素がfreezeされるように変わっていたということが判明。
Rails6.0
Model.columns.first.frozen? => false
Rails6.1
Model.columns.first.frozen? => true
Rails6.1のコミットを追ってみると、2020年5月に以下のコミットでcolumnsメソッドで返る要素をfreezeさせる変更がされているのを発見。
発端は以下のディスカッションで、「Model.column_namesがコピーではなく参照を返しているので"!"を付けたほうが良くない?」→「ほんならきちんとfreezeするようにしといたほうがええやろ!PR作っといたで!」みたいな流れの中で上のPRが出されたっぽい。
どのように解決したか
この記事ではwice_gridの内部のコードにあまり言及していないため詳細は省くが、ActiveRecord::ConnectionAdapters::PostgreSQL::Column
に新たな属性を追加せずとも必要なデータの受け渡しを行う方法が他にもあったため、その変更を加えるモンキーパッチを当てて対処した。
まとめ
今回のエラーで原因の特定に苦労した理由として、Railsのアップグレードガイドやリリースノート内に上記の仕様変更が書かれていなかった点が大きかった。おそらく「推奨されない書き方が禁止されるようになった」という話なので特にリリースノートに書くまでもないと判断されたのだろうと勝手に思っている。このエラーのおかげでgem内部のコードをしっかり読んだり、モンキーパッチの作成をする機会ができたので良かった。厄介なエラーも時が経つと良い思い出になるのかもしれない…。
↓「アウトプットサボってんじゃねーよ」と言われた気がした。。。がんばります。