RailsのActiveRecordを使ったモデルに定義されたassociation情報を全て取得する reflect_on_all_associations のソースコードを読んでみる

こんにちは!kossyです!




諸事情ありブログを書かずにおりました、、、

月8 ~ 10本ペースは守りたいと思いつつも、なかなか時間の確保が厳しいですね、、、(根性が足りないと言われればその通りなのですが)




今回は、RailsActiveRecordを使ったモデルに定義されたassociation情報を全て取得する reflect_on_all_associations のソースコードを読んでみたので、

備忘録としてブログに残してみたいと思います。




環境
Ruby 2.6.9
Rails 6.1.4




reflect_on_all_associations

まずは適当な箇所にbinding.pryを仕込んで、stepメソッドで処理の内部を見てみます。

From: /usr/local/bundle/gems/activerecord-6.0.4.1/lib/active_record/reflection.rb:104 ActiveRecord::Reflection::ClassMethods#reflect_on_all_associations:

    103: def reflect_on_all_associations(macro = nil)
 => 104:   association_reflections = reflections.values
    105:   association_reflections.select! { |reflection| reflection.macro == macro } if macro
    106:   association_reflections
    107: end

[1] pry(User)>

定義元はこちらですね。

github.com

いろいろ実行する前に、コメントアウト部分を読んでみます。

Returns an array of AssociationReflection objects for all the associations in the class.

If you only want to reflect on a certain association type, pass in the symbol :has_many, :has_one, :belongs_to as the first parameter.

クラス内のすべての関連付けのAssociationReflectionオブジェクトの配列を返します。

特定の関連付けタイプのみを反映する場合は、最初のパラメーターとしてシンボル:has_many、:has_one、:belongs_toを渡します。

出典: https://github.com/rails/rails/blob/main/activerecord/lib/active_record/reflection.rb

返り値は配列になるとのこと。一旦exitで抜けて、メソッドの中に入らずに返り値を確認してみます。

$  User.reflect_on_all_associations
=> [#<ActiveRecord::Reflection::HasManyReflection:0x000055b2c512eaa8
  @active_record=
   User(id: integer, email: string, encrypted_password: string, reset_password_token: string, reset_password_sent_at: datetime, remember_created_at: datetime, unique_session_id: string, created_at: datetime, updated_at: datetime),
  @constructable=true,
  @foreign_type=nil,
  @klass=nil,
  @name=:posts,
  @options={:dependent=>:destroy},
  @plural_name="posts",
  @scope=nil,
  @type=nil>]

$ User.reflect_on_all_associations.class
=> Array

$ User.reflect_on_all_associations.size
=> 1

$ User.reflect_on_all_associations.first.class
=> ActiveRecord::Reflection::HasManyReflection

返り値は配列になっていて、ActiveRecord::Reflection::HasManyReflectionクラスのオブジェクトが配列の中身に格納されていました。


ソースコードを追ってみる

ここからはもう一度stepメソッドを使ってメソッドの中身を追ってみます。

From: /usr/local/bundle/gems/activerecord-6.0.4.1/lib/active_record/reflection.rb:104 ActiveRecord::Reflection::ClassMethods#reflect_on_all_associations:

    103: def reflect_on_all_associations(macro = nil)
 => 104:   association_reflections = reflections.values
    105:   association_reflections.select! { |reflection| reflection.macro == macro } if macro
    106:   association_reflections
    107: end

# 引数を渡して実行してないため、nilが返る
>  macro
=> nil

# hashが返り値になっている
> reflections
=> {"posts"=>
  #<ActiveRecord::Reflection::HasManyReflection:0x000055b2c512eaa8
   @active_record=
    User(id: integer, email: string, encrypted_password: string, reset_password_token: string, reset_password_sent_at: datetime, remember_created_at: datetime, unique_session_id: string, created_at: datetime, updated_at: datetime),
   @constructable=true,
   @foreign_type=nil,
   @klass=nil,
   @name=:posts,
   @options={:dependent=>:destroy},
   @plural_name="posts",
   @scope=nil,
   @type=nil>}

# posts keyの中身が配列で返っている
> reflections.values
=> [#<ActiveRecord::Reflection::HasManyReflection:0x000055b2c512eaa8
  @active_record=
   User(id: integer, email: string, encrypted_password: string, reset_password_token: string, reset_password_sent_at: datetime, remember_created_at: datetime, unique_session_id: string, created_at: datetime, updated_at: datetime),
  @constructable=true,
  @foreign_type=nil,
  @klass=nil,
  @name=:posts,
  @options={:dependent=>:destroy},
  @plural_name="posts",
  @scope=nil,
  @type=nil>]

一通り処理の内容は理解できました。reflectionsオブジェクトの中身をよしなにいじって、モデルに定義されたassociation情報を返している感じですね。


応用的な使い方

以下のように書くと、そのモデルに定義されているassociation名の配列を得ることができます。

$ User.reflect_on_all_associations.map(&:name)
=> [:posts]

例えば、has_manyなレコードが存在する場合に何かしらの処理を加えたい、という場合に以下のように書くことができます。

has_many_association_names = User.reflect_on_all_associations(:has_many).map(&:name)

if has_many_association_names.any? { |name| user.public_send(name).exists? }
  # 何かしらの処理
else
  # 何かしらの処理
end

こう書くことで、新たに関連を追加した時に、わざわざ定数にsymbolを追加しなくてもよくなり、コードの変更箇所を減らすことができます。




勉強になりました。