friendly_idとは
通常、railsでのルーティングは
/users/1とか/posts/1などのように
/users/:id
と形のURLとなりますが、
/users/reizist
というようなフレンドリーなURLにしたい時、ありますよね。
そんな時のためのgemです。
Why friendly_id?
正直、目的を達成しようと思えばこのgemを使わなくても実装できます。
では何故今更friendly_idなのでしょうか?
それは、rails-style-guideでUse the friendly_id gemと書いてあるからです。
そして、コードの見通しが立てやすいという理由もあると思います。
自前で実装しようとすると、明確なルールが与えられないので実現方法は複数あります。
更に、コードがController, Model, routes.rbのそれぞれに散らばり、初見では
URLにnameを使うためだけにあちこちを探しまわるのは辛い気がします(個人的意見です)。
実際にこのgemを使うにあたって、githubのREADMEくらいは読む必要がありますが、
逆にそこだけ読めばほぼほぼやりたいことは実現できるかと思います。
とはいえ、英語がだらだらと書いてあるのを読むのがめんどくさい!という方もいると思ったので
改めてfriendly_idについて記事を書くことにしました。
導入
基本
基本的な使用方法の説明です。
今回は、nameというカラムをもつUserモデルを題材にします。
尚、話を単純化するためにrails g scaffold User name:string
というようにscaffoldを使用しています。
モデルに以下の2行を記述します。
class User < ActiveRecord::Base
include FriendlyId
friendly_id :name
end
FriendlyIdをincludeし、URLに表示させたいカラム名を指定します。
Controllerでは、以下のように変更をします。
def set_user
+ @user = User.friendly.find(params[:id])
- @user = User.find(params[:id])
end
動作は以下のようになります。
/users
/users/:id
本来はusers/1
のようなURLになるところが、nameがURLに使われていることがわかります。
しかし、このままでは問題になるケースもあります。
試しに、「Jonathan Joestar」というnameでユーザー登録をしました。
URLを見てみましょう。
おや?「/users/Jonathan%20Joestar」というURLになってしまっています。
これは、URLエンコーディングでは「スペース」は「%20」と表記されるためです。
でもこれは見づらいですよね。フォーマットの指定はできないのでしょうか?
slug
そこでslugの導入です。ヒューマンリーダブルなフォーマットに直したカラムを別途用意することでこれを解決します。
まずはslugカラムを追加します。ついでにindexも貼っときましょう。
(ここでnameカラムにindex張ってなかったりなど色々忘れてたことに気づいた)
class AddSlugToUsers < ActiveRecord::Migration
def change
add_column :users, :slug, :string, null: false
add_index :users, :slug, unique: true
end
end
そして、Modelに戻りuseオプションを追記します。
class User < ActiveRecord::Base
include FriendlyId
friendly_id :name, use: :slugged
end
もう一度先ほど登録したデータを見てみます。
URLが「jonathan-joestar」に変わりましたね!
この変更された表記ですが、デフォルトではActiveSupportのparameterizeメソッドを使っているようですね。
さて、ここで1つ問題があります。
デフォルトでは、「slugがnilのときにのみslugの値は更新される」ことになっているのです。
試しに、先ほど「Jonathan Joestar」という名前で登録したユーザーの名前を変更してみます。
名前は「Joseph Joestar」に更新されたのに、URLは「jonathan-joestar」のままです。
この場合は、should_generate_new_friendly_id?
メソッドをオーバーライドします。
class User < ActiveRecord::Base
include FriendlyId
friendly_id :name, use: :slugged
private
def should_generate_new_friendly_id?
name_changed? || super
end
end
今回は、「nameが変更されたとき及びデフォルト(slugがnil)でslugを更新する」ことにします。
もう一度名前を変更してみます。
今度は正しくURLも更新されました。
slugが正しく更新されていることがわかります。
その他
- Translate Slugs
- Scoped Slugs
ここらは必要になったときに都度ドキュメントを参照すればよいかと思いよく読んでません。
friendly_idの問題点
便利なfriendly_idですが、実は問題点もあります。
それはパフォーマンスです。
詳しくはbenchmarkを見てもらえればと思いますが、3倍ほど遅くなるケースもあるようですね。気には留めておく必要がありそうです。
所感
ある程度コードの見通しが立てやすくなり、「URLでid以外を使いたい」という目的を達するだけであれば、それなりに便利だなぁと思いました。
とはいえ、きちんと実装しようとするとメソッドのオーバーライドが必要であったり、ある意味無駄なカラムを追加しなくてはいけなかったりという意味で辛みはありそうです。
どうしてもURLをid以外のものにしたい!という要件が発生した場合は、色々と考慮が必要になるのかなぁと思いました。
最後に、friendly_idをつかわなかった場合の最低限の実装の一例を軽く紹介してこの記事を終わらせようと思います。
独自実装
class User < ActiveRecord::Base
# to_paramメソッドをオーバーライドしてid以外のparamを指定
def to_param
name
end
end
# 中略
def set_user
# @user = User.find(params[:id])
@user = User.find_by(name: params[:name])
end
# 中略
Rails.application.routes.draw do
resources :users, param: :name
end
なお、今回のソースをfriendly_id_sampleにまとめておいておきますのでもしよろしければご参照頂ければと思います。
ここまで読んでいただきありがとうございました。