ActiveRecord::QueryMethods#extendingはscopeじゃなかった


勝手に期待して勝手に失望した備忘録。

いきさつ

仕事上、Railsで扱うモデルがファットになっていたのでやれConcernだのServiceだのDecoratorだのを使ってドメインごとの切り出しをやっている最中なんだけれども、根本的に単一のモデルで扱っているメソッドが多いよねという問題は大して変わっていない。
特にあるモデルについていくつかのクエリは管理機能でしか出番がないから普段はincludeしなければいいのではと思い、特定の条件でのみモジュールをextendする方法を模索していた。

ActiveRecord::QueryMethods#extendingとの出会い

上記の方法を実現するために悪戦苦闘中していたら件名にもあるextendingに出会った。
Rails4.0から追加されたメソッドで、Railsドキュメント1やAPIdockによれば、「scopeのように使える」という書かれ方をしている。

Used to extend a scope with additional methods, either through a module or through a block provided.
http://apidock.com/rails/ActiveRecord/QueryMethods/extending

モジュールやブロックメソッドを引数に与えて、モデルをscopeで拡張する。
返されたオブジェクトをさらに拡張することもできる。
http://railsdoc.com/references/extending

そうであれば願ったり叶ったりなのだが、この記事の結論としては、これらの記述は誤りですよという主張になる。
なぜか。extendingで追加されるメソッドはscopeで定義した場合と挙動が違うからだ。

そもそもscopeとは

scopeは内部的にはクラスメソッドである。が、厳密にはちょっと違う。

この記事はRails3ベースなので少し古いが、概略をさっと抑えるのに役立つ2
http://kotatu.org/blog/2014/10/11/why-should-use-scopes-over-class-methods/

差異になる部分はこないだストックして大変参考になった記事を参照のこと。scopeはブロック内のがnilになった場合には裏側でallに置き換わっている。
http://qiita.com/hamajyotan/items/3b3ddd022694c6a22a41

APIdockにも同様のことが書いてある。3

つまり、scopeという言葉を使うからには筆者としては「ブロックがnilだったらall返ってくるんだよね?」という挙動を期待したいのである。

が、違ったんだな、これが。

extendingは内部的にはextending!を呼んでいるので該当するソースをAPIdock経由で引用する。4

# File activerecord/lib/active_record/relation/query_methods.rb, line 832
    def extending!(*modules, &block) # :nodoc:
      modules << Module.new(&block) if block
      modules.flatten!

      self.extending_values += modules
      extend(*extending_values) if extending_values.any?

      self
    end

ソースを読むとなんてことはない、ただモジュールをまるっとextendして特異メソッド定義しているだけでしたというお話である。つまり、extendingで追加したメソッドがnilになったからallになってくれるかというとそんなことはありませんでしたというわけである。

RailsGuidesの記述

RailsGuidesではどのように書かれているかというと、全く違った文脈で使用されている。
説明とサンプルコードを引用する。

Railsは自動的に関連付けのプロキシオブジェクトをビルドしますが、開発者はこれをカスタマイズすることができます。(中略)拡張を多くの関連付けで共有したい場合は、名前付きの拡張モジュールを使用することもできます。

module FindRecentExtension
  def find_recent
    where("created_at > ?", 5.days.ago)
  end
end

class Customer < ActiveRecord::Base
  has_many :orders, -> { extending FindRecentExtension }
end

class Supplier < ActiveRecord::Base
  has_many :deliveries, -> { extending FindRecentExtension }
end

ここでは、関連付けを別クラスに分割するための表記としてextendingが使用されており、他の記述は見当たらない。現状ではscopeの代わりとして使うのはちょっと違うのではないかと思う。

いきさつに戻る

extendingするときにメソッドがscopeとして解釈ができるようになると自分としては嬉しい。
そこでどう変更したら実現できるかについて手元でトライしているが、苦戦しているので多分すぐには解決しない。
取りあえずこの記事では現状でわかっていることについて記述した次第である。

脚注


  1. そもそもrailsdoc.comは間違いや適当な記述が多いので陳腐化したjQueryの日本語ドキュメント同様「ああアレアレ解消サービス」くらいの位置づけとみなしている 

  2. 参照ブログのさらに原文は http://blog.plataformatec.com.br/2013/02/active-record-scopes-vs-class-methods/ 

  3. この箇所についてはソース追いかけたので今回の内容に関しては合っているはず 

  4. ここもソース読んで裏を取った