データ不整合が発生した時や既存のカラムの値を変更したい時など、データを手動で更新しなければいけません。
本番データを直接操作することになるため、手動更新は慎重に行う必要があります。
今回はRuby on Railsアプリケーションのデータを安全に手動で更新する方法について紹介します。
よくある手動データ更新方法
たとえば、1対多で紐づいているEvent – Entry間でデータの不整合が発生し、event_id = 10
に紐づいているEntryをevent_id = 11
に手動で変更する必要が生じたとします。
対処方法としてよく行われるのは以下のようなrails console
による手動更新でしょう。
# 更新前の確認
Entry.where(event_id: 10).count
=> 5
Entry.where(event_id: 11).count
=> 0
# 更新作業
Entry.where(event_id: 10).update_all(event_id: 11)
# 更新後の確認
Entry.where(event_id: 10).count
=> 0
Entry.where(event_id: 11).count
=> 5
rails console
でデータ更新をする場合「開発環境でひと通りコマンドを実行してエラーがないことを確認してから本番作業を行う」というプロセスがよく採用されていると思います。
今回紹介する方法: トランザクション内でデータ更新を行う
データ数が少なかったり複雑なデータ更新でなかったりする場合はrails console
による手動更新で問題ありません。
しかし複雑なデータ更新をする場合は、なにか問題があったら更新前の状態へ戻せるようにしておきたいです。
そのような場合は、トランザクションを活用したデータ更新がオススメです。
データ更新をモデル名.transaction
で囲い、問題があった際にロールバックが実行できるようにします。
トランザクション内で例外が発生するとロールバックが実行されます。
raise ActiveRecord::Rollback
を利用すると意図的に例外を発生させることができ、その結果ロールバックが実行されます。
さきほど紹介した手動データ更新をトランザクションで実行するスクリプトは以下の通りです。
# app/script/maintenance/20201103_fix_envent_id.rb
ActiveRecord::Base.transaction do
# update_atのタイムスタンプが更新されないようにする
Entry.record_timestamps = false
puts "Before event_id = 10: #{Entry.where(event_id: 10).count}"
puts "Before event_id = 11: #{Entry.where(event_id: 11).count}"
Entry.where(event_id: 10).each do |entry|
begin
entry.update!(event_id: 11)
rescue => e
puts 'entryの更新失敗'
puts e
puts "entry.id: #{entry.id}"
end
end
puts "After entry_id = 10: #{Entry.where(event_id: 10).count}"
puts "After entry_id = 11: #{Entry.where(event_id: 11).count}"
print "Are you sure?(yes/no) > "
answer = gets.strip
if answer == "yes"
puts "committed"
else
puts "rollback"
raise ActiveRecord::Rollback
end
# 冒頭でfalseにしたので元(true)に戻す
Entry.record_timestamps = true
end
スクリプトのポイントは、データ更新完了後gets
を利用してコマンドからの入力を待機している点です。
yes
以外の文字を入力するとraise ActiveRecord::Rollback
が呼び出され、ロールバックが実行されます。
データ更新前後でレコード数がどのように変化したかなど、適宜レコードの状態を出力させておくことで、問題がないことを確実にチェックしてから安全にデータ更新を完了できます。
スクリプトはrails runner
で実行します。
今回は開発環境に対してrails runner
を実行しています。本番環境に対して実行する場合は-e production
のオプションを追加します。
$ rails runner app/script/maintenance/20201103_fix_envent_id.rb
> Before Entry.where(event_id: 10).count: 5
> Before Entry.where(event_id: 11).count: 0
> After Entry.where(event_id: 10).count: 0
> After Entry.where(event_id: 11).count: 5
> Are you sure?(yes/no) > yes
> committed
参考: Railsアプリケーションのupdateメソッドまとめ
Railsアプリケーションにはデータ更新に関するメソッドが複数存在します。
代表的なメソッドの違いは以下の通りです。
メソッド | バリデーション | コールバック | updated_atの更新 | 複数カラム更新の可否 |
---|---|---|---|---|
update | ○ | ○ | ○ | ○ |
update_attributes | ○ | ○ | ○ | ○ |
update_attribute | × | ○ | ○ | × |
update_all | × | × | × | ○ |
update_columns | × | × | × | ○ |
update_column | × | × | × | × |
単純にレコードの値を変更したいだけなら『update_columns』、整合性を担保しつつデータ更新を行いたい場合は『update』を使うとよいでしょう。
また、update
でデータ更新を行う場合、更新対象のレコードのupdated_at
が変わってしまうと困る場合はrecord_timestamps
をfalse
にしてから更新するとよいでしょう。
更新後はrecord_timestamps
をtrue
に戻すのを忘れないでください。
まとめ
以上で、トランザクションを利用したデータ更新方法の紹介を終わります。
- 安全にデータ更新をしたい場合、transactionで囲われたスクリプトをrails runnerで実行するのがオススメ
- 『raise ActiveRecord::Rollback』で意図的にロールバックが可能になる
- レコードの値を変えるだけなら「update_columns」、整合性を担保したいのであれば「update」でデータの更新する
この記事がいいなと思いましたらTwitter(@nishina555)のフォローもよろしくお願いします!