ActiveRecord::QueryMethods#eager_loadがeager loadingを行う仕組み
ActiveRecord::QueryMethods#eager_load
でeager loadingをする際に、
ActiveRecord::Associations::JoinDependency
がどのような働きをしてレコードの読み込みを行っているかコードリーディングをしたので、そのメモ。
そもそもeager_loadって何
JOINで関連先オブジェクトをeager loadingするためのscope。
ActiveRecordのjoinsとpreloadとincludesとeager_loadの違い
eager_loadを呼んでからクエリが走るまで
Tweet.eager_load(:favorites).to_a
をrbtraceを使って実際にトレースして少し加工したものを以下に示す。
ActiveRecord::QueryMethods#eager_load
ActiveRecord::QueryMethods#eager_load!
ActiveRecord::Relation#exec_queries
ActiveRecord::FinderMethods#find_with_associations
ActiveRecord::FinderMethods#construct_join_dependency
ActiveRecord::Associations::JoinDependency#aliases
ActiveRecord::Associations::JoinDependency::Aliases#columns
ActiveRecord::QueryMethods#select
ActiveRecord::FinderMethods#apply_join_dependency
ActiveRecord::ConnectionAdapters::DatabaseStatements#select_all
ActiveRecord::Associations::JoinDependency#instanciate
まずeager_load
を呼ぶと引数がrelationにeager_load_values
として追加される。この時点ではrelationなのでクエリは走らない。
find
とかfirst
みたいな、クエリが実行されるメソッド(ActiveRecord::FinderMethods
)が呼ばれるとexec_queries
が走り、ここでJOINなどが行われる。
ActiveRecord::Relation#exec_queries
exec_queries
は、eager loadingを行ったassociationのキャッシュを持つActiveRecord::BaseのArrayを返すことになる。
def exec_queries
@records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel, bind_values)
preload = preload_values
preload += includes_values unless eager_loading?
preloader = ActiveRecord::Associations::Preloader.new
preload.each do |associations|
preloader.preload @records, associations
end
@records.each { |record| record.readonly! } if readonly_value
@loaded = true
@records
end
preload
を呼んだ場合の処理とeager_load
を読んだ場合の処理の分岐がここに入っている。
eager_load
を呼んだ場合はeager_loading?
がtrueになるため、ここではfind_with_associations
がメインになる。
ActiveRecord::FinderMethods#find_with_associations
eager_load
を呼んだときのメインの処理がここに入っていて、このメソッドを理解することが重要だと思う。
このメソッドの途中にbinding.pryを挟むとわかりやすい。
def find_with_associations
join_dependency = construct_join_dependency
aliases = join_dependency.aliases
relation = select aliases.columns
relation = apply_join_dependency(relation, join_dependency)
if block_given?
yield relation
else
if ActiveRecord::NullRelation === relation
[]
else
rows = connection.select_all(relation.arel, 'SQL', relation.bind_values.dup)
join_dependency.instantiate(rows, aliases)
end
end
end
join_dependencyとは
ActiveRecordがeager loadingを行うために使うクラスは2つある。
- Preloader
-
preload
や単独のincludes
を呼んだ時に使われる - JOINしない
-
- JoinDependency
-
eager_load
を呼んだ時に使われる - JOINする
-
そのうちのJoinDependencyのことである。
construct_join_dependency
Tweet.eager_load(:favorites).to_a
の場合、[:favorites]
がeager_load_values
となり、
ActiveRecord::Associations::JoinDependency.new(Tweet, [:favorites], [])
を返す。(おわり)
join_dependency.aliases
ActiveRecordがJOINをすると、id AS t1_r0, tweet_id AS t1_r1, ...
みたいな汚いカラム名でSELECTされるのを見たことがあると思う。
この実際のカラム名と汚いカラム名の対応関係を持っているのがaliasesで、これがないとActiveRecord::Base
のカラムに対応させることができない。
一部を覗き見ると、
[#<struct ActiveRecord::Associations::JoinDependency::Aliases::Column name="id", alias="t1_r0">,
#<struct ActiveRecord::Associations::JoinDependency::Aliases::Column name="tweet_id", alias="t1_r1">,
#<struct ActiveRecord::Associations::JoinDependency::Aliases::Column name="user_id", alias="t1_r2">,
#<struct ActiveRecord::Associations::JoinDependency::Aliases::Column name="created_at", alias="t1_r3">,
#<struct ActiveRecord::Associations::JoinDependency::Aliases::Column name="updated_at", alias="t1_r4">]>]>
みたいになっている。
relationの構築
relation = select aliases.columns
relation = apply_join_dependency(relation, join_dependency)
1行目で必要な汚いカラムのSELECTをrelationに追加し、2行目で必要なテーブルのJOINをrelationに追加している。
クエリ実行
rows = connection.select_all(relation.arel, 'SQL', relation.bind_values.dup)
# => #<ActiveRecord::Result:0x007f97d2d286d0
# @column_types={},
# @columns=["t0_r0", "t0_r1", "t0_r2", "t0_r3", "t0_r4", "t1_r0", "t1_r1", "t1_r2", "t1_r3", "t1_r4"],
# @hash_rows=nil,
# @rows=
# [[1, nil, 1, 2014-11-22 19:40:29 UTC, 2014-11-22 19:40:29 UTC, 1, 1, 2, 2014-11-22 19:40:29 UTC, 2014-11-22 19:40:29 UTC],
# [3, nil, 3, 2014-11-22 19:40:29 UTC, 2014-11-22 19:40:29 UTC, 2, 3, 4, 2014-11-22 19:40:29 UTC, 2014-11-22 19:40:29 UTC],
# [3, nil, 3, 2014-11-22 19:40:29 UTC, 2014-11-22 19:40:29 UTC, 3, 3, 5, 2014-11-22 19:40:29 UTC, 2014-11-22 19:40:29 UTC],
# [6, nil, 6, 2014-11-22 19:40:29 UTC, 2014-11-22 19:40:29 UTC, 4, 6, 7, 2014-11-22 19:40:29 UTC, 2014-11-22 19:40:29 UTC],
# ...
join_dependency.instantiate(rows, aliases)
# => [#<Tweet id: 1, tweet_id: nil, user_id: 1, created_at: "2014-11-22 19:40:29", updated_at: "2014-11-22 19:40:29">,
# #<Tweet id: 3, tweet_id: nil, user_id: 3, created_at: "2014-11-22 19:40:29", updated_at: "2014-11-22 19:40:29">,
# #<Tweet id: 6, tweet_id: nil, user_id: 6, created_at: "2014-11-22 19:40:29", updated_at: "2014-11-22 19:40:29">,
# #<Tweet id: 10, tweet_id: nil, user_id: 10, created_at: "2014-11-22 19:40:29", updated_at: "2014-11-22 19:40:29">,
# #<Tweet id: 15, tweet_id: nil, user_id: 15, created_at: "2014-11-22 19:40:29", updated_at: "2014-11-22 19:40:29">]
JOINクエリの結果なので、rowsには1行にテーブル2つ分が入っている。
rowsには汚いカラム名が割り振られているので、先ほどのaliasesを使ってjoin_dependency.instanciateが適切なカラムに値を入れてActiveRecord::Baseのインスタンスにする。
まとめ
eager_loadは以下の流れでeager loadingを行う。
- eager_loadでrelationにeager_load_valuesが追加される
- eager_load_valuesからjoin_dependencyが作られる
- join_dependencyがカラム名のaliasを作る
- selectとjoinのscopeを追加してクエリを実行し、aliasでActiveRecord::Baseインスタンスにする
Author And Source
この問題について(ActiveRecord::QueryMethods#eager_loadがeager loadingを行う仕組み), 我々は、より多くの情報をここで見つけました https://qiita.com/k0kubun/items/b4730dd79420bb342f17著者帰属:元の著者の情報は、元のURLに含まれています。著作権は原作者に属する。
Content is automatically searched and collected through network algorithms . If there is a violation . Please contact us . We will adjust (correct author information ,or delete content ) as soon as possible .