その他
    ホーム技術発信DoRubyRuby 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にアクセスすると、論理削除されたデータも含めたページネートが実現されています。

    参考サイト