ARで論理削除する為のモジュールを作ってみた.

Railsで論理削除が必要になったのでとりあえず調べてみた.希望としては

  • プラグインに頼らず手軽に使いたい
  • ARの機能そのままで使いたい

調べてみると以下の二つがでてきた.

acts_as_paranoidがググってみると結構多いが別に高機能を求めてる訳じゃないのでこれは使いたくない、っでRails作るときに参考にしているskipのソースを読むとモジュールにしているのがあった.

こっちは手軽で便利だけど論理削除を実装で意識する必要が強いのでこれを参考にオレオレモジュールを作ってみた.

論理削除オレオレモジュール

module Models
  #
  # 論理削除するようにします.
  #
  # 論理削除にはdeleted_atカラムを使用します.マイグレーションにdatetime型のカラムを追加してください.
  # 削除の判定にはdeleted_atがnullでない場合は削除済みとして扱われます.
  #
  # # find等のメソッドから削除されたレコードを取得しないようにする例
  # class Member < ActiveRecord::Base
  #   include Models::LogicalDestroyable
  #   default_scope :conditions => {:deleted_at => nil}
  # end
  #
  # # default_scopeを指定する場合
  # class Member < ActiveRecord::Base
  #   include Models::LogicalDestroyable
  #   hide_destroyed_with_default_scope
  # end
  #
  module LogicalDestroyable
    def self.included(base)
      base.alias_method_chain :destroy, :logical_destroyable
      base.named_scope(:active, {:conditions => {:deleted_at => nil}})
      base.attr_protected :deleted_at
    end
    
    #
    # default_scopeを使って削除されたレコードを検索結果から隠します
    #
    # findメソッドなどで削除項目も対象にしたい場合はwith_exclusive_scopeを使ってください.
    # 
    def self.hide_destroyed_with_default_scope
      # create時にも初期値として使われるけど大丈夫かな?
      self.default_scope :conditions => {:deleted_at => nil}
      #default_scopeは最後のdefault_scopingの値しか見ないので使えないっぽいからdefault_scopeを使うときは都度指定する必要がある.
      #options = {:conditions => {:deleted_at => nil}}
      #base.default_scoping.insert 0, { :find => options, :create => options[:conditions] }
    end
    
    def destroy_with_logical_destroyable
      before_destroy_with_logical_destroyable
      current_time = current_time_from_proper_timezone
      ret=update_attribute(:deleted_at, current_time)
      after_destroy_with_logical_destroyable
      ret
    end

    def before_destroy_with_logical_destroyable;end
    def after_destroy_with_logical_destroyable;end
    
    def destroyed?
      !self.deleted_at.nil?
    end
    alias_method :deleted?, :destroyed?

    def recover
      update_attribute(:deleted_at, nil)
    end
  end
end

あとがき

とりあえずモジュール作ってみたけどdeleteとdestroyって似てるけど内部の処理が違うんだよなー.

  • delete
    • DELETE文(SQL)で消す
  • destroy
    • ARオブジェクトを一旦作ってから削除
    • before_destroyコールバックで関連付けされてるやつも消したりするように成ってる.

関連づけ先が勝手に消されないようにbefore_destroyコールバックとは別のコールバックするようにしたけど.
deleteをオーバライドして関連まで影響ない仕様をそのまま論理削除に置き換えて作れば良かったかも.
物理削除の場合はdestroyするようにすればARの仕様を大きく変えないしなー.