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
メソッドです。初心者向けの教科書ではまず説明されていないと思いますが、これを活用するとルーティングの自由度が格段に増します。