読者です 読者をやめる 読者になる 読者になる

woshidan's loose leaf

ぼんやり勉強しています。

has_manyの指定がModelで使用可能なメソッドのリストとして追加されるまで

今更ふと思いついたのですが、もしかして has_manyメソッドとかは、 そういうクラスのアソシエーションの設定用配列にpushする、みたいな感じなのかという気がしたので、 それだけで追って見る。

(結論としてはhas_manyメソッド読んだときに、パラメータを元に設定用のクラスのインスタンスを作って、それをアソシエーションの設定用ハッシュにmergeしてる感じがしました。)

railsのgemでこういうキーワード? メソッド? の記述をしたとき、 自動的にこう言う処理ができる属性ができるよと言われているとき、 言われたときに見た事ある書き方な気がする(act_as_voterとか)し、 ruby黒魔術的によくある書き方なのでしょうか...。

内容

  • has_manyメソッドの定義から、中身の分かるbuildメソッドを探す
  • buildメソッドの各行で呼び出されているメソッドをもう少し確認する
    • define_extensions
    • create_reflection
    • define_accessors
    • define_callbacks
    • define_validations
  • まとめ

has_manyメソッドの定義から、中身の分かるbuildメソッドを探す

# associations.rb
  def has_many(name, scope = nil, options = {}, &extension)
    reflection = Builder::HasMany.build(self, name, scope, options, &extension)
    Reflection.add_reflection self, name, reflection
  end
# activerecord/lib/active_record/associations/builder/has_many.rb
module ActiveRecord::Associations::Builder
  class HasMany < CollectionAssociation #:nodoc:
    def self.macro
      :has_many
    end

    def self.valid_options(options)
      super + [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of, :counter_cache, :join_table, :foreign_type]
    end

    def self.valid_dependent_options
      [:destroy, :delete_all, :nullify, :restrict_with_error, :restrict_with_exception]
    end
  end
end
# activerecord/lib/active_record/associations/builder/collection_association.rb
# This class is inherited by the has_many and has_many_and_belongs_to_many association classes

require 'active_record/associations'

module ActiveRecord::Associations::Builder
  class CollectionAssociation < Association #:nodoc:

    CALLBACKS = [:before_add, :after_add, :before_remove, :after_remove]

    def self.valid_options(options)
      super + [:table_name, :before_add,
               :after_add, :before_remove, :after_remove, :extend]
    end

    def self.define_callbacks(model, reflection)

    def self.define_extensions(model, name)

    def self.define_callback(model, callback_name, name, options)

    # Defines the setter and getter methods for the collection_singular_ids.
    def self.define_readers(mixin, name)

    def self.define_writers(mixin, name)

    def self.wrap_scope(scope, mod)
  end
end

ActiveRecord::Associations::Builderにはbuildメソッドが無いのでsuperクラスっぽい。 (あとで、適当なrails console立ち上げて、superclassとかのメソッド叩けばいいのでは? と思ったのは内緒ですね)

# activerecord/lib/active_record/associations/builder/association.rb
module ActiveRecord::Associations::Builder
  class Association #:nodoc:
    class << self
      attr_accessor :extensions
    end
    self.extensions = []

    VALID_OPTIONS = [:class_name, :class, :foreign_key, :validate] # :nodoc:

    def self.build(model, name, scope, options, &block)
      if model.dangerous_attribute_method?(name)
        raise ArgumentError, "You tried to define an association named #{name} on the model #{model.name}, but " \
                             "this will conflict with a method #{name} already defined by Active Record. " \
                             "Please choose a different association name."
      end

      extension = define_extensions model, name, &block
      reflection = create_reflection model, name, scope, options, extension
      define_accessors model, reflection
      define_callbacks model, reflection
      define_validations model, reflection
      reflection
    end

おおーこれっぽい。

以上をまとめると、Model ( < ActiveRecord::Base )のクラスでhas_manyを読んだとき、

