RailsでActiveRecordモデルのコレクションに対してメソッドを追加する


ActiveRecord::Baseを継承しているモデルのコレクションに対してメソッドを追加したいことがたまにあると思います。例えば次のような画面で

今表示しているsalesの平均価格を表示したいというような場合です。viewには次ような感じで書きたいんじゃないでしょうか。

📄app/views/sales/index.html.erb
...
Average of prices: <%= @sales.average_price %>
...

Railsではこんなことも簡単に実現できます。モデルにメソッドを追加すれば良いだけ。

📄app/models/sale.rb
class Sale < ActiveRecord::Base

  def self.average_price
    all.average(:price)
  end
end

(rails4で動作確認しています。rails3のallは配列を返すので、scopedにすれば動くと思います)

一体どういう仕組でこのメソッドが呼び出されてるんでしょうか。ちょっとpry-stack_explorerで確認してみると・・・

📄call stack
Showing all accessible frames in stack (116 in total):
--
=> #0  average_price <Sale.average_price()>
   #1 [block]   block in run <PryByebug::Processor#run(initial=?, &block)>
   #2 [method]  run <PryByebug::Processor#run(initial=?, &block)>
   #3 [method]  start_with_pry_byebug <Pry.start_with_pry_byebug(target=?, options=?)>
   #4 [method]  pry <Object#pry(object=?, hash=?)>
   #5 [method]  average_price <Sale.average_price()>
   #6 [block]   block in method_missing <ActiveRecord::Delegation::ClassSpecificRelation#method_missing(method, *args, &block)>
   #7 [method]  scoping <ActiveRecord::Relation#scoping()>
   #8 [method]  method_missing <ActiveRecord::Delegation::ClassSpecificRelation#method_missing(method, *args, &block)>
   #9 [method]  _app_views_sales_index_html_erb__3326902891793499470_70306254138640 <ActionView::CompiledTemplates#_app_views_sales_index_html_erb__3326902891793499470_70306254138640(local_assigns, output_buffer)>
   #10 [block]   block in render <ActionView::Template#render(view, locals, buffer=?, &block)>
...

method_missingを使ってデリゲートしているようです。黒魔術の定石ですね。このActiveRecord::Delegation::ClassSpecificRelationをのぞいてみます。

📄activerecord/lib/active_record/relation/delegation.rb
# File activerecord/lib/active_record/relation/delegation.rb, line 57
      def method_missing(method, *args, &block)
        if @klass.respond_to?(method)
          self.class.delegate_to_scoped_klass(method)
          scoping { @klass.send(method, *args, &block) }
        elsif Array.method_defined?(method)
          self.class.delegate method, :to => :to_a
          to_a.send(method, *args, &block)
        elsif arel.respond_to?(method)
          self.class.delegate method, :to => :arel
          arel.send(method, *args, &block)
        else
          super
        end
      end

なるほどこうなっているのか。。Arrayにもデリゲートできるようですね(to_aされてしまうのでscopeチェインができなくなりますが)。う〜む・・こうしてみると、やはりrubyは偉大だと言わざるをえない。

関連する記事