是非に及ばず

プログラミングに関する話題などを書いていきます(主にRailsやAndroidアプリ開発について)

Rails 2.3.2のvalidates_uniqueness_ofの不具合について

Rails 2.3.2がリリースされたので、さっそく2.2.2からアップデートしてみた。
特に問題ないなと思っていたら、validates_uniqueness_ofでエラーが発生したので対処方法をメモ。

どんなエラーなのか

Railsには重複をチェックするためのバリデーションとしてvalidates_uniqueness_ofが用意されている。
これを日本語のようにマルチバイトな文字列を値とするカラムを指定すると、
SQLの部分でエラーとなるケースがある。

エラー再現コード

-- app/models/profile.rb --
class Profile < ActiveRecord::Base
  validates_uniqueness_of :nickname
end

profile = Profile.new(:nickname => 'サンプルユーザ')
profile.valid?
ここでエラー
=> : SELECT "profiles".id FROM "profiles" WHERE ("profiles"."nickname" = E'サンプルユー・')  LIMIT 1

原因

明らかにコレだろ、jk。

-- $RUBY_HOME/lib/ruby/gems/1.8/gems/activerecord-2.3.2/lib/active_record/validations.rb --
def validates_uniqueness_of(*attr_names)
・・・
value = column.limit ? value.to_s[0, column.limit] : value.to_s
・・・
end

どうしてこのようになってしまうのかは、以下の例を見てもらえれば分かるだろう。
単純にvalue[0, limit]としてしまうと、文字列として正しく扱えない。

