【Rails】 N+1問題 (eager_loadとpreload)アプリで学べ

##はじめに
最近SQLの知識がついてきたので、N+1問題を改めて勉強。
そしたらまだまだわかっていない箇所があったので、アウトプットしていく。
アウトプットのやり方は 教材アプリで実施（下記URL参照）
実際に手を動かして知識の定着をはかる

https://www.techpit.jp/courses/79/curriculums/82/sections/623/parts/2162

僕らがスクールで扱っていたデータ数なんて毛みたいなものなので、今回は７００近いデータ数で実施。
データ数多いので、表示スピードにもろ影響することがわかる。

### 対策メソッド

![スクリーンショット 2021-08-03 15.37.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1183228/13dbfa6c-5228-9138-d154-7449df90ccfd.png)

詳細はいっぱい記事があるので、割愛します。
下記２つの記事がわかりやすかったので見て下さい。

https://qiita.com/m2mtu/items/dd4e96a913690dbdf56b

https://qiita.com/mingliangli/items/00146ed0236a5e59eb51


それでは実際に手を動かしていこうと思います。

### １つ目 preloadを使う。

```erb:app/views/profiles/_articles.html.erb
<% @articles.each do |article| %>
  <div class="box">
    <article class="media">
      <div class="media-content">
        <div class="content">
          <h3>
            <span class="icon has-text-success">
              <i class="fas fa-file-alt"></i>
            </span>
            <%= article.title %>
          </h3>
          <p><%= article.description %></p>
          <% article.tags.each do |tag| %>
            <span class="tag is-light"><%= tag.name %></span>
          <% end %>
        </div>
      </div>
    </article>
  </div>
<% end %>
```
@articlesの中身を確認。
コントローラを見に行きます。

```ruby:app/controllers/profiles_controller.rb
class ProfilesController < ApplicationController
   def index
    @user = User.find(1)
    raise Forbidden unless user_safe?

    @skill_categories = user_reccomend_skill_categories
   - @articles = @user.articles  ←このコードが原因
   + @articles = @user.articles.preload(:tags)
  end
  
  # 略
```

メソッド一覧から今回のケースを探すと"ループ内で関連テーブルの値を使用する場合"が合致します。
この事からeager_loadか、preloadを使い、記事データの取得時にタグデータを一括取得する対応が効きそうです。
また今回のケースでは関連テーブルでの絞り込みが無く、LEFT OUTER JOIN句を使うメリットが無いのでpreloadを使用します。

```
Completed 200 OK in 6792ms ➩ Completed 200 OK in 5137ms 
速度改善することができた。
```

### ２つ目 eager_loadを使う。


```ruby:log
 Skill Load (0.1ms)  SELECT "skills".* FROM "skills" WHERE "skills"."skill_category_id" = ?  [["skill_category_id", 1265]]
  ↳ app/views/profiles/_user.html.erb:42
```
発行SQLの下に↳ app/views/profiles/_user.html.erb:42というログが出力されています。

```erb:app/views/profiles/_user.html.erb
  <% @skill_categories.each do |category| %>
    <div class="box is-medium">
      <h4 class="title is-6">
        <span class="icon has-text-success">
          <i class="fas fa-toolbox"></i>
        </span>
        <%= category.name %>
      </h4>

      <% category.skills.each do |skill| %> ←４２行目
        <span class="icon has-text-success">
          <i class="fas fa-check"></i>
        </span>
        <span class="tag is-light"><%= skill.name %></span>
      <% end %>
    </div>
  <% end %>
```
categoryは@skill_categories.eachのブロック変数である事が確認できました。
では@skill_categoriesとはなんなのでしょうか？

@skill_categoriesをコントローラに記載してあると思うのでみにいきます。

```ruby:app/controllers/profiles_controller.rb
 class ProfilesController < ApplicationController
   def index
    @user = User.find(1)
    raise Forbidden unless user_safe?

    @skill_categories = user_reccomend_skill_categories  ←このコード
    @articles = @user.articles.preload(:tags)
    end

  private

  def user_safe?
    @user.user_cautions.all? do |user_caution|
      Time.zone.now > user_caution.caution_freeze.end_time
    end
  end

  def user_reccomend_skill_categories
    @user.skills.map(&:skill_category).
      filter { |skill_category| skill_category.reccomend }.uniq
  end
end

```

@skill_categoriesはprivateメソッドのuser_reccomend_skill_categoriesメソッドの返り値の様です。
同じくapp/controllers/profiles_controller.rb内で定義されていますね。

これです。

```ruby
  def user_reccomend_skill_categories
    @user.skills.map(&:skill_category).
      filter { |skill_category| skill_category.reccomend }.uniq
  end
```

このメソッドは下記の内容がかいてあります。

`
①@userに紐づくskillsを全て取得
②mapメソッドでskill毎に紐づくskill_categoryを取得
③filterメソッドでskill_categoryインスタンスのreccomend属性がtrueのインスタンスのみを取得
④ ③までの実行結果である、複数のskill_categoryを要素に持つArray内のskill_categoryインスタンスを一意にしたArrayを返す

※ちなみにfilterメソッドは、条件に一致する要素のみ抜き出し新たな配列を作ることができるメソッドです。

では。メソッドを修正していきます。


```ruby
  def user_reccomend_skill_categories
     SkillCategory.eager_load(:skills).
      where(reccomend: true).
      where(skills: { user_id: @user.id })
  end
```
メソッド一覧から今回のケースを探すと"ループ内で関連テーブルの値を使用する場合"が合致します。
この事からeager_loadか、preloadを使い、スキルカテゴリーデータの取得時にスキルデータを一括取得する対応が効きそうです。
またデータ取得時にWHERE句を使用し絞り込みを行う事で1度のSQLでデータ取得が行えます。
今回のケースでは、whereメソッドを使用する為、eager_loadを使用します。

ではviewを表示してログを確認すると・・

```
Completed 200 OK in 6792ms ➩ Completed 200 OK in 2609ms 
初回からここまで速度改善することができた。
```

こんな感じで、今回はeager_loadとpreloadを使ってみました。

##さいごに

知ってるだけで、大きくパフォーマンス向上できるのでこれからも学習していき、
データベースの知識を構築していきたいです。