Ruby on Railsで複合キーを扱う(1)

2012/03/25

Ruby on Railsでは、データベーステーブルの主キーとしてidというカラムを使うのがデフォルトです。

誤解される方も多いのですが、もちろん主キーの名前は変更できます。たとえば、Userモデルに対応するusersテーブルの主キーがuidである場合、次のように書けばOKです。

class User < ActiveRecord::Base
  self.primary_key = "uid"
end

本稿のテーマからは外れますが、テーブルの名前も指定できます。テーブルuser_masterUserモデルで取り扱いたいなら、次のように書きます。

class User < ActiveRecord::Base
  self.table_name = "user_master"
  self.primary_key = "uid"
end

では、主キーが1個ではなく複数個ある場合はどうなるでしょうか。つまり、複合キーを用いてデータベースが設計されている場合です。

Ruby on Railsそれ自体は複合キーを扱えるようになっていませんが、それを可能にするGemライブラリ composite_primary_keys が存在します。本稿ではこのGemライブラリに依拠しつつRuby on Railsで複合キーを持つデータベーステーブルにアクセスする方法を紹介します。

データベースの専門家の間では「複合キー」の長所・短所に関して様々な議論があります。本稿はその議論には関与しません。複合キーを用いて設計されているデータベースがすでに存在して、そのデータベースのスキーマを変更できないという条件下で、Ruby on Railsアプリケーションの開発をするにはどうすればいいか、というテーマの話です。

なお、本稿が依拠するソフトウェアのバージョンは以下の通りです:

  • Ruby 1.9.3
  • Ruby on Rails 3.2.2
  • SQlite3 3.7

OSには依存しないはずです。

ソースコード中のハッシュをRuby 1.9記法で書いているためRuby 1.8.7ではソースコードが動きません。注意してください。

なお、本稿に記載したソースコードを理解するためにはRSpecとFactoryGirlの基礎的な知識が必要です。これらについての説明は省略します。

セットアップ

synthetos という名前の新規Railsアプリケーションを作ることにします。

% rails new synthetos --skip-bundle
% cd synthetos

Gemfile を修正。

source 'https://rubygems.org'

gem 'rails', '3.2.2'
gem 'sqlite3'

group :assets do
  gem 'sass-rails',   '~> 3.2.3'
  gem 'coffee-rails', '~> 3.2.1'
end

gem 'jquery-rails'

gem "composite_primary_keys", "~> 5.0.4"

group :test, :development do
  gem "rspec-rails", "~> 2.9.0"
  gem "factory_girl_rails"
end

Gemライブラリcomposite_primary_keysをインストールしています。また、試験コードを書くためにRSpecとFactoryGirlもインストールします。

% bundle install
% rails g rspec:install

Department, Product モデルを作成

% rails g model department
% rails g model product

db/migrate/..._create_departments.rb を修正。

class CreateDepartments < ActiveRecord::Migration
  def change
    create_table :departments, id: false do |t|
      t.string :code, null: false
      t.integer :seq_number, null: false
      t.string :name, null: false
      t.date :established_on, null: false
      
      t.timestamps
    end
    
    add_index :departments, [ :code, :seq_number ], unique: true
  end
end

id: false オプションで通常主キー(id)の作成を抑制しています。departmentsテーブルの主キーはcode, seq_numberの2つです。この組み合わせは一意(unique)である必要があるので、add_index, ..., unique: true でUNIQUE制約を課しています。

db/migrate/..._create_products.rb を修正。

class CreateProducts < ActiveRecord::Migration
  def change
    create_table :products, id: false do |t|
      t.string :code, null: false
      t.integer :model_number, null: false
      t.string :department_code, null: false
      t.integer :department_seq_number, null: false
      t.string :name, null: false
      t.text :description

      t.timestamps
    end
    
    add_index :products, [ :code, :model_number ], unique: true
  end
end

DepartmentモデルとProductモデルは1対多の関係にあります。productsテーブルの主キーはcode, model_numberの2つです。productsテーブルからdepartmentsテーブルを参照するには、department_code, department_seq_numberという複合キーを用います。

マイグレーションを実行します。

% rake db:migrate

app/models/department.rb を修正。

class Department < ActiveRecord::Base
  self.primary_keys = :code, :seq_number
  has_many :products, foreign_key: [ :department_code, :department_seq_number ]
end

composite_primary_keysのおかげで、様々なところで複合キーが使えるようになっています。

app/models/product.rb を修正。

class Product < ActiveRecord::Base
  self.primary_keys = :code, :model_number
  belongs_to :department,
    foreign_key: [ :department_code, :department_seq_number ],
    primary_key: [ :code, :seq_number ]
end

ProductモデルとDepartmentモデルを関連づけています。foreign_keyオプションには参照元テーブルの複合キー、primary_keyオプションには参照先テーブルの複合キーを指定します。この辺りは単一キーの場合と同じです。

以上で準備完了です。

試験コード

RSpecで試験コードを書いて、うまく動くかどうか確認しましょう。

spec/factories/departments.rbを修正。

FactoryGirl.define do
  factory :department do
    sequence(:code) { |n| "department_%02d" % n }
    sequence(:seq_number) { |n| n }
    name { code.camelize }
    established_on { 2.years.ago }
  end
end

spec/factories/products.rbを修正。

FactoryGirl.define do
  factory :product do
    sequence(:code) { |n| "product_%02d" % n }
    sequence(:model_number) { |n| n }
    name { code.camelize }
  end
end

spec/models/department_spec.rbを修正。

# coding: utf-8

require 'spec_helper'

describe Department do
  subject { FactoryGirl.create(:department) }
  
  it "部門(department)を複合キーで検索できる" do
    subject.code = "robot"
    subject.seq_number = 7
    subject.name = "Robot"
    subject.save!
    
    dep = Department.find("robot,7")
    dep.name.should == subject.name
  end
end

codeが "robot" で、seq_numberが 7 である部門を検索するには

Department.find("robot,7")

と書きます。

spec/models/product_spec.rbを修正。

# coding: utf-8

require 'spec_helper'

describe Product do
  let(:department) { FactoryGirl.create(:department) }
  
  it "部門(department)と関連づけできる" do
    product = FactoryGirl.build(:product)
    product.code = "alpha"
    product.model_number = 2012
    product.department_code = department.code
    product.department_seq_number = department.seq_number
    product.save!
    
    p = Product.find("alpha,2012")
    p.department.code.should == department.code
  end
end

試験を実施します。

% rake

ターミナルに「2 examples, 0 failures」と表示されました。成功です。

複合キーでレコードを検索したり、複合キーを持つモデル同士を関連づけできることが分かりました。次回は、ルーティングをうまく扱えるかどうか検証してみたいと思います。