是非に及ばず

プログラミングに関する話題などを書いていきます(主に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