まりぴよこのブログ

日々の日記。技術ネタでまとまりきってないものの記録、伝わる文章の書き方を練習とか。

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 を使う方法がオススメされていた。 この記事が詳しい。

qiita.com

・・・なのでここで「なるほど!」の場合、以下は全く読む必要はない。。

自前実装の時にRailsフレームワークへの理解が必要な箇所があったので、自分用の忘備録としてまとめてみた。

その1の模範解答ページ

gem friendly_idを使う方法解説ページ

その2の模範解答ページ

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