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の違い](http://qiita.com/k0kubun/items/80c5a5494f53bb88dc58)

## eager\_loadを呼んでからクエリが走るまで

`Tweet.eager_load(:favorites).to_a`を[rbtrace](https://github.com/tmm1/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を返すことになる。

```ruby
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を挟むとわかりやすい。

```ruby
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`となり、

```ruby
ActiveRecord::Associations::JoinDependency.new(Tweet, [:favorites], [])
```

を返す。（おわり）

### join\_dependency.aliases

ActiveRecordがJOINをすると、`id AS t1_r0, tweet_id AS t1_r1, ...`みたいな汚いカラム名でSELECTされるのを見たことがあると思う。
この実際のカラム名と汚いカラム名の対応関係を持っているのがaliasesで、これがないと`ActiveRecord::Base`のカラムに対応させることができない。

一部を覗き見ると、

```ruby
[#<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の構築

```ruby
relation = select aliases.columns
relation = apply_join_dependency(relation, join_dependency)
```

1行目で必要な汚いカラムのSELECTをrelationに追加し、2行目で必要なテーブルのJOINをrelationに追加している。

### クエリ実行

```ruby
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行にテーブル２つ分が入っている。
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インスタンスにする