🍇

【railsソースコード】has_manyを深掘る

2024/01/21に公開

Railsのversion: 7.1.3

はじめにまとめ

  • has_manyメソッドの定義場所: activerecord/lib/active_record/associations.rb
  • Associationの孫クラスを作成して、それをreflectionとして記録している

きっかけ

「Railsって、〇〇って書けば✕✕って動くんでしょ」 はなんとなくわかってきたけど、

「どういう仕組みでそう動くのか」 についても知ってみたくなったため

(has_manyを選んだのは、最近たまたま業務でさわっていたから)

見ていく

見ていくこと

  1. has_manyメソッドはどこに置かれているのか?
  2. has_manyメソッドのコードがしていること

※もし間違っているところなどありましたら、ご指摘・ご意見いただけますとありがたいです

1.has_manyメソッドはどこに置かれているのか?

has_manyメソッドが定義されているファイル: activerecord/lib/active_record/associations.rb

そのファイル内でのhas_manyメソッドの位置

activerecord/lib/active_record/associations.rb
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クラスとは?

ここで定義されているクラスらしい

activerecord/lib/active_record/associations/builder/has_many.rb
# frozen_string_literal: true

module ActiveRecord::Associations::Builder # :nodoc:
  class HasMany < CollectionAssociation # :nodoc:
    (略)
  end
end

class HasMany < CollectionAssociationとあり、CollectionAssociationを継承している。CollectionAssociationを見に行ってみる

activerecord/lib/active_record/associations/collection_association.rb
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_manyhas_many :throughの関連付けが行われたときに利用されるらしい

上記コードブロックの末尾にclass CollectionAssociation < Associationとあるので、次はAssociationを見に行ってみる

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

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

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が記録されているということになる

長くなってきたので、最後にReflectionadd_reflectionメソッドが記載されているところをあげて終わりにします

activerecord/lib/active_record/reflection.rb
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