アソシエーションとN+1問題について
目次
アソシエーションとは
簡単にいうと、複数のテーブルを紐付けること。
例えば、ユーザー(users)のテーブルには「鈴木」と「高橋」というデータがある。
投稿(posts)のテーブルには「好きな食べ物は寿司」「趣味は車」「趣味はピアノ」「趣味は車」「よくジムで筋トレする」みたいなデータがある。
この時に「鈴木」が「投稿したもの」と、「高橋」が「投稿したもの」に分けたい、これが紐付け。
例えば今回のケースだと、「鈴木」が投稿したのは「好きな食べ物は寿司」「趣味はピアノ」、「高橋」が投稿したのは「趣味は車」「よくジムで筋トレする」みたいな感じ。
具体的にどうする?
上記の例を考えるとき。
「鈴木」が投稿したのは「好きな食べ物は寿司」「趣味はピアノ」みたいに、1人のユーザー対し、複数の投稿が存在するという関係になる。
端的にいうと、1対多数(親と子)の関係であり、これを定義してあげればよい。
そのためには、親のモデル(今回はUserモデル)に以下を記述。
has_many :posts, dependent: :destroy # 紐づく子のモデル名については複数形で # また、dependent: :destroyとすることで、親のデータが消えると、一緒に子のデータも消えるようになる。
子のモデル(今回はPostモデル)に以下を記述。
belongs_to :user # 紐づく親のモデル名については単数形で。
ただし、データベース上での紐付けも必要
上記だとモデル同士が紐づいているイメージ?なので、データベース上でテーブル同士の紐付けをする必要がある。
最初の例を考えると、鈴木の投稿がuser_id:1、高橋の投稿がuser_id:2みたいにしてあげれば良い。
すると、「好きな食べ物は寿司」のuser_id:1、だからこれは鈴木の投稿!みたいな判別ができる。
これを言い換えると、紐付けの関係を作るためには多数(子)に、親モデル_idを付与してあげると良いということ。
そのためには以下2つの方法が考えられる。
1. そもそも、モデルを作る際に定義してあげる。
今回の場合はPostモデルを作る際に、以下のような記述を行う。
rails generate model posts(モデル名) title:string body:text user:references
上記の記載例で、Postモデルのほか、postsテーブルを作成するマイグレーションファイルも作成される。
そしてマイグレーション ファイルに、titleカラム、bodyカラムだけでなく、user:referencesとすることで、user_idカラムが作成される記述が出来上がる。
2. 後から定義する
その場合は関連付けを規定する、マイグレーションファイルを作成する。以下、マイグレーションファイル作成の記載例。
rails generate migration AddUserIdToPosts
作成されたマイグレーションファイルに以下を記述し、実行
class AddUserIdToPosts < ActiveRecord::Migration[5.2] def change add_reference :posts, :user, foreign_key: true end end
以下、結果はモデルを作る際に定義した場合と同様。
N+1問題とは?
アソシエーションを行った際、必ず考慮するべきなのが、N+1問題。
例えば、投稿を繰り返しで全部表示したい時。
一旦、@posts = Post.all みたいに投稿データをインスタンス変数に代入する。
そして、繰り返しの処理をして、表示するためには以下の通り。
<%= @posts.each do |post| %> post <% end %>
これで、見た目は問題ないけど、実は中身ではとんでもないことが起きているって話!
実は@posts = Post.all の時点。
ここでは、当然postsテーブルからデータを引っ張ってきている(1回目、これが+1の部分。
しかし、SQLさんは「あれ、postsテーブルの中にuser_id: 1 がある…。あっ、そういえばusersテーブルと紐づいてたじゃん!よし、usersテーブルのuser_idが1のレコードを確認しよう!」となる(2回目)。
そして、SQLさんは「あれ、よく見ると postsテーブルの中にuser_id: 2 もある!?user_idが2のレコードも確認しなきゃ…」となる(3回目)。
つまり、余計なお世話だよ!みたいなことをしている。(もちろん、投稿に紐づいている投稿者の名前を表示したい時などはusersテーブルの参照も必要。とはいえ、user_idごとに参照するんじゃなくて、usersテーブルをまとめて見てくれよ…、見たいな感じ)
なので、なんと @posts = Post.all では3回もSQLが動いているということ!
言い換えると、たった1回の処理なのに、postsテーブルに1回、usersテーブルに2回、計3回アクセスしていて、余計な動作をしているということ。
まとめると、テーブルとテーブルが紐づいているとき。そのテーブルを参照する際、そのテーブルを1回、もう片方のテーブルを紐づいているid数(n個)だけ参照しているということ。
これの何が問題かというと、必要以上にSQLが発行され、特にもデータの量が増えるほど、めちゃくちゃアプリの動作が重くなってしまう可能性がある。
N+1問題の解決方法
解決するにはincludesメソッドを使用すればOK。
先ほどの例だと以下のように記述。
@posts = Post.all.includes(:user)