Rails で DBカラム変更と同時に既存データを移行する
既に本番稼働中のサービスでDBのカラムを変更する場合に、デプロイと同時に既存データを移行しないと整合性が取れない場合がある。
rake タスクとしてデータ移行処理を記述しておいて、本番サーバで実行する方法などもあるけど、 migration_data
gem を使うとマイグレーションと同期してデータ移行処理を叩いてくれて便利だったので簡単な使い方とハマったポイントをメモしておく。
データ移行の処理を書く
データ移行の処理をどこに書くかというと、 migration ファイルの data
メソッドに書く。
公式サイトから。
class CreateUsers < ActiveRecord::Migration def change # Database schema changes as usual end def data User.create!(name: 'Andrey', email: 'ka8725@gmail.com') end def rollback User.find_by(name: 'Andrey', email: 'ka8725@gmail.com').destroy end end
data
メソッドにデータ移行の処理を書いておけば、 rails db:migrate
時に実行してくれるとのこと。
rollback
は rails db:rollback
時に実行してくれるのかな。
spec を書く
data
rollback
に書いた処理は spec でテストを書くことも出来る。
データ移行はとてもセンシティブな処理なのでテストが簡単に書けるのはとても心強い。
再び公式サイトから。
require 'spec_helper' require 'migration_data/testing' require_migration 'create_users' describe CreateUsers do describe '#data' do it 'works' do expect { described_class.new.data }.to_not raise_exception end end describe '#rollback' do before do described_class.new.data end it 'works' do expect { described_class.new.rollback }.to_not raise_exception end end end
ハマったポイント
ただ、spec が通ったからと言って本番環境で通るとは限らない。
特に複数の migration ファイルをまとめて実行する際に以下のような問題に遭遇した。
- model と カラムの不一致でデータの保存が出来ない
- 古いカラム情報が残って正しくデータの更新が行えない
1つずつ解説する。
model と カラムの不一致でデータの保存が出来ない
例えば1つ目の migration ファイルで Child
テーブルのカラム hoge
を追加して、2つ目の migration ファイルで同テーブルにカラム fuga
を追加しつつ model では fuga
の validates :fuga, presence: true
を行ってるとする。
そして、hoge
への更新処理を1つ目の migration ファイルの data
メソッドに activerecord-import
の gem を使って以下のように書いているとする。
以下のようなパターン
- migration ファイル1
- children テーブルにカラム
hoge
を追加 - data メソッドにデータ移行処理を記述
- children テーブルにカラム
def data children = Child.includes(:parent).map do |child| child.hoge = child.parent.old_hoge child end Child.import children, on_duplicate_key_update: [:hoge] end
- migration ファイル2
- children テーブルにカラム
fuga
を追加 - Child model で
validates :fuga, presence: true
を実装
- children テーブルにカラム
ここで、これら2つの migration ファイルをまとめて db:migrate
しようとした場合にエラーが発生する。
原因は、上記 data
が実行される際に後から追加されるはずの fuga
へのバリデーションのコードが実行されてしまうが、まだ fuga
はカラムとして追加されていないため未定義な変数へのアクセスエラーとなる。
この data
メソッドを書いた時点では後に同テーブルにどんなカラムが追加され、どんなバリデーションが追加されるか分からないため単体の spec が通ったとしても安心できない。
回避方法としては validate: false
オプションでバリデーションせずに更新する方法がある。
def data children = Child.includes(:parent).map do |child| child.hoge = child.parent.old_hoge child end Child.import children, on_duplicate_key_update: [:hoge], validate: false end
バリデーションを通さないので若干不安ではあるが・・・他に回避方法あるかなぁ。
古いカラム情報が残って正しくデータの更新が行えない
複数の migration ファイルで data
を定義すると、最初に実行されたメソッド内で where
などが呼ばれたテーブルのカラムの情報がキャッシュされてしまい、その後の migration ファイルで追加したカラムが data
メソッド内で認識できずに更新などが失敗する事がある。
以下のようなパターン
- migration ファイル1
- children テーブルにカラム
hoge
を追加 - data メソッドで Child.where(...) などの処理を行う
- children テーブルにカラム
def data Child.where(...) # ここでカラムの情報がキャッシュされる end
- migration ファイル2
- children テーブルにカラム
fuga
を追加 - data メソッドで同じように Child.where(...) などして、レコードに対して
update(fuga: 'hello')
を行う
- children テーブルにカラム
def data Child.where(...).update(fuga: 'hello') end
ここで fuga
の更新が無視される!!!!
こんな時は、2つ目以降の data メソッド内で、カラム情報をリセットする Class.reset_column_information
を呼んであげればいい
def data Child.reset_column_information # これを追加 Child.where(...).update(fuga: 'hello') end
fuga
が正常に更新されるようになる。
migration ファイルってデプロイのタイミングによってどんな風にまとめられて実行されるか分からないので、毎回上記のような対策をしておくのが良いのかも。
関連記事を見つけました。さすが onk さん。 blog.onk.ninja