【Rails】transactionとActiveRecord::Rollbackを利用して安全にデータ更新を行う

Ruby

データ不整合が発生した時や既存のカラムの値を変更したい時など、データを手動で更新しなければいけません。
本番データを直接操作することになるため、手動更新は慎重に行う必要があります。

今回は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_timestampsfalseにしてから更新するとよいでしょう。
更新後はrecord_timestampstrueに戻すのを忘れないでください。

まとめ

以上で、トランザクションを利用したデータ更新方法の紹介を終わります。

今回のまとめ
  • 安全にデータ更新をしたい場合、transactionで囲われたスクリプトをrails runnerで実行するのがオススメ
  • 『raise ActiveRecord::Rollback』で意図的にロールバックが可能になる
  • レコードの値を変えるだけなら「update_columns」、整合性を担保したいのであれば「update」でデータの更新する

この記事がいいなと思いましたらTwitter(@nishina555)のフォローもよろしくお願いします!