Rails で includes して N+1 問題対策

## はじめに
気を緩めてると気づいたら N+1 問題って起きてたりする。

## N+1 問題とは何か
簡単に言うと必要以上に SQL が走るせいでパフォーマンスが低下する問題です。
例えば

User と Article の2つのモデルがある状態で、

```app/models/user.rb
class User < ActiveRecord::Base
  has_many :articles
end
```
```app/models/article.rb
class Article < ActiveRecord::Base
  belongs_to :user
end
```
上記のように User が複数の Article を持っているとする。
そして下記のコードで Article の title を表示する。

```app/controllers/users_controller.rb
class ArticlesController < ApplicationController
  def index
    @articles = Article.all
  end
end

```
```app/views/articles/index.html.haml
- @articles.each do |article|
  = article.title
  = article.user.name
```
このコードだと以下のようなログを見ると SQL が発行される。

```
SELECT 'articles'.* FROM 'articles' # Article.all の実行

SELECT 'users'.* FROM 'users' WHERE 'users'.'id' = 1 LIMIT 1 # article.user.name を実行する際に走る
SELECT 'users'.* FROM 'users' WHERE 'users'.'id' = 2 LIMIT 1
```

このように Article の数だけ

```
SELECT 'users'.* FROM 'users' WHERE 'users'.'id' = n LIMIT 1
```
この SQL が発行されることになり、これが N+1 問題。

## やりたいこと
N+1 問題を解決したい。
今回は `includes` を使って多段ネストの N+1 問題も解決する。
`includes` の挙動に関しては今回は省略。

## 解決方法

### 1 対 N の場合
つまり先程の例である。この場合コントローラで `@articles` を取得する際に

```
@articles = Article.all.includes(:user)
```
にすると

```
SELECT 'article'.* FROM 'articles'

SELECT 'users'.* FROM 'users' WHERE 'users'.'id' IN (1, 2)
```
の2つの SQL が発行されるだけになり、N+1 問題が解決する。
<br>
### N 対 1 対 N の場合
先程のモデルに加えて、User が Comment も持っている場合。

```app/models/user.rb
class User < ActiveRecord::Base
  has_many :articles
  has_many :comments
end
```
```
class Comment < ActiveRecord::Base
  belongs_to :user
end
```
```app/views/articles/index.html.haml
- @articles.each do |article|
  = article.title
  = article.user.name
  - article.user.comments.each do |comment|
    = comment.content
```
上記のコードの時、Article の取得を先程と同じように

```
@articles = Article.all.includes(:user)
```
とすると、`article.user.comments` の `user.comments` 部分で N+1 問題が起きてしまう。
この場合は

```
@articles = Article.all.includes(user: :comments)
```
とすると N+1 問題が解消される。
<br>

### N 対 1　対 N 対 1 の場合
先程の例に加えて、さらに comment が 1 つのカテゴリに所属している場合(例がくそみたいに悪いですが気にしないでください。)

```app/views/articles/index.html.haml
- @articles.each do |article|
  = article.title
  = article.user.name
  - article.user.comments.each do |comment|
    = comment.content
    = comment.category.name
```
この時は

```
@articles = Article.all.includes(user: {comments: :category})
```
で解決する。
またコメントが id が 1 のカテゴリに属しているか調べる場合(あまり使わない)

```app/views/articles/index.html.haml
- article.user.comments.where(category_id: 1).present?
```

このように view で `where` を使ってしまうと、たとえ Article の取得を

```
@articles = Article.all.includes(user: {comments: :category})
```
としていても N+1 問題がおきてしまうので、

```
@articles = Article.all.includes(user: :comments)
```
として

```
- article.user.comments.pluck(:category_id).include?(1)
```
のようにすると N+1 が解決されるはず。
<br>

### 1 対 複数の N の場合
先程までのモデリングで view が

```app/views/users/index.html.haml
- @users.each do |user|
  - user.articles.each do |article|
    = article.title
  - user.comments.each do |comment|
    = comment.content
```
の場合は、User の取得時に

```
@users = User.all.includes(:articles, :comments)
```
とすればよい

## まとめ
N+1 問題は確かにパフォーマンスを下げはするが、N+1 問題を解決するために可読性の低いコードになってしまったり、余計に処理が複雑になってしまうことがあるので無理に直さなくてもいい場合もあると思う。
また、今回は `includes` を使ったがやり方はケースバイケースで違ったりもするので出くわした状況に応じて対策する必要がある。

途中くそみたいなモデリングのケースを出したが、あれは実際に私がどうしようか割りと悩んだところ。
今回は `includes` と `include?` を使ったがもっといい方法があれば教えてほしい。