Posted at

friendly_idについて

More than 3 years have passed since last update.


friendly_idとは

通常、railsでのルーティングは

/users/1とか/posts/1などのように

/users/:id と形のURLとなりますが、

/users/reizistというようなフレンドリーなURLにしたい時、ありますよね。

そんな時のためのgemです。


Why friendly_id?

正直、目的を達成しようと思えばこのgemを使わなくても実装できます。

では何故今更friendly_idなのでしょうか?

それは、rails-style-guideUse 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行を記述します。


user.rb

class User < ActiveRecord::Base

include FriendlyId
friendly_id :name
end

FriendlyIdをincludeし、URLに表示させたいカラム名を指定します。

Controllerでは、以下のように変更をします。


users_controller.rb

def set_user

+ @user = User.friendly.find(params[:id])
- @user = User.find(params[:id])
end

動作は以下のようになります。


/users

スクリーンショット 2014-12-11 21.53.42.png


/users/:id

スクリーンショット 2014-12-11 21.53.57.png

本来はusers/1のようなURLになるところが、nameがURLに使われていることがわかります。

しかし、このままでは問題になるケースもあります。

試しに、「Jonathan Joestar」というnameでユーザー登録をしました。

スクリーンショット 2014-12-11 22.19.41.png

URLを見てみましょう。

スクリーンショット 2014-12-11 22.19.52.png

おや?「/users/Jonathan%20Joestar」というURLになってしまっています。

これは、URLエンコーディングでは「スペース」は「%20」と表記されるためです。

でもこれは見づらいですよね。フォーマットの指定はできないのでしょうか?


slug

そこでslugの導入です。ヒューマンリーダブルなフォーマットに直したカラムを別途用意することでこれを解決します。

まずはslugカラムを追加します。ついでにindexも貼っときましょう。

(ここでnameカラムにindex張ってなかったりなど色々忘れてたことに気づいた)


add_slug_to_users.rb

class AddSlugToUsers < ActiveRecord::Migration

def change
add_column :users, :slug, :string, null: false
add_index :users, :slug, unique: true
end
end

そして、Modelに戻りuseオプションを追記します。


user.rb

class User < ActiveRecord::Base

include FriendlyId
friendly_id :name, use: :slugged
end

もう一度先ほど登録したデータを見てみます。

スクリーンショット 2014-12-13 15.03.20.jpg

URLが「jonathan-joestar」に変わりましたね!

この変更された表記ですが、デフォルトではActiveSupportのparameterizeメソッドを使っているようですね。

さて、ここで1つ問題があります。

デフォルトでは、「slugがnilのときにのみslugの値は更新される」ことになっているのです。

試しに、先ほど「Jonathan Joestar」という名前で登録したユーザーの名前を変更してみます。

スクリーンショット 2014-12-13 15.15.45.jpg

名前は「Joseph Joestar」に更新されたのに、URLは「jonathan-joestar」のままです。

この場合は、should_generate_new_friendly_id?メソッドをオーバーライドします。


user.rb

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を更新する」ことにします。

もう一度名前を変更してみます。

スクリーンショット 2014-12-13 15.19.54.jpg

今度は正しくURLも更新されました。

スクリーンショット 2014-12-13 15.21.09.jpg

slugが正しく更新されていることがわかります。


その他


  • Translate Slugs

  • Scoped Slugs

ここらは必要になったときに都度ドキュメントを参照すればよいかと思いよく読んでません。


friendly_idの問題点

便利なfriendly_idですが、実は問題点もあります。

それはパフォーマンスです。

詳しくはbenchmarkを見てもらえればと思いますが、3倍ほど遅くなるケースもあるようですね。気には留めておく必要がありそうです。


所感

ある程度コードの見通しが立てやすくなり、「URLでid以外を使いたい」という目的を達するだけであれば、それなりに便利だなぁと思いました。

とはいえ、きちんと実装しようとするとメソッドのオーバーライドが必要であったり、ある意味無駄なカラムを追加しなくてはいけなかったりという意味で辛みはありそうです。

どうしてもURLをid以外のものにしたい!という要件が発生した場合は、色々と考慮が必要になるのかなぁと思いました。

最後に、friendly_idをつかわなかった場合の最低限の実装の一例を軽く紹介してこの記事を終わらせようと思います。


独自実装


user.rb

class User < ActiveRecord::Base

# to_paramメソッドをオーバーライドしてid以外のparamを指定
def to_param
name
end
end


users_controller.rb

# 中略

def set_user
# @user = User.find(params[:id])
@user = User.find_by(name: params[:name])
end
# 中略


routes.rb

Rails.application.routes.draw do

resources :users, param: :name
end

なお、今回のソースをfriendly_id_sampleにまとめておいておきますのでもしよろしければご参照頂ければと思います。

ここまで読んでいただきありがとうございました。