Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Rails: 一括削除でのトランザクションとロールバックの注意点

業務で以下のような処理を書いていたのですが、色々とハマったので自分用のまとめがてらに紹介します。

  • 複数テーブルのレコードをバッチで一括削除する
  • どれかのレコードの削除に失敗したらロールバックし、履歴テーブルに失敗記録を保存する

Rails

ApplicationRecord.transaction do
  books.each(&:destroy!)
  movies.each(&:destroy!)
  # 履歴テーブルへ成功時の保存処理
rescue => e
  # 履歴テーブルへ失敗時の保存処理
  raise ActiveRecord::Rollback
end

全件削除できないときにロールバックする方法

レコードを一斉削除するメソッドとしては destroy_all がありますが、これは true / false を返します。
https://apidock.com/rails/ActiveRecord/Relation/destroy_all

def destroy_all
  records.each(&:destroy).tap { reset }
end

よって、全件削除できないときにロールバックしたかったら以下のような方法を取ることになります。

業務では each(&:destroy!) を選択しました。

テスト(RSpec)

before do
  movies_double = instance_double('movies_double')
  allow(::Movie).to receive(:where).and_return(movies_double)
  allow(movies_double).to receive(:each).and_raise(StandardError)
end

# 削除に失敗したとき、ロールバックや履歴テーブルへの保存が行われることをテストする

スタブ化する対象

Railsのコードを補足して再掲します。

def destroy_records
  ApplicationRecord.transaction do
    books.each(&:destroy!)
    movies.each(&:destroy!)
    # 履歴テーブルへ成功時の保存処理
  rescue => e
    # 履歴テーブルへ失敗時の保存処理
    raise ActiveRecord::Rollback
  end
end

def books
  ::Book.where(...)
end

def movies
  ::Movie.where(...)
end

レコード削除に失敗した想定でテストを書きます。

今回は、削除に失敗するような条件を before_destroy で記述していないので、予期せぬエラーで削除に失敗した、という想定で、削除対象のオブジェクトが例外を投げるようにスタブ化を行います。
ここで、ロールバックされることをテストしたいわけですので、実際に一度はレコードを削除しないといけません。
books の方をスタブ化してエラーをraiseするようにすると、 books は一度も削除されません。

そこで、 movies をスタブ化して、 books の方は実際に削除してからロールバックされてるか確認することにしています。

スタブ化するクラス

movies をスタブ化したいのですが、このインスタンスのクラス Movie::ActiveRecord_Relation はprivate constantのため、今回のテストで使用できません。

before do
  allow_any_instance_of(Movie::ActiveRecord_Relation).to receive(:each).and_raise(StandardError)
end

とすると、以下のようにエラーになります。

NameError:
  private constant #<Class:0x0000556b07354db0>::ActiveRecord_Relation referenced

そこで、movies インスタンスを ::Movie.where で返すタイミングでスタブ化しています。

before do
  movies_double = instance_double('movies_double')
  allow(::Movie).to receive(:where).and_return(movies_double)
  allow(movies_double).to receive(:each).and_raise(StandardError)
end

最後に

こういった処理はまとめて学習する機会がなかなか無いので、軽い内容ですが復習のために記事にしてみました。



CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。