ActiveRecord ～ 複数テーブルにまたがる検索（preload, eager_load, include, joins）

# これはなに？
preload, eager_load, include, joins が実際にどういうSQLを吐くかを確かめた。
※Rails4/5両方で確認。

#### 説明用テーブルについて
説明に使用している各テーブルは blogs ← entries ← comments という感じにFK参照している。[DDLは最後の方に記載](https://qiita.com/leon-joel/items/f26556c9e56833983856#%E5%8F%82%E8%80%83-%E8%AA%AC%E6%98%8E%E3%81%A7%E4%BD%BF%E7%94%A8%E3%81%97%E3%81%A6%E3%81%84%E3%82%8B%E3%83%86%E3%83%BC%E3%83%96%E3%83%AB%E3%81%AE%E6%A7%8B%E6%88%90)。

# いきなりまとめ

<dl>
<dt>preload</dt>
  <dd>SELECT複数回（親子で1回ずつ）</dd>
  <dd>子テーブルでの絞り込みは不可</dd>

<dt>eager_load</dt>
  <dd>LEFT OUTER JOIN</dd>
  <dd>子/孫テーブルでの絞り込みも可能</dd>

<dt>include</dt>
  <dd>preloadとeager_loadのハイブリッド</dd>
  <dd>
    <strong>絞り込み条件なし OR 親テーブルへの絞り込みのみの場合 => preload</strong><br>
    <strong>子/孫テーブルへの絞り込み条件が指定されている場合 => eager_load</strong>
  </dd>
<br>
<dt>joins ※これだけ少し毛色が異なる</dt>
  <dd>INNER JOIN</dd>
  <dd>結果がベタに（＝1行1インスタンスで）返ってくる</dd>
</dl>


# preload: 2段階SELECT（子テーブルの先読み）

 - 親テーブル/子テーブルで1回ずつSELECTが走る
 - 子テーブル側では絞り込めない

```rb
# 子テーブル(?)側の先読み
# SELECT * FROM blogs WHERE id IN (1, 2)
# SELECT * FROM entries WHERE blog_id IN (1, 2)
r = Blog.preload(:entries).where(id: [1, 2])

r.each do |b|
  p b
  p b.entries
end

# 子テーブル側では絞り込めない
r = Blog.preload(:entries).where(entrires: { id: [1, 2] } )
# PG::UndefinedTable - ERROR:  missing FROM-clause entry for table "entrires"
# LINE 1: SELECT "blogs".* FROM "blogs" WHERE "entrires"."id" IN ($1, ...
#                                             ^
```

# eager_load: LEFT OUTER JOIN

```rb
# SELECT *
#   FROM entries E LEFT OUTER JOIN comments C ON C.entry_id = E.id
#  WHERE E.id IN (3, 4, 5)
r = Entry.eager_load(:comments).where(id: [*3..5])

r.each do |b|
  p b
  b.comments.each do |c|
    puts "======="
    p c
  end
end
```

2段階に（孫テーブルも）LEFT OUTER JOIN + 子テーブルでの絞り込み

```rb:ruby
r = Blog.eager_load(entries: :comments).where(entries: {id: [1, 2]})
```

```sql:SQL
SELECT * 
  FROM blogs B 
       LEFT OUTER JOIN entries E ON E.blog_id = B.id 
       LEFT OUTER JOIN comments C ON C.entry_id = E.id 
 WHERE E.id IN (1, 2)
```

子/孫テーブルの絞り込み条件をハッシュで渡せない場合でも references をかまさなくて良い（includesとは違う点）

```rb:ruby
r = Blog.eager_load(entries: :comments).where("comments.id <= ?", 2)
```

# includes: `preload` と `eager_load` を自動切り替え

絞り込みなし or 親テーブルでの絞り込みのみ の場合は `preload`（親/子テーブルで1回ずつSELECTが走る）

子テーブル/孫テーブルで絞り込んだ場合は `eager_load`（LEFT OUTER JOIN）

ではまず、普通に子テーブルまで先読みした例。絞り込みは親テーブルに対してのみ。

```rb
# 以下2つのSELECTが実行される
# SELECT * FROM entries WHERE id = 1
# SELECT * FROM comments WHERE entry_id IN (1)
r = Entry.includes(:comments)
         .where(id: 1)

r.each do |e|
  # 親テーブル側のインスタンス
  p e
  # コレクションとして保持されている
  p e.comments
end
```

2段階に（孫テーブルまで）先読みする例。
こちらも絞り込みは親テーブルのみ。

```rb
# SELECTが3回走る
# SELECT * FROM blogs WHERE (2 <= id)
# SELECT * FROM entries WHERE blog_id IN (2, 3)
# SELECT * FROM comments WHERE entry_id IN (1, 2)
r = Blog.includes(entries: :comments).where("? <= id", 2)

r.each do |b|
  puts "###### Blog #{b.id}"
  p b
  b.entries.each do |e|
    puts "  ====== Entry #{e.id}"
    p e

    e.comments.each do |c|
      puts "    -------"
      p c
    end
  end
end
```

子テーブル側で絞り込むと LEFT OUTER JOIN に。

```rb:ruby
r = Blog.includes(entries: :comments).where(entries: { id: 1 })
```

```sql:SQL
SELECT *
  FROM blogs B
       LEFT OUTER JOIN entries E ON E.blog_id = B.id -- ←OUTERを使っているが、ここはINNERでも結果は同じ
       LEFT OUTER JOIN comments C ON C.entry_id = E.id
 WHERE E.id = 1 --←子テーブルentriesのidで絞り込んでいる
```

絞り込み条件をハッシュで渡せないときは `references` をかます必要があるっぽい。生成されるSQLはWHERE句以外は上のと同じ。

```rb:ruby
r = Blog.includes(entries: :comments).references(:entries).where("entries.id < ?", 2)

```

孫テーブルで絞り込む ⇒やっぱりLEFT OUTER JOIN

```rb:ruby
r = Blog.includes(entries: :comments).where(comments: { id: [*1..3] })
```

```sql:SQL
SELECT *
  FROM blogs B
       LEFT OUTER JOIN entries E ON E.blog_id = B.id
       LEFT OUTER JOIN comments C ON C.entry_id = E.id
 WHERE C.id IN (1, 2, 3) -- 孫テーブルcommentsのidで絞り込んでいる
```
# joins: 単なる INNER JOIN
こいつだけ他のメソッドとは毛色がちょっと違う。

 - 先読みではなく、結果がベタに（＝1レコード1インスタンスで）返ってくる
 - JOINしたテーブルのカラムは `select` で明示的に指定しないと見えない

```rb
# SELECT blogs.*, entries.title AS entry_title, entries.body
#   FROM blogs
#        INNER JOIN entries
#        ON entries.blog_id = blogs.id
#  WHERE blogs.id = 1
#
r = Blog.joins(:entries)
        .where(id: 1)
        .select("blogs.*, entries.title AS entry_title, entries.body")

r.each do |e|
  p e             # Blogの属性しか見えない
  p e.attributes  # Blog以外の属性が格納されていることが分かる

  # blogsテーブルの属性も、entriesテーブルの属性も読める
  puts "blog title: #{e.title}, entry title: #{e.entry_title}"
end
```

2段階（子/孫テーブルへの）INNER JOINもいける。

```rb:ruby
r = Blog.joins(entries: :comments)
        .where(id: 1)
        .select("blogs.*, entries.title AS entry_title, entries.body, comments.body AS comment_body")
```

# 参考: 説明で使用しているテーブルの構成

```sql:DDL
-- ブログテーブル
CREATE TABLE blogs
(
    id INT(11) PRIMARY KEY NOT NULL AUTO_INCREMENT,
    title VARCHAR(255),
    created_at DATETIME NOT NULL,
    updated_at DATETIME NOT NULL
);

-- エントリーテーブル
CREATE TABLE entries
(
    id INT(11) PRIMARY KEY NOT NULL AUTO_INCREMENT,
    title VARCHAR(255),
    body TEXT,
    created_at DATETIME NOT NULL,
    updated_at DATETIME NOT NULL,
    blog_id INT(11),
    CONSTRAINT fk_rails_1c7d1d2722 FOREIGN KEY (blog_id) REFERENCES blogs (id)
);
CREATE INDEX index_entries_on_blog_id ON entries (blog_id);

-- コメントテーブル
CREATE TABLE comments
(
    id INT(11) PRIMARY KEY NOT NULL AUTO_INCREMENT,
    body TEXT,
    status VARCHAR(255),
    entry_id INT(11),
    created_at DATETIME NOT NULL,
    updated_at DATETIME NOT NULL,
    CONSTRAINT fk_rails_24508b7aa5 FOREIGN KEY (entry_id) REFERENCES entries (id)
);
CREATE INDEX index_comments_on_entry_id ON comments (entry_id);
```


# 環境

| OS | Ruby | Rails(ActiveRecord) | 
|:-:|:-:|:-:|
| Windows7 x64 | 2.2 | 4.2.6 |
| macOS Mojave | 2.6.1 | 5.2.2 |

# 参考サイト
[ActiveRecordのjoinsとpreloadとincludesとeager_loadの違い - Qiita](http://qiita.com/k0kubun/items/80c5a5494f53bb88dc58)
[似ているようで全然違う!?Activerecordにおけるincludesとjoinsの振る舞いまとめ - Qiita](http://qiita.com/south37/items/b2c81932756d2cd84d7d)
[Rails Guides - Active Record クエリインターフェイス](http://railsguides.jp/active_record_querying.html)