1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

includeしてるのにN+1問題が発生した理由

Last updated at Posted at 2022-12-01

初めに

イベントの日程調整アプリのrailsAPI作成中に躓いた時の備忘録

  1. answers_controllerのupdateアクションで紐づくschedule_answers(回答ステータス)を編集しようとするとN+1問題が発生した
  2. HTTPリクエスト送信一度目はN+1問題が起きるがschedule_answerの更新は保存はされており、そのまま続けて送信するとupdateは完了

ER図

スクリーンショット 2022-12-01 15.46.31.png

原因

accepts_nested_attributes_forで、

  • answersモデルとschedule_answers
  • possible_datesとschedule_answers

とを紐付けていたため、たとえanswer経由でも、schedules_answersに変更があるとpossible_dateにチェックが入り、結果N+1問題が発生していた。
ただ、更新の保存はされているため、2回目の送信ではschedules_answerに変更がなく、N+1問題が起きなかった。

こういった中間テーブルにaccepts_nested_attributes_forを使うのは気をつけた方がよさそう...

それぞれのモデル

answer.rb
class Answer < ApplicationRecord
  validates :event_id, uniqueness: { scope: :user_id }
  belongs_to :user
  belongs_to :event
  has_many :schedule_answers, dependent: :destroy

  accepts_nested_attributes_for :schedule_answers
end
possible_date.rb
class PossibleDate < ApplicationRecord
  validates :proposed_at, presence: true, uniqueness: { scope: :event_id }
  belongs_to :event
  has_many :schedule_answers, dependent: :destroy

  accepts_nested_attributes_for :schedule_answers
end
schedule_answer.rb
class ScheduleAnswer < ApplicationRecord
  extend Enumerize

  enumerize :status, in: { bad: 0, normal: 1, good: 2 }, default: 0, scope: true
  validates :status, presence: true
  belongs_to :answer
  belongs_to :possible_date
end

APIコントローラー

answers_controller.rb
class Api::AnswersController < ApplicationController
  before_action :set_answer, only: %i(update)

  def update
    if @answer.update(answer_params)
      render json: @answer, status: :created
    else
      render json: @answer.errors, status: :unprocessable_entity
    end
  end

  private

  def set_answer
    event = Event.where(deleted_at: nil).find_by(hashed_url: params[:hashed_url])
    @answer = event.answers.find_by(user_id: params[:user_id])
  end


  def answer_params
    params.require(:answer).permit(
      :user_id, # ログインユーザーから取得
      :comment,
      schedule_answers_attributes: %i(id possible_date_id status)
    )
  end
end

以下のようにset_answerメソッドにincludesの追加を色々と試したが、うまくいかなかった。

  def set_answer
    event = Event.includes(:possible_dates, answers: :schedule_answers).where(deleted_at: nil).find_by(hashed_url: params[:hashed_url])
    @answer = event.answers.find_by(user_id: params[:user_id])
  end

こちらの記事を参考にして色々試しました

リクエストボディー

api/answers#update
{
	"comment": "update",
	"user_id": 1,
	"schedule_answers": [
		{
			"id": 1,
			"status": "normal"
		},
		{
			"id": 2,
			"status": "bad"
		}
	]
}

N+1問題のログ

Bullet::Notification::UnoptimizedQueryError (user: root
PUT /api/answers/c38d9f25-18ce-4133-8ce9-8d7e3abca9fb?user_id=1
USE eager loading detected
  ScheduleAnswer => [:possible_date]
  Add to your query: .includes([:possible_date])
Call stack
  /usr/src/app/app/controllers/api/answers_controller.rb:22:in `update'

):

解決策

accepts_nested_attributes_forを使わずに実装することにしました。

answer.rb
class Answer < ApplicationRecord
  validates :event_id, uniqueness: { scope: :user_id }
  belongs_to :user
  belongs_to :event
  has_many :schedule_answers, dependent: :destroy

  def bulk_create_schedule_answers(schedule_answers_params)
    schedule_answers_params.each do |schedule_answers_param|
      schedule_answers.build(possible_date_id: schedule_answers_param[:possible_date_id],
                             status: schedule_answers_param[:status])
    end
  end

  def bulk_update_schedule_answers(schedule_answers_params)
    schedule_answers_params.each do |schedule_answers_param|
      schedule_answer = schedule_answers.find_by_id(schedule_answers_param[:id])
      next errors.add(:base, 'scheudle_answer not found') if schedule_answer.blank?

      schedule_answer.update(status: schedule_answers_param[:status])
    end
  end
end
answers_controller.rb
class Api::AnswersController < ApplicationController
  before_action :set_answer, only: %i(show update)
  before_action :set_event, only: %i(create)

  # 個人の回答状況
  def show
    render json: @answer
  end

  # コメント・出欠入力
  def create
    answer = @event.answers.new(answer_params)
    answer.bulk_create_schedule_answers(schedule_answers_params)
    if answer.save
      render json: answer
    else
      render json: answer.errors, status: :unprocessable_entity
    end
  end

  def update
    @answer.assign_attributes(answer_params)
    @answer.bulk_update_schedule_answers(schedule_answers_params)
    if @answer.save
      render json: @answer, status: :created
    else
      render json: @answer.errors, status: :unprocessable_entity
    end
  end

  private

  def set_answer
    @answer = Answer.find(params[:id])
  end

  def set_event
    @event = Event.where(deleted_at: nil).find_by(hashed_url: params[:hashed_url])
  end

  def answer_params
    params.require(:answer).permit(
      :user_id, # ログインユーザーから取得
      :comment
    )
  end

  def schedule_answers_params
    params[:schedule_answers]
  end
end

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?