accepts_nested_attributes_forを使わずに複数リソースを同時登録する(クイズアプリを例に)

## 想定読者

Railsを使ってポートフォリオを作成している方
特にクイズアプリを作成している方

## どういうときの話

私はクイズアプリを作ってるときにこの問題に当たりました。
一つのフォーム画面で、
①問題文
②選択肢1
③選択肢2
④選択肢3
⑤選択肢4
を同時に登録させたいとします。

で、モデルはこんな感じです。

```ruby:app/models/question.rb
class Question < ApplicationRecord
  has_many :choices, dependent: :destroy
end
```

```ruby:app/models/choice.rb
class Choice < ApplicationRecord
  belongs_to :question
end
```

```ruby:db/migrate/時間_create_choices.rb
class CreateChoices < ActiveRecord::Migration[5.2]
  def change
    create_table :choices do |t|
      t.references :question, foreign_key: true
      t.string :content, null: false
      t.boolean :is_answer

      t.timestamps
    end
  end
end
```

- 「問題 has many 選択肢s」です。
- 選択肢テーブルはis_answerという「正答か誤答か」というカラムを持っています。

よって単純なform_withとモデルでは実装できません。フォームに合わせてDB設計を変える、みたいなのは本末転倒です。（私の記事で恐縮ですが、クイズアプリのDB設計に関して、以下記事を書きました)
https://qiita.com/kumackey/items/7ccbc949458bd0af22bd

他にも、以下の例が思いつきます。

- 就活サイトのユーザ登録画面で、has manyな経歴を同時に登録する
- SNSのポスト投稿画面で、投稿自体とタグ複数を同時登録する
- レシピサイトで、レシピ名と、複数の手順を同時に登録する

## accepts_nested_attributes_forではダメ？

非推奨らしいです。
実は私はaccepts_nested_attributes_forに関しては逆に詳しくないため、詳細は以下URL等をご覧いただければと思います。
https://moneyforward.com/engineers_blog/2018/12/15/formobject/
https://tech.recruit-mp.co.jp/server-side/rails-development-policy/
https://tech.libinc.co.jp/entry/2019/04/05/113000

## フォームオブジェクトを作る

```ruby:app/forms/register_quiz_form
class RegisterQuizForm
  include ActiveModel::Model
  include ActiveModel::Attributes
  include ActiveModel::Validations

  attribute :question_content, :string
  attribute :correct_choice, :string
  attribute :incorrect_choice_1, :string
  attribute :incorrect_choice_2, :string
  attribute :incorrect_choice_3, :string

  validates :question_content, presence: true, length: { maximum: 140 }
  validates :correct_choice, presence: true, length: { maximum: 40 }
  validates :incorrect_choice_1, presence: true, length: { maximum: 40 }
  validates :incorrect_choice_2, length: { maximum: 40 }
  validates :incorrect_choice_3, length: { maximum: 40 }

  def save
    return false unless valid?
    question = Question.new(content: question_content)
    question.save # 問題文の登録

    choice = question.choices.build(content: correct_choice, is_answer: true)
    choice.save # 正解選択肢の保存

    question.choices.create(content: incorrect_choice_1, is_answer: false)
    question.choices.create(content: incorrect_choice_2, is_answer: false)
    question.choices.create(content: incorrect_choice_3, is_answer: false)
    # 不正解選択肢の登録
  end
end
```

フォームオブジェクト自体に知見が無い方は、以下記事をご参照いただければと思います。
https://tech.medpeer.co.jp/entry/2017/05/09/070758
https://qiita.com/kamohicokamo/items/d2ea4d71f86d99261b1a

`return false unless valid?`によって、ヴァリデーションが通らないときにfalseを返します。これにより、コントローラーで使用する際も、以下のような実装が可能になります。

```ruby
if @register_quiz_form.save
  (ヴァリデーションが通ってsaveに成功したときの処理)
else
  (ヴァリデーションが通らずsaveに失敗したときの処理)
end
```

`if @user.save`に近い感覚で使うことができます。

## コントローラーとビュー

```ruby:app/controllers/questions_controller.rb
class QuestionsController < ApplicationController
  def new
    @register_quiz_form = RegisterQuizForm.new
  end

  def create
    @register_quiz_form = RegisterQuizForm.new(create_question_params)
    if @register_quiz_form.save
      redirect_to (成功したときのパス)
    else
      render :new
    end
  end

  def create_question_params
    params.require(:register_quiz_form).permit(
      :question_content, :correct_choice, 
      :incorrect_choice_1, :incorrect_choice_2, :incorrect_choice_3)
  end
end
```

※ 以下Viewですが、筆者がslimに慣れているのでslimで記述しています。erbに置き換えてお読み頂けると幸いです。

```ruby:app/views/questions/new.html.slim
= form_with model: @register_quiz_form, url: questions_path, local: true do |f|
  = render '(エラーメッセージ表示用のパーシャル)', object: @register_quiz_form
  .form-group
    = f.label :question_content
    = f.text_field :question_content, class: 'form-control'
  .form-group
    = f.label :correct_choice
    = f.text_field :correct_choice, class: 'form-control'
  .form-group
    = f.label :incorrect_choice_1
    = f.text_field :incorrect_choice_1, class: 'form-control'
  .form-group
    = f.label :incorrect_choice_2
    = f.text_field :incorrect_choice_2, class: 'form-control'
  .form-group
    = f.label :incorrect_choice_3
    = f.text_field :incorrect_choice_3, class: 'form-control'
  = f.submit '登録する', class: 'btn btn-raised btn-success'
```

ちなみにフォームブジェクトという設計は、ActiveRecordのattributesと、フォーム画面で表示させたいことが異なる、というケースのいずれでも効果を発揮します。
私はフォームオブジェクトが大好きすぎて、ポートフォリオには3つのフォームオブジェクトがあります。笑

間違い等あればご指摘頂けると嬉しいです。