ActiveRecord::Baseのサブクラスでhas_manyと書いたとき
なんかいまいちだけど、置いとこう。。
まとめ
has_manyメソッドを書くと、has_manyメソッドに与えたパラメータを元に、
has_manyのアソシエーションのメソッドのマクロ名などが登録されたクラスのインスタンスをFactoryPatternっぽい感じで作り、
そのインスタンスをActiveRecordの持っている動的に追加したメソッド一覧みたいな_reflections
というハッシュに追加している。
has_manyのアソシエーションのメソッドのマクロなどが登録されたメソッドが作成されたあと、
それを使った(追いきれてないからそう言う気がするだけ)getterやwriterは
その場でModelにメソッドを追加するためのモジュールを作成/呼び出しをして、
そのmoduleのclass_eval
メソッドにブロックを与える形で追加してる。
たぶん、belongs_toとかvalidateとか他のオプションも処理をしたいから、 こういう処理になっている気がしましたが...
なんか空っぽのメソッドがあったり、細かい記法が分からなかったりしてなぞが多い。
内容
- きっかけとして
- ActiveRecordでhas_manyを呼び出したとき
- Builder::HasMany.buildの中身をもう少し見る
- create_reflection
- define_accessors
- define_callbacks
- reflectionを作成すると...
きっかけとして
acts_as_votable
というgemで、以下のように、その属性の指定を足すかのようなメソッドを書くと、
その場でModelクラスにクラスメソッドを定義するかのような実装のgemがあるのですが、
# https://github.com/ryanto/acts_as_votable/blob/master/lib/acts_as_votable/extenders/voter.rb def acts_as_voter(*args) require 'acts_as_votable/voter' include ActsAsVotable::Voter class_eval do def self.voter? true end end end
あ、もしかしてhas_many
などもそうでは? と思って調べてみる事にしたのでした。
ActiveRecordでhas_manyを呼び出したとき
has_many
がDSLの単語ではなくメソッド名としたら、ActiveRecord
クラス内でdef has_many
と検索したらどこかで引っかかると思います。
というわけで検索した結果、active_record/lib/active_record/associations.rb
で発見。
# Option examples: # has_many :comments, -> { order "posted_on" } # has_many :comments, -> { includes :author } # has_many :people, -> { where(deleted: false).order("name") }, class_name: "Person" # has_many :tracks, -> { order "position" }, dependent: :destroy # has_many :comments, dependent: :nullify # has_many :tags, as: :taggable # has_many :reports, -> { readonly } # has_many :subscribers, through: :subscriptions, source: :user def has_many(name, scope = nil, options = {}, &extension) reflection = Builder::HasMany.build(self, name, scope, options, &extension) Reflection.add_reflection self, name, reflection end
has_manyでassociationのメソッドの名前と、scope, optionsなどそれらしい引数を取っているので、 has_manyはやっぱり文法めいたメソッドのようです。
ところで、reflectionというものを作成して追加しているようですが、実際何をしているのでしょうか。
reflectionというのは文字列を使ってクラスを生成したり、メソッド名の文字列を使ってメソッドを呼び出したりする類いの行為を言っているようで、要するに見たままですね。。
Builder::HasMany.build(self, name, scope, options, &extension)
メソッドに、
self(=ここではModelのクラス?), name(メソッド名)を与えて、Modelのクラスに登録したいメソッドを作成しているようです。
もう少し追ってみましょう。
Builder::HasMany.buildの中身をもう少し見る
Builder::HasMany.build
でreflectionを生成(=メソッドに与えた文字列を使って追加したいメソッドを定義している?)している様子を
もう少し追ってみます。
Builder::HasMany.build
メソッドの内容は、Builder::HasMany
クラス、そのスーパークラスのCollectionAssociation
クラスを通り越して、
ActiveRecord::Associations::Builder::Assocation
クラス(active_record/lib/active_record/associations/builder/association.rb
)にあります。
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
create_reflection
reflection = create_reflection model, name, scope, options, extension
ここは、受け取ったパラメータからhas_manyメソッドに必要な条件のバリデーションや性質を表す値のセットを持った メソッドのProductに当たるクラスを返しているようです(Factoryパターンっぽい)。
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 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 model, reflection define_callbacks model, reflection
では、それぞれModelのクラスにメソッドを追加しているように見えます。
define_accessors
# Post.first.comments and Post.first.comments= methods are defined by this method... def self.define_accessors(model, reflection) mixin = model.generated_association_methods # Modelクラスのassociationメソッド追加用のmoduleを取得/作成 name = reflection.name define_readers(mixin, name) define_writers(mixin, name) end def self.define_readers(mixin, name) # class_evalメソッドは、ブロックをクラス定義やモジュール定義の中のコードであるように実行 mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 def #{name}(*args) # ここのassocitaionはHasManyのインスタンスのような気がしたけど、気だけ... association(:#{name}).reader(*args) end CODE end
# ActiveRecord::Core module # Modelクラスのassociationメソッド追加用のmoduleを取得/作成 def generated_association_methods @generated_association_methods ||= begin mod = const_set(:GeneratedAssociationMethods, Module.new) include mod mod end 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 # before_destroyのコールバックが追加されているような気がするのですが、 # 詳細が追えなかった... def self.add_destroy_callbacks(model, reflection) name = reflection.name model.before_destroy lambda { |o| o.association(name).handle_dependency } end
なお、define_validations model, reflection
はただ置いてあるだけみたいです。
def self.define_validations(model, reflection) # noop end
reflectionが持っているname属性等を使って、modelクラスにgetterやコールバックを追加しているみたいなのは分かりました。 では、builderが返したreflectionをどうしているかもう少し見てみます。
reflectionを作成すると...
def has_many(name, scope = nil, options = {}, &extension) reflection = Builder::HasMany.build(self, name, scope, options, &extension) Reflection.add_reflection self, name, reflection end
modelにgetterやsetterをその場でmodelに登録していた感じですが、 buildメソッド内で作成したアソシエーションのメソッドそのものはまだクラスに登録していなさそうです。
# ActiveRecord::Reflection def self.add_reflection(ar, name, reflection) ar._reflections = ar._reflections.merge(name.to_s => reflection) end
というわけで、上記のように、ActiveRecordのクラスが持っているらしい、self._reflections
というハッシュに
メソッド名をキー、アソシエーションのメソッドの中身と値とした要素を追加していそうなのですが、
ここで時間切れです。というか、まあ、じつは
module Reflection # :nodoc: extend ActiveSupport::Concern included do class_attribute :_reflections class_attribute :aggregate_reflections self._reflections = {} self.aggregate_reflections = {} end
この記法の意味が分からないのですが。。