【railsソースコード】has_manyを深掘る
Railsのversion: 7.1.3
はじめにまとめ
-
has_manyメソッドの定義場所:activerecord/lib/active_record/associations.rb -
Associationの孫クラスを作成して、それをreflectionとして記録している
きっかけ
「Railsって、〇〇って書けば✕✕って動くんでしょ」 はなんとなくわかってきたけど、
「どういう仕組みでそう動くのか」 についても知ってみたくなったため
(has_manyを選んだのは、最近たまたま業務でさわっていたから)
見ていく
見ていくこと
-
has_manyメソッドはどこに置かれているのか? -
has_manyメソッドのコードがしていること
※もし間違っているところなどありましたら、ご指摘・ご意見いただけますとありがたいです
1.has_manyメソッドはどこに置かれているのか?
has_manyメソッドが定義されているファイル: activerecord/lib/active_record/associations.rb
そのファイル内でのhas_manyメソッドの位置
module ActiveRecord
(略)
module Associations # :nodoc:
extend ActiveSupport::Autoload
extend ActiveSupport::Concern
(略)
private
(略)
module ClassMethods
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メソッドはActiveRecordモジュールをはじめ複数のモジュールの中で定義されている
理由がわからなかったこと
-
has_manyメソッドが定義されているmodule ClassMethodsが、なぜprivateキーワードの下にあるのか
2. has_manyメソッドのコードがしていること
改めて、has_manyメソッドのコードはこちら
def has_many(name, scope = nil, **options, &extension)
reflection = Builder::HasMany.build(self, name, scope, options, &extension)
Reflection.add_reflection self, name, reflection
end
1行目reflection = Builder::HasMany.build(self, name, scope, options, &extension)
Builder::HasManyクラスのインスタンスを作成し、それを変数reflectionに代入している
Builder::HasManyクラスとは?
ここで定義されているクラスらしい
# frozen_string_literal: true
module ActiveRecord::Associations::Builder # :nodoc:
class HasMany < CollectionAssociation # :nodoc:
(略)
end
end
class HasMany < CollectionAssociationとあり、CollectionAssociationを継承している。CollectionAssociationを見に行ってみる
module ActiveRecord
module Associations
# = Active Record Association Collection
#
# CollectionAssociation is an abstract class that provides common stuff to
# ease the implementation of association proxies that represent
# collections. See the class hierarchy in Association.
#
# CollectionAssociation:
# HasManyAssociation => has_many
# HasManyThroughAssociation + ThroughAssociation => has_many :through
#
# The CollectionAssociation class provides common methods to the collections
# defined by +has_and_belongs_to_many+, +has_many+ or +has_many+ with
# the <tt>:through association</tt> option.
(略)
class CollectionAssociation < Association # :nodoc:
(略)
コメントを読むと、このクラスはhas_manyとhas_many :throughの関連付けが行われたときに利用されるらしい
上記コードブロックの末尾にclass CollectionAssociation < Associationとあるので、次はAssociationを見に行ってみる
module ActiveRecord
module Associations
# = Active Record Associations
#
# This is the root class of all associations ('+ Foo' signifies an included module Foo):
#
# Association
# SingularAssociation
# HasOneAssociation + ForeignAssociation
# HasOneThroughAssociation + ThroughAssociation
# BelongsToAssociation
# BelongsToPolymorphicAssociation
# CollectionAssociation
# HasManyAssociation + ForeignAssociation
# HasManyThroughAssociation + ThroughAssociation
#
# Associations in Active Record are middlemen between the object that
# holds the association, known as the <tt>owner</tt>, and the associated
# result set, known as the <tt>target</tt>. Association metadata is available in
# <tt>reflection</tt>, which is an instance of +ActiveRecord::Reflection::AssociationReflection+.
#
(略)
class Association # :nodoc:
attr_reader :owner, :target, :reflection, :disable_joins
delegate :options, to: :reflection
def initialize(owner, reflection)
reflection.check_validity!
@owner, @reflection = owner, reflection
@disable_joins = @reflection.options[:disable_joins] || false
reset
reset_scope
@skip_strict_loading = nil
end
(略)
...has_manyメソッドの1行目から深いところまで来てしまったのでこれ以上は深追いしません
1点だけ言及したいのは
-
has_manyメソッドの1行目reflection = Builder::HasMany.build(self, name, scope, options, &extension)では、Associationクラスの孫クラスであるBuilder::HasManyクラスが使われている
ということ
出発点のBuilder::hasManyクラスではinitializeメソッドがなく、どこでやってるんだろと気になっていたため見つかってスッキリした
2行目Reflection.add_reflection self, name, 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
2行目のコードを見ていきます
add_reflectionというメソッドが実行されている。引数には先ほど作成した変数reflectionも含まれている
そもそも、この行に3度も出てきているreflectionとは何なのか?
情報工学においてリフレクション (reflection) とは、プログラムの実行過程でプログラム自身の構造を読み取ったり書き換えたりする技術のことを指す
https://ja.wikipedia.org/wiki/リフレクション_(情報工学)
copilotでは次の回答が来た
"reflection"は、プログラムが自身の構造や特性について情報を取得するための機能を指します。具体的には、クラスやオブジェクトのメソッドや属性、関連するクラスやモジュールなどに関する情報を取得することができます
話をコードに戻すと、Reflection.add_reflection self, name, reflectionでは、has_manyが書かれたモデル(例: author)と引数に書かれたモデル(例: books)のAssociationが記録されているということになる
長くなってきたので、最後にReflectionとadd_reflectionメソッドが記載されているところをあげて終わりにします
module ActiveRecord
# = Active Record Reflection
module Reflection # :nodoc:
extend ActiveSupport::Concern
(略)
class << self
def add_reflection(ar, name, reflection)
ar.clear_reflections_cache
name = -name.to_s
ar._reflections = ar._reflections.except(name).merge!(name => reflection)
end
おさらい
-
has_manyメソッドの定義場所:activerecord/lib/active_record/associations.rb -
has_manyメソッドはAssociationクラスを作成して、それをreflectionとして記録している - reflectionというプログラミングの基本概念みたいな言葉がある。意味は、プログラムが自身の構造や特性について情報を取得するための機能
よだん
コードリーディング記事を書くのはおすすめかもしれない。記事を書く、イコール人に読まれること前提なので、コードリーディングもひとりでのほほんをやるより集中できたから
参考資料
公式ドキュメント has_manyの部分: https://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#method-i-has_many
Discussion