24
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Railsのコードを読む アソシエーションについて

Last updated at Posted at 2016-10-16

今回はActiveRecordのhas_manyやbelongs_toをモデルで呼び出したときにどのような処理が行われているのかを追ってみました。

まずは自作モデルでアソシエーションを定義

models/user.rb
class User < ApplicationRecord
  has_one :profile
end
models/profile.rb
class Profile < ApplicationRecord
  belongs_to :user
end

アソシエーションの作成

Builder::HasOne.buildでアソシエーションに必要なコールバック、バリデーションの設定を行う。
リフレクションにはアソシエーションの情報が保存されている。

activerecord/lib/active_record/association.rb
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から順に追っていきます。

activerecord/lib/active_record/associations/builder/association.rb
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を呼び出します。

activerecord/lib/active_record/reflection.rb
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メソッドを定義しているメソッドです。

activerecord/lib/active_record/associations/builder/singular_association.rb
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に関連先にアクセスするためのメソッドが定義される。

activerecord/lib/active_record/associations/builder/association.rb
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情報をどこかに持ってるのかな?

activerecord/lib/active_record/reflection.rb
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ではなぜないのだろう。。。
まあ必要無いという判断もわからなくもないような気はする。

activerecord/lib/active_record/associations/builder/association.rb
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

activerecord/lib/active_record/associations/builder/collection_association.rb
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のオブジェクトにリフレクションを追加

activerecord/lib/active_record/association.rb
def has_one(name, scope = nil, options = {})
  reflection = Builder::HasOne.build(self, name, scope, options)
  Reflection.add_reflection self, name, reflection
end
activerecord/lib/active_record/reflection.rb
def self.add_reflection(ar, name, reflection)
  ar.clear_reflections_cache
  ar._reflections = ar._reflections.merge(name.to_s => reflection)
end

まとめ

アソシエーションのクラスマクロを呼び出すと以下の事が行われる。

  1. アソシエーション指定時に渡されたextensionの設定
  2. リフレクションの作成と追加
  3. アソシエーションのみに関連するコールバックの設定 (after_addなど after_saveは違うところ)
  4. アソシエーションメソッドを定義 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/

24
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
24
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?