Rails 2.3 で acts_as_paranoid を使ってみた

Railsで論理削除を行う方法の1つとして、acts_as_paranoidというプラグインがあるので、Rails2.3で試してみた。なお、rails3用にはrails3_acts_as_paranoidがあるようです。

acts_as_paranoidとは?

データベースのテーブルのカラムに deleted_at を作って、deleted_atに日付があれば論理削除された状態、deleted_at が null なら削除されていない状態というのを管理してくれるプラグイン

install

$ cd RAILS_ROOT
$ ruby script/plugin install git://github.com/technoweenie/acts_as_paranoid.git

データベースを作成

migrationを作成して、カラム名 deleted_at、属性 datetime を持つテーブルを作成します。

  • ./db/migrate/001_create_table_hoges.rb
class CreateTableHoges < ActiveRecord::Migration
  def self.up
    create_table(:hoges) do |t|
      t.column :name, :string, :null => false
      t.column :deleted_at, :datetime
    end
  end

  def self.down
    drop_table :hoges
  end
end

モデルに宣言

class Hoge < ActiveRecord::Base
  acts_as_paranoid
end

論理削除されるか実験

$ ruby script/console
Hoge.new(:name => "fuga").save
true

Hoge.find(:all)
[
    [0] #<Hoge:0xb75546ac> {
                :id => 1,
              :name => "fuga",
        :deleted_at => nil
    }
]

Hoge.destroy_all
[
    [0] #<Hoge:0xb755091c> {
                :id => 1,
              :name => "fuga",
        :deleted_at => Fri Sep 02 19:40:30 +0900 2011
    }
]

Hoge.find(:all)
[]

Hoge.find_with_deleted(:all)
[
    [0] #<Hoge:0xb753e820> {
                :id => 1,
              :name => "fuga",
        :deleted_at => Fri Sep 02 19:40:30 +0900 2011
    }
]

その他の使い方

こちらを見ると分かります。

    # Overrides some basic methods for the current model so that calling #destroy sets a 'deleted_at' field to the current timestamp.
    # This assumes the table has a deleted_at date/time field. Most normal model operations will work, but there will be some oddities.
    #
    # class Widget < ActiveRecord::Base
    # acts_as_paranoid
    # end
    #
    # Widget.find(:all)
    # # SELECT * FROM widgets WHERE widgets.deleted_at IS NULL
    #
    # Widget.find(:first, :conditions => ['title = ?', 'test'], :order => 'title')
    # # SELECT * FROM widgets WHERE widgets.deleted_at IS NULL AND title = 'test' ORDER BY title LIMIT 1
    #
    # Widget.find_with_deleted(:all)
    # # SELECT * FROM widgets
    #
    # Widget.find_only_deleted(:all)
    # # SELECT * FROM widgets WHERE widgets.deleted_at IS NOT NULL
    #
    # Widget.find_with_deleted(1).deleted?
    # # Returns true if the record was previously destroyed, false if not
    #
    # Widget.count
    # # SELECT COUNT(*) FROM widgets WHERE widgets.deleted_at IS NULL
    #
    # Widget.count ['title = ?', 'test']
    # # SELECT COUNT(*) FROM widgets WHERE widgets.deleted_at IS NULL AND title = 'test'
    #
    # Widget.count_with_deleted
    # # SELECT COUNT(*) FROM widgets
    #
    # Widget.count_only_deleted
    # # SELECT COUNT(*) FROM widgets WHERE widgets.deleted_at IS NOT NULL
    #
    # Widget.delete_all
    # # UPDATE widgets SET deleted_at = '2005-09-17 17:46:36'
    #
    # Widget.delete_all!
    # # DELETE FROM widgets
    #
    # @widget.destroy
    # # UPDATE widgets SET deleted_at = '2005-09-17 17:46:36' WHERE id = 1
    #
    # @widget.destroy!
    # # DELETE FROM widgets WHERE id = 1
    #

関連モデルとの関係

関連モデルにも宣言してあれば、一緒に論理削除してくれます。参考

注意点

参考

残念な事に、acts_as_paranoidはsave(createやupdate)までは面倒を見てくれません。save時には全件を対象に一意の調査してしまうのです。
これを回避するには:scopeを指定してしまうのが良いでしょう。