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

概要 原著者の許諾を得て翻訳・公開いたします。 英語記事: Routing form objects with Rails 著者: Christoph Lupprich タイトルは内容に即したものに変えました。 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を作る(翻訳)