# associations.rb
  def has_many(name, scope = nil, options = {}, &extension)
    reflection = Builder::HasMany.build(self, name, scope, options, &extension)
    Reflection.add_reflection self, name, reflection
  end

ActiveRecord::Associations::Builder::Association.buildが呼び出されて、 その中で、Modelクラスにメソッドの定義を追加しているっぽい。

module ActiveRecord::Associations::Builder
  class Association #:nodoc:
    class << self
    ...
    def self.build(model, name, scope, options, &block)
    ... # 危険な定義をしようとしたときに警告を投げる
      extension = define_extensions model, name, &block
      reflection = create_reflection model, name, scope, options, extension
      define_accessors model, reflection
      define_callbacks model, reflection
      define_validations model, reflection
      reflection
  end

buildメソッドの各行で呼び出されているメソッドをもう少し確認する

extension = define_extensions model, name, &block
reflection = create_reflection model, name, scope, options, extension
define_accessors model, reflection
define_callbacks model, reflection
define_validations model, reflection
reflection

この辺りをもうちょっと読んでみる。

define_extensions

# association.rb
# build内で呼び出してエラーがでないように空でいいから定義してあるって感じ
# サブクラスでは定義されている
def self.define_extensions(model, name)
end

# collection_association.rb
def self.define_extensions(model, name)
  if block_given?
    extension_module_name = "#{model.name.demodulize}#{name.to_s.camelize}AssociationExtension"
    extension = Module.new(&Proc.new)
    model.parent.const_set(extension_module_name, extension)
  end
end

モデルの拡張を利用するための設定?

create_reflection

# association.rb
def self.create_reflection(model, name, scope, options, extension = nil)
  raise ArgumentError, "association names must be a Symbol" unless name.kind_of?(Symbol)

  if scope.is_a?(Hash)
    options = scope
    scope   = nil
  end

  validate_options(options)

  scope = build_scope(scope, extension)

  ActiveRecord::Reflection.create(macro, name, scope, options, model)
end

5つがそれぞれ設定作ってるのかなーって思ったけど、これは、 create_reflection が設定作ってる気がする!

ところで、reflectionって一般的な意味以上にデザインパターンかなんかの名前だろうか。

[reflection design pattern]

Factory patternに関係している英語っぽいので、あとで読んでみよう。。

http://www.oodesign.com/factory-pattern.html

デザインパターン忘れてきたのでもう一回勉強したいけど、とりあえず、始めたモバイル基礎とRubyとDBやりたい。。 なんかこの辺が、設定を作ってるっぽい気がするけど、それで、もうちょっと読んでみると、

# ActiveRecord::Reflection
    def self.create(macro, name, scope, options, ar)
      klass = case macro
              when :composed_of
                AggregateReflection
              when :has_many
                HasManyReflection
              when :has_one
                HasOneReflection
              when :belongs_to
                BelongsToReflection
              else
                raise "Unsupported Macro: #{macro}"
              end

      reflection = klass.new(name, scope, options, ar)
      options[:through] ? ThroughReflection.new(reflection) : reflection
    end

となっているから、渡されたオプションに応じて、 設定用の変数を作るクラスのインスタンスを作成して返しているみたい。

HasManyReflectionのクラスだけ探してみると、

# ActiveRecord::Reflection
class HasManyReflection < AssociationReflection # :nodoc:
  def initialize(name, scope, options, active_record)
    super(name, scope, options, active_record)
  end

  def macro; :has_many; end

  def collection?; true; end
end

define_accessors

