我們先想像一個情況:你目前的專案中,有user及comment兩種model:
class User < ActiveRecord::Base has_many :comments end class Comment < ActiveRecord::Base belongs_to :user end
如果在一個頁面需要將使用者列表,並同時顯示他所擁有的回應數時,我們直覺上會在迴圈中,一個個進行user.comments.count,並同時產生一個db query,這樣的情況就是經典的「N+1」問題。
讓我們把counter cache住吧!
在系統架構上還滿常用到的一個手法是,將「讀取機會比寫入機會大很多」的數值記錄起來,例如這次會講到的counter cache,就是乾脆在user資料表上開一個comments_count欄位,在新增或刪除comment的同時,記錄他目前所擁有的comment數。如果你對rails有一定程度的了解,應該會想到使用ActiveRecord callback的機制在新增或刪除comment後進行user.comments_count的更新。但是也因為這是一個還算常見的作法,Rails本身就內建了這個功能,能夠讓我們所做的工減少很多。
進入正題
1. 新增counter欄位
首先我們要先在user資料表中開一個專門儲存comment數的欄位,由於rails「Convention over configuration」的核心精神,這個欄位應該被命名為 [resource複數]_count
,否則rails可能會認不得他。以這個例子來說,就是要新增一個叫 comments_count
的欄位,並且預設值為0。新增欄位的過程就不贅述了。
2. 修改relation選項
接下來要做的是在comment.rb中的relation上加上一個選項::counter_cache
,告知Rails這個relation是要做counter的同步動作的。程式碼變成:class Comment < ActiveRecord::Base
belongs_to :user, :counter_cache => true
end
如果目前的專案增加這個欄位時,並沒有存在任何資料,也就是所有的user所擁有的comment是0,這樣的情況下其實這樣已經ok了。但如果之前就已經有一些user有創造comment,這時我們就要做counter的更新。
更新counter cache
可能有些人會想說,直接遍尋所有user,讓他們的 comments_count = user.comments.count 就好。很不幸的,Rails對於counter欄位有保護機制,我們不能這樣直接修改他,而必須使用
update_counters(id, counters)
或 reset_counters(id, *counters)
兩種方法,以reset_counter來說,migration的程式碼就像這樣:User.find_each do |user| User.reset_counters user.id, :comments end