ActiveRecord::Base の setter メソッドの謎
仕事仲間から ActiveRecord::Base#[:属性名]= と ActiveRecord::Base#(属性名)= の違いをたずねられた。
そこでちょっとしらべてみた。
たとえば Entry という AR クラスがあるとして、そこに title という属性があるとしよう。
entry = Entry.find(:first) entry.title # => "Hello World!" entry[:title] # => "Hello World!" entry.title = "こんにちは" v[:title] = "おはよう"
のように entry[:title] と entry.title の両方を使うことができるのだ。
ActiveRecord::Base#, ActiveRecord::Base#= の定義は以下のとおり。
def [](attr_name) read_attribute(attr_name) end def []=(attr_name, value) write_attribute(attr_name, value) end
これは素直な実装だ。read_attribute, write_attribute の詳細には立ち入らないが、AR オブジェクトのカラムごとの値が格納されている @attributes というハッシュを参照・更新している。
では、ActiveRecord::Base#(属性名) はどうであろう?これは ActiveRecord::Base#method_missing を見るとわかる。
def method_missing(method_id, *args, &block) method_name = method_id.to_s if @attributes.include?(method_name) or # (A) (md = /\?$/.match(method_name) and @attributes.include?(query_method_name = md.pre_match) and method_name = query_method_name) define_read_methods if self.class.read_methods.empty? && self.class.generate_read_methods md ? query_attribute(method_name) : read_attribute(method_name) elsif self.class.primary_key.to_s == method_name id elsif md = self.class.match_attribute_method?(method_name) #(B) attribute_name, method_type = md.pre_match, md.to_s if @attributes.include?(attribute_name) __send__("attribute#{method_type}", attribute_name, *args, &block) else superd end else super end end
たとえば、entry.title の場合、まず Entry クラスには title メソッドは定義されていないので、上の method_missing が呼び出される。すると (A) の条件に一致して、内部が実行される。具体的には、define_read_methods が呼び出され、ここで Entry#title 等、すべての属性の getter メソッドが定義される。
さて最後までわからなかったのが Entry#title= 等の setter メソッドである。どうやら、これも method_missing で処理されているらしいのだが、具体的にどこで処理されているのかわからない。いろいろ調べてみると、驚くべきことに上の (B) で処理されていることがわかった。
attribute methods というテクニックを使っているのだ。AR クラス関係のファイルのひとつに attribute_methods.rb があるので中身を見てほしい。全文を掲載したが、短くするためにコメントを省略した。
module ActiveRecord module AttributeMethods DEFAULT_SUFFIXES = %w(= ? _before_type_cast) def self.included(base) base.extend ClassMethods base.attribute_method_suffix *DEFAULT_SUFFIXES end module ClassMethods def attribute_method_suffix(*suffixes) attribute_method_suffixes.concat suffixes rebuild_attribute_method_regexp end def match_attribute_method?(method_name) rebuild_attribute_method_regexp unless defined?(@@attribute_method_regexp) && @@attribute_method_regexp @@attribute_method_regexp.match(method_name) end private def rebuild_attribute_method_regexp suffixes = attribute_method_suffixes.map { |s| Regexp.escape(s) } @@attribute_method_regexp = /(#{suffixes.join('|')})$/.freeze end def attribute_method_suffixes @@attribute_method_suffixes ||= [] end end private def attribute?(attribute_name) query_attribute(attribute_name) end # Handle *= for method_missing. def attribute=(attribute_name, value) write_attribute(attribute_name, value) end def attribute_before_type_cast(attribute_name) read_attribute_before_type_cast(attribute_name) end end end
module ClassMethods 以下のメソッドは、おなじみのテクニックで ActiveRecord::Base に mix-in される。つまり ActiveRecord::Base のインスタンスメソッドとして振る舞うということである。attribute methods はどう使うかというと、以下の例のように使う。(ソースコードコメントより)
class Person < ActiveRecord::Base attribute_method_suffix '_changed?' private def attribute_changed?(attr) ... end end person = Person.find(1) person.name_changed? # => false person.name = 'Hubert' person.name_changed? # => true
上の例では _changed? という接尾辞を定義している。すると name_changed? が自動的に使えるようになる。仮に address という属性があれば address_changed? も自動的に使える。いずれにしろ実体は attribute_changed? であり、 name_changed? でも address_changed? でも同じメソッドが呼び出されることになる。
さて、ActiveRecord の実例をみると、DEFAULT_SUFFIXES = %w(= ? _before_type_cast) と定義されていることからわかるように、基本的に =, ?, _before_type_cast という3種類の接尾辞があらかじめ定義されているのだ。
つまりこれで、Entry#title= が使えることがわかった。Entry#title= の実体は上の attribute= である。この内部処理は、[]= と同じく write_attribute の呼び出しである。代入のために特別な処理を書かず、attribute methods という汎用的なテクニックで処理しているあたり、さすがは Rails とうなってしまった。