コメントついてたので、少し楽だー。

    # Defines the setter and getter methods for the association
    # class Post < ActiveRecord::Base
    #   has_many :comments
    # end
    #
    # Post.first.comments and Post.first.comments= methods are defined by this method...
    def self.define_accessors(model, reflection)
      mixin = model.generated_association_methods
      name = reflection.name
      define_readers(mixin, name)
      define_writers(mixin, name)
    end

    def self.define_readers(mixin, name)
      mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
        def #{name}(*args)
          association(:#{name}).reader(*args)
        end
      CODE
    end

    def self.define_writers(mixin, name)
      mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
        def #{name}=(value)
          association(:#{name}).writer(value)
        end
      CODE
    end

define_callbacks

def self.define_callbacks(model, reflection)
  if dependent = reflection.options[:dependent]
    check_dependent_options(dependent)
    add_destroy_callbacks(model, reflection)
  end

  Association.extensions.each do |extension|
    extension.build model, reflection
  end
end

define_validations

# association.rb
def self.define_validations(model, reflection)
  # noop
end

noopは何の貢献もしない奴、とのこと。サブクラスの実装を見てみる。

module ActiveRecord::Associations::Builder
  class CollectionAssociation < Association #:nodoc:
  ...

ない。

module ActiveRecord::Associations::Builder
  class HasMany < CollectionAssociation #:nodoc:
    def self.macro
      :has_many
    end

    def self.valid_options(options)
      super + [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of, :counter_cache, :join_table, :foreign_type]
    end

    def self.valid_dependent_options
      [:destroy, :delete_all, :nullify, :restrict_with_error, :restrict_with_exception]
    end
  end
end

ない。あ、もしかして本当に仕事してない...?

まとめ

まとめると、

はじめにModelへの拡張が使えるようにする。

def self.define_extensions(model, name)
  if block_given?
    extension_module_name = "#{model.name.demodulize}#{name.to_s.camelize}AssociationExtension"
    extension = Module.new(&Proc.new)
    model.parent.const_set(extension_module_name, extension)
  end
end

どのアクセサが使えるかどうかは、オプションからReflectionを選ぶ形で行われる。

    def self.create(macro, name, scope, options, ar)
      klass = case macro
              when :composed_of
                AggregateReflection
              when :has_many
                HasManyReflection
              when :has_one
                HasOneReflection
              when :belongs_to
                BelongsToReflection
              else
                raise "Unsupported Macro: #{macro}"
              end

      reflection = klass.new(name, scope, options, ar)
      options[:through] ? ThroughReflection.new(reflection) : reflection
    end

has_manyなどのaccessorは上記の選択に基づいて

def self.define_accessors(model, reflection)
  mixin = model.generated_association_methods
  name = reflection.name
  define_readers(mixin, name)
  define_writers(mixin, name)
end

で定義。 Reflectionや拡張モジュールによってはアクセサだけでなく、コールバックが必要になる場合があるので、

    def self.define_callbacks(model, reflection)
      if dependent = reflection.options[:dependent]
        check_dependent_options(dependent)
        add_destroy_callbacks(model, reflection)
      end

      Association.extensions.each do |extension|
        extension.build model, reflection
      end
    end

(このコールバックは、例えば、has_manyの1の方の要素をdestroyしたら子要素もdestroyするメソッドを呼ぶぞ的なものっぽい。以下コード参照)

def self.add_destroy_callbacks(model, reflection)
  name = reflection.name
  model.before_destroy lambda { |o| o.association(name).handle_dependency }
end

実際のhas_manyなどの定義は、Associaton#build メソッドの返り値、reflectionのインスタンスを受け取った、 has_many.rb内の

# associations.rb
  def has_many(name, scope = nil, options = {}, &extension)
    reflection = Builder::HasMany.build(self, name, scope, options, &extension)
    Reflection.add_reflection self, name, reflection # このadd_reflectionメソッド!
  end

  def self.add_reflection(ar, name, reflection)
    ar._reflections = ar._reflections.merge(name.to_s => reflection)
  end

にて行われる。

読んだログそのままに近いから、もう少し直接のコードを少なくして、短くまとめ直して本家の方にも置こうかな。 でもreflectionsのハッシュに追加されたら、has_manyとかが呼び出される、というのは分かった気がしたけど、 それがどこでメソッド呼び出しになっているのか、分かってからでいいかね...。