ホーム DoRuby Ruby on Railsでacts_as_paranoidを使い倒す

Ruby on Railsでacts_as_paranoidを使い倒す

この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。

KBMJのプログラマのx5rです。

今日はRuby on Railsプラグインのacts_as_paranoidについて説明します。
これはDBのモデルデータを削除をする時に物理削除ではなく、論理削除をするためのプラグインです。

Railsの2.0系がリリースされましたが、まだ1.2系で開発することも多いと思います。
しかし、このacts_as_paranoidプラグイン、使い方は簡単ですが、Railsの1.2.5で利用する時に一部の機能でエラーが発生して使用できなかったり、また、私の希望する機能がありませんでしたので、このacts_as_paranoidプラグインを一部改良しました。

そこで、私が行ったエラーの対処法と拡張を説明したいと思います。
なお、以下#{RAILS_ROOT}で全て作業しているものとします。

CRUDアプリの作成

前回のAmazonAPIの使い方を説明した時は、Railsのバージョンは2.0系でしたが、今回はRails1.8.5を利用します。

まず、CRUD処理が必要なので、scaffoldを利用して作成します。
以下、単純なので簡単に流れだけ書いておきます。

 $ script/generate model Book #Bookモデルの作成
 $ less db/migrate/001_create_books.rb #Bookモデルのマイグレーションを編集
 class CreateBooks < ActiveRecord::Migration
   def self.up
     create_table :books do |t|
       t.column :title,        :string
       t.column :author,       :string
       t.column :date,         :date
       t.column :created_at,   :datetime
       t.column :updated_at,   :datetime
     end
   end
    def self.down
     drop_table :books
   end
 end
 $ rake db:migrate #マイグレート
 $ script/generate scaffold Book Book #scaffoldを作成
 $ script/server #サーバ起動 

では、http://localhost:3000/bookにアクセスします。
実際にデータを追加して削除すると、レコードが削除、つまり物理削除されていることがわかります。

acts_as_paranoidの導入

acts_as_paranoidのインストール

acts_as_paranoidプラグインをインストールします。

 $ ruby script/plugin source http://techno-weenie.net/svn/projects/plugins
 $ ruby script/plugin install acts_as_paranoid 

 deleted_atカラムを追加

acts_as_paranoidプラグインではdeleted_atカラムを論理削除のためのフラグとして機能しますので、deleted_atカラムを追加します。

 $ script/generate migration AddBooksDeletedAt #マイグレーションファイルの作成
 $ less db/migrate/002_add_books_deleted_at.rb #マイグレーションファイルの編集
 class AddBooksDeletedAt < ActiveRecord::Migration
   def self.up
     add_column :book, :deleted_at,   :datetime,  :comment => "削除日時"
   end
    def self.down
     remove_column :book, :deleted_at
   end
 end
 $ rake db:migrate #マイグレーション 

acts_at_paranoidの設定

Bookモデルにacts_as_paranoidクラスメソッドを追加します。

 $ less app/models/book.rb
 class Book < ActiveRecord::Base
   acts_as_paranoid
 end 

動作確認

実際に削除してみると、

 undefined method `construct_count_options_from_args' for Book:Class 

というエラーが表示されます。

これは最新のacts_as_paranoidプラグインがRails2.0に向けた対応をしたことによる影響らしいです。

そこで
RAILS_ROOT/vendor/plugins/acts_as_paranoid/lib/caboose/acts/paranoid.rb
の約90行目あたりの

 calculate_with_deleted(:count, *construct_count_options_from_args(*args)) 

 calculate_with_deleted(:count, *construct_count_options_from_legacy_args(*args)) 

に変更します。

これで削除をしてみるとうまく動作することが確認できます。

論理削除されたものも取得する

さて、場合によっては論理削除されたデータも含めた形で表示したいということもあるかと思います。
そして、acts_as_pluginは削除フラグのたっているものも含めて取得するためのメソッドも用意しています。

それが以下の2つの方法です。

  • :with_deletedオプションを指定
  • find_with_deletedメソッドを利用

はじめにBookControllerに次のようなアクションを追加し、簡単なテンプレートも作成して試してみます。

BookControllerにlist_with_deletedアクションを追加

$ less app/controllers/book_controller.rb
 class BookController < ApplicationController
    # --- 省略 ---
   def list_with_deleted
     #データ取得処理を記述する
   end
  end 

テンプレートを作成

 $ less app/views/book/list_with_deleted.rhtml
 <%- @books.each do |book| -%>
 
   <%= book.title %> - <%= book.author %> - <%= book.date %>
 <%- end -%> 

 :with_deletedオプションを指定

list_with_deletedアクションにデータ取得処理を記述します。

 $ less app/controllers/book_controller.rb
  class BookController < ApplicationController
    # --- 省略 ---
   def list_with_deleted
     @books = Book.find(:all, :with_deleted => true)
   end
 end 

http://localhost:3000/book/list_with_deletedにアクセスしてみます。すると

 Unknown key(s): with_deleted 

と表示されてしまいます。

そこで[#8896] find(:all, :with_deleted => true) broken (at least in version 0.3.1/July-2006)を参考にRAILS_ROOT/vendor/plugins/acts_as_paranoid/lib/caboose/acts/paranoid.rbの約58行目あたりの

 class << self
   alias_method :find_every_with_deleted,    :find_every
   alias_method :calculate_with_deleted,     :calculate
   alias_method :delete_all!,                :delete_all
 end 

に1行追加して

 class << self
   VALID_FIND_OPTIONS << :with_deleted unless VALID_FIND_OPTIONS.include?(:with_deleted)
   alias_method :find_every_with_deleted,    :find_every
   alias_method :calculate_with_deleted,     :calculate
   alias_method :delete_all!,                :delete_all
 end 

のようにします。
これで論理削除されたものも含まれて表示されます。

find_with_deletedメソッドを利用

find_with_deletedメソッドで論理削除されたものも取得する方法があります。

以下で説明するページネートで利用するためにこちらも確認してみます。
list_with_deletedアクションにデータ取得処理を記述します。

$ less app/controllers/book_controller.rb
 class BookController < ApplicationController
    # --- 省略 ---
   def list_with_deleted
     @books = Book.find_with_deleted(:all)
   end
 end 

http://localhost:3000/book/list_with_deletedにアクセスしてみます。すると

undefined method `extract_options!' for [:all]:Array 

