Ruby on Railsで複合キーを扱う(5)
2012/03/29
前回は、文字列と「期間」を複合キーとして用いているデータベーステーブルをRailsで扱う話の続きで、モデル間を1対多で関連づける方法について書きました。
今回は多対多です。
Category, CategoryProductLink モデルを作成
% rails g model category % rails g model category_product_link
db/migrate/..._create_categories.rbを修正。
class CreateCategories < ActiveRecord::Migration
def change
create_table :categories, id: false do |t|
t.string :code, null: false
t.string :name, null: false
t.date :started_on, null: false
t.date :ended_on, null: true
t.timestamps
end
add_index :categories, [ :code, :started_on ], unique: true
end
end
db/migrate/..._create_category_product_links.rbを修正。
class CreateCategoryProductLinks < ActiveRecord::Migration
def change
create_table :category_product_links, id: false do |t|
t.string :category_code
t.string :product_code
t.date :started_on, null: false
t.date :ended_on, null: true
t.timestamps
end
add_index :category_product_links,
[ :category_code, :product_code, :started_on ], unique: true,
name: "index_category_product_links_on_codes"
end
end
% rake db:migrate
app/models/category.rbを修正。
class Category < ActiveRecord::Base include DurationLimited end
app/models/category_product_link.rbを修正。
class CategoryProductLink < ActiveRecord::Base include DurationLimited end
FactoryGirl
spec/factories/categories.rbを修正。
FactoryGirl.define do
factory :category do
sequence(:code) { |n| "category_%02d" % n }
name { code.camelize }
started_on { 2.years.ago }
ended_on { nil }
end
end
spec/factories/category_product_links.rbを修正。
FactoryGirl.define do
factory :category_product_link do
started_on { 2.years.ago }
ended_on { nil }
category_code do |link|
FactoryGirl.create(:category,
started_on: link.started_on, ended_on: link.ended_on).code
end
product_code do |link|
FactoryGirl.create(:product,
started_on: link.started_on, ended_on: link.ended_on).code
end
end
end
これで準備完了です。
RSpecによる試験コード
spec/models/category_spec.rbを修正。
# coding: utf-8
require 'spec_helper'
describe Category do
subject { FactoryGirl.create(:category, code: "c0",
started_on: Date.new(1999, 1, 1), ended_on: nil) }
let(:product0a) { FactoryGirl.create(:product,
code: "p0", name: "Product0a",
started_on: Date.new(2000, 1, 1), ended_on: Date.new(2002, 1, 1)) }
let(:product0b) { FactoryGirl.create(:product,
code: "p0", name: "Product0b",
started_on: Date.new(2002, 1, 1), ended_on: nil) }
let(:product1) { FactoryGirl.create(:product,
code: "p1", name: "Product1",
started_on: Date.new(1999, 1, 1), ended_on: nil) }
before do
product0a
product0b
product1
end
it "2001年1月1日当時の製品(product)リストと関連づけできる" do
FactoryGirl.create(:category_product_link,
category_code: subject.code, product_code: "p0",
started_on: Date.new(2000, 1, 1), ended_on: nil)
FactoryGirl.create(:category_product_link,
category_code: subject.code, product_code: "p1",
started_on: Date.new(1999, 1, 1), ended_on: nil)
DurationLimited.current_date = Date.new(2001, 1, 1)
cat = Category.find(subject.code)
cat.should have(2).products
cat.products.map(&:name).sort.should == [ "Product0a", "Product1" ]
end
end
spec/models/category_product_link_spec.rb は使わないので削除してください。
実装
まずはテストが正しく失敗することを確認します。
% rake spec
(省略)
Failures:
1) Category 2001年1月1日当時の製品(product)リストと関連づけできる
Failure/Error: cat.should have(2).products
NoMethodError:
undefined method `products' for #<Category:0x00000003c9db98>
# ./spec/models/category_spec.rb:35:in `block (2 levels) in <top (required)>'
Finished in 0.06834 seconds
4 examples, 1 failure
続いて、app/models/category.rbを修正。
class Category < ActiveRecord::Base
include DurationLimited
def products
@product_codes ||= CategoryProductLink.where(category_code: code).
select(:product_code).map(&:product_code)
Product.where(code: @product_codes)
end
end
これでテストは成功します。
% rake spec (省略) Finished in 0.06973 seconds 4 examples, 0 failures
単一の主キーidを用いているテーブルの場合は、
has_many :category_product_links has_many :products, through: :category_product_links
と簡単に書けましたが、文字列と期間を複合キーとして用いている我々の例ではちょっと複雑なコードとなりました。
しかし、Ruby on Railsでも少し頑張れば複合キーを採用しているデータベースも扱えることが示せたかと思います。
次回は、我々の例でもhas_many :products, through: :category_product_linksみたいな書き方で簡単に関連づけをする方法がないかどうか探ってみます。
