Friendly_Id

在rails裡找資料的時候通常都會用id當索引去例如:

User.find(params[:id])

所以網址上就會像這樣http://localhost:3000/user/1
最後的那個數字其實就是資料庫會遞增的數字,然而這樣的網址其實沒什麼太大的意義,而且很容易被猜,因為只要把後面的網址改個數字遞增上去就可以看到很多東西了,那要如何改變網址讓他變得比較有功能性而且不會被猜,這時候就可以透過friendly_id這個gem了。

安裝所需要的gem

gemfile
gem 'babosa'
#解決中文字網址問題的gem

gem 'friendly_id', '~> 5.1.0'
#寫這邊文章時最新的版本

安裝時如果有遇到問題的話把這兩個gem放在gemfile的最上面試試看,有些gem跟babosa會有衝突
執行bundle install

建立 friendly_id

rails generate friendly_id

執行之後會建立出兩個檔案

 create  db/migrate/20150220082838_create_friendly_id_slugs.rb
 create  config/initializers/friendly_id.rb

執行rake dbmigrate

這個動作會建立friendly_id_slugs的table(等等會講到這是做什麼用的)
另一個就是建立friendly_id的設定檔,在這個設定檔裡面可以去設定一些參數讓friendly_id更容易使用,裡面也有教一些使用上的小技巧。

#friendly_id基本用法
這邊我們用scaffold建立一個基本的CURD來實作。

 rails g scaffold User name:string
    rake db:migrate

之後在建立好的user model加上friendly_id

app/models/user.rb
class User < ActiveRecord::Base
  validates :name, presence: true
  extend FriendlyId 
  friendly_id :name  
end

之後就可以用Model.friendly.find的方式找資料

UserController.rb
def show
    #@user = User.find(params[:id])#原來的方式

    @user = User.friendly.find(params[:id])#用friendlyid的方式

 end

用下面的方式也可以

User.rb
# 用在frienldy_id所設定的欄位當索引去找資料,這邊設定的是name這個欄位

User.friendly.find('sean') 
#用friendly method一樣可以找id

User.friendly.find(3)  
#原本的find method一樣可以用

User.find(23)   
#error 找不到

User.find('sean')                   

如果有很多地方要從.find(params[:id])變成.friendly.find(params[:id])的話,可以直接去friendly_id的config把 config.use :finders註解打開,這樣不用改code就可以直接用。

使用slug

現在在url的路徑上是使用friendly_id所設定的欄位資料當作id(這邊是設定name),但如果名字中間有space的話在url上就用%20代替space,例如原本是john cena在url上就會變john%20cena看起來就不是這麼順眼。這邊我們可以加上一個slug的欄位,用這個欄位來代替name的欄位當作索引值。

建立一個migration檔案為user加上slug欄位

rails g migration add_slug_to_users slug:index

invoke  active_record
create    db/migrate/20150220083718_add_slug_to_users.rb

rake db:migrate

修改user model

加上use: :slugged

app/models/user.rb
friendly_id :name, use: :slugged

用rails c 來看會看到原本已經存在的資料上多加了slug欄位並且設定為nil,這時候把所有的資料做save的話friendlyid就會自動產生slug資料

#在rails c 裡執行下面一行程式,friendlyid就會自動產生slug資料
User.find_each(&:save)

之後在查詢該筆資料會看到

:id => 27,
:name => "john cena",
:created_at => Wed, 27 Apr 2016 10:44:50 UTC +00:00,
:updated_at => Wed, 27 Apr 2016 10:44:50 UTC +00:00,
#friendlyid用name欄位的資料產生了slug的資料
:slug => "john-cena",

url也會從http://localhost:3000/users/john%20cena變成http://localhost:3000/users/john-cena

中文字怎麼辦?

如果輸入中文字的話會發現slug變成像c6400f40-eea9-44bc-9cf3-00ba125479f6這樣子的亂碼,url也會變成
http://localhost:3000/users/c6400f40-eea9-44bc-9cf3-00ba125479f6,在一開始我們有裝了一個gem叫做babosa來解決中文網址問題,那們實際上要怎麼使用呢?很簡單我們只要model裡覆寫normalize_friendly_id這個method就好了,之後網址就可以顯示中文字了。

User.rb
  def normalize_friendly_id(input)
     input.to_s.to_slug.normalize.to_s
  end

變更name但是slug不會跟著變更?!

在更新資料的時候會發現這一個問題,假設我原先的name是john cena,然後我修改了名稱改成john Connor,這時候會發現我的網址還是停在 http://localhost:3000/users/john-cena ,然後我該筆資料的slug並沒有跟著更新還是停留在john cena,WHY?!
這是因為在更新name資料的時候friendlyid預設並不會自動幫你更新slug所以要自己更新,更新的方法就是把slug的欄位設成nil並且儲存,這樣friendlyid就會依照你name的資料去更新slug的資料。

UsersControllers.rb
def edit
  @user = User.friendly.find(params[:id])
  @user.slug = nil
  @user.save
end

有沒有更簡單的更新方式?

答案是有的! (那上面講那麼多是在講屁)
其實只要覆寫should_generate_new_friendly_id這個method就會自動去更新了。

User.rb
  def should_generate_new_friendly_id?
    slug.blank? || name_changed?
  end

我想要不同的名稱都連到同一個頁面可以嗎?

