前提知識
Rails アプリにおいて、テーブルの追加やカラムの追加は簡単なものの、カラムの削除やリネームは慎重に行う必要がある。たとえアプリからそのカラムを参照してないとしても、いきなりカラムを削除するとエラーになる可能性が大いにある。
というのも Rails にはスキーマキャッシュというものがあり、テーブルのカラム情報をモデルがキャッシュしているからだ。このキャッシュはたとえばいわゆる N+1 クエリ問題を避けるために includes (eager_load) するときに参照される。
SELECT 句で t0_r0
のような機械的に別名が振られるようなクエリを見たことがある Rails エンジニアは多いと思う。
機械的に全カラムを取得するためにスキーマキャッシュを利用しているため、このようなクエリが実行されてる中でカラムを削除したりリネームしたりすると、スキーマキャッシュをもとに並べらたカラムの一部が消えてしまうため、不正なクエリとなってエラーになってしまう。 このせいで Rails においてカラムの削除やリネームは面倒なものになっている。
Rails 5.x 以降のやりかた
Rails 5.0 からは ignored_columns
というオプションが追加され、ここに追加されたカラムは Rails からは見えなくなる。
このオプションはまさに Rails のオンラインスキーマ変更のやりにくさを解消するために追加された。僕が Rails 5.0 で最も好きな改善の一つだ。
https://github.com/rails/rails/pull/21720
なのでカラム削除の手順としては、
- アプリケーションコード内でそのカラムを参照している箇所を直す (これはスキーマキャッシュとは無関係に当然やる必要がある)
Model.ignored_columns
にカラム名を追加してデプロイ (1と同時でもよい)- 実際にカラムを削除またはリネーム
Model.ignored_columns
を元に戻してデプロイ
という流れになる。
Rails 4.x でのやりかた
ignored_columns は使えないが、DB からカラム情報を取得するところにモンキーパッチをあてて特定のカラムを隠すようにすればなんとかいける。 具体的には MySQL を使っている場合は mysql2 adapter のメソッドをフックする。
module ActiveRecordInvisibleColumn INVISIBLE_COLUMNS = { 'foos' => ['bar'], }.freeze def columns(table_name) super.delete_if { |column| INVISIBLE_COLUMNS.fetch(table_name, []).include?(column.name) } end end ActiveSupport.on_load :active_record do ActiveRecord::ConnectionAdapters::Mysql2Adapter.prepend(ActiveRecordInvisibleColumn) end
この方法は foos というテーブル名が (たとえ複数の DB を使っていても) グローバルに一意という前提を置いている点に注意する必要がある。 これで Rails 5.x の ignored_columns と同じような効果が得られるので、あとは同じ流れでカラムの削除を進めることができる。 過去に某巨大 Rails アプリで実際に成功したのでまぁたぶんだいたいいけると思う。
とはいえ Rails 5.0 以上にさっさとバージョンアップして ignored_columns を使ったほうがいいことは言うまでもない。