irb(main):001:0> $KCODE = 'u'
irb(main):002:0> require 'jcode'
irb(main):003:0> value = 'サンプルユーザ'
irb(main):004:0> limit = 20
irb(main):005:0> value = limit ? value.to_s[0, limit] : value.to_s
=> "サンプルユー・    ← 文字化けするにしても、"サンプルユー・"とならないといけない
irb(main):006:0> value = 'サンプルユーザ'
irb(main):007:0> value = limit ? value.to_s.split(//)[0, limit].join : value.to_s
=> "サンプルユーザ"

対処方法

validates_uniqueness_ofの内容をまるまるコピーして、lib/validates_uniqueness_of.rbとかに置く。
そしたら、config/environments.rbの最後あたりにrequire 'validates_uniqueness_of.rb'しておく。
で、問題の箇所をこう直す。
(ちなみにvalidates_uniqueness_offの定義は$RUBY_HOME/lib/ruby/gems/1.8/gems/activerecord-2.3.2/lib/active_record/validations.rbにある)

-- #{RAILS_ROOT}/lib/validates_uniqueness_of.rb --
module ActiveRecord
  module Validations
    module ClassMethods

      def validates_uniqueness_of(*attr_names)
          ・・・省略・・・
          if value.nil?
            comparison_operator = "IS ?"
          elsif column.text?
            comparison_operator = "#{connection.case_sensitive_equality_operator} ?"
            #value = column.limit ? value.to_s[0, column.limit] : value.to_s
            value = column.limit ? value.to_s.split(//)[0, column.limit].join : value.to_s
          else
            comparison_operator = "= ?"
          end
         ・・・省略・・・
      end

    end # EOF ClassMethods
  end # EOF Validations
end  # EOF ActiveRecord

Rails2.2でPassengerのRailsBaseURIが動かない問題の解決方法

Rails2.2.2をPassengerのRailsBaseURIで動かそうと思ったら、
動かなくて困ったので解決方法をメモしておく。
config/environment.rbに以下の内容を記述したら解決した。
(自分は開発環境ではmongrelで動かしてるので、productionモード時のみ対策方法を入れている)

/appで動かす場合の例

-- config/environment.rb --
Rails::Initializer.run do |config|
  if ENV['RAILS_ENV'] == 'production'
    config.action_controller.relative_url_root = '/app
  end

rspecでrake db:createの対象がtestのみになる件

Rails2.2.2とrspec1.1.12(rspec-rails1.1.12)で確認。
通常、こうするとconfig/database.ymlのdevelopmentを参照する。

$ export RAILS_ENV=development
$ rake db:create

しかし、rspecをconfig.gemで利用するようにした場合、
同様のコマンドでconfig/database.ymlのtestのみを参照するようになってしまう。
まぁ、rake db:migrateはちゃんとdevelopmentを見てくれるので影響は小さいけど。
微妙だよなぁ、これ。

[2009/05/16 追記]
やはり、rspecまたはrspec_railsが悪さをしているように思える。
productionモードで動かしている場合でも、rspec_railsを使用していると、
RAILS_ENV == test、ENV['RAILS_ENV'] == 'procution'
となってしまう。
ググっても同じ現象で困っている人を見かけないのだが、本当に大丈夫なんだろうか?

とりあえず、開発環境(development)で、このような状況になったら
db:create、db:dropを以下のように書き換えれば良い。
{RAILS_ROOT}/lib/tasks/databases.rakeに以下を記述する。

namespace :db do
  desc 'Create the database defined in config/database.yml for the current RAILS_ENV'
  task :create => :environment do
    ENV['RAILS_ENV'] ||= 'development'
    puts "target=#{ENV['RAILS_ENV']}"
    create_database(ActiveRecord::Base.configurations[ENV['RAILS_ENV']])
  end

  desc 'Drops the database for the current RAILS_ENV'
  task :drop => :environment do
    ENV['RAILS_ENV'] ||= 'development'
    puts "target=#{ENV['RAILS_ENV']}"
    config = ActiveRecord::Base.configurations[ENV['RAILS_ENV']]
    begin
      drop_database(config)
    rescue Exception => e
      puts "Couldn't drop #{config['database']} : #{e.inspect}"
    end
  end
end

やっている事は本来のdb:create、db:dropがRAILS_ENVを参照しているところを

  • ENV['RAILS_ENV']を参照にするようにした
  • ENV['RAILS_EMV']が未定義の場合は、developmentとして扱うようにする

という感じにしただけ。これで当面の問題は凌げるだろう。

productionモードでもrspec_railsを使っているとRAILS_ENVはtestになってしまうので、RAILS_ENVの値で動作を変えたい場合は、ENV['RAILS_ENV']を使用するほうが良いだろう。

Railsのi18nを利用して完全なエラーメッセージを取得する

Rails2.2からi18nを使って各種メッセージを日本語化出来るようになったが、
普通にerror_message_onを使うと・・・

error_message_on(:user, :name)
=> "を入力してください。"

これでは困るので、第3引数に項目名を渡す必要がある。
例えば、@user.nameが未入力の場合、以下のようにすると完全なエラーメッセージを取得できる。

#第3引数に"User.human_attribute_name('name')"を渡す
error_message_on(:user, :name, User.human_attribute_name('name'))
=> "名前を入力してください。"

毎回これをやるのは大変なので、ヘルパーにして使ってます。

app/helpers/application_helper.rb

# フィールドのエラーメッセージを1つ取り出す
#
#== 使用例
# <%= full_error_message_on(:user, :name) %>
#
#+object+:: オブジェクト名
#+method+:: メソッド名
#+css_class+:: エラーメッセージを囲むdivタグに指定するクラス名
#==戻り値
# エラーメッセージ。エラーがない場合はnil
module ApplicationHelper
  def full_error_message_on(object, method, css_class = 'formError')
    obj = instance_variable_get("@#{object}")
    prepend_text = obj.class.human_attribute_name(method.to_s)
    errors = obj.errors[method.to_sym]
    if errors.is_a?(Array) && errors.size > 0
      content_tag("div", "#{prepend_text}#{errors.first}", :class => css_class)
    end
  end
end

成功しないコミュニティサイトのたった1つの理由

セカンドライフはなぜ成功しなかったのかを読んで、 なるほど!と思ったので自分用にまとめておく。

ここでいうコミュニティサイトとは、ユーザがサイトにログインして なんらかの行動を取るというスタイルのサイトの事。

ユーザに継続してログインさせ続けるための動機付けが圧倒的に不足している

アバターであったり、なんらかのゲームを遊ぶ事で得られるポイントによるランキングの順位など、
何かユーザにとってメリットがなければ、ユーザはログインなどという面倒な事はしてくれない。
ユーザにとってのメリットとは、「自己顕示欲に対するリターン」である。
アバター、ランキング順位などはいずれも自己顕示欲を満たすものだ。
サービス開始当初はユーザがほとんどいないのは当然の事だが、
ユーザが少ないからユーザが集まらないという悪循環を断ち切れない原因はほとんどの場合、この点にある。

言われてみると、確かに当たり前の事なんだけど、新しいサイトの企画段階で こうした要素を入れる事ができないと
ダメなんだよね。予算の問題とか様々な原因で、それが出来ない場合もあるかもしれないけど、
そんな事だから成功しないんだよなぁ。

SliceHostを借りたら最初にやるべき事

経緯

会社の後輩と共同で携帯向けのSNSサイトを作ろうという話になったので、ドメインとサーバを用意する事にした。
仕事ではRailsを使っているし、Rails(Ruby)が気に入っているので、

  • CentOS5が使える
  • root権がもらえる
  • そこそこ安い

という条件で検討した結果、SliceHostに決めた。
Railsを使う関係上、メモリには余裕が欲しいし、ディスクも10GBより多いほうがいい。
ということで、メモリ512MB、HDD20GBの月額38ドルのプランを申し込んだ。
あと、ドメインはjpドメインにした。jpドメインって高い印象があったけど、3390円で済んだ。安い!
クレジットカードでの申し込みで、申し込んでから数分で使えるようになった。

SliceHostの時刻設定

ここから本題。
SliceHostは海外にあるサーバなので、当然時刻は海外向けに設定されている。
日本向けサービスを行うときにこれでは困る。
そこで、最初に時刻設定を日本時間に合わせるべきだと思う。

/etc/localtimeの変更
# cp  -p /usr/share/zoneinfo/Japan /etc/localtime
ntpのインストールと設定

まず、ntpをyumでインストールして初期設定ファイルをバックアップする

# yum install ntp
# cp -p /etc/ntp.conf /etc/ntp.conf.default

次に/etc/ntp.confをviとかで以下のように編集する。

trict default ignore
restrict 127.0.0.1

server -4 ntp.nict.jp iburst
server -4 ntp.nict.jp iburst
server -4 ntp.nict.jp iburst
restrict 133.243.238.0 mask 255.255.255.0

server 127.127.1.0      # local clock
fudge 127.127.1.0 stratum 10

driftfile /var/lib/ntp/drift
keys /etc/ntp/keys

次にサービス起動。ついでに自動で起動するようする。

# /etc/init.d/ntpd start
# chkconfig ntpd on
# chkconfig --list |grep ntpd
ntpd            0:off   1:off   2:on    3:on    4:on    5:on    6:off

とりあえず、ここまで。
iptablesの設定とかいろいろあるけど、気が向いたらまた書くかも・・・

Rails(TMail)で携帯のメールアドレスをパースするとエラーになる件の対応方法について

Ruby(Rails)で携帯の写メールを受け取るプログラムを作っていたら、ドット(.)が連続するメールアドレスでエラーになった。本来はRFC違反だから使ってはいけないんだけど、DoCoMoとかauでは使えてしまうので対応する必要がある。
対応方法をまとめてみた。
※ただし、この方法で対応できるのは"example...@ezweb.ne.jp"などの先頭がドットで始まらないメールアドレスのみ。先頭がドットで始まっているメールアドレスの場合は、対応出来ていない事に注意。それでもこの方法は実行する価値があると思う。

何が問題なのか

まず、これで何が問題になるのかについて軽く説明する。
以下のようにドットが3回連続するなどRFCに違反しているメールアドレスをTMailで処理しようとすると
メールアドレスがnilになってしまう。FromだけでなくToなどメールアドレスであればおそらく全て同様。
TMail(ActionMailer)を使ってメールを送信したり、メールを受信してFromアドレスへメールを返信する
などの処理が出来ない。

$ irb
irb> require 'rubygems'
irb> require 'action_mailer'
irb> mail = TMail::Mail.new
irb> mail.from = 'example...@exampl.com'
irb> mail.from
=> nil ← 設定したメールアドレスがnilになってしまう!

対応方法は?

対応方法は2通りある。

  • パターン1
    • 既存のparser.rbを新しいparser.rbで上書きする
  • パターン2
    • アプリケーションのディレクトリ内にparser.rbを別ファイル名で置く(tmail_parser.rbなど)
    • TMailを使用するソース内でParserを新しいparser.rbと置き換える

ここでは、既存の環境を壊したくないのでパターン2の方法を使う。

1. Raccをインストールする

parser.yをraccコマンドでコンパイル?してTMailが使用しているparser.rbを作成する事になるため、raccをダウンロードしてインストールしておく。

# wget http://i.loveruby.net/archive/racc/racc-1.4.5-all.tar.gz
# tar xvzf racc-1.4.5-all.tar.gz
# cd racc-1.4.5-all
# ruby setup.rb
2. TMailをrubygemsのサイトからダウンロードする

"rubygemsのサイト"から自分が使用しているバージョンのTMailをDLする。Railsを使っている場合、TMailを個別にgem installしていないはずなので、ActionMailerのディレクトリ内を確認する。

Rails2.1.0しか入っていない環境の場合
$RUBY_HOME/lib/ruby/gems/1.8/gems/actionmailer-2.1.0/lib/action_mailer/vendor/tmail-1.2.3
3. perser.yを編集してドットが連続しても対応できるようにする

ダウンロードしてきたtmail-{version}.tgzを展開して、tmail-{version}/lib/tmail/parser.yを編集する。
以下はtmail-1.2.3のparser.yと編集後のファイルのdiff。
他のバージョンでも同様に編集すれば良いはず。

-        | local_head '.' { val[0].push ''; val[0] }
+        | local_head dots { (val[1] + 1).times { val[0].push '' }; val[0] }

-   dots      : '.'     { 0 }
-             | '.' '.' { 1 }
+   dot_repeat : '.'
+              | dot_repeat '.' { val[0] + val[1] }
+
+   dots      : dot_repeat     { val[0].size - 1 }
4. Raccでparser.yからparser.rbを作成する
racc ./parser.y -E -o ./tmail_parser.rb 
5. 作成したtmail_parser.rbを{RAILS_ROOT}/lib/tmail_parser.rbに置く
6. TMailを使用するソース内でparserをtmail_parserに置き換える
TMail.instance_eval{remove_const 'Parser'}
require 'tmail_parser'
mail = TMail::Mail.parse(mail_data)
※mail_dataは受信したメールの内容