Hatena::ブログ(Diary)

akimatter このページをアンテナに追加 RSSフィード

2006-10-25

composite_primary_keys

| 11:13 |  composite_primary_keysを含むブックマーク  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 ちゃんとテストは通る

| 16:55 |  composite_primary_keys ちゃんとテストは通るを含むブックマーク  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ではvarcharがダメなの?

| 17:35 |  composite_primary_keysではvarcharがダメなの?を含むブックマーク  composite_primary_keysではvarcharがダメなの?のブックマークコメント

とりあえずテストケースをざーっと読んでみた。set_primary_keysを使っているのは以下の四つ。クラスDDLをセットで挙げます。

class ProductTariff < ActiveRecord::Base
	set_primary_keys :product_id, :tariff_id, :tariff_start_date
	belongs_to :product, :foreign_key => :product_id
	belongs_to :tariff,  :foreign_key => [:tariff_id, :tariff_start_date]
end
CREATE TABLE `product_tariffs` (
  `product_id` int(11) NOT NULL,
  `tariff_id` int(11) NOT NULL,
  `tariff_start_date` date NOT NULL,
  PRIMARY KEY  (`product_id`,`tariff_id`,`tariff_start_date`)
) TYPE=InnoDB;

class ReferenceCode < ActiveRecord::Base
  set_primary_keys :reference_type_id, :reference_code
  
  belongs_to :reference_type, :foreign_key => "reference_type_id"
  
  validates_presence_of :reference_code, :code_label, :abbreviation
end
CREATE TABLE `reference_codes` (
  `reference_type_id` int(11) NOT NULL,
  `reference_code` int(11) NOT NULL,
  `code_label` varchar(50) default NULL,
  `abbreviation` varchar(50) default NULL,
  `description` varchar(50) default NULL,
  PRIMARY KEY  (`reference_type_id`,`reference_code`)
) TYPE=InnoDB;

class Suburb < ActiveRecord::Base
  set_primary_keys :city_id, :suburb_id
  has_many :streets,  :foreign_key => [:city_id, :suburb_id]
end
CREATE TABLE `suburbs` (
  `city_id` int(11) NOT NULL,
  `suburb_id` int(11) NOT NULL,
  `name` varchar(50) NOT NULL,
  PRIMARY KEY  (`city_id`,`suburb_id`)
) TYPE=InnoDB;

class Tariff < ActiveRecord::Base
	set_primary_keys [:tariff_id, :start_date]
	has_many :product_tariffs, :foreign_key => [:tariff_id, :tariff_start_date]
	has_one :product_tariff, :foreign_key => [:tariff_id, :tariff_start_date]
	has_many :products, :through => :product_tariffs, :foreign_key => [:tariff_id, :tariff_start_date]
end
CREATE TABLE `tariffs` (
  `tariff_id` int(11) NOT NULL,
  `start_date` date NOT NULL,
  `amount` integer(11) default NULL,
  PRIMARY KEY  (`tariff_id`,`start_date`)
) TYPE=InnoDB;

というわけで、intとdateはオッケーだけど、PKにvarcharダメなんじゃねーの疑惑が発生。この疑惑を現在調査中。

composite_primary_keysは悪くないっぽい

| 18:54 |  composite_primary_keysは悪くないっぽいを含むブックマーク  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を吐きやがったのはどいつだ、という犯人探しになってきました。

犯人はMysqlAdapter

| 20:49 |  犯人はMysqlAdapterを含むブックマーク  犯人は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モデルが。

結局、真犯人はだれ?

| 23:15 |  結局、真犯人はだれ?を含むブックマーク  結局、真犯人はだれ?のブックマークコメント

結局色々試してみて分かったことは、PKが1つのvarcharのカラムの場合だけどうしてもうまく動かないってことでした。

composite_primary_keysは複合キーを扱うためのものなんで、主キーが2つ以上のものを扱うためのものなんで、その守備範囲はきっちり守ってます。Railsはオートナンバー型のPKを期待している。ってことは、varchar一つのPKは誰の守備範囲なのさ?

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

23:25 |  うさうさ【右脳左脳占い】を含むブックマーク  うさうさ【右脳左脳占い】のブックマークコメント

http://www.nimaigai.com/howto.html

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

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

MiyazimaMiyazima 2006/10/25 16:15 お世話になります(きっと)みやじまです。
早速調べて頂いてありがとうございます。
なんかダメぽいですね>複合キー
まあ既存スキーマに対応しなきゃダメな時点でRailsでやる意味が7割減くらいなので、申し訳ない気持ちでいっぱいです…
自分で作るしかないですかねー?>CRUD可能なAR
本気でやるならHibernateの実装とか参考になるんでしょうか?激しく大変な気が…
結局find_by_sql多用モードですかねえ?

akmakm 2006/10/25 16:49 みやじまさん、僕こそお世話になりたいです。composite_primary_keys の結論ですが、あまりにも上手く行かないので、色々試していたら、どうも僕が何か間違っているっぽいです。お騒がせしてすみません。もうちょっと待ってください。

MiyazimaMiyazima 2006/10/25 17:01 お、マジですか?超期待w

akmakm 2006/10/25 17:37 うーん、PKにvarcharはNG疑惑が発生してしまいました。もちょっと調べます。

akmakm 2006/10/25 22:52 http://capsctrl.que.jp/kdmsnr/wiki/bliki/?EnterpriseRailsを思い出してARの代わりにrBatisは?とか思ったんですけど、まだリリースがひとつもありませんでした・・・http://rubyforge.org/projects/rbatis/

ちなみにHibernateはこれまでの経験だと、セッション上にあるレコードにマッピングされたオブジェクトをひとつしか許さない、という制約を筆頭に結構癖のある仕組みですんで、あまり参考にしない方がよさそうです。

MiyazimaMiyazima 2006/10/26 01:44 もしや解決しちゃってます?ていうかしちゃってますね!さすが!
基本varcharのみPKテーブルはなかったと思うので、おそらくイケるはず。懸念が一つ減りました。ありがとうございます!

rBatisは僕も考えましたが、結局Railsの恩恵は受けないことに変わりない上、ARのfind_by_sqlで十分かなと思って消えました。
いやー、これでお世話になれる可能性が高まりました。

akmakm 2006/10/26 11:17 varcharのみPKテーブルがないとのことで、僕もちょっと安心しました。

トラックバック - http://d.hatena.ne.jp/akm/20061025
最近読んだ本
  • 情熱プログラマー ソフトウェア開発者の幸せな生き方
  • 禁煙セラピー[セラピーシリーズ]
  • 入門git
  • 入門Git
  • もやしもん(8) (イブニングKC)
  • JRuby 徹底入門
  • 入門Subversion Windows/Linux対応
  • Ship It! ソフトウェアプロジェクト 成功のための達人式ガイドブック
  • プログラミングRuby 第2版 言語編
  • プログラミングRuby 第2版 ライブラリ編
Connection: close