Rails邪道resource定義

背景

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つ。

  1. users_path を使っている部分を全部 user_index_path に書き換える。具体的には link_to と form_for と redirect_to の中。
  2. 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 以外のカラムを利用

する目的の動作にたどりついた。

とは言え、やっぱこれは邪道なんでしょうねぇ…。

なんか他にいい方法あったら教えてください。

More