2010/10/31

Rails3 にて ActiveRecord で数値列の形式バリデーションが機能しない

今日 Rails3 でハマったところ。

環境
・Ruby : 1.8.7
・Rails : 3.0.0
・DB : PostgreSQL
・OS : Ubuntu 10.04

手っ取り早く試すために scaffold で
% rails g scaffold products name:string price:integer

こんなマイグレーションになる。
class CreateProducts < ActiveRecord::Migration
  def self.up
    create_table :products do |t|
      t.string :name
      t.integer :price
 
      t.timestamps
    end
  end

  def self.down
    drop_table :products
  end
end
バリデーションを設定する
class Product < ActiveRecord::Base
  validates :price, :presence => true, :format => { :with => /\d+/ } # 数値だけ入力可
end
これで、ブラウザから http://localhost:3000/products にアクセスし、新規にデータを作成する。 その際、name : 'test', price : aaaa と不正な値を入力すると、当然 Invalid price にな・・・・・・らない!!! なんと price が 0 で登録が成功してしまう。 実際 rails c として下記を実行するとよくわかる。
irb(main):001:0> p = Product.new
=> #<Product id: nil, name: nil, price: nil, created_at: nil, updated_at: nil>
irb(main):002:0> p.price = 'aaaa'
=> "aaaa"
irb(main):003:0> puts p.price
0
=> nil
ちなみに 100aaa を price に代入すると price は 100 になる。数値じゃない文字は無視してるのか? フォームで数値の形式バリデーションを行いたいなら ActiveModel を使うしかないのか?
class ProductForm
  extend ActiveModel::Naming
  include ActiveModel::Conversion
  include ActiveModel::Validations

  attr_accessor :name, :price
  validates :price, :presence => true, :format => { :with => /\d+/ }
end
こんな感じのフォーム用モデルを作成すれば、price に aaaa を入力して検証するとエラーになる。

というわけで、ちゃんとした web アプリを作るなら、scaffold のように ActiveRecord のインスタンスを form_for に使うわけにはいかないだろう。
ActiveModel で便利になったんじゃなくて、ActiveModel が必須になっただけなんじゃないだろうか。

しばらく Rails に触ってなかったので、これが Rails3 からの挙動かどうかはわからない。
確か 2.0.2 とかのころは違ったと思うんだけど。
DB にはいろいろな型を利用できるが、Web から入力されたデータは全て文字列。
当然どこかで変換する必要はある。
そして「正規表現は文字列を対象にするものなので、数値列の検証に利用すべきでない」というのも理解できる。
どこまで自動的に内部で変換するのか?いつ変換するのか?あたりの問題なのかな?
ともかく文字列を数値に変換してくれるのはありがたいが、数値にならない文字を切り捨てるのはやりすぎだと思う。
これは ActiveRecord がより DB のレイヤに近づいたということなんだろうか。

[追記]
無視してるというか to_i の仕様ですね。

2010/10/21

私が Google Chrome を使う理由

今日 Google Chrome のバージョンアップがきていたので、早速アップデートしておいた。
せっかくだし、なぜ Google Chrome を使うのかをアウトプットしておきたいと思う。
開発用マシンには一応、IE, firefox, Opera, Safari, Chrome と入っているが、Chrome 以外は表示確認にしか使わない。(Windows 用 Safari での表示確認にどこまで意味があるかはわからないけど)

1.高速であること
相変わらずブラウザは描画や JavaScript の高速を競い続けている。
当然競い続け、進化し続けるのはありがたいことなんだけど、それなりのスペックのマシンを使っているので、もはや体感では「どれも速いよ」としか思わない。
なので、他のブラウザでもいいのだけど立ち上がりも高速なので速度のバランスはかなりいいんじゃないだろうか?
重要な点ではあるが、他より優れているというほどではないと思う。

2.拡張機能
firefox ほどの柔軟さはないにしろ、基本的には十分満足行く拡張機能がそろっていると思う。
むしろ、インストールして再起動する必要がない、バージョンアップで互換性云々言われたことがない、といった点の方がありがたい。
次期 Opera ではとうとう拡張機能が加わるそうだけど、どれほどのものになるんだろうか?

