ActiveRecord で id 以外のカラムをプライマリキーにして、しかも型が int じゃないときの話

http://www.hsbt.org/diary/20120524.html#p01 見てそういえばハマった話思い出したので、次みたときなんでこうしたんだっけ、とならないように書いておく。

既に存在するDBをそのまま使う必要があったりする時で、しかもプライマリキーが int(11) とかじゃないときは、少し残念な気持ちになりながら以下のようなマイグレーションファイルを用意することになる。

class CreatePrefMaster < ActiveRecord::Migration
  def change
    create_table :pref_master, id: false do |t|
      t.column :code, :"char(2)", null: false
      t.column :name, :"varchar(64)"
    end

    execute "ALTER TABLE pref_master ADD PRIMARY KEY (code)"
  end
end

テーブル名が複数形でアンダースコア繋ぎという Rails の標準的な命名規則ではない場合もあるので、モデルのほうで table_name を指定する。

class PrefMaster < ActiveRecord::Base
  self.table_name = "pref_master"
  attr_protected :title, :body
end

さらに、config/application.rb で、db/schema.rb じゃなく db/structure.sql を吐くよう指定する。

module Aaa
  class Application < Rails::Application
    # Use SQL instead of Active Record's schema dumper when creating the database.
    # This is necessary if your schema can't be completely dumped by the schema dumper,
    # like if you have constraints or database-specific column types
    config.active_record.schema_format = :sql
    # ↑ここのコメントアウトはずす。
    # ...
  end
end

なぜかと言うと、上記の手順でマイグレーションして schema.rb を吐いたりすると以下のようなファイルができる。create_table オプションに直接 :primary_key オプションを指定しているため、Rails 的な primary_key の解釈で int な型のカラムで create table してしまう。

ActiveRecord::Schema.define(:version => 20120531162643) do

  create_table "pref_master", :primary_key => "code", :force => true do |t|
    t.string "name", :limit => 64
  end

end

テストが通らずなんでかなーと思って見たら何か意図した型でない状態になっていて気付く。
structure.sql なら安心。

CREATE TABLE `pref_master` (
  `code` char(2) COLLATE utf8_unicode_ci NOT NULL,
  `name` varchar(64) COLLATE utf8_unicode_ci DEFAULT NULL,
  PRIMARY KEY (`code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

Rails の標準からはずれてくるといろいろつらいおもいするのでこういう事態を避けられるなら、そっちのほうがいいと思う。