不行我就不會寫了 答案是用[:history]
當我把名稱從john cena改成john cena1當然網址就會從http://localhost:3000/users/john-cena
變成http://localhost:3000/users/john-cena1404,之後如果再用原本的網址去連的話就會出現 not found (廢話) 但有時候會有種情況是想要不同的網址都可以連到同一比“資料”,例如john-cena1,john-cena2,john-cena3,john-cena4都可以連到john-cena的頁面去,這時候我們就可以用friendlyid所提供的[:history]達到這件事

user.rb
#在use裡面多加一個history的屬性,這樣就會在最一開始建立的friendly_id_slugs這個table建立歷史紀錄

friendly_id :name, use: [:slugged, :history]

之後只要使用曾經有使用過的slug的話都可以連到現在的資料上,實際的更新流程是這樣的

1.原本的資料 name = john cena # 
2.更新資料 name = john cena1
3.不管用user/john-cena或是user/john-cena1都會連到user/john-cena1
4.更新資料name = john cena2
5.現在用user/john-cena 或 user/john-cena1 或 user/john-cena2 都可以連到user/john-cena2
6.更新資料name = john cena
7.現在用user/john-cena 或 user/john-cena1 或 user/john-cena2 都可以連到user/john-cena

統一網址

在用了history後只要用曾經用過的slug都可以連到現在的資料就像上面的表一樣,但這邊會發現一件事情就是就算最後會連到同一筆資料但網址看起來還是不一樣,例如:用user/john-cena2可以連到user/john-cena但網址還是顯示user/john-cena2,但如果想要用user/john-cena2可以連到user/john-cena但網址要顯示user/john-cena該怎麼做呢 (這邊跟繞口令一樣) 這邊就要去改寫userController的show的部分了。

userControllers.rb
def show
 if request.path != user_path(@user)
       return redirect_to @user, :status => :moved_permanently
   end
 end

如何清除slug history

如果有太多的history的話第一個會變成難管理,第二其實這種不一定每個slug都會用到的history資料很佔資料庫空間
所以沒用的還是要把它清除。這邊其實我卡了有點久因為在rails c裡一直找不到table,當初建立的名字叫做friendly_id_slugs所以就想說用FriendlyIdSlug去找這個table,結果怎麼找都找不到 直到我膝蓋中了一箭 不是直到我看到這一篇friendly_id, Delete slug from history才知道原來要用FriendlyId::Slug去找啊!!這一小節算是自己踩過的雷然後紀錄一下。

重複的slug?!

在設定user name 或是 peoduct name 或是 post title的時候可能會遇到id重複性的問題,如果有不同的同人叫一樣的名稱怎麼辦?其實friendlyid也幫你處理好這件事情了,他會自動在重複的名稱後面加上一個UUID的亂碼區別他

car = Car.create :title => "Peugeot 206"
car2 = Car.create :title => "Peugeot 206"
car.friendly_id #=> "peugeot-206"
car2.friendly_id #=> "peugeot-206-f9f3789a-daec-4156-af1d-fab81aa16ee5"

使用slug_candidates客製化url或是處理重複id

User.rb
  #原本改用`:name`的地方改成`:slug_candidates`

  friendly_id :slug_candidates , use: [:slugged]
  #建立一個slug_candidates method處理條件

    def slug_candidates
      [
        :name,
        [:name, :city]
      ]
    end

slug_candidates的概念是這樣的,當指定的欄位出現重複的名稱的時候,會去找下一個指定的欄位去組成該筆資料的slug
所以要用slug_candidates的時候,也要有其他欄位可以指定使用,像這邊我就會在user table裡再加入一個city欄位(建立方法就不詳細講了就用建立的migration檔去做吧)。

#假設我現在有一筆資料
name:john cena , city:taiwan # url會是users/john-cena
#我又新建一筆資料也叫做john cena
name:john cena , city:tokyo #這時候url就會變成 users/john-cena-tokyo

當然條件式是可以一直增加下去的,前提就是要有足夠的欄位可以當作條件來用。

slug 唯一值

如果要slug或是name是唯一值的話(不重複),有兩個地方要修改
第一步是在model的validates加上uniqueness: true

User.rb
validates :name, presence: true, uniqueness: true

第二步是在index上加上unique: true

migration/xxxxxxx_add_index_to_user_name.rb
add_index :users, :slug ,unique: true
add_index :users, :name , unique: true

以user來說通常user name不會是唯一值,但slug可能會是唯一值,如果以product來說的話通常product name跟slug會是唯一值,一般來說不會有兩個不同的商品叫做一樣的名字。

可能會有人問說為什麼在model的validates要加上uniqueness: true,然後在model table的地方也要加, unique: true不是只要加一個就好了嗎?
給同樣跟我一樣有好奇心的人的回答是在model加的validates uniqueness: true是指在驗證表單的時候所做的,但並不能保證在資料庫裡也是唯一資料所以在資料庫的部分也要加上unique: true,而且如果只加了資料庫的部分的話,出現重複資料時會直接爆error頁面,對使用者來說不是個很好的用戶體驗,所以在model裡加上validates uniqueness: true這樣出現重複資料時至少會有一個notice跟使用者說名稱重複了請換一個名稱之類的提醒。

結尾

呼~沒想到一個簡單的friendly_id竟然會寫到這麼長,其實還有些東西沒寫有興趣的人就去看看官方的資料吧,我應該也沒辦法把所有情況都寫出來,如果有看不懂的地方請留言跟我說,我會盡量把文章改的淺顯易懂一點。

參考資料

Railsでfriendly_idを使って検索エンジンにわかりやすいURLを作成する
美化網址 GEM - Friendly_id
friendly_id使用Guide,很多使用技巧
使用 Babosa 配合 Friendly_id 解決中文網址問題

comments powered by Disqus