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を作る(翻訳)

デザインも頼めるシステム開発会社をお探しならBPS株式会社までどうぞ 開発エンジニア積極採用中です! Ruby on Rails の開発なら実績豊富なBPS

この記事の著者

hachi8833

Twitter: @hachi8833、GitHub: @hachi8833 コボラー、ITコンサル、ローカライズ業界、Rails開発を経てTechRachoの編集・記事作成を担当。 これまでにRuby on Rails チュートリアル第2版の半分ほど、Railsガイドの初期翻訳ではほぼすべてを翻訳。その後も折に触れてそれぞれ一部を翻訳。 かと思うと、正規表現の粋を尽くした日本語エラーチェックサービス enno.jpを運営。 実は最近Go言語が好き。 仕事に関係ないすっとこブログ「あけてくれ」は2000年頃から多少の中断をはさんで継続、現在はnote.muに移転。

hachi8833の書いた記事

週刊Railsウォッチ

インフラ

ActiveSupport探訪シリーズ