背景
Rails 3 デビューのくせに初版のアジャレイルズ本で勉強した過去があったり、いびつな知識を持っていたので
- routing は昔懐かしい書き方が頭に残っている
- URI の path が複数系なのがなんかキモイ
という二つのわがままから、単数形の URI を作りたかった。ので、実際にやってみた。
※ 先に結論だけ言うと Rails 使っといて Rail Way からわざわざ外れようとするのはあんまり嬉しくないです。でもやっぱ URI は RESTful とは違う視点でもこだわりたいじゃないですか。
resourcesの基礎
- routing で resource を定義する helper があり、用意されているメソッドは resource, resources
- resource(単数形)は一つのresource用? 用途を思いつかなかったので resources(複数形) を試す
とりあえず scaffold user
改めて例は user で、こんな感じに scaffold してみる。
rails g scaffold user nick:string, name:string
このとき migration はこんな感じ。
db/migrate/xxx_create_users.rb
class CreateUsers < ActiveRecord::Migration
def change
create_table :users do |t|
t.string :nick
t.string :name
t.timestamps
end
end
end
routing は
config/routes.rb
resources :users
うん、シンプルそのもの。Model は
app/models/user.rb
class User < ActiveRecord::Base
attr_accessible :nick, :name
end
Controller は例の感じで CRUD を生成する。
app/controllers/users_controller.rb
class UsersController < ApplicationController
def index
...
end
def show
...
end
...
end
このときrake routes は以下のようになる。
users GET /users(.:format) users#index
POST /users(.:format) users#create
new_user GET /users/new(.:format) users#new
edit_user GET /users/:id/edit(.:format) users#edit
user GET /users/:id(.:format) users#show
PUT /users/:id(.:format) users#update
DELETE /users/:id(.:format) users#destroy
この routing がどう変わるかが大事。
「resources 単数形」に書き換える
config/routes.rb
resources :user
すると当然 Controller のファイル名と class名に影響が出る。
app/controllers/user_controller.rb
class UserController < ApplicationController
...
end
この段階で rake routes すると
user_index GET /user(.:format) user#index
POST /user(.:format) user#create
new_user GET /user/new(.:format) user#new
edit_user GET /user/:id/edit(.:format) user#edit
user GET /user/:id(.:format) user#show
PUT /user/:id(.:format) user#update
DELETE /user/:id(.:format) user#destroy
users がなくなって user_index になっていることが分かる。このままでは先ほど scaffold したコードは動かない。そこで対処方法は2つ。
- users_path を使っている部分を全部 user_index_path に書き換える。具体的には link_to と form_for と redirect_to の中。
- resources :user, :as => 'users' で users にマップし直す
2 の方が圧倒的に楽だし、scaffold の form_for は create も update も action が同じになっていないと動かないので 2 にすべき。するとこのときの routes はこんな感じ。
users GET /user(.:format) user#index
POST /user(.:format) user#create
new_user GET /user/new(.:format) user#new
edit_user GET /user/:id/edit(.:format) user#edit
user GET /user/:id(.:format) user#show
PUT /user/:id(.:format) user#update
DELETE /user/:id(.:format) user#destroy
これで URI を単数形にしつつ scaffold の動作を維持できた。
id以外でshowしたい
Rails の resources は id を中心にしており、この id は DB の key になっている。
しかしどうだろう。あるサービスで自分のアカウントの情報が例えば
/users/123456
みたいな URI だったら。
ちょっとガッカリだよね。そこでこれを強引に変更してみる。
get '/user/:nick' => 'user#show', :as => 'user'
resources :user, :as => 'users'
これで URI を作る際の user_path というメソッドを上書きできる。これは routing は先に書いたものが優先されることを利用している。
Rails routes are matched in the order they are specified, so if you have a resources :photos above a get 'photos/poll' the show action’s route for the resources line will be matched before the get line. To fix this, move the get line above the resources line so that it is matched first.
Ruby on Rails Guides: Rails Routing from the Outside In
ただし、このままでは全体的にはやはり :id を前提にして動く。したがってもう少しいじらないといけない。
Controller で
@user = User.find(params[:nick])
してた部分はこうなる。
@user = User.where(:nick => params[:nick]).first
あるいは
@user = User.find_by_nick(params[:nick])
ただしこれらの方法は find + id での検索と違って見つからなくても例外が起きないため、このあとに
if @user
...
else
render 'public/404.html', :status => 404
end
みたいにしてちゃんと 404 を返すようにしないといけない。
url_forで省略が効かなくなる
また URI 生成の部分で
link_to 'foo', @user
redirect_to @user
になっているところはそれぞれ
link_to 'foo', user_path(:nick => @user.nick)
redirect_to user_path(:nick => @user.nick)
と書き換える必要がある。これは前者の書き方が最終的に url_for(@user) を呼んでいて、url_for は
rails の named route で Symbol を使うことの利点と欠点 - QA@IT
で moro さんが書いているようにとっても便利にいい具合に仕事をしてくれるんだけど、残念ながら :id を前提にして動くため。
ちなみに
url_for(:nick => @user.nick)
とやってしまうと query string になって美しくない。これを避けるには controller から指定する必要があり、それだったら user_path(:nick => @user.nick) の方が短い。
showだけじゃダメ
new も対応していないので routing は最終的にこうなる。
get '/user/new' => 'user#new', :as => 'new_user'
get '/user/:nick' => 'user#show', :as => 'user'
resources :user, :as => 'users'
このとき new が上になっていないとせっかく定義しても全部 /user/:nick に「食われてしまう」ので、new の定義の方が上になっていないといけない。
※ 逆にこのままでは :nick には new を使えない。結局 namespace のことを考えてないので、まだ実用的ではない。
まとめ
以上、
- resource の scaffold の動作を満たしたまま
- 単数形の Controllerに変更し
- :id 以外のカラムを利用
する目的の動作にたどりついた。
とは言え、やっぱこれは邪道なんでしょうねぇ…。
なんか他にいい方法あったら教えてください。