Rails のルーティング
Rails 3 でほとんど今の形に落ち着いて、Rails でアプリケーション開発するプログラマたちの間ではすっかり定着した感のあるルーティング。みなさん routes.rb を書いていますか?特に ROI の文脈での「リソース」を意識してルーティング設計すれば、誰が担当してもそれなりに同じような成果物ができあがって助かる〜という実感があります。
:id is 何?
ただ、ルーティング定義のパスに含まれる :id については、どうしたもんかな〜と思っています。たとえば Rails の API ドキュメント http://api.rubyonrails.org/classes/ActionDispatch/Routing/Mapper/Resources.html#method-i-resources を見てみると、下記のような記述があります。
# (例1)
# resources :photos
# ↑このように定義した場合
GET /photos
GET /photos/new
POST /photos
GET /photos/:id
GET /photos/:id/edit
PATCH/PUT /photos/:id
DELETE /photos/:id
# (例2)
# resources :photos do
# resources :comments
# end
# ↑このように定義した場合
GET /photos/:photo_id/comments
GET /photos/:photo_id/comments/new
POST /photos/:photo_id/comments
GET /photos/:photo_id/comments/:id
GET /photos/:photo_id/comments/:id/edit
PATCH/PUT /photos/:photo_id/comments/:id
DELETE /photos/:photo_id/comments/:id
(例1)の方で :id は Photo モデルの id を指していますが、リソースをネストしている(例2)での :id は Comment モデルの id を意味しています。ふむ、なかなかややこしいですね。
ぼくとしては、今のところは Model モデルの column カラムを指す場合は :model_column としておくのがよい、というお気持ちでいます。リソースがネストしているかどうかに関わらず、たとえば :photo_id とか :comment_id とか :user_name というのを用いることにしてしまえばほとんどのパターンで一意に定まるし、考えることが減ってよさそうです。
before_action :set_xxxx
Rails に触れているとよく見かけるやつです。コントローラに :set_user というメソッドがあって、その中で @user
に show とか edit とかしたい User モデルのレコードを入れておいてくれる便利な機構です。Rails の scaffold で生成したコードにも出てくるので、レールの上にある標準的な機構と言ってよさそうです。
さっき、ルーティングの話で「Photo モデルの id カラムなら、いつでも :photo_id としておくのがよい」と書きました。そうしておくと、どのコントローラにおいても
def set_photo
@photo = Photo.find(params[:photo_id])
end
と書けて便利ですね。これが :id だったり :photo_id だったりと揺らいでいたら、共通化ができなくなります。だから「基本的に :model_column 形式で揃えていきましょう」と思ったわけです。
ルーティング警察になりたいの?
揃えていきたいと思ったとして、じゃあぼくは今後ずっと「えっと、以前にもコメントしたことあるんですけど、自分はこうこうこういう理由でこうするのがいいと思っていて、だから今回のこのルーティング定義もこの形式に揃えてほしいです」というコメントをレビューのたびに書き続けるルーティング警察になりたいのかというと、そういう未来はけっこうしんどいと感じました。
じゃあ、どうしようか。人を動かすには「便利」が便利です。
「便利」を考える
「こうした方が便利だよ!」「たしかに」となれば、人々がそっちに流れやすくなりそうです。エサで釣るスタイル。
ようやく本題なわけですが、じゃあ「ルーティング定義をうまいことやったら、レコードの自動取得まで完了する」ってのはどうだろうか?雑な実装で試してみました。
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
before_action :auto_set
def auto_set
params.select { |key, _| key =~ /__/ }.each do |key, value|
model_name, column_name = key.split('__')
model = Object.const_get(model_name.capitalize)
record = model.find_by([[column_name, value]].to_h)
instance_variable_set("@#{model_name}", record)
end
end
end
ApplicationController に auto_set というメソッドを用意し、before_action に登録しておきます。
もし params に photo__id: 3
というデータが渡ってきたら @photo
に Photo の id が 3 のレコードを代入しておいてくれます。同様に user__name: 'june29'
が渡ってきたら @user
に User の name が june29 のレコードが自動で代入されます。
このような仕組みになっていれば、ルーティング定義も、下記のようにしたくならないでしょうか…?
resources :photos, param: :photo__id
resources :users, param: :user__name
なお、アンダースコア2つを区切り文字にしているのはただの一案で、既存の Rails アプリケーションの既存のルーティング定義のじゃまにならなければなんでもよい、くらいの意図です。
引き続き考えていきます
先述の例だと id で探すときも find_by(id:) しちゃうので、これだと find(:id) と挙動が違うからイマイチだな〜と思ったりしています。
みなさんの現場では、ルーティング定義の治安はよいですか?なにか「うちではこうしているよ」「私はこう考えているよ」というものがあれば、ぜひぜひ教えてほしいです。