composite_primary_keys

昨日は大ボケしてURLを間違えてた。トップは http://wiki.rubyonrails.com/ じゃなくて http://wiki.rubyonrails.org/ ですね。

寝不足だったせいか composite_primary_keys を使ってfindできるわーい、とか思って眠りについたため、朝になってCRUDのRead以外はどうなのよ?と気づく。テストケースを書いてみると・・・うわーん、だめだー。
生成されるINSERTやUPDATE、DELETEのWHERE句が文字列のプライマリキーを0と比較しちゃったりしてる。数値のキーならオッケーっぽいけど。
composite_primary_keysのサイトにもsaveのsの時も書いてないしな、まだ対応してないのかな。残念。

composite_primary_keys ちゃんとテストは通る

どうにも上手く行かなさ過ぎる。こんなにヘボイライブラリなら、http://groups.google.com/group/compositekeys のフォーラムに "Fix to update_without_callbacks" とかエントリがあるはずがない、と確信して、自分が何か間違っているとしか思えず、とりあえず

rake build_mysql_databases
rake test_mysql

で試してみたら、49 targets, 172 assertions, 3 failures, 0 errors と出ました。failureは3つともDummyTestってやつで起きているので、多分無視していいのかなと判断。そんなわけで、やっぱり自分がおかしいことを確信しました。お騒がせして申し訳ないです。再チャレンジします。

composite_primary_keysは悪くないっぽい

なんで文字列のはずのカラムが、SQLのWHERE文で使われるときには、0という数値になってしまうのかを追っかけてましたが、composite_primary_keysの中ではどうもそんなことやってなさ気。
で初心に返って、実際のオブジェクトがどうなっているのかをチェックするために、モデルのcolumnsをinspectしてみたところ、PKのキーのtypeがなんと:integerで@number=true。そりゃ、文字列も0になるわってわけで、マイグレーションをチェック。でもおかしくなーい!
んじゃとりあえず、SQLで直接PKの型を変えてvarcharに。そしたら大体パスしました*1


と言うわけで、PKにvarcharダメなんじゃねーの疑惑は(おそらく)晴れまして、次は:type => :stringというマイグレーションのカラム定義に対してintのカラムを生成するSQLを吐きやがったのはどいつだ、という犯人探しになってきました。

*1:destroyだけは何故か変なwhere文が吐かれて条件に引っ掛かってくれない

犯人はMysqlAdapter

というわけで何でそんな理不尽なことが起きるのかを突き止めました。ActiveRecord::ConnectionAdapters::MysqlAdapterのnative_database_typesメソッドの中身。

      def native_database_types #:nodoc
        {
          :primary_key => "int(11) DEFAULT NULL auto_increment PRIMARY KEY",
          :string      => { :name => "varchar", :limit => 255 },
          :text        => { :name => "text" },
          :integer     => { :name => "int", :limit => 11 },
          :float       => { :name => "float" },
          :datetime    => { :name => "datetime" },
          :timestamp   => { :name => "datetime" },
          :time        => { :name => "time" },
          :date        => { :name => "date" },
          :binary      => { :name => "blob" },
          :boolean     => { :name => "tinyint", :limit => 1 }
        }
      end

という風になっているので、primary_keyに指定されたカラムは嫌でもint(11)になってしまいます。

ただし、これは、set_primary_key/set_primary_keysを使って複合キーでないキーを指定した場合の話です。varcharのカラムひとつをPKに使用とした場合とか。複合キーの場合は、まだ未確認だけど、昨日紹介したcomposite_migrationsが解決してくれちゃってるっぽいっす。

で、とりあえず回避する方法として、composite_migrationsプラグインのcomposite_migrations.rbを以下のように変更してみたら期待していたCREATE TABLE文が作成されるようになりました。

ActiveRecord::ConnectionAdapters::ColumnDefinition.class_eval <<-'EOF'
  def to_sql
    if name.is_a? Array
      column_sql = "PRIMARY KEY (#{name.join(',')})"
    elsif type == :primary_key
      column_sql = "PRIMARY KEY (#{name})"
    else
      column_sql = "#{base.quote_column_name(name)} #{type_to_sql(type.to_sym, limit)}"
      add_column_options!(column_sql, :null => null, :default => default)
    end
    column_sql
  end
EOF

ActiveRecord::ConnectionAdapters::ColumnDefinition.send(:alias_method, :to_s, :to_sql)

ActiveRecord::ConnectionAdapters::SchemaStatements.class_eval do
    def create_table_yield_before_pk(name, options = {})
      table_definition = ActiveRecord::ConnectionAdapters::TableDefinition.new(self)

      yield table_definition

      table_definition.primary_key(options[:primary_key] || "id") unless options[:id] == false
      
      if options[:force]
        drop_table(name) rescue nil
      end

      create_sql = "CREATE#{' TEMPORARY' if options[:temporary]} TABLE "
      create_sql << "#{name} ("
      create_sql << table_definition.to_sql
      create_sql << ") #{options[:options]}"
      execute create_sql
    end
end
ActiveRecord::ConnectionAdapters::SchemaStatements.send(:alias_method, :create_table_yield_after_pk, :create_table)
ActiveRecord::ConnectionAdapters::SchemaStatements.send(:alias_method, :create_table, :create_table_yield_before_pk)


ActiveRecord::ConnectionAdapters::TableDefinition.class_eval do
  
  def primary_key_for_string(name)
    col = self[name] 
    return primary_key_int(name) if col.nil? || name.is_a?(Array) || (col.type != :string)
    column = ActiveRecord::ConnectionAdapters::ColumnDefinition.new(@base, name, :primary_key)
    @columns << column unless @columns.include? column
    self
  end
  
end

ActiveRecord::ConnectionAdapters::TableDefinition.send(:alias_method, :primary_key_int, :primary_key)
ActiveRecord::ConnectionAdapters::TableDefinition.send(:alias_method, :primary_key, :primary_key_for_string)

コードが汚いけど、とりあえずこれでマイグレーションはOK。

でも何かおかしいんですよ。varchar1つだけのPKのモデルが。

結局、真犯人はだれ?

結局色々試してみて分かったことは、PKが1つのvarcharのカラムの場合だけどうしてもうまく動かないってことでした。
composite_primary_keysは複合キーを扱うためのものなんで、主キーが2つ以上のものを扱うためのものなんで、その守備範囲はきっちり守ってます。Railsはオートナンバー型のPKを期待している。ってことは、varchar一つのPKは誰の守備範囲なのさ?

うさうさ【右脳左脳占い】

http://www.nimaigai.com/howto.html
最初「ささ男」かと思って解説を読んでたら、何か結構当たっているけど絵がやな感じだったんでもう一回見直してみたら少しマシな絵の方でした。あー良かった。