というエラーが表示されます。
これもRails2.0に向けた対応によるものです。

そこで論理削除プラグイン(バグ修正) – Rails開発日記のサイトを参考に対応します。
上記のサイトではActiveSupportに直接変更を加える方法が説明されていますが、それはあまりやりたくないので、RubyおよびRailsの特性を生かして、クラス(モジュール)を拡張して対応してみます。
上記サイトのリンク先

のソースを今回のRailsアプリに反映させます。


RAILS_ROOT/libにactive_support_extend.rbというファイルを作成して以下のように記述します。

 $ less lib/active_support_extend.rb
  module ActiveSupport #:nodoc:
   module CoreExtensions #:nodoc:
     module Array #:nodoc:
       module ExtractOptions
         # Extract options from a set of arguments. Removes and returns the last element in the array if it's a hash, otherwise returns a blank hash.
         #
         #   def options(*args)
         #     args.extract_options!
         #   end
         #
         #   options(1, 2)           # => {}
         #   options(1, 2, :a => :b) # => {:a=>:b}
         def extract_options!
           last.is_a?(::Hash) ? pop : {}
         end
       end
     end
   end
 end
  class Array
   include ActiveSupport::CoreExtensions::Array::ExtractOptions
 end 

そしてRAILS_ROOT/config/environment.rbに

 $ less config/environment.rb # 一番下に記述
 require "active_support_extend" 

を記述してWebrickを再起動します。
そして、再度http://localhost:3000/book/list_with_deletedにアクセスしてみます。
これで論理削除されたレコードも表示されているのがわかります。

ページネートでfind_with_deletedを利用する

 ActionController::Paginationを拡張

上記で、論理削除されたデータの取得ができるようになりましたが、一覧画面でページネートを利用した際にも論理削除されたデータを取得したいことがあります。

そこでActionCcontrollerのpagination.rbを参考にページネートを拡張してみます。


RAILS_ROOT/vendor/plugins/acts_as_paranoid/lib/の下にpagination_extend.rbというファイルを作成し、以下のように記述します。

$ less vendor/plugins/acts_as_paranoid/lib/pagination_extend.rb
 module ActionController
   module Pagination
     def paginate_with_deleted(collection_id, options={})
       Pagination.validate_options!(collection_id, options, true)
       paginator_and_collection_for_with_deleted(collection_id, options)
     end
     module ClassMethods
       def paginate_with_deleted(collection_id, options={})
         Pagination.validate_options!(collection_id, options, false)
         module_eval do
           before_filter :create_paginators_and_retrieve_collections_with_deleted
           OPTIONS[self] ||= Hash.new
           OPTIONS[self][collection_id] = options
         end
       end
     end
     protected
     def create_paginators_and_retrieve_collections_with_deleted #:nodoc:
       Pagination::OPTIONS[self.class].each do |collection_id, options|
         next unless options[:actions].include? action_name if
           options[:actions]
          paginator, collection =
           paginator_and_collection_for_with_deleted(collection_id, options)
          paginator_name = "@#{options[:singular_name]}_pages"
         self.instance_variable_set(paginator_name, paginator)
          collection_name = "@#{collection_id.to_s}"
         self.instance_variable_set(collection_name, collection)
       end
     end
     def find_collection_for_pagination_with_deleted(model, options, paginator)
       model.find_with_deleted(:all, :conditions => options[:conditions],
                  :order => options[:order_by] || options[:order],
                  :joins => options[:join] || options[:joins], :include => options[:include],
                  :select => options[:select], :limit => options[:per_page],
                  :offset => paginator.current.offset)
     end
     def count_collection_for_pagination_with_deleted(model, options)
       model.count_with_deleted(:conditions => options[:conditions],
                   :joins => options[:join] || options[:joins],
                   :include => options[:include],
                   :select => options[:count])
     end
     def paginator_and_collection_for_with_deleted(collection_id, options) #:nodoc:
       klass = options[:class_name].constantize
       page  = params[options[:parameter]]
       count = count_collection_for_pagination_with_deleted(klass, options)
       paginator = Paginator.new(self, count, options[:per_page], page)
       collection = find_collection_for_pagination_with_deleted(klass, options, paginator)
        return paginator, collection
     end
   end
 end 

やっていることは最終的にfind_with_deletedメソッドを実行するために、そこまでのルートで呼ばれるメソッド名に「_with_deleted」を付けているだけです。
DRYではないですが、まあよしとします。

そして、RAILS_ROOT/vendor/plugins/acts_as_paranoid/init.rbでロードします。

$ less vendor/plugins/acts_as_paranoid/init.rb
 # 先頭に追加
 require 'pagination_extend'
 # 以下省略 

BookControllerのlistアクションを修正

 $ less app/controllers/book_controller.rb
 class BookController < ApplicationController
   # --- 省略 ---
   def list
     @book_pages, @books = paginate_with_deleted :books, :per_page => 10
   end
   # --- 省略 ---
 end 

動作確認

http://localhost:3000/book/listにアクセスすると、論理削除されたデータも含めたページネートが実現されています。

参考サイト

記事を共有

最近人気な記事