Rails4でFormオブジェクトを作る際に気をつける3つのポイント
ActiveRecord / Rails / Ruby
この記事はRuby on Rails Advent Calendar 2014の2日目です。
1日目は@miyukkiさんの「結局Ruby on RailsとPHPってどっちが優れてるの?」でした。おつかれさまでした。
Formオブジェクトとは
Formオブジェクトはその名の通り入力フォーム用のオブジェクトです。
フォームとモデルがうまく対応しているときはActiveRecordをそのまま使えば良いのですが、 複数モデルを作りたかったりモデルとは違うValidationを行いたかったりする場合にはFormオブジェクトを使うと便利です。
Formオブジェクトのサンプルコードはこんな感じになります。
class Blog::SiteForm
include Virtus.model
extend ActiveModel::Naming
include ActiveModel::Conversion
include ActiveModel::Validations
attribute :title, String
attribute :description, String
#追加するvalidationがあればこのあたりに書く
def site
@site ||= Blog::Site.new(title: title, description: description)
end
def site=(site)
@site = site
self.title = site.title
self.description = site.description
@site
end
def persisted?
site.persisted?
end
def save(user)
site.user = user
valid? && site.save
end
def url
if persisted?
Rails.application.routes.url_helpers.blog_site_path(site.id)
else
Rails.application.routes.url_helpers.blog_sites_path
end
end
def valid?
result = super
unless site.valid?
key = :title
site.errors[key].each do |error|
errors.add(key, error)
end
return false
end
return result
end
end
この例ではBlog::Site
がActiveRecordモデルになっています。
では、以下で気をつけるポイントを紹介します。
form_forにFormオブジェクトを渡してもURLが生成されない
form_for
にActiveRecordオブジェクトを渡してもちゃんとURLが生成されません。
これはform_for
が受け取ったオブジェクトのクラス名を元にURLを生成しているからです。
例えばBlog::Site
オブジェクトを渡せばblog_sites_path
ヘルパーを実行してくれるのですが、Blog::SiteForm
オブジェクトを渡すとblog_site_forms_path
ヘルパーを実行しようとして失敗してしまいます。
なので、次のようにURLを直接指定しましょう。
- form_for @site_form, url: @site_form.url do
...
追記(2014/12/3)
@joker1007さんから「FormオブジェクトのURLの渡し方について」という記事で突っ込みをいただきました。
今回のような場合はpolymorphic_pathを使って
- form_for @site_form, url: polymorphic_path(@site_form.site) do
とした方が良さそうです。こうすればurl
, persisted?
メソッドは不要になります。
合わせて
extend ActiveModel::Naming
include ActiveModel::Conversion
include ActiveModel::Validations
はinclude ActiveModel::Model
に置き換えられるという指摘もいただいたのですが、手元のコードだとcreate
アクション実行時にエラーが出てしまったので、こちらはひとまずこのままにしておきます。原因特定したら別途追記します。← Virtus.model
を使う場合は単純にはActiveModel::Model
はincludeできないようです。
validate_uniqueness_ofが使えない
一意性制約を検証するにはデータベースのUNIQUE制約を利用する必要があります。
ただ、Formオブジェクトからはデータベース制約を直接扱えません。
なので、validate_uniqueness_of
はActiveRecordオブジェクト(今回の例だとBlog::Site
オブジェクト)で実行する必要があります。
具体的にはサンプルコードのようにFormオブジェクトのvalid?
メソッドでActiveRecordオブジェクトのvalid?
を呼び出せば良いかと思います。
errorsは統合する必要がある
2つ目のポイントで紹介したようにFormオブジェクト以外のモデルでvalidationを行った場合は気をつけることがもう1つあります。
通常validationに失敗するとモデルオブジェクトのerrors
にエラー情報が格納されるのですが、その情報はFormオブジェクトのerrors
には自動的には統合されません。
なので、Formオブジェクトのエラーとして扱うには各オブジェクトのerrors
をFormオブジェクトに統合する必要があります。
サンプルコードではvalid?
メソッドでその処理をしています。
def valid?
result = super
unless site.valid?
key = :title
site.errors[key].each do |error|
errors.add(key, error)
end
return false
end
return result
end
まとめ
Formオブジェクトは単なるクラスなので、自分でいろいろと面倒を見る必要がありますが うまく使うとコードがすっきりするのでチャンスがあれば是非活用してみてください。
3日目は@awakiaさんです。よろしくお願いします。