Rails の パスを ID以外のものに置き換える方法(独自実装時のポイントまとめ)
パスにIDが入ったらイヤな場合
Qiitaみたいに、ユーザーのマイページを表示する時に、ユーザーIDではなく名前をパスにしたい!みたいな時。
http://your.domein.com/users/1 ↓ http://your.domein.com/users/mm36
みたいに置き換えたい。
・・というか、置き換えてるRailsアプリのコードを見てて、どうしてそうなってるのか調べた時の勉強記録。
Rails的にはアルアルの有名事象のようなので、今更Qiitaに更に追加しても仕方ないかな・・と思ったので 自分的理解をブログに書いてみることにした。
解決方法(大きく分けて2つ)
- その1: gem friendly_idを使う
- その2: Rails の機能を上書きする形で自前実装する
調べたところ、1の friendly_id を使う方法がオススメされていた。 この記事が詳しい。
・・・なのでここで「なるほど!」の場合、以下は全く読む必要はない。。
自前実装の時にRailsのフレームワークへの理解が必要な箇所があったので、自分用の忘備録としてまとめてみた。
その1の模範解答ページ
gem friendly_idを使う方法解説ページ
その2の模範解答ページ
Rails の機能を上書きする形で自前実装する方法解説ページ
- Railsで、URLにIDでなく名前を入力して、アクセスする方法 - Qiita
- RailsでURLにモデルのID以外の値を表示するTips - Rails Webook
- to_param - リファレンス - - Railsドキュメント
本題
自前実装する方法に、初心者的には幾つか抑えなければならない基本事項があったので、ちゃんと理解できた証として以下を書いてみることに!
モデルの to_param を定義する
ActiveRecordの標準機能を上書きすることになる。
to_paramを上書き実装すると、URLのidの部分にid以外のものを指定できるようになる。
class User < ActiveRecord::Base def to_param name end end
rails consoleで試してみると
irb > user = User.first irb > user.to_param # => "mm36" irb > app.user_path(user) # => "/users/mm36"
となる。
こうなると、Controllerに渡ってくる params[:id]
も to_param
で返ってくる値になるので、
Controllerの edit, show, update, destroy のように、idから操作対象のオブジェクトを取得するタイプのアクションで
to_param
の値で一意にオブジェクトが引けるようにしておかないといけない。
普通に実装するとこんな感じ。
class UsersController < ApplicationController before_action :set_user, only: [ :edit, :show, :update, :destroy ] private def set_user @user = User.find_by_param(params[:id]) end def permitted_params params.permit(user: [ :name ]) end end
注意したポイント
- before_actionで共通のprivate methodを使用するようにして、DRYに。
- Strong Parameterで値が渡ってくるように
permitted_params
の指定を必要な属性名に変更。 - モデルの方で、
find_by_param
をClass methodとして追加しておくルールにすると、一貫性が出ていい感じになる。
class User < ActiveRecord::Base def self.find_by_param(id) self.find_by_name!(id) end end
キーの代わりにnameで引く状態にしているので find できなかった場合は例外を発生させている ( find_by_name!
)
複数モデルで同じパスの指定方法をさせる場合
複数のモデルでIDをnameにする場合は、 concerns moduleにしておくとよい。 (もちろんname以外の他の属性値でもよい)
module FindByName extend ActiveSupport::Concern class_methods do def find_by_param(id) self.find_by_name!(id) end end def to_param self.name end end
モデルの方は共通モジュールをinclude
class Product < ActiveRecord::Base include FindByName end