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
みたいな書き方で簡単に関連づけをする方法がないかどうか探ってみます。