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
モデルの拡張を利用するための設定?
- http://stackoverflow.com/questions/2328984/rails-extending-activerecordbase
- http://edgeguides.rubyonrails.org/active_support_core_extensions.html
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とかが呼び出される、というのは分かった気がしたけど、 それがどこでメソッド呼び出しになっているのか、分かってからでいいかね...。