2009年8月8日土曜日

[Rails][プラグイン]acts_asプラグインの作りかた - 超入門 -

ブログなどについている、月別アーカイブ作成支援に汎用モジュールがほしかったので、(勉強も兼ねて)簡単なプラグインを作成しました。
(スキルレベル:知識ゼロ。プラグインてなに?)

少し調べてみたところ、ActiveRecordを拡張する acts_as_xxx という手法(フレームワーク?)が適当そうだったため、その作法に則ります。テーブルやカラムの追加等、あまり複雑なことはせず、ActiveRecord作成日付フィールド(普通は created_at)だけを参照します。

参考にしたのは、↓の記事と acts_as_taggable_on_steroidsプラグインのソース。

プラグイン作成

まずは、pluginをgenerateします。プラグイン名は acts_as_archivable。
$ script/generate plugin acts_as_archivable
generate を実行すると、vendor/plugin 以下に acts_as_archivable ディレクトリ(と中にプラグインの雛形)が作られます。
generateされたファイルのうち、編集したのは以下の2つだけです。
  • init.rb
  • lib/acts_as_archivable.rb
init.rb には次のコードを追加します。
  1. # init.rb  
  2. require 'acts_as_archivable'  
  3.   
  4. ActiveRecord::Base.class_eval do  
  5.   include ActiveRecord::Acts::Archivable  
  6. end  


init.rb は Railsによりアプリケーション初期化時に読み込まれます(西和則著「Ruby on Rails入門―優しいRailsの育て方」)。ここでは、ActiveRecord::Baseクラスに、ActiveRecord::Acts::Archivable モジュールをMix-inしているだけです。

Archivableモジュールの実体は、lib/acts_as_archivable.rb の中で定義します。
  1. # acts_as_archivable.rb  
  2. module ActiveRecord  
  3. module Acts  
  4.  module Archivable  
  5.    def self.included(base)  
  6.      base.extend(ActMacro)  
  7.    end  
  8.  end  
  9.   
  10.  module ActMacro  
  11.    # acts_as_ メソッド(ActiveRecordクラス内で呼び出す)  
  12.    # 例外処理、クラスメソッドのインクルード  
  13.    def acts_as_archivable(col_name = "created_at")  
  14.      # 指定された作成日カラムがActiveRecordクラスになければ例外を発生させる  
  15.      raise NoCreatedAtColumnError unless self.column_names.include? col_name  
  16.      # 指定された作成日カラムの型が:datetimeでなければ例外を発生させる  
  17.      raise TypeError unless self.columns_hash[col_name].type.equal? :datetime  
  18.      # クラスメソッドをインクルード  
  19.      self.extend(ClassMethods)  
  20.      self.set_created_col_name col_name  
  21.    end  
  22.   
  23.    class NoCreatedAtColumnError < StandardError; end  
  24.    class TypeError < StandardError; end  
  25.  end  
  26.   
  27.  module ClassMethods  
  28.    # ActiveRecordと、作成日カラム名を紐付けるマップ  
  29.    @@class_col_hash = Hash.new  
  30.    def set_created_col_name col_name  
  31.      @@class_col_hash.store self.class.name, col_name  
  32.    end  
  33.    def get_created_col_name  
  34.      @@class_col_hash[self.class.name]  
  35.    end  
  36.   
  37.    # 月別アーカイブ作成支援メソッド  
  38.    # ここでは、月ごとに作成されたActiveRecordの件数をもつハッシュを作成して返している  
  39.    # ex. {2008=>{5=>10,8=>3,11=>6},2009=>{1=>5,3=>5,...}}  
  40.    # パフォーマンス/使い易さを考えると、もう少しやりようがあると思うけどとりあえず。。。  
  41.    def monthly_archive_counts  
  42.      counts = Hash.new  
  43.      records = self.find(:all)  
  44.      records.each do |record|  
  45.        year = record[get_created_col_name].year  
  46.        month = record[get_created_col_name].month  
  47.        if counts[year] == nil  
  48.          counts.store year, Hash.new(0)  
  49.        end  
  50.        counts[year][month] += 1  
  51.      end  
  52.      return counts  
  53.    end  
  54.  end  
  55. end  
  56. end  


self.includedというのはModuleクラスのメソッドで、リファレンスマニュアルの説明には
self が include されたときに対象のクラスまたはモジュールを引数にインタプリタから呼び出されます。

とあります。

acts_as_archivableが、(プラグインに慣れている人にはたぶんおなじみの)、拡張対象のActiveRecordクラスから呼び出すメソッドになります。
作成日カラムとして"created_at"以外を使う場合に対応できるよう、作成日カラム名を与えるようにしています(デフォルト値は"created_at")。
  • 指定された作成日カラムが存在しない場合
  • 存在するが :datetime タイプでない場合
には、例外を発生させています。
例外処理のあと、ActiveRecordと作成日カラム名の対応づけと、目的となるクラスメソッドのインクルードを行います。

使いかた

準備として
  1. class Bookmark < ActiveRecord::Base  
  2.   acts_as_archivable  
  3. end  

のように ActiveRecord クラス内で acts_as_archivable メソッドを呼ぶだけです。これで、Bookmarkクラスに monthly_archive_counts メソッドが追加されます。

bookmarksテーブルに
sqlite> select id, created_at from bookmarks;
5|2008-10-01 09:00:00
6|2008-10-01 09:00:00
7|2008-10-01 09:00:00
8|2008-11-01 09:00:00
9|2009-02-01 09:00:00
10|2009-02-01 09:00:00
11|2009-04-01 09:00:00
12|2009-04-01 09:00:00
13|2009-04-01 09:00:00
14|2009-04-01 09:00:00
16|2009-06-01 09:00:00

というデータが入っているとき、Bookmark.monthly_archive_counts は次のハッシュを返します。

$ script/console
Loading development environment (Rails 2.3.3)
>> counts = Bookmark.monthly_archive_counts
=> {2008=>{10=>3, 11=>1}, 2009=>{2=>2, 4=>4, 6=>1}}
>> counts[2009][4]
=> 4


ビューでの使用イメージはこんな感じ。


ちゃんと使えるプラグインを作ろうとすると、テーブルやカラムを追加したり色々複雑になりますが、acts_asプラグインの骨格としてはこんな感じでしょうか。

よくできてるなー。
Mix-inの強力さを(あらためて)実感。

0 件のコメント:

コメントを投稿