Tech Racho エンジニアの「?」を「!」に。
  • 開発

Rails: Form Objectと`#to_model`を使ってバリデーションをモデルから分離する(翻訳)

概要

原著者の許諾を得て翻訳・公開いたします。

タイトルは内容に即したものに変えました。

Rails: Form Objectと#to_modelを使ってバリデーションをモデルから分離する(翻訳)

Rails 5.2リリースノートをひととおり読んでみて、ActiveStorageなどに興味を惹かれたので、試してみたくなりました。本記事ではプレリリース版を用いてシンプルなアプリをビルドしました。

目的は、ユーザーがアンケート(questionnaire)を作成して結果を回収できるアプリを作成することです。最初にForm Objectを用いて、アンケートのタイトルと質問リストを取得します。

# db/migrate/create_questionnaires.rb
create_table "questionnaires", force: :cascade do |t|
  t.string "title"
  t.string "questions", default: [], null: false, array: true
  t.datetime "created_at", null: false
  t.datetime "updated_at", null: false
end
# app/forms/new_questionnaire_form.rb
class NewQuestionnaireForm
  include ActiveModel::Model

  attr_accessor :title, :questions

  validates :title, presence: true

  def save
    Questionnaire.new(title: title, questions: questions).save
  end
end

このように書いてみたかった理由は、Ectoを見たときに、バリデーションをモデルから切り離せるのがとても便利だと思えたからです(条件付きバリデーションが不要になります!)。

コントローラはいたってシンプルで、scaffoldしたコントローラと大差ありません。以下はnewアクションとcreateアクションです。

# app/controllers/questionnaires_controller.rb
class QuestionnairesController < ApplicationController
  def new
    @questionnaire = NewQuestionnaireForm.new
  end

  def create
    @questionnaire = NewQuestionnaireForm.new(questionnaire_params)

    if @questionnaire.save
      redirect_to @questionnaire, notice: 'Questionnaire was successfully created.'
    else
      render :new
    end
  end
end

ビューでは、新しいform_withヘルパーを次のように使っています。

<!-- app/views/questionnaires/_form.html.erb -->
= form_with(model: @questionnaire, local: true) do |form|
  = form.label :title
  = form.text_field :title

アプリを起動してquestionnaires/newにアクセスしてみると、undefined method 'new_questionnaire_forms_path' for ...エラーメッセージが表示されました...。

うう残念。Railsは、form_withに渡したForm Objectのクラス名を受け取ると、対応するコントローラへのパスであると自動的に推論しますが、コントローラの名前だけが合っていません。

この修正方法はいくつか考えられます。form_withヘルパーの投稿先のURLを上書きする方法もあれば、Form Objectの#model_nameを上書きして、今扱っているのがQuestionnaireであるかのように見せかける方法もあります(この方法が有用なこともありますが、この状況ではダーティハックの恐れがあります)。

もっとよい方法を見つけるために、先ほどのForm Objectに立ち戻りましょう。このForm Objectは、Questionnaireという単一のオブジェクトだけを扱っています。しかも#saveメソッドでQuestionnaireを作成しています。先のForm Objectからモデルへの変換をシャドウイング(shadowing)として表すことができそうです。そしてActiveModel::Conversion#to_modelというメソッドがあることに気が付きました。このメソッドを使うようForm Objectを書き直すと次のようになります。

class NewQuestionnaireForm
   include ActiveModel::Model

  def to_model
    Questionnaire.new(title: title, questions: questions)
  end

  def save
    to_model.save
  end
end

questionnaires/newにアクセスすると、今度はちゃんとフォームが表示されます。タイトルを入力して[Submit]ボタンを押すと、データベースにquestionnaireのモデルが新しく作成されます。できました!

関連記事

Rails: Form ObjectとVirtusを使って属性をサニタイズする(翻訳)

Rails: dry-rbでForm Objectを作る(翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。