0%

Ruby on Rails 網站開發 練習 - 015 ( 效能改善 N+1 Query)

練習 - 015 (效能改善 N+1 Query)

上一堂課,已經可以正確的顯示投票紀錄。但是透過rails console可以看到 server 的 log,每次投票都會計算一次。 (N+1 Query)

圖


為了改善效能問題,此時我們就需要使用Counter Cache

Counter Cache 概念

  • 當我們的票寫入到投票紀錄的當下,同時會更新一份候選人票數到「候選人表單」的欄位中。
  • 這樣我們在讀取候選人票數的時候,就可以不用再 count 票數的欄位。
  • 因為在候選人的table已經有專門的欄位記載票數。
  • 這個欄位的「數字」會在每次按下vote的時候更新欄位的數字。

所以我們現在需要幫原本的Candidate Model增加欄位:

  1. 欄位名稱(依照慣例<關聯的table名字_count>):vote_logs_count
  2. 欄位資料型態:integer

打開終端機輸入指令產生一個migration,rails g migration add_counter_cache_to_candidate

再到 db/migrate/ 可以找到剛剛透過終端機指令產生的 migration 檔案,檔案裡長出來的class名稱AddCounterCacheToCandidate其實會對應上我們輸入的名字。


上網找尋 Rails 新增欄位的方法add_column

  • add_column(要加入的表單名稱, 欄位名稱, 資料型態, option = {})
    圖

進到AddCounterCacheToCandidate migration檔案在change方法裡面增加一行程式碼。

1
2
add_column :candidates, :vote_logs_count, :integer, default: 0
#加入欄位:加進candidate表單, 欄位名稱vote_logs_count, 型態是integer, 預設值為0

這裡要非常注意table名稱是複數(candidates)!
這裡要非常注意table名稱是複數(andidates)!
這裡要非常注意table名稱是複數(candidates)!

這裡已經確認migration檔案裡面的內容沒有問題,接下來就是要把這個migration「具現化」,使用終端機執行rails db:migrate

接著打開db/migrate/schema.rb來看看是否剛剛新增的欄位有加入成功。登愣!!!恭喜成功加入。

但是不是增欄位這樣就有效果,還要進到VoteLog Model裡面做一件很神奇的事情。要在原本的belongs_to :candidate後面加上counter_cache: true

增加counter_cache: true的用意,就是當我們把那張票寫入到資料庫的同時,也會執行「更新票數」的動作。把票數更新到哪裡呢?

  • 會把票數更新到candidates表單的vote_logs_count欄位

來確認看看是否有成功寫入。重新整理瀏覽器。確認目前頁面資料。


按下vote,投給ddd候選人8票。

確定每次的票數都會同步更新到畫面上,目前ddd候選人有10票。

觀察一下終端機上rails server顯示的紀錄,每次投票的時候有做了哪些事情,先截圖其中一筆資料。

確實在寫入票數資料的同時也去更新了candidates表單的vote_logs_count欄位。

但是我們也會發現現在rails server 每次撈資料的時候,都會再去撈一次,是因為在view/index.html.erb<%= candidate.vote_logs.count %> 它會去vote_logs這個表單
去撈count。

不是從candidates表單裡面的vote_logs_count欄位撈資料。所以要更改為<%= candidate.vote_logs_count %>

這時候重新整理瀏覽器畫面,看到的票數資料才是正確的。對eee候選人按下vote


我們可以發現 rails server 的紀錄顯示只有撈一次而已。

是不是發現效能有變更好一點了。但其實還可以再更優化。就是在view/index.html.erb<%= candidate.vote_logs_count %>去修改。

我們現在是讓它直接讀取那個欄位的資料,但是其實還有一個更「漂亮」的寫法。我們一樣透過vote_logs,畢竟candidatehas_many :vote_logs

所以我們可以把<%= candidate.vote_logs_count %>改成
<%= candidate.vote_logs.size %>

為什麼用這樣的寫法?目前重新整理頁面看到的結果兩種寫法顯示的結果是一樣的,可以為什麼要用.size而不是直接讀取vote_logs_count欄位。為什麼用.size的寫法?

  • 原因就是:以程式碼的可讀性,使用<%= candidate.vote_logs.size %> 比較可以看出關聯性。

不曉得到這邊大家有沒有發現一個事情,現在顯示的票數,好像跟一開始的不太一樣。打開rails console來看一下目前總票數。總共投了24票。

可是瀏覽器畫面顯示的票數似乎不太一樣。

是因為後來才加入的counter cache,現在瀏覽器畫面顯示的票數是counter cashe紀錄的票數。

我們可以google搜尋關鍵字:rails api reset counter_cache

找到了可以使用的方法,但是我們要測試看看,可以用rake語法來寫一個task。要寫在哪個目錄底下呢?
我們到db/lib/tasks目錄裡面新增一個任務rest_counter.rake檔案。

這樣以後在終端機輸入rake -T就可以在任務列表中找到我們今天新增的這個reset_counter這個 task。


接著我們就要來寫程式了,進到reset_counter.rake,在tesk ...do...end裡面來寫程式。

再多增加一行puts "prepare to reset counter",表示執行reset_counter前會先印出prepare to reset counter

執行完畢印出done!,所以在最下面增加一行puts "done!"

終端機輸入rails db:reset_counter

這時候我們再回到瀏覽器重新整理畫面,會發現票數就更新了。
從原本的 0票 8票 1票 —> 7票 10票 7票。這樣票數就正確了!!!


目前我們已經有 新增/修改/刪除/投票 都有成功寫出來了。但是現在我們的code長得樣子是滿雜亂的,需要再整理一下。把一些功能寫成方法,這樣整個專案的程式碼可讀性會更高。

下一堂課程,整理程式碼

參考來源:為你自己學 Ruby on Rails (https://railsbook.tw/)