Globalizeを使ったRailsを5系対応する際にはまった

要約

  • columns_hash等でI18n対応したカラムと同名のカラムを参照してると落ちる
  • だいたいGlobalizeのtranslateしたときの消し忘れなのでつらい
  • チェックして消す&今後増えないようにするgemを作った

問題

  • GlobalizeをRails5対応版(5.1.0.beta2)にあげる
  • 一部の処理が落ちる(´・_・`)

原因

News.columns_hash.keysは、そのモデルのカラム名を配列で返します。
GlobalizeのRails 5未対応版の5.0.1が入っているとDBのカラムをそのまま配列にして返します。
しかし、Rails 5対応版の5.1.0.beta2では、I18n対応したカラム名と同名のカラムを除いたものを返します。
そのため返ってくる値がバージョンによって代わり、それに依存している処理がおかしくなります。
ただし、この影響を受けるカラムは多くの場合参照できないため、通常は仕様ミスや顕在化してないバグの可能性が高いです。

解説

Globalize gemでは、あるモデルのカラムに対してtranslate指定をすることで、translate用のテーブルの同名カラムを参照するようになります。

例えば以下のようなNewsモデルがある場合を考えます。

# == Schema Information
#
# Table name: news
#
#  id                     :integer          not null, primary key
#  title                  :string(255)
#  description            :string(255)
class News
  translate :title, :description
end

このモデルのtitleとdescriptionはtranslateされているので、newsテーブルの同名のカラムは使われません。 Globalizeによって、I18n対応した別テーブル(news_translates)の該当するlocaleのデータを取ってくるように変更されます。 そのため、translateによって処理が上書きされたカラムと同じ名前のカラムが元のテーブルにあっても、アクセスすることは出来ません。

ですが、Globalize 5.0系ではcolumns_hash等で全てのカラムの一覧を取得した場合、このアクセス不能なカラムが一覧に入っています。 そのため、上書き前のカラムがあるという前提のコードを書けてしまいます。

News.columns_hash.keys
=> ["id", "title", "description", "created_at", "updated_at"]

Globalizeの5.1系では、内部的に以下の方法でtranslateに設定されたカラム名が除外されるため、columns_hash等を使っても上書き前のカラムにアクセスできなくなります。

News.columns_hash.keys
=> ["id", "created_at", "updated_at"]
  • Rails4系の場合
    • もとのcolumns_hashからtranslateしたカラムを消している。
    • Globalize::ActiveRecord::ClassMethods#columns_hash
  • Rails5系の場合
    • ignored_columnsでActiveRecord的にカラムを無効化
    • Rails5から追加されたメソッド
    • Globalize::ActiveRecord::ActMacro#allow_translation_of_attributes

そのため、 columns_hash を利用して上書き前のカラムがあることを前提としたコードを書いている場合、処理が失敗します。

対策

おそらくGlobalizeの意図している挙動は、translate設定したカラムは全てI18n対応のテーブルのものに完全に上書きするものです。 そのため、 columns_hash でアクセス出来てしまうのは予期せぬ事であり、上書き元にはアクセスしないようにするのが正しそうです。

また、上書き元のカラムへのアクセスは普通に使っていると出来ません。 そのため、columns_hash でtranslateしたカラムに対して処理を行っている場合は高確率で無駄な挙動かバグの可能性が高いです。 これらのカラムはそもそもremove_columnsするべきですし、今後も増えないようにしていく必要があります。

ただし、既存のカラムをtranslateしたような場合にtranslate前のカラムを消さずにそのまま残してしまうということはありえるため、増えないように注意するのは大変です。

対策をgem化した

毎回レビューでtranslateによって上書きされるカラムをチェックしするのは大変なため、そういったことをしてくれるgemを作りました。
https://github.com/ota42y/globalize_overlap_checker

このgemではglobalizeによって上書きされるカラムを探しだし、remove_columnsのmigrationを作るrake taskを提供します。

rake globalize_overlap_checker:generate_remove_overlap_migration

これにより、translateで上書きされるカラムの一覧と、それを消すmigrationを生成できます。

また、initalizerの中で以下のように設定をする事で、この問題を避けることができます。

GlobalizeOverlapChecker.prohibition_overlap!

RAILS_ENV=testでこのメソッドを読ぶと、migration実行時にtranslateによって消えるカラムを見つけてエラーを起こしてくれます。 これにより、translateしたのに残っているカラムの存在を検知出来るため、レビュー時に気をつける必要がなくなります。 これらの機能により、今あるおかしいカラムを全部消すのと、今後増えるのを止めることが出来ます。

Globalize 5.1系にあげることでcolumns_hashを使っても上書き元のカラムにアクセスできなくなるため大きな問題は起きません。 しかし、アクセスできないカラムは結局残るので、謎のカラムが増えることを防ぐのには依然として機能します。