3.同期
個人的にはコレが一番大きい。
ブックマークの同期など、ツールを使えばできたが Chrome では設定も自動入力も拡張機能の同期までとれる。そして何もツールを使う必要がない。
つまり、Chrome をインストールしたその時からほとんどの設定を引き継ぐことができる。
どっかで更新すれば、別の場所でもすぐに反映される。
自宅マシンと職場のマシンの同期はもちろんのこと、開発環境を仮想環境で作る自分にはとてもありがたい。
新しいマシンに設定ファイルを移行する必要がないのも楽でイイ。
ただ、拡張機能の設定も同期取ってくれないかなぁ。

4.シークレットモード
別にやましい物を見るだけがシークレットモードの使い道じゃないわけで。
拡張機能などをとっぱらった状態や、クッキーなどのプロファイルデータを削除した状態を簡単に作り出せるのは便利。
ログイン時の表示と未ログイン時の表示を同時に比較できるので、アプリの表示テスト時などに便利である。

5.シンプルなUI
ウィンドウタイトル部分とタブ表示の合体、検索ボックスとナビゲーションの合体。
通常状態で Chrome より表示領域を広く確保するのはかなり厳しいだろうね。
あと、拡張機能も勝手に動いてるか、1,2カ所にアイコンが出てくるだけなので統一感があっていい。
firefox ではあっちこっちにかってに配置されたりする。(はてブは右下、colorzilla は左下とか)

ま、それと GMail に Google Reader に Google Calendar と Google のサービスに依存しまくりなので、Chrome が一番それらサービスを使いやすいだろうということもある。
その他のブラウザが差別的・意図的にじゃなくても Google のサービスが使いにくくなるようなバージョンアップをしてしまうことがあっても、Chrome だけはそれはないだろうからね。

firefox や Chrome が頑張ってくれたおかげで、Web はイイ方向に転換しどんどん使いやすくなった。ありがたいことだ。

しかし、今回のバージョンアップで何が変わったんだろうか?
正直メジャーバージョンアップにしては変更がマイナーすぎないだろうか?

2010/10/15

これは迷う・・・

au秋冬モデル情報判明。写真も流出。IS04、IS05、IS06の発売は12月下旬以降。Androidタブレットもあり

これは本当だろうか?
IS03 が Android 2.1 なのに一ヶ月後に 2.2 のを出すかなぁ?
本当だとしたら IS03 の強みは一ヶ月早く手に入ることか?
カメラと防水の IS04 か、最新OSの IS05 か非常に迷う。
18日が楽しみだ。

Rails3でのロケールファイル

第16回: Devise によるユーザー認証機能の日本語化(2) - Ruby on Rails 3.0 日記 - Ruby on Rails with OIAX

タイトルこそ Devise の日本語化っぽいけど、Rails3 でどのようなポリシーでロケールを設定するかが記載されている。
参考になる。

2010/10/05

Google Chrome の GMail で等幅フォント

Thunderbird もいいけど、ブラウザの使う GMail でかなり十分な気がしてきた。
キーボードショートカットなどを使えばメーラより高速に使えるし、プラットフォームを選ばない。
いや、今更だけど。
でもメールは等幅フォントで見たい。
というわけで、Chrome の GMail で等幅フォントで見ることができる方法を調べた。
だいたい以下の3つかな?
Gmailを固定ピッチの等幅フォント(MonoSpace)で表示する方法(2009年03月版) [C!]
Gmailのフォントを細かく設定できるGreasemonkey「Gmail Fonts Manager」 [C!]
Gmail Fixed Width Text - Google Chrome 拡張機能ギャラリー
どれも結局はただの JavaScript だけど、結局最後のヤツにした。
設定できるという上では Gmail Fonts Manager の方がいいかもしれないけど、拡張機能ギャラリーに登録されている専用感がよかっただけ(笑)文字のサイズとかブラウザの設定でどうとでもなるし。

いやーしかし、メールが等幅じゃないってだけでいらっとくるもんだねぇ。
心が狭い

2010/10/04

Rails3で汎用検索機能を作る

業務アプリでは CRUD 以外で必ずと言ってもいいほど実装する機能があります。検索です。
検索機能って、たいていはページの上部に検索フィールドがあって、入力して検索ボタンを押すとページの下部に検索結果が表示される。
こんなのばっかりです。
それを毎回ページ全体をゴリゴリ作るって疲れませんか?
Rails で scaffold すると index は一覧表示になりますが、ここが検索になる画面のほうが圧倒的に多くないですか?

