N + 1問題の解決(Ruby on Rails)

現在作成しているオリジナルアプリケーションに投稿へのタグ付け機能を実装したのですが、N + 1問題が発生していると感じたためこれを解決しようとしましたが以下のようなエラーが発生したため解決のために四苦八苦しました。スクールのメンターに相談してみたところ無事解決することが出来ましたので、アウトプットします。もし皆さんがRailsでのウェブアプリ開発で「N + 1問題」の解消を行う際に同様のエラーが出た場合、参考になれば幸いです。(例には他のミニアプリを用いています。)

スクリーンショット 2021-05-20 18.11.05

N + 1問題とは
例としてある投稿(ツイート)にタグが紐づいている場合、それらの関係性としてはツイートモデルから見た場合、沢山のタグが存在していることと、タグモデルから見た場合、沢山のツイートが存在しているということになります。これは即ち多対多のアソシエーションになります。

class Tweet < ApplicationRecord
 has_many :tags
end
class Tag < ApplicationRecord
 has_many :tweets
end

モデルの記述としては以上のような記述となります。(タグ付け機能には中間テーブルを用いて実装していますが、一例ですので記述について割愛しています。)そして、タグ付けされた投稿をビューに表示する場合、一つの投稿(ツイート)に対して関連付けされているモデル(この場合はタグモデル)にアクセスすることによって、そのツイートに紐づけられたタグを取得しています。しかし、これを一つの投稿に対して一回ずつ行うため、沢山のツイートを一度に表示する場合、その分タグモデルへのアクセスが必要となるため、余分にデータベースへアクセスすることになります。これはアプリケーションのパフォーマンスの低下へと繋がってしまいます。これを「N + 1問題」と言います。(詳しく知りたい方はN + 1問題で検索しますと参考になる記事が出てきますので、そちらを参考にしてください。)アプリへの負荷をかけないためにも、出来れば一回のアクセスで関連付けされたモデルの情報を取得したいところです。これを解決するために「includesメソッド」を用います。

発生したエラーとその解決について
includesメソッドの使い方としては、

モデル名.includes(:紐付くモデル名)

とします。以上の記述でモデルの情報に紐づく他モデルに一度のアクセスでまとめて情報を取得することが出来ます。実際のコントローラーの記述に対して以下のように追記しました。

def index
 @tweets = Tweet.includes(:tag).all.order(created_at: :desc)
end

実際にツイートモデルに紐づいているのはタグモデルであるため、includes(:tag)と追記したところ上記画像のようなエラーが発生しました。エラーメッセージである「Association named 'tag' was not found on Tweet; perhaps you misspelled it?」からわかることはツイートモデルのアソシエーションに「tag」という名前が見つからなかったということになります。私自身はこの意味が分からず詰まってしまったわけではありますが、テーブルの名前ではなく実際に存在するモデルの名前である「tag」をincludesしている上で実際にアソシエーションを見ても「tag」があるため他の原因が分かりませんでした(この段階では「tag」という記述ではなく「has_many :tags」となっているところが重要であることに気づかなかった)。そのため質問してみたところ容易に解決することが出来て、驚きました。
原因としてはエラーの文章そのままです。ツイートモデルのアソシエーションですが上述した通りに「has_many :tags」です。エラー文は「tag」が見つからないという意味でした。そのため、記述を以下のようにすると解決しました。

def index
   @tweets = Tweet.includes(:tags).all.order(created_at: :desc)
   # Tweetモデルのアソシエーションhas_manyの関係から「tag」ではなく「tags」と記述する
 end

どういうことかと言いますと、タグ自体は登録されたタグが一つだけの投稿に紐ついているわけではないということです。要するに「belongs_to」ではないということで「has_many」ということです。belongs_toの例でいえばあるツイートはある一人のユーザーに紐づいているということで、ユーザーモデルで見たらhas_many :tweets、ツイートモデルで見ればbelongs_to :userとなります。あるツイートに対して紐付けられたユーザーの情報も表示したいという場合、includesメソッドを用いた記述は以下の通りです。

def index
 @tweets = Tweet.includes(:user)
end

これは1対多のアソシエーションであるため、「includes(:user)」と「s」が付かない記述となります。要するにツイート側から見たらユーザーはある一人であること。今回の場合ツイートから見たタグは沢山あるということなのでincludesの引数を(:tags)と複数形にしてあげる必要があります。
今回はメンターに解決してもらい解説して頂きましたが、エラー文を翻訳し、仮説を立てて実証してみれば自力でどうにかなるということを実感しました。しかしそのためには場数を踏んで慣れる必要があるとも感じました。これからの転職活動をしっかりとこなしてIT企業に就職し、技術の理解を深めていきたいです。

この記事が気に入ったらサポートをしてみませんか?