Ruby on Railsで複合キーを扱う(7) -- 補遺

2012/04/01

この連載は第6回が最終回ですと宣言しましたが、大事なことを書き忘れていることに気がつきました。

ルーティングのことです。ちょっと書き足しておきます。

ルーティング

第6回終了時点のRailsアプリケーションsynthetosは、モデルが動くだけで、Webサイトとしてはまったく機能しません。第2回で作ったままですから当然です。

いま、config/routes.rbはこんな感じです。

Synthetos::Application.routes.draw do
  resources :departments do
    resources :products
  end
end

Gemライブラリcomposite_primary_keysのおかげで、

http://localhost:3000/departments/robot,1/products

のようなURLをうまく扱えていました。

さて、第3回以降の改造により「現在時刻(current date)」という概念が登場しました。これをURLに組み込む必要があります。次のようなURLになればよさそうです。

http://localhost:3000/2012-04-01/departments/robot/products

これで2012年4月1日における「robot」事業部の製品リストを表示しようというわけです。

この形式のルーティングを可能にするには、config/routes.rbを次のように修正します。

Synthetos::Application.routes.draw do
  scope path: ":current_date", constraints: { current_date: /(19|20)\d\d-\d\d-\d\d/ } do
    resources :departments do
      resources :products
    end
  end
end

シードデータ

db/seeds.rbを修正。

date = Date.new(2010, 1, 1)

%w(robot automobile ship).each do |code|
  Department.create!({
    code: code,
    name: code.capitalize,
    started_on: date,
    ended_on: nil
  }, without_protection: true)
end

%w(alpha bravo).each_with_index do |code, index|
  Product.create!({
    code: code,
    started_on: date.advance(years: index),
    ended_on: code == "alpha" ? date.advance(years: 1) : nil,
    department_code: "robot",
    name: code.capitalize,
    description: ""
  }, without_protection: true)
end

製品alphaは2011年1月1日で終了、製品bravoは2011年1月1日から開始で終了日は設定されていません。

部門の一覧

app/controllers/application_controller.rbを修正。

class ApplicationController < ActionController::Base
  protect_from_forgery

  before_filter :set_current_date
  
  def set_current_date
    DurationLimited.current_date = Date.parse(params[:current_date])
  end
end

params[:current_date]には"2012-04-01"という文字列がセットされています。それをparseして日付オブジェクトに変換して、DurationLimited.current_dateにセットしています。

app/controllers/departments_controller.rbを修正。

class DepartmentsController < ApplicationController
  def index
    @departments = Department.order("code")
  end
end

app/views/departments/index.html.erbを修正。

<h1>Departments#index</h1>

<ul>
  <% @departments.each do |d| %>
    <li>
      <%= d.name %>: <%= link_to "Products", department_products_path(params[:current_date], d) %>
    </li>
  <% end %>
</ul>

修正前、link_toメソッドの第2引数は[ d, :products ]と簡単に書けたのですが、少し面倒になりました。

ここで作りたいURLパスは/:current_date/departments/:product_id/productsというパターンをしています。変化する部分が2カ所あります。:current_date:product_idです。そこに挿入する値をdepartment_products_pathメソッドに渡しています。Departmentオブジェクトdは、後述するto_paramメソッドよって文字列に変換されます。

さて、ここまで修正したところでブラウザによる表示確認をすると、次のようなエラーが出ます:

composite_primary_keys_7_1

どこかでnilに対してjoinメソッドを呼んでしまっているようですが、7行目にはそれらしいところはありませんね。

こういうときは、エラー画面の「Full Trace」リンクをクリックします。

composite_primary_keys_7_2

ActiveModelのlib/active_model/conversion.rbの52行目で例外が発生していることが分かります。該当部分の抜粋が以下のコードです:

    def to_key
      persisted? ? [id] : nil
    end

    def to_param
      persisted? ? to_key.join('-') : nil
    end

確かにjoinが使われています。要は、主キーが設定されていないのが問題のようです。codeに設定しましょう。

app/models/duration_limited.rbを修正します。

module DurationLimited
  extend ActiveSupport::Concern
  mattr_accessor :current_date
  
  included do
    self.primary_key = "code"
    
    default_scope do
      where("started_on <= ? AND (ended_on > ? OR ended_on IS NULL)",
        DurationLimited.current_date, DurationLimited.current_date)
    end
  end

  (省略)
end

self.primary_key = "code"という行を追加しています。

これでエラーは解消されます。

composite_primary_keys_7_3

製品の一覧と詳細

ここから先は、説明抜きでソースコードだけ示します。

app/controllers/products_controller.rbを修正。

class ProductsController < ApplicationController
  def index
    @department = Department.find(params[:department_id])
    @products = @department.products.order("products.code")
  end
  
  def show
    @department = Department.find(params[:department_id])
    @product = @department.products.find(params[:id])
  end
end

app/views/departments/index.html.erbを修正。

<h1>Products#index</h1>

<ul>
  <% @products.each do |p| %>
    <li>
      <%= link_to p.name, department_product_path(params[:current_date], @department, p) %>
    </li>
  <% end %>
</ul>

app/views/departments/index.html.erbを修正。

<h1>Products#show</h1>

<ul>
  <li>Name: <%= @product.name %></li>
  <li>Code: <%= @product.code %></li>
  <li>Description: <%= @product.description %></li>
</ul>

今回の記事の肝はconfig/routes.rbで使用したscopeメソッドです。初心者向けの教科書ではまず説明されていないと思いますが、これを活用するとルーティングの自由度が格段に増します。