今回はActiveRecordのhas_manyやbelongs_toをモデルで呼び出したときにどのような処理が行われているのかを追ってみました。
まずは自作モデルでアソシエーションを定義
class User < ApplicationRecord
has_one :profile
end
class Profile < ApplicationRecord
belongs_to :user
end
アソシエーションの作成
Builder::HasOne.buildでアソシエーションに必要なコールバック、バリデーションの設定を行う。
リフレクションにはアソシエーションの情報が保存されている。
def has_one(name, scope = nil, options = {})
reflection = Builder::HasOne.build(self, name, scope, options)
Reflection.add_reflection self, name, reflection
end
# Build::HasOneの親子関係は以下の通り
self.ancestors
=> [ActiveRecord::Associations::Builder::HasOne,
ActiveRecord::Associations::Builder::SingularAssociation,
ActiveRecord::Associations::Builder::Association,
・・・
]
リフレクションクラスにアソシエーションの情報を作成
define_extensionsから順に追っていきます。
def self.build(model, name, scope, options, &block)
if model.dangerous_attribute_method?(name)
raise ArgumentError, "エラーメッセージ"
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
define_extensions
アソシエーションを定義する時にブロックを渡すと拡張を行える。
ただしhas_many has_many_and_belongs_toのみ
Personモデルに記述してオーバーライドすれば良いんじゃないかって思いましたが、AccountからPeopleを参照した時にだけ有効にしたいときに使えそう。
参考サイト
class Account < ApplicationRecord
has_many :people do
def find_or_create_by_name(name)
first_name, last_name = name.split(" ", 2)
find_or_create_by(first_name: first_name, last_name: last_name)
end
end
end
person = Account.first.people.find_or_create_by_name("David Heinemeier Hansson")
person.first_name # => "David"
person.last_name # => "Heinemeier Hansson"
create_reflection
create_reflectionではhas_many belongs_toなどに渡すオプションが正しいものかバリデーションチェックとscopeの設定を行い、Reflection.createを呼び出します。
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
# Reflectionの継承関係はこちら
[3] pry(ActiveRecord::Reflection)> klass.ancestors
=> [ActiveRecord::Reflection::HasOneReflection,
ActiveRecord::Reflection::AssociationReflection,
ActiveRecord::Reflection::MacroReflection,
ActiveRecord::Reflection::AbstractReflection
・・・
]
上記の klass.new で下記initializeが呼ばれ各値がセットされる
# AggregateReflection < MacroReflection
def initialize(name, scope, options, active_record)
super
@automatic_inverse_of = nil
@type = options[:as] && (options[:foreign_type] || "#{options[:as]}_type")
@foreign_type = options[:foreign_type] || "#{name}_type"
@constructable = calculate_constructable(macro, options)
@association_scope_cache = {}
@scope_lock = Mutex.new
end
# class MacroReflection < AbstractReflection
def initialize(name, scope, options, active_record)
@name = name
@scope = scope
@options = options
@active_record = active_record
@klass = options[:anonymous_class]
@plural_name = active_record.pluralize_table_names ?
name.to_s.pluralize : name.to_s
end
define_accessors
UserとProfileが紐付いている時にUserのオブジェクトからprofileメソッドでアクセス、またProfileからuserメソッドでアクセス出来るようにprofile userメソッドを定義しているメソッドです。
def self.define_accessors(model, reflection)
super
define_constructors(model.generated_association_methods, reflection.name) if reflection.constructable?
end
generated_association_methodsでActiveRecord::CoreにincludeされるModuleが返却される。
define_readers define_writersで返却されたModuleに関連先にアクセスするためのメソッドが定義される。
def self.define_accessors(model, reflection)
mixin = model.generated_association_methods
name = reflection.name
define_readers(mixin, name)
define_writers(mixin, name)
end
singular_association.rb ではdefine_accessotrsといいつつもcreate_userやbuild_userなどの生成系メソッドもdefine_constructorsを呼び出しているため作成される。
reflection.constructable?は下記にて返却される値を参照しているため has_oneの場合にはthroughオプション、polymorphic関連のbelongs_to側では作成されない。
なぜhas_manyのthroughオプションはなぜ生成されるのだろうか。。。
中間テーブルのRefrection情報をどこかに持ってるのかな?
class HasOneReflection < AssociationReflection # :nodoc:
# 省略
private
def calculate_constructable(macro, options)
!options[:through]
end
end
class BelongsToReflection < AssociationReflection # :nodoc:
# 省略
private
def calculate_constructable(macro, options)
!polymorphic?
end
end
define_callbacks
dependentオプションを設定したときには子レコードを削除するためのコールバックとextension.buildではautosaveのためのコールバックを設定している。
has_manyではbefore_add after_add before_remove after_removeを設定しているがhas_oneではなぜないのだろう。。。
まあ必要無いという判断もわからなくもないような気はする。
module ActiveRecord::Associations::Builder # :nodoc:
class Association #:nodoc:
# 省略
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
end
end
has_many has_many_and_belongs_toのdefine_callbacks
module ActiveRecord::Associations::Builder # :nodoc:
class CollectionAssociation < Association #:nodoc:
# 省略
def self.define_callbacks(model, reflection)
super
name = reflection.name
options = reflection.options
CALLBACKS.each { |callback_name|
define_callback(model, callback_name, name, options)
}
end
end
end
define_validations
ここでは何もしていない
def self.define_validations(model, reflection)
# noop
end
ActiveRecordのオブジェクトにリフレクションを追加
def has_one(name, scope = nil, options = {})
reflection = Builder::HasOne.build(self, name, scope, options)
Reflection.add_reflection self, name, reflection
end
def self.add_reflection(ar, name, reflection)
ar.clear_reflections_cache
ar._reflections = ar._reflections.merge(name.to_s => reflection)
end
まとめ
アソシエーションのクラスマクロを呼び出すと以下の事が行われる。
- アソシエーション指定時に渡されたextensionの設定
- リフレクションの作成と追加
- アソシエーションのみに関連するコールバックの設定 (after_addなど after_saveは違うところ)
- アソシエーションメソッドを定義 user.profile のprofileメソッドやuser.create_profile など
登場クラス
Association
# アソシエーションに関連するModuleのautoloadやアソシエーションのクラスマクロを定義
ActiveRecord::Associations
# アソシエーション定義するときにリフレクションの設定やコールバックの設定を行う
ActiveRecord::Associations::Builder::Association
# belongs_toやhas_oneなど1対のアソシエーションを担当
ActiveRecord::Associations::Builder::SingularAssociation < Association
ActiveRecord::Associations::Builder::HasOne < SingularAssociation
# has_manyなど多対のアソシエーションを担当
ActiveRecord::Associations::Builder::CollectionAssociation < Association
ActiveRecord::Associations::Builder::HasMany < CollectionAssociation
Reflection
# アソシエーション名 スコープ(lambda) オプション(class_name, throughなど) 外部キーの情報などを保持
ActiveRecord::Reflection::AbstractReflection
ActiveRecord::Reflection::MacroReflection < AbstractReflection
ActiveRecord::Reflection::AssociationReflection < MacroReflection
ActiveRecord::Reflection::HasOneReflection < AssociationReflection
ActiveRecord::Reflection::HasManyReflection < AssociationReflection
個人的に気に入ったコード
アソシエーションメソッドを定義するところでCoreモジュールからincludeした空のModuleを返却して、受け取ったAssociation側ではdefine_readers define_writerの中でclass_evalを使ってメソッドを追加させているところが個人的にお気に入り。
非常に拡張しやすい。
module ActiveRecord
module Core
def generated_association_methods
@generated_association_methods ||= begin
mod = const_set(:GeneratedAssociationMethods, Module.new)
include mod
mod
end
end
end
end
module ActiveRecord::Associations::Builder # :nodoc:
class Association #:nodoc:
def self.define_accessors(model, reflection)
mixin = model.generated_association_methods
name = reflection.name
define_readers(mixin, name)
define_writers(mixin, name)
end
end
end
参考にしたサイト
http://woshidan.hatenadiary.jp/entry/2015/04/18/194612
http://callahan.io/blog/2014/10/08/behind-the-scenes-of-the-has-many-active-record-association/