railsで規約に沿わない古いデータベースを扱う

背景

railsではrails g scaffoldなどでModelを作成すれば、自動的にidが付与されます。
しかもprimary_keyでauto_incrementでかつindexも張られるので、普段はidを気にする必要はありません。

railsを使い、自分でデータ構造を決める場合はrailsの流儀に則った方が楽で、問題も起こりません。
しかし、古いデータを活用した場合、流儀にそぐわない事もあり得ます。

今回は規約に沿わない場合の対応について大きく分けて2つの場合について説明します。
なお環境は以下の物で検証しています。

  • ruby 2.0.0p247 (2013-06-27 revision 41674) [x86_64-darwin12.4.0]
  • Rails 4.0.0
  • composite_primary_keys (6.0.0)

主キーがidではない

主キーがidではない場合として、具体的に商品(Item)テーブルの主キーが文字列である場合を考えます。

主キーがidで無い場合のポイントはcreate_tabelid: falseです。
これによってidが自動生成されることはなくなります。
他のオプションでprimary_keyと言う物もありますが、これはinteger型で名前をidから変更するだけの物です。

下記の例ではnull: falseとする事でnot nullな制約を追加し、add_index :items, :code, unique: trueでユニーク制約を定義しています。

Migrationファイル

class CreateItems < ActiveRecord::Migration

  def change
    create_table :items, id: false do |t|
      # 商品コード
      t.string :code, limit: 8, null: false
      # 商品名
      t.string :name
    end
    add_index :items, :code, unique: true
  end

end

モデル側にはself.primary_keyで主キーを設定します。
これによってfindメソッドで主キーによる取得が可能になります。

Item Model

class Item < ActiveRecord::Base
  # 主キー設定
  self.primary_key = :code
end

複合主キーを扱いたい

次は複合主キーを扱う場合についてです。
具体的に先ほど使った商品(Item)について、各商品に任意のタグ(Tag)がついているとします。
タグは商品コード(item_code)とタグコード(code)の複合主キーを持っています。

railsで複合主キーを扱う場合、composite_primary_keysのgemを使用します。
composite_primary_keysの準備としてGemfilegem 'composite_primary_keys'を追加しbundle installします。

Migrationファイルのポイントは、:item_code:codeの組み合わせがユニークである制約の定義です。
複合主キーがユニークである制約はadd_index :tags, [:item_code, :code], unique: trueだけでも十分です。
複合主キーが2つ3つと長くなる場合、インデックス名が長すぎてエラーが発生する事もあるので、その場合はnameでインデックス名を指定してあげましょう。

Migrationファイル

class CreateTags < ActiveRecord::Migration
  def change
    create_table :tags, id: false do |t|
      # 商品コード
      t.string :item_code, limit: 8
      # タグコード
      t.string :code, limit: 8
      # タグ名称
      t.string :name
    end
    add_index :tags, [:item_code, :code], unique: true, name: 'composite_index'
  end
end

モデルに関しては、先ほどはself.primary_keyを使いましたが、複合主キーの場合はself.primary_keysを使います。
これによってTag.find 'item_code', 'code'と、findメソッドで複合主キーを扱うことが可能になります。

また、foreign_keyによって、どのカラムが外部キーなのかを指定することで、関連を定義することが可能です。

Tag Model

class Tag < ActiveRecord::Base
  # 主キー設定
  self.primary_keys = :item_code, :code

  # 商品との関連
  belongs_to :item, foreign_key: :item_code
end

Item Model

class Item < ActiveRecord::Base
  # 主キー設定
  self.primary_key = :code

  # タグとの関連
  has_many :tags, foreign_key: :item_code
end

問題

composite_primary_keysによって複合主キーを取り扱うことが出来るようになりました。
しかし、少し問題が発生する場合もあります。
それはModelに対してto_jsonを呼び出した場合などで、以下のエラーが発生します。

TypeError: ["item_code", "code"] is not a symbol

根本ではserializable_hashを呼び出すところでのエラーなので、モデルでserializable_hashをオーバーライドする事で解決します。

def serializable_hash(options={})
  options = {
    :only => [:item_code, :code, :name_code]
  }.update(options)
  super(options)
end