業務で以下のような処理を書いていたのですが、色々とハマったので自分用のまとめがてらに紹介します。
- 複数テーブルのレコードをバッチで一括削除する
- どれかのレコードの削除に失敗したらロールバックし、履歴テーブルに失敗記録を保存する
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
よって、全件削除できないときにロールバックしたかったら以下のような方法を取ることになります。
destroyed?
で全件削除されているか確認するeach(&:destroy!)
する- 例外メッセージを吐き出すので、ログ出力したい場合などはこちらの方がよいかも
業務では 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
最後に
こういった処理はまとめて学習する機会がなかなか無いので、軽い内容ですが復習のために記事にしてみました。