soranoba
soranoba Author of soranoba.net
programming

after_destroyは削除回数と実行回数が一致する訳ではない

例えば投稿Aに対するFavの数を表示しようと思った際に、都度SQLでCOUNTを実行するのは避けたいという時に、Fav数をレコードに書き込む実装をとあるシステムでやっていました。
具体的にはcounter_cultureを使っていました。

しかし、カウンターが壊れる(過剰に減算される)ことが頻発していました。
この記事は、それを調査した際の備忘録です。

TL; DR

  • after_destroydestroy!が失敗しなかったら実行される
  • after_comomit on: :destroyはレコードの削除に成功した際に実行される
    • ただし、Rails >= 5.1.0の挙動

counter_cultureが使っているのはCOALESCE

まずtransactionを使っていなかったので、それによって同時に+1した値を書き込んで、本来合計+2されるべきところが+1しかされなかったというケースを疑いました。
ただ、counter_cultureはCOALESCEを使っているので、それはなさそうです。

before_destroy vs after_destroy vs after_commit

次に減算に偏っていたので、-1のトリガーを疑いました。
counter_cultureはbefore_destroyで判定を行なっていたので、これは駄目そうです。

そこでafter_destroyにする方針を考えましたがこれもうまくいきません。

class Post < ApplicationRecord
  after_destroy do
    puts :after_destroy
  end
  after_commit on: :destroy do
    puts :after_commit_on_destroy
  end
end
[1] pry(main)> post1 = Post.create!()
=> #<Post:0x00007f822d13b828>
[2] pry(main)> post2 = Post.find(post1.id)
=> #<Post:0x00007f822b177af0>
[3] pry(main)> post1.destroy!
after_destroy
after_commit_on_destroy
=> #<Post:0x00007f822d13b828>
[4] pry(main)> post2.destroy!
after_destroy
=> #<Post:0x00007f822b177af0>

after_destroydestroy!によってレコードが削除されなくても(つまり、既に削除されている)実行されますが、after_comomit on: :destroyはレコードを削除した際にしか呼ばれないという違いがあるようです。
ちなみにこれはRails >= 5.1.0の挙動であり、それ以前はafter_commit on: :destoryafter_destroyと同じ挙動をするようでした。

まとめ

after_destroyではなくafter_comomit on: :destroyを使うようにしましょう。
ちなみに、counter_cultureの修正PRはこちら

(Updated: )

comments powered by Disqus