【rails6.1新機能】N+1問題を簡単に検知できる方法を紹介します

今回の記事はN+1問題についてです。

railsに限らず、webアプリケーションではかなり重要視される問題です。

解決方法自体はかなり簡単なのですが、初学者にとってはN+1問題を意識しながら開発を進めるのは少し根気が必要です。

rails6.0以前は、bulletというgemを使用すれば、問題を検知してくれました。

が、今回のrails6.1からはActiveRecordのデフォルト機能でつけてくれています。

まだまだ勉強中の僕にとっては、かなりありがたいです。

ということで今回は、この新機能を簡単に紹介していきます。

versions
  • ruby 2.6.5
  • rails 6.1.1

N+1問題とは何かを知ろう

結論

繰り返し処理の中で、SQLが毎回発行されることで、サーバーに負荷がかかってしまうこと

初学者の方は、ログをみる癖が身についていないことが多いため、言葉で説明されてもわかりにくいかと思います。

そのため、以下の画像をみてください。

このような一覧画面があったときに、

messageに紐づくuserのSQLが3つ発行されているのがお分かりになりますか?

これが、N+1問題です。

今はメッセージが3つしかありませんが、これが100万とかになったら恐ろしいですね。

準備をしよう

N+1問題の概要がつかめたら、早速実装をしていきましょう。

1. rails newでディレクトリを作成

~$ rails new n1-problem
~$ cd n1-problem
n1-problem$ rails db:create 

2. deviseをインストール

gem "devise"
n1-problem$ bundle install
n1-problem$ rails g devise:install
n1-problem$ rails g devise:views
class ApplicationController < ActionController::Base
  before_action :configure_permitted_parameters, if: :devise_controller?

  protected

  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:sign_up, keys: [:name])
  end
end
<h2>Sign up</h2>

<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
  <%= render "devise/shared/error_messages", resource: resource %>

  <div class="field">
    <%= f.label :name %><br />
    <%= f.text_field :name %>
  </div>

  <div class="field">
    <%= f.label :email %><br />
    <%= f.email_field :email, autofocus: true, autocomplete: "email" %>
  </div>

  <div class="field">
    <%= f.label :password %>
    <% if @minimum_password_length %>
    <em>(<%= @minimum_password_length %> characters minimum)</em>
    <% end %><br />
    <%= f.password_field :password, autocomplete: "new-password" %>
  </div>

  <div class="field">
    <%= f.label :password_confirmation %><br />
    <%= f.password_field :password_confirmation, autocomplete: "new-password" %>
  </div>

  <div class="actions">
    <%= f.submit "Sign up" %>
  </div>
<% end %>

<%= render "devise/shared/links" %>

3. scaffoldで一気に作成

n1-problem$ rails g scaffold message content:text user:references
n1-problem$ rails db:migrate
<p id="notice"><%= notice %></p>

<h1>Messages</h1>

<table>
  <thead>
    <tr>
      <th>Content</th>
      <th>User</th>
      <th colspan="3"></th>
    </tr>
  </thead>

  <tbody>
    <% @messages.each do |message| %>
      <tr>
        <td><%= message.content %></td>
        <td><%= message.user.name %></td>
        <td><%= link_to 'Show', message %></td>
        <td><%= link_to 'Edit', edit_message_path(message) %></td>
        <td><%= link_to 'Destroy', message, method: :delete, data: { confirm: 'Are you sure?' } %></td>
      </tr>
    <% end %>
  </tbody>
</table>

<br>

<%= link_to 'New Message', new_message_path %>
<%= link_to 'logout', destroy_user_session_path, method: :delete %>

これで準備は完了です。あとはユーザーをいくつか登録して、各ユーザーでメッセージを投稿しておいてください。

N+1問題を検知しよう

方法はいくつかあるようなのですが、今回は2つ紹介します。

messageに紐づくuserを取得するので、message.rbに記述をしていきます。

モデルにあるアソシエーション全てに対して検知する

class Message < ApplicationRecord
  self.strict_loading_by_default = true
  belongs_to :user
end

これで完了です。

 self.strict_loading_by_default = true

たったこれだけで、全てのアソシエーションに対してN+1問題を検知してくれます。

単体のアソシエーションに対して検知する

class Message < ApplicationRecord
  belongs_to :user, strict_loading: true
end

アソシエーションのオプションに、

strict_loading: true

を追加するだけです。

どちらの記述をしても、以下の画像のようなエラーがでます。

正直こんな簡単にN+1問題を検知してくれるのはありがたいです。

N+1問題を解決しよう

最後は肝心の解決方法です。

  def index
    @messages = Message.all.includes(:user)
  end

messages_controller.rbのindexアクションに、includesメソッドを記述します。

すると、

冒頭の画像と見比べると、SQLの発行回数が減っていますね。

まとめ

  • N+1問題は、SQLが多く発行されてしまうこと
  • rails6.1では、N+1問題を検知してくれるメソッドが用意されている
  • includesメソッドを使用して、N+1問題を解決する

検知できるからと言って多用するのはNGです。
ご自身でコードの論理を理解した上で有効活用してくださいね。

初学者の意識が向きづらいN+1問題。

何度も復習して、初学者から脱却しましょう。

参考

rails 6.1から追加されるN+1問題発生を抑止するstrict_loading機能

論理削除とeager_loadでN+1問題が発生する件

N+1問題

Ruby on Rails 6.1 リリースノート