はじめに
先日、ユーザーがBookmarkした投稿の一覧を出したくてコードで悩んでいたら、
こんな書き方はどう?と教えてもらったのですが...
@posts = Post.includes(:user).where(user_id: current_user.id)
このincludesってたまに見るけどよくわからないなーってことで
自分の勉強のために調べてまとめてみました。
includes攻略のためには「N+1問題」の理解が重要!
いや「N+1問題」ってなんだよって思いますよね。
ざっくり言ってしまうと「N+1問題」=超非効率な状態です。
マクドナルドに行ったらハンバーガーを最初に買ってきて、その次にナゲットを買ってきて、
その後にさらにポテトを買ってくるような状態で、「いやそれまとめて一緒に買えたよね?」みたいな。
こんなことをしているとSQLが過剰に動かされてしまい、疲労困憊によりパフォーマンスが低下してしまいそうです。
(SQL=データベースの操作を行います。Railsの裏側ではSQLが実行されていますが詳しくは下記で)
includesメソッドとは、シンプルに言うと、この「N+1問題」を解決してくれるメソッドです。
このincludesメソッドを使うことで、関連している複数のテーブルから
データを取得してくるときのアクセス回数を大きく減らすことができます。
「N+1問題」が起きやすいのはどんなとき?
「N+1問題」が起きやすいのは、わかりやすく言うと「UserとPost」の関係のような
いわゆる「1 : Many」の状態のときです。
ここで言うなら「顧客とハンバーガー」みたいな感じでしょうか。
一人の顧客はハンバーガーをいくつでも購入できる関係ですよね。
has_many :hamburgers
belongs_to :customer
顧客とハンバーガーのテーブルの一例です。
customer | name |
---|---|
1 | Apiyo |
2 | Bpiyo |
3 | Cpiyo |
4 | Dpiyo |
hamburger_id | name | customer |
---|---|---|
1 | チーズ | 2 |
2 | てりやき | 1 |
3 | フィッシュ | 4 |
4 | ベジタブル | 1 |
さて、ここで「各顧客に紐づくハンバーガー」を表示したかったとします。
まずは全てのユーザーの取得が必要です。
@customers = Customer.all
これでこの例だとid_4までの顧客が読み込まれます。
次は紐づくハンバーガーの情報を取得しなければいけません。
@customers.each do |customer|
customer.hamburgers.each do |hamburger|
hamburger.name
end
end
流れとしては①まず顧客テーブルで順番に顧客を取り出します。
②その顧客が購入したハンバーガーの情報を取得するため、ハンバーガーのテーブルにアクセスします。
③処理を終えたら、顧客テーブルに戻り、存在する顧客idの分だけ同じ処理を繰り返します。
このように「各顧客に紐づくハンバーガー」を表示したいために
「Customerテーブルに1回+hamburgerテーブルに対してcustomerの数だけ=N回」
のアクセスが起きてしまうのです。その分だけSQLが発行されています。
ここで本題のincludesメソッド
customersテーブルにアクセスするのと一緒に、hamburgerテーブルの情報も得られれば、
SQLの負担も減ってブラック企業状態から脱出できそうです。
@customers = Customer.all #これを下記に変更
@customers = customer.includes(:hamburgers) #includesメソッド使用
この[@customers = Customer.all]を消して、includesメソッドを使用しこのように記述することで、
顧客の情報と一緒に、紐づくハンバーガーの情報も取得できます。
includesを使うことで、データベースへのアクセス回数を減らし、
アプリケーションの応答速度を上げることができるのです。
SQLの動きにも違いが現れます。
#「N+1問題」の状態のSQL IDの分だけ繰り返す
SELECT 'customers'.* FROM 'customers'
SELECT 'hamburgers'.* FROM 'hamburgers' WHERE 'hamburgers'.'customer_id' = 1
SELECT 'hamburgers'.* FROM 'hamburgers' WHERE 'hamburgers'.'customer_id' = 2...
#includesメソッド使用すると、、、
SELECT 'customers'.* FROM customers'
SELECT 'hamburgers'.* FROM 'hamburgers' WHERE 'hamburgers'.'customer_id' IN (1,2,3,4)
includesメソッドを使ったことによって、関連するデータをまとめて取得できるようになりました!
例えて言うならハンバーガー・ポテト・ナゲットをばらばらで買っていたけど、
まとめてハッピーセットで買ってくれたからレシート少なくて済んだよ!みたいな感じでしょうか(笑)
ちょっとうまく言えませんが、こちらのincludesメソッド応用編もありますので参考記事を以下に貼っておきます。
https://nyoken.com/rails-includes
https://pikawaka.com/rails/includes