というわけで、汎用検索機能(というか簡易フレームワーク)を作ろうってのが今回の趣旨です。
大体、検索機能で検索ごとに違うのは大まかには下記の三つでしょう。
・検索フィールド
・検索ロジック
・結果表示(一行分)
極力この三点に注力できるようにしたい。
具体的には、検索条件は汎用的なコンテナクラスに格納する。
コンテナクラスは、検索条件だけを格納できるように form_for で利用できるようにしたい。
そうすれば、他にどんなパラメータがあっても params[:form] みたいにして検索条件だけをいつでも取得できる。
検索の通信は AJAX で単一のコントローラに飛ばすことにする。
そして検索を実行するロジッククラスだけ実装すればいいようにする。
各コントローラに検索アクションを作るとなると、たとえ共通化していてもどうしてもお約束的なものを書かざるをえない。
何検索を行うかは、クエリ文字列にモードとして埋め込むことにする。

まず、汎用検索条件格納クラスをつくる。
検索条件というカテゴリのパラメータだけ格納したいので、form_for で使えるモデルクラスを作る。
# -*- coding: utf-8 -*-
# 検索パラメータを格納する汎用クラス
class Q
  # model_name を実装してくれる。これがないと form_for できない
  extend ActiveModel::Naming
  # to_key などを実装してくれる。これがないと form_for できない
  include ActiveModel::Conversion

  # オブジェクトを初期化する
  # 内部にパラメータ用のハッシュを持つ
  def initialize(params = {})
    @params = params || {}
  end

  # 指定のキーで格納している値を取得する
  # キーは文字列でもシンボルでもよいが、内部ではシンボルで保持している
  def [](key)
    @params[key.to_sym]
  end

  # 指定のキーで指定の値を格納する
  # キーは文字列でもシンボルでもよいが、内部ではシンボルで保持している
  def []=(key, value)
    @params[key.to_sym] = value
  end

  # ActiveModel::Conversion を include するために必要
  def persisted?
    false
  end

  # メソッドが存在しなくてもいいようにオーバーライドする
  def respond_to?(name, priv=false)
    true
  end

  # メソッドでアクセスされた場合、ハッシュから値を取得する
  # 代入メソッドの場合、ハッシュに値を格納する
  def method_missing(name, *args)
    if name.to_s.ends_with?('=')
      @params[name.to_s.chomp('=').to_sym] = *args[0]
    else
      @params[name.to_sym]
    end
  end

  # to_s をオーバーライド
  # 内部に格納しているパラメータをハッシュ形式で出力する
  def to_s
    @params.to_s
  end

  # 内部に格納しているパラメータをクエリ文字列形式で出力する
  # ただし、内部のハッシュが空の場合は空文字を出力する
  def to_query_string
    kvs = @params.map do |k, v|
      "q[#{URI.encode(k.to_s)}]=#{URI.encode(v.to_s)}"
    end
    if @params.length == 0
      ''
    else
      '?' + kvs.join('&')
    end
  end

  # 指定のキーの値をキーごと削除する
  def delete!(key)
    @params.delete key.to_sym
  end
end
なんでこんなクラスを作ったかというと、毎度毎度検索フォームモデルクラスを作るのが嫌だったから。
もちろんWEB+DB PRESS vol.58で作られているような、検索フォームクラスを作ってもいいんですが、それだと検索機能の数だけモデルを作らなければならない。
また、フォームクラスの属性定義と、form_for でのフィールド定義で二重の定義を行わなければならない。
検索条件はフォーム主体でいいと思うので、こんなクラスを作った。
クラス名はクエリ文字列にしたときにできるだけ短くしたかったので、一文字で。q なら Google などでも使われているし、クエリということが解りやすいだろうから。
ほんとは delete メソッドを作りたくなかった。
というのもどのようなパラメータ名で値を飛ばしても大丈夫なように極力特別なメソッドは実装したくなかった。
しかし、ページングやソート機能を実装しているとどうしても削除が必要になった。
! 付けたパラメータはまず使わないだろうと思ったのと、キー削除してるし破壊的だよなということで ! をつけた。

次は検索ロジッククラスのジェネレータを作ろう。
% rails g generator finder

lib/generators/finder/finder_generator.rb
# -*- coding: utf-8 -*-
# 検索ロジッククラスを作るジェネレータクラス
class FinderGenerator < Rails::Generators::NamedBase
  source_root File.expand_path('../templates', __FILE__)

  # 各種ファイルを作る
  def create_finder_files
    template 'finder.rb', File.join('app/finders', class_path, "#{file_name}_finder.rb")
    template 'form.html.erb', File.join("app/views/search/#{file_name}", class_path, '_form.html.erb')
    template 'result.html.erb', File.join("app/views/search/#{file_name}", class_path, '_result.html.erb')
  end
