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メソッドよって文字列に変換されます。
さて、ここまで修正したところでブラウザによる表示確認をすると、次のようなエラーが出ます:

どこかでnilに対してjoinメソッドを呼んでしまっているようですが、7行目にはそれらしいところはありませんね。
こういうときは、エラー画面の「Full Trace」リンクをクリックします。

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"という行を追加しています。
これでエラーは解消されます。

製品の一覧と詳細
ここから先は、説明抜きでソースコードだけ示します。
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メソッドです。初心者向けの教科書ではまず説明されていないと思いますが、これを活用するとルーティングの自由度が格段に増します。
