friendly_idについて

### friendly_idとは

通常、railsでのルーティングは
/users/1とか/posts/1などのように
``` /users/:id ``` と形のURLとなりますが、
``` /users/reizist ```というようなフレンドリーなURLにしたい時、ありますよね。
そんな時のためのgemです。 

### Why friendly_id?

正直、目的を達成しようと思えばこのgemを使わなくても実装できます。
では何故今更friendly_idなのでしょうか？
それは、[rails-style-guide](https://github.com/bbatsov/rails-style-guide#activerecord)で**Use the friendly_id gem**と書いてあるからです。
そして、コードの見通しが立てやすいという理由もあると思います。
自前で実装しようとすると、明確なルールが与えられないので実現方法は複数あります。
更に、コードがController, Model, routes.rbのそれぞれに散らばり、初見では
URLにnameを使うためだけにあちこちを探しまわるのは辛い気がします（個人的意見です）。

実際にこのgemを使うにあたって、[githubのREADME](https://github.com/norman/friendly_id)くらいは読む必要がありますが、
逆にそこだけ読めばほぼほぼやりたいことは実現できるかと思います。
とはいえ、英語がだらだらと書いてあるのを読むのがめんどくさい！という方もいると思ったので
改めてfriendly_idについて記事を書くことにしました。

### 導入
#### 基本
基本的な使用方法の説明です。
今回は、nameというカラムをもつUserモデルを題材にします。
尚、話を単純化するために``` rails g scaffold User name:string ```というようにscaffoldを使用しています。

モデルに以下の2行を記述します。


``` ruby:user.rb
class User < ActiveRecord::Base
  include FriendlyId
  friendly_id :name
end
```

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

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

``` diff: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](https://qiita-image-store.s3.amazonaws.com/0/27966/7732a6f2-4476-1ff8-8f7b-e2602804cf10.png)

##### ``` /users/:id ```
![スクリーンショット 2014-12-11 21.53.57.png](https://qiita-image-store.s3.amazonaws.com/0/27966/7312e44f-fd29-5196-392b-eef87c08905a.png)

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

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

試しに、「Jonathan Joestar」というnameでユーザー登録をしました。
![スクリーンショット 2014-12-11 22.19.41.png](https://qiita-image-store.s3.amazonaws.com/0/27966/559bfdda-6f5f-d7cd-d86e-c51870f8efb6.png)

URLを見てみましょう。

![スクリーンショット 2014-12-11 22.19.52.png](https://qiita-image-store.s3.amazonaws.com/0/27966/9826dbe5-6fe9-dd16-3de8-ff13a428caf1.png)

おや？「/users/Jonathan%20Joestar」というURLになってしまっています。
これは、URLエンコーディングでは「スペース」は「%20」と表記されるためです。
でもこれは見づらいですよね。フォーマットの指定はできないのでしょうか？

#### slug

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

まずはslugカラムを追加します。ついでにindexも貼っときましょう。
（ここでnameカラムにindex張ってなかったりなど色々忘れてたことに気づいた）

``` ruby: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オプションを追記します。

``` ruby:user.rb
class User < ActiveRecord::Base
  include FriendlyId
  friendly_id :name, use: :slugged
end
```

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

![スクリーンショット 2014-12-13 15.03.20.jpg](https://qiita-image-store.s3.amazonaws.com/0/27966/f5870e9b-d115-6642-74af-a92f0032985c.jpeg)

URLが「jonathan-joestar」に変わりましたね！
この変更された表記ですが、デフォルトではActiveSupportの[parameterizeメソッド](http://api.rubyonrails.org/classes/ActiveSupport/Inflector.html#method-i-parameterize)を使っているようですね。

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

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

![スクリーンショット 2014-12-13 15.15.45.jpg](https://qiita-image-store.s3.amazonaws.com/0/27966/5d1c8c3c-ac10-e4bc-4b17-a0b0ec231201.jpeg)

名前は「Joseph Joestar」に更新されたのに、URLは「jonathan-joestar」のままです。
この場合は、``` should_generate_new_friendly_id? ```メソッドをオーバーライドします。

``` ruby: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](https://qiita-image-store.s3.amazonaws.com/0/27966/1d6a0e96-c607-d04e-5527-0af21a46baf4.jpeg)

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

![スクリーンショット 2014-12-13 15.21.09.jpg](https://qiita-image-store.s3.amazonaws.com/0/27966/4ba0eb08-d5e4-4278-2ed4-738c4ba06fcf.jpeg)

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

#### その他
* Translate Slugs
* Scoped Slugs

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

#### friendly_idの問題点

便利なfriendly_idですが、実は問題点もあります。
それはパフォーマンスです。

詳しくは[benchmark](https://gist.github.com/norman/1129745)を見てもらえればと思いますが、3倍ほど遅くなるケースもあるようですね。気には留めておく必要がありそうです。


### 所感

ある程度コードの見通しが立てやすくなり、「URLでid以外を使いたい」という目的を達するだけであれば、それなりに便利だなぁと思いました。
とはいえ、きちんと実装しようとするとメソッドのオーバーライドが必要であったり、ある意味無駄なカラムを追加しなくてはいけなかったりという意味で辛みはありそうです。
どうしてもURLをid以外のものにしたい！という要件が発生した場合は、色々と考慮が必要になるのかなぁと思いました。

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


#### 独自実装

``` ruby:user.rb
class User < ActiveRecord::Base
  # to_paramメソッドをオーバーライドしてid以外のparamを指定
  def to_param
    name
  end
end
```

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

``` ruby:routes.rb
Rails.application.routes.draw do
  resources :users, param: :name
end
```

なお、今回のソースを[friendly_id_sample](https://github.com/reizist/friendly_id_sample)にまとめておいておきますのでもしよろしければご参照頂ければと思います。
ここまで読んでいただきありがとうございました。