end
lib/generators/finder/templates/finder.rb
# -*- coding: utf-8 -*-
class <%= class_name %>Finder
  # 検索を実行する
  def find(q)
    return nil unless q
  end
end
lib/generators/finder/templates/form.rb
# からっぽ
lib/generators/finder/templates/result.rb
<table id="<%%= q[:mode] %>_result" class="grid">
  <thead>
    <tr>
      <th></th>
      <th></th>
      <th></th>
    </tr>
  </thead>
  <tbody>
  <%% result.each do |record| %>
    <tr>
      <td></td>
      <td></td>
      <td></td>
    </tr>
  <%% end %>
  </tbody>
</table>
正直、result.rb も適当である。どうせデータによって変わるので。 これでこんな感じで、そのモード専用の検索ロジッククラスと検索条件フォームと検索結果表示パーシャルファイルができるようになる。
% rails g finder mode
次は検索コントローラを作ろう。
class SearchController < ApplicationController
  def index
    @q = Q.new(params[:q])
    finder = get_finder(@q.mode)
    @search_result = finder.find(@q)
  end
end
application_controller.rb に get_finder メソッドを実装する。
protected
  def get_finder(mode)
    eval("#{mode.classify}Finder.new")
  end
次は app/views/search/index.rjs を作る。 検索結果の表示は search_result という div の中を書き換えるだけだ。
if @search_result.nil? || @search_result.length == 0
  page[:search_result].replace_html :partial => 'search/not_found'
else
  page[:search_result].replace_html :partial => "search/#{@q[:mode]}/result", :locals => { :q => @q, :result => @search_result }
end
app/views/search/_not_found.html.erb はとりあえず「見つかりませんでした」とだけいれておけばいい。 jQuery を使っている場合はこれだとうまくいかなくて、別途対応を入れる必要がある。 それはまた別のエントリで。 検索画面自体は、ほとんどが上部に検索フィールド、下部に結果表示なのでその枠組みも用意しておいたほうが楽かもしれない。 app/views/search/_base.html.erb を作ろう。
<!-- 検索フォーム -->
<%= form_for @q, :url => '/search', :html => { :method => :get }, :remote => true do |f| %>
  <%= f.hidden_field :mode %>
  <%= render "search/#{@q[:mode]}/form", :f => f %>
  <%= f.submit '検索' %>
<% end %>

<!-- 検索結果 -->
<div id="search_result">
</div>
おっと、ルーティングの設定を忘れていたね。/search だけでアクセスできるようにしたい。
match '/search' => 'search#index'
おおむねこんな感じでokだろう。 たとえばユーザ検索画面を作りたい場合。 app/controllers/users_controller.rb
def index
  @q = params[:q] ? Q.new(params[:q]) : Q.new(:mode => 'user')
end
app/views/users/index.html/erb
<%= render :partial => 'search/base', :mode => 'user' %>
これだけで枠組みはオーケー、あとは rails g finder user で作成されたファイルだけ実装すればいい。

今後はこれに、ソート機能とページングの機能を共通化として追加してみるよ。
正直スマートな方法じゃないかもしれないし、RESTful じゃないかもしれないけど、検索を集中化するって結構便利だと思う。
のちのち様々な直接ダウンロード機能を実装する場合などには管理しやすくなるかも。

Ruby で UUID

Ruby で UUID を使うには uuidtools という gem を使えばよいらしい。
uuid という gem もあるけど、uuidtools の方がメンテされてるとか。
ちなみに使い方
require 'uuidtools'
UUIDTools::UUID.random_create.to_s
=>"7983d06a-f0b9-45a1-aa81-3b2024ec79a7"

2010/10/02

Ubuntu の Chrome で css が反映されない

掲題通り、少し前から Ubuntu での Chrome で css が読み込まれておらず、あらゆるサイトがだっさい見栄えになってた。
シークレットモードにすると、正しく表示されるのでなんらかのプラグインが悪さをしているんだろうと思い、調べてみるとどうやら AdThwart が悪さをしているようだ。
ただ、どの設定を見ても Windows の Chrome と同じ設定なのだ。
もちろん Windows 上の Chrome では全く問題なくサイトが表示されるので、原因不明である。
仕方ないので、もういいやと AdThwart をアンインストールすることにした。
対して恩恵にあずかっている実感もなかったし。