soranoba
soranoba Author of soranoba.net
programming

ActiveRecordでRDBのゴミ掃除を都度行う

開発時間が限られる個人開発だと、Railsの有り難みを感じる、この頃です。

さて、ActiveRecordを使っているとdestroy時に後片付けをする事が多いかと思います。
通常のhas_manyであれば標準の機能で事足りますが、それ以外にも、依存関係が消えた時に不要になったレコードを一緒に削除したい事はあるのではないでしょうか。

ER図


適当な例を挙げると、UserやGroup側にbelongs_to、Image側にhas_manyがあるような場合です。
この場合、使われなくなったImageを消す方法として、以下の2つの方法がパッと考えられます。

  • 依存するレコード削除時に毎回調べる方法
  • 定期的にバッチでゴミ掃除をする

今回は頻度が多くない想定なので前者で実装します。後者だとsidekiq-cron使ったりすれば良さそうです。

コード

after_commit do
  reference_gc(:image)
end

# @param columns [Array<Symbol>]
def reference_gc(*columns)
  columns.each do |column|
    # belongs_to :image の情報を取得する
    association = self.class.reflect_on_association(column)
    next if association.nil?

    # class (Image) を取得
    reference_klass = association.klass
    # 現在参照しているImage
    current_reference_obj = method(column).call
    # 更新されている場合, 変更前のImage
    previous_reference_obj = reference_klass.find_by(
      id: previous_changes[association.foreign_key]&.first
    )

    [current_reference_obj, previous_reference_obj].each do |reference_obj|
      next if reference_obj.nil?

      # Image側に書かれたhas_manyのプロパティ名を取得 (:group, :user)
      property_names = reference_klass.reflect_on_all_associations(:has_many).map(&:name)
      count = property_names.inject(0) { |sum, item| sum + reference_obj.method(item).call.count }
      reference_obj.destroy! if count.zero?
    end
  end
end

最適化はしていませんが、こんな感じでしょうか。場合によってコード変更が必要そうですが。
コードは煮るなり焼くなりお好きにどうぞ。

(Updated: )

comments powered by Disqus