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

2012/03/28

前回から、文字列と「期間」を複合キーとして用いているデータベーステーブルをRailsで扱う話を書いています。

ある程度使いやすそうなDepartment.findメソッドを作ったところで終わりましたので、今回はProduct#departmentメソッドやDepartment#productsメソッドを作ります。モデル間の関連づけです。

RSpecによる試験コード(1)

まずは、Product#departmentメソッドから。

新規ファイルspec/model/product_spec.rb を次のように作成します。

# coding: utf-8

require 'spec_helper'

describe Product do
  let(:department0) { FactoryGirl.create(:department,
      code: "robot", name: "Department0",
      started_on: Date.new(2000, 1, 1), ended_on: Date.new(2002, 1, 1)) }
  let(:department1) { FactoryGirl.create(:department,
      code: "robot", name: "Department1",
      started_on: Date.new(2002, 1, 1), ended_on: nil) }
  
  before do
    department0
    department1
  end
  
  it "2001年1月1日当時の部門(department)と関連づけできる" do
    product = FactoryGirl.build(:product)
    product.code = "alpha"
    product.started_on = Date.new(2000, 1, 1)
    product.ended_on = Date.new(2004, 1, 1)
    product.department_code = "robot"
    product.save!

    DurationLimited.current_date = Date.new(2001, 1, 1)

    p = Product.find("alpha")
    p.department.name.should == department0.name
  end
  
  it "2002年1月1日当時の部門(department)と関連づけできる" do
    product = FactoryGirl.build(:product)
    product.code = "alpha"
    product.started_on = Date.new(2000, 1, 1)
    product.ended_on = Date.new(2004, 1, 1)
    product.department_code = "robot"
    product.save!
    
    DurationLimited.current_date = Date.new(2002, 1, 1)

    p = Product.find("alpha")
    p.department.name.should == department1.name
  end
end

また、新規ファイルspec/factories/products.rbを次のように作成します。

FactoryGirl.define do
  factory :product do
    sequence(:code) { |n| "product_%02d" % n }
    name { code.camelize }
    started_on { 2.years.ago }
    ended_on { nil }
    department_code do |p|
      FactoryGirl.create(:department,
        started_on: p.started_on, ended_on: p.ended_on).code
    end
  end
end

実装(1)

今回も途中経過は省いて、ソースコードの最終形のみを示します。

app/models/product.rbを次のように修正します。

class Product < ActiveRecord::Base
  include DurationLimited
  
  def department
    Department.where(code: department_code).first
  end
end

これでテストは通ります。

単一の主キーidを用いているテーブルの場合は、

  belongs_to :department

と書くだけでしたが、ActiveRecordには頼れないのでこのようにメソッドを作っていくことになります。

「これではRailsの良さが生かせない」と思われた方がいるかもしれません。しかし、「単一の主キー」というRailsの本流から外れるわけなので、多少面倒になるのは仕方がありません。私に言わせれば、「たった3行で書けたよ」と威張りたいところです。

RSpecによる試験コード(2)

次に、Department#productsメソッドを実装します。

spec/model/department_spec.rb を次のように修正します。

# coding: utf-8

require 'spec_helper'

describe Department do
  let(:department0) { FactoryGirl.create(:department,
      code: "robot", name: "Department0",
      started_on: Date.new(2000, 1, 1), ended_on: Date.new(2002, 1, 1)) }
  let(:department1) { FactoryGirl.create(:department,
      code: "robot", name: "Department1",
      started_on: Date.new(2002, 1, 1), ended_on: nil) }
  let(:department2) { FactoryGirl.create(:department,
      code: "ship", name: "Department1",
      started_on: Date.new(2002, 1, 1), ended_on: nil) }
  
  before do
    department0
    department1
    department2
  end
  
  it "部門(department)を複合キーで検索できる" do
    DurationLimited.current_date = Date.new(2001, 1, 1)
    
    dep0 = Department.find("robot")
    dep0.name.should == department0.name
    
    DurationLimited.current_date = Date.new(2003, 1, 1)
    
    dep1 = Department.find("robot")
    dep1.name.should == department1.name
  end
  
  it "2001年1月1日当時の製品(product)リストを取得できる" do
    FactoryGirl.create(:product, name: "p0", department_code: "robot",
      started_on: Date.new(1999, 1, 1), ended_on: Date.new(2000, 8, 1))
    FactoryGirl.create(:product, name: "p1", department_code: "robot",
      started_on: Date.new(2000, 1, 1), ended_on: Date.new(2002, 12, 1))
    FactoryGirl.create(:product, name: "p2", department_code: "robot",
      started_on: Date.new(2001, 1, 1), ended_on: nil)
    FactoryGirl.create(:product, name: "p3", department_code: "robot",
      started_on: Date.new(2002, 1, 1), ended_on: nil)
    FactoryGirl.create(:product, name: "p4", department_code: "ship",
      started_on: Date.new(2000, 1, 1), ended_on: nil)
    
    DurationLimited.current_date = Date.new(2001, 1, 1)
    
    dep = Department.find("robot")
    dep.should have(2).products
    
    products = dep.products.order("products.started_on")
    
    products[0].name.should == "p1"
    products[1].name.should == "p2"
  end
end

dep.products が返す配列の要素が2であること、dep.products.order(...)という書き方もできること、などを確かめています。

実装(2)

app/models/department.rbを次のように修正します。

class Department < ActiveRecord::Base
  include DurationLimited
  
  def products
    Product.where(department_code: code)
  end
end

試験コードを書くのは大変でしたが、実装はあっけなく終わりました。

次回は、Category(カテゴリー)モデルを追加して、Productモデルとの間に多対多の関係を構築してみます。