初めに
イベントの日程調整アプリのrailsAPI作成中に躓いた時の備忘録
- answers_controllerのupdateアクションで紐づくschedule_answers(回答ステータス)を編集しようとするとN+1問題が発生した
- HTTPリクエスト送信一度目はN+1問題が起きるがschedule_answerの更新は保存はされており、そのまま続けて送信するとupdateは完了
ER図
原因
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