Date: 2009-12-16
Tags: ruby-on-rails

Rails + SQLServer で data型を扱うとハマる話

はまります。

Rails + SQLServer で data型を使おうとすると、とってもはまります。

あ、 Ruby on Rails 2.2.2 + sqlserever_adapter-1.0.0 での話です。

まず、DBを以下のように用意するわけですよ:

create_table "contacts" do |t|
  t.string   :name
  t.date     :birthday
  t.timestamps
end

で、フォームを用意するわけですよ:

<%= f.text_field :name %>
<%= f.date_select :birthday, :use_month_numbers => true %>

それを以下のようなコントローラで受けて処理するわけですよ:

contact = Contact.new
contact.attributes = params[:contact]

ところが、このフォームで実際に 1950/12/25 と言う日付をPOSTするとエラーになるわけですよ。

原因は、上記で作られる select box は年・月・日の3つのセレクトボックスとして表示されて、POSTすると {'birthday(1i)'=>'1950', 'birthday(2i)'=>'12', 'birthday(3i)'=>'25',.. というようなデータがサーバーに送られるわけですが、 contact.attributes = params[:contact] としている箇所で Rails が色々やってくれようとして、そこで SQLServer では Date 型が無いために、一見原因が分からないややこしいエラーが発生します。

ということで、コードを追いかけてみました。

SQLServer用の sqlserver_adapter を使うと、dateを使う!ってdb/migrationsに書いても、DBに用意される型は datetime になります。型の対応は以下のような感じです:

class SQLServerAdapter < AbstractAdapter

  def native_database_types
    {
      :primary_key => "int NOT NULL IDENTITY(1, 1) PRIMARY KEY",
      :string      => { :name => "varchar", :limit => 255  },
      :text        => { :name => "text" },
      :integer     => { :name => "int" },
      :float       => { :name => "float", :limit => 8 },
      :decimal     => { :name => "decimal" },
      :datetime    => { :name => "datetime" },
      :timestamp   => { :name => "datetime" },
      :time        => { :name => "datetime" },
      :date        => { :name => "datetime" },
      :binary      => { :name => "image"},
      :boolean     => { :name => "bit"}
    }
  end

で、ActiveRecordの仕組みによって、DBの型から自動的にRails内でのデータ型が以下のように決まります:

module ActiveRecord
  module ConnectionAdapters #:nodoc:
    class Column
      def klass
        case type
          when :integer       then Fixnum
          when :float         then Float
          when :decimal       then BigDecimal
          when :datetime      then Time
          when :date          then Date
          when :timestamp     then Time
          when :time          then Time
          when :text, :string then String
          when :binary        then String
          when :boolean       then Object
        end
      end

そうすると、Date型のつもりの値がdatetimeを経由して結果としてTime型で扱われてしまうという問題が発生します(多分sqlserver_adapter の問題)。

このため、1970年より前の年数を扱いたいからDate型を指定したはずなのに、実際はRails内部のデータ型がTime型なので、1950年でPOSTするとTime型で扱える範囲を超えていて、エラーになります。

エラー起こしてる直接のコードの箇所は ActiveRecord::Base#execute_callstack_for_multiparameter_attributes でした。

この問題を回避するため、色々調べたら こういう手 は見つけたんですが、リリース直前なので今回は避けて、結局データをstringで持つことにしてContactモデルに以下の実装を加えてごまかしました:

class DateColumn
  def self.klass
    Date
  end
end

class Contact < ActiveRecord::Base

  def column_for_attribute(name)
    if name.to_s == 'birthday1'
      ::DateColumn
    else
      self.class.columns_hash[name.to_s]
    end
  end

  attr_accessor :birthday1
  def birthday1= value
    @birthday1 = value
    self.birthday = value.strftime('%Y/%m/%d') if value
  end
  def birthday1
    Date.new(*self.birthday.split('/').collect{|n|n.to_i}) rescue @birthday1
  end
end

もっと良い手をご存じの方はご連絡下さい!><