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_master
をUser
モデルで取り扱いたいなら、次のように書きます。
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」と表示されました。成功です。
複合キーでレコードを検索したり、複合キーを持つモデル同士を関連づけできることが分かりました。次回は、ルーティングをうまく扱えるかどうか検証してみたいと思います。