はじめに
Ruby on Railsでアプリケーションを構築していて「仕様変更に伴いカラムを使わなくなったので削除しよう」という機会はあるかと思います。その際、ソースコードをチェックして当該カラムを参照している箇所が無かったからといって、いきなり remove_column
をしてしまうのはちょっと待った方が良いかもしれません。
特に、下記のような冗長構成を取っている場合は注意が必要です。
何が発生するのか
例えば、以下のように「projects」「tasks」テーブルを作成していたとします。(projectsとtasksは「1 対 多」の関係)
class CreateProjects < ActiveRecord::Migration[7.1]
def change
create_table :projects do |t|
t.string :name, null: false, comment: 'プロジェクト名'
t.string :category, comment: 'カテゴリ名' # このカラムを削除します
t.timestamps
end
end
end
class CreateTasks < ActiveRecord::Migration[7.1]
def change
create_table :tasks do |t|
t.references :project
t.string :name, null: false, comment: 'タスク名'
t.timestamps
end
end
end
class Project < ApplicationRecord
has_many :tasks
end
class Task < ApplicationRecord
belongs_to :project
end
categoryカラムを使わなくなったので、ソースコード上で参照している箇所が無いことを確かめたのち、カラム削除用のマイグレーションファイルを作って「サーバ1」「サーバ2」で動かしているRailsアプリケーションへ反映します。
class RemoveCategoryToProjects < ActiveRecord::Migration[7.1]
def change
remove_column :projects, :category, :string
end
end
すると、変更をアプリケーションへ反映中に、サーバ2で下記のエラーが流れ始めました。
An error occurred when inspecting the object: #<ActiveRecord::StatementInvalid: Mysql2::Error: Unknown column 'projects.category' in 'field list'>
ActiveRecordがテーブル情報をキャッシュしている
実は、ActiveRecordはテーブル情報をキャッシュしており、eager_load
等のメソッドではキャッシュ情報を元にクエリを組み立てています。
irb(main):003> Task.eager_load(:project)
SQL (1.2ms) SELECT `tasks`.`id` AS t0_r0, `tasks`.`project_id` AS t0_r1, `tasks`.`name` AS t0_r2, `tasks`.`created_at` AS t0_r3, `tasks`.`updated_at` AS t0_r4, `projects`.`id` AS t1_r0, `projects`.`name` AS t1_r1, `projects`.`category` AS t1_r2, `projects`.`created_at` AS t1_r3, `projects`.`updated_at` AS t1_r4 FROM `tasks` LEFT OUTER JOIN `projects` ON `projects`.`id` = `tasks`.`project_id` /* loading for pp */ LIMIT 11
An error occurred when inspecting the object: #<ActiveRecord::StatementInvalid: Mysql2::Error: Unknown column 'projects.category' in 'field list'>
今回の例は、「サーバ1への変更反映が完了してcategoryカラムが削除されたが、サーバ2が持っているキャッシュがcategoryカラムを参照し続けてエラーを引き起こした」というものでした。
サーバ2のキャッシュも更新されれば解決しますが、どうしても複数サーバへの変更反映(≒キャッシュ更新)にタイムラグが発生する以上、稼働中のアプリケーションでいきなりカラムを削除するのは危険なようです。
ignored_columnsを使う
安全にカラムを削除するには、ignored_columns
を用いた2段階のアプローチが推奨されているようです。
具体的には、まずは削除予定のカラムに対し ignored_columns
を指定して参照とキャッシュを断ち、その後に remove_column
のマイグレーションを実行するというものです。
class Project < ApplicationRecord
has_many :tasks
# カラムが存在していても参照・キャッシュしなくなる
self.ignored_columns += [:category]
end
まだcategoryカラムがprojectsテーブルに存在している状態でも、当該カラムを参照しなくなっていることが確認できます。
irb(main):005> Project.columns_hash["category"]
=> nil
まとめ
- 削除する予定のカラムを直接参照していなくても、ActiveRecordがキャッシュとして情報を保持している
- 安全にカラムを削除するならば、先に
ignored_columns
指定をかけたソースコードを反映させてから、remove_column
を行う
参考
ActiveRecord::ModelSchema::ClassMethods
Rails: Active Recordモデルのカラムを安全に削除する(翻訳)
ActiveRecord の Schema Cache と運用 Tips - Please Sleep