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さんです。よろしくお願いします。

参考文献