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

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

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

### ER図
![スクリーンショット 2022-12-01 15.46.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2629116/6d84b59d-e9f6-54ee-646c-8982a35d4a4c.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を使うのは気をつけた方がよさそう...

### それぞれのモデル
```ruby: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
```

```ruby: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
```

```ruby: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コントローラー
```ruby: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の追加を色々と試したが、うまくいかなかった。
```ruby:
  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
```
こちらの記事を参考にして色々試しました

https://qiita.com/mball/items/5d4228cd3523f7a1ad04

### リクエストボディー
```json: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を使わずに実装することにしました。

```ruby: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
```

```ruby: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

```