フィールドが存在しない項目の検証(validate)

前回までに、ユーザーデフォルトの設定が出来るようになったが、現状では入力された値のチェックはしていない。だから予想外の入力がされると、エラーが発生してしまう。入力値の検証機能(validate)が必要だ。
ところが、今回は複数のオブジェクトをyamlに変換して1つのフィールドに保存するようにしている。検証したいのはyamlに変換する前の個々のハッシュオブジェクトの値だ。こんな時はどのように処理するべきなのか?以下は、試行錯誤の結果、その処理方法のメモ。

エラーのチェック

app/models/default.rb
モデル(defaultテーブルを管理)
標準的なvalidate

値のチェックの対象がyamlフィールドそのものであれば、以下のように簡潔に表現することが出来る。(例:yamlフィールドはintegerタイプで、値が直接保存されると仮定して、1から100の範囲外の時はエラーにする場合)

class Default < ActiveRecord::Base
  belongs_to :user
  validates_exclusion_of :yaml, :in=>(1..100), :message=>"1〜100の範囲の数値でお願いします。"
end

簡潔です。とても分かり易い表現。

1フィールドに複数のオブジェクトがまとめて保存されている場合

しかし、今回、yamlフィールドには'per_page=>10'のようなハッシュが、yamlに変換されて保存されている。検証したいのは、default.yaml['per_page']が1〜100の範囲内であるかどうか。以下のようにやってみた。

class Default < ActiveRecord::Base
  belongs_to :user
  
  # unless 1 <= default['per_page'].to_i && 
  #             default['per_page'].to_i <= 100
  # 上記条件式は、範囲オブジェクトを利用すれば簡潔に表現できる。但し、以下の注意点あり。
  # Rubyでは、演算子の優先順位は、=== .. の順である。
  #    1..100  === 101 --> 1..(100 === 101)と解釈されて、エラーになる。
  #   (1..100) === 101 --> falseが返り正常終了。
  def validate
    default = YAML.load(yaml)
    unless (1..100) === default['per_page'].to_i
      errors.add('1ページ当りの表示件数:', "1〜100までの数値でお願いします。")
    end
  end
end
  • YAML.load(yaml)は、YAML.load(self.yaml)とメソッドを呼んでいることになる。エラーが発生した時はデータベースの更新は無いが、モデルが保持する値は入力されたそのままの値に変化するようだ。

発生したエラーの表現

エラーが発生した場合、標準的なscaffoldと同じように、以下のようなエラー表示を実現したい。

app/views/defaults/_form.rhtml
ビュー(環境設定の入力ページ)
  • 追記1と、追記2の役割は上記図のようになる。
  • app/models/default.rbのvalidateメソッドの中で指定したerrors.add('1ページ当りの表示件数:',...)、オレンジ色の部分が、エラーを表現する場合の項目を区別するキーになる。今までフィールド名である必要があると思っていたが、このような対応関係を理解して自由に書き換えてOKのようだ。
<%= error_messages_for 'default' %><%#<------ 追記1 %>

<table>
<tr>
  <th align="right">
  <label for="per_page">1ページ当りの表示件数:</label>
  </th>
  <td <%= field_with_errors_class(@default, '1ページ当りの表示件数:') %>><%#<------ 追記2 %>
  <%= text_field_tag 'per_page', @default_hash['per_page'], :name => "default[per_page]" %>
  </td>
</tr>
</table>
app/controllers/defaults_controller.rb
コントローラー
  • 追記1のためには、@defaultにdefaultオブジェクトが代入されている必要がある。(@defaultがnilだとエラーになる。)
  • error_messages_for 'default'のオレンジ色の部分defaultは、モデル名を指定している。@defaultとしている変数名は何でも良い。(例:@itemや@objなど自由に設定して大丈夫だと思う。)
class DefaultsController < ApplicationController
  def edit_preference
    @default = current_user.default
    @default_hash = default_user(true)
  end
  
  def set_app_default
    @default = current_user.default
    @default_hash = default_app
    render :action => 'edit_preference'
  end
  
  def update_preference
    current_user.create_default(:yaml => {}.to_yaml) if current_user.default.nil?
    @default = current_user.default
    @default_hash = default_user.merge(params[:default])

    # アプリケーションデフォルトと同じ項目は削除したいが、削除してしまうとvalidateが正常に機能しない。
    # @default_hash.delete_if {|key, value| default_app[key] == value}

    if current_user.default.update_attributes(:yaml => @default_hash.to_yaml)
      flash[:notice] = 'ユーザー環境を更新しました。'
      redirect_to :controller => 'csvs', :action => 'list'
    else
      render :action => 'edit_preference'
      # 強制リロードを有効にして、キャンセルした時にキャッシュに残らないようにする。
      @default_hash = default_user(true)
    end
  end
end
app/helpers/application_helper.rb
ペルパー
  • 追記2のペルパメソッドは以下のように定義してある。対応する項目にエラーが発生していたら、class='fieldWithErrors'属性を設定する。
module ApplicationHelper
...(途中省略)...
  def field_with_errors_class(obj, field)
    return unless obj
    "class='fieldWithErrors'" if obj.errors.on(field)
  end
...(途中省略)...


以上で、ユーザー環境設定で入力した値を検証できるようになった。しかし、フィールドに直接、値を保存する場合と比較して、苦労が多い...。もし、yamlに変換しなければ、今回のような余計なコードは必要なかったはず。Railsにはマイグレーションがあり、フィールドの追加、削除も簡単に対応できるので、素直に1フィールド、1ユーザー設定で標準的に保存した方が良かったかもしれない。

      • Railsで標準的なユーザーデフォルトの取り扱い方法ってあるのだろうか?経験豊かなRials、Rubyプログラマの方はどんな方法をとっているのだろうか。