目次
この記事はアピリッツの技術ブログ「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にアクセスすると、論理削除されたデータも含めたページネートが実現されています。