LoginSignup
16
30

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-03-25

想定読者

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

どういうときの話

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

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

app/models/question.rb
class Question < ApplicationRecord
  has_many :choices, dependent: :destroy
end
app/models/choice.rb
class Choice < ApplicationRecord
  belongs_to :question
end
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

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

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を返します。これにより、コントローラーで使用する際も、以下のような実装が可能になります。

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

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

コントローラーとビュー

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に置き換えてお読み頂けると幸いです。

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つのフォームオブジェクトがあります。笑

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

16
30
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
16
30