# 純手工打造!!!-實作票選系統 首先先提及到RESTful,全名為Representational State Transfer,簡單的說,就是把每個網址當做資源(Resource)來看待 在之前可能會遇到不同的工程師寫出來的路徑都不一樣 ```htmlmixed= /member_edit.php?id=2 /edit_member.php?id=2 ``` 網址沒有統一的規範,但在rails比較沒這困擾,如果 member 當做「資源」的話,那「編輯會員個人資料」的網址大概會長這樣: ```htmlmixed= /members/2/edit ``` 如果要查看「會員個人資料」: ```htmlmixed= /members/2 ``` ### 資源 Resource 要符合 RESTful 的網址設計,除了自己一條一條寫之外,更建議直接使用 Rails 提供的 resources 方法: ```ruby= Rails.application.routes.draw do resources :candidates end ``` 使用 `$ rails routes` 指令查看一下 ```ruby= $ rails routes Prefix Verb URI Pattern Controller#Action users GET /users(.:format) users#index POST /users(.:format) users#create new_user GET /users/new(.:format) users#new edit_user GET /users/:id/edit(.:format) users#edit user GET /users/:id(.:format) users#show PATCH /users/:id(.:format) users#update PUT /users/:id(.:format) users#update DELETE /users/:id(.:format) users#destroy ``` > 如果要知道更深入的用法 [Resource用法](https://railsbook.tw/chapters/11-routes.html) ## 第一步-建立routes 在routes.rb,建立resources ```ruby= Rails.application.routes.draw do resources :candidates end ``` ## 第二步-建立Controller 在controller的資料夾內新增 candidates_controller.rb 檔並輸入 ```ruby= class CandidatesController < ApplicationController end ``` 這時能發現,candidates_controller.rb 以及 CandidatesController有什麼不一樣(? 檔名會以蛇式的方式命名,而類別會以駝峰式的方式命名,而且類別必須以大寫為首! 這是rails的規範!必須要謹記在心!!!! 之後在rails server上便會出現此錯誤訊息,別擔心,就是在告訴你他找不到index這個檔案! ![](https://i.imgur.com/a9fC7Tr.png) 所以再回過頭來在加上 ```ruby= class CandidatesController < ApplicationController def index end end ``` 此時會看到這個錯誤訊息,來到第三步! ![](https://i.imgur.com/qNLcRWS.png) ## 第三步-建立views 此時請注意,rails有一個慣例,如果上述controller的名字會對應到view裡頭,views裡頭必須置放一個資料夾,例如:views/candidates/index.erb 所以我們馬上在views裡頭建立一個新的資料夾為candidates(需小寫且為複數),並在裡頭新增一個index.html.erb的檔案 ![](https://i.imgur.com/9zsvNmp.png) 那這時就會問了,這個檔案到底是html還是erb? >從後面進行解讀,他是一個erb檔,當翻譯完成後請把他當作html來對待! 這邊可以玩一個小把戲,如果再routes.rb內再加一行 `get '/hello.php' ,to:'candidates#index'` 意思是製作一個網址後方為hello.php的網址,會引導(to)至index這個方法也就是index.html.erb。混淆使用者以為這個是PHP撰寫的網站!!! ## 第四步-新增model model是存放資料的地方,那麼就要思考我們這個投票系統需要哪些資料? ![](https://i.imgur.com/T7fEaDS.png) 你可能會需要候選人名字、政黨、年齡、政見以及投票數! >目前如果要新增一個model檔可以直接在編譯器裡新增但還需要自己手動寫一個Migration檔,但目前還太吃力,所以建議直接在終端機上使用 `rails g model Candidate name party age:integer politics:text votes:integer`這樣會順便幫你新增一個Migration檔出來! 剛剛這段指令不只幫我們建立了一個model出來也自動在db/migrate內新增了一個20200504125704_create_candidates.rb預設檔是長這樣: ```ruby= class CreateCandidates < ActiveRecord::Migration[6.0] def change create_table :candidates do |t| t.string :name t.string :party t.integer :age t.text :politics t.integer :votes #後面要再接 ,default:0 因為我們預設是0票 t.timestamps end end end ``` **如果你model的類別是大寫單數,那麼migration檔裡的資料表就是小寫複數** 結果又出現錯誤了.... ![](https://i.imgur.com/m15gsat.png) 但不要擔心,仔細看訊息能發現她在說明有一個Migration尚未被處理,執行`$ rails db:migrate`試試看!就沒有錯誤訊息了!!並且會在db/migrate資料夾裡發現一個development.sqlite3的檔案,這個檔案讀不出來但他是一個資料庫一個名叫sqlite3的資料庫。 ## 第五步-新增表單 在index.html.erb檔新增一個超連結並且從rails routes 得知如果要新增東西必須要在 /candidates/new的地方才能使用 ```htmlmixed= <a href="/candidates/new">Add Canidate</a> ``` ![](https://i.imgur.com/DqH6cCO.png) 重新整理後一樣會出現要你新增一個new的action這時能夠立即反應是controller那邊少定義了一個new的方法! ```ruby= class CandidatesController < ApplicationController def index end def new end end ``` ![](https://i.imgur.com/415lgSr.png) 一樣的問題,直接在views裡給他一個new.html.erb檔!完成! ![](https://i.imgur.com/WHbHCla.png) 仔細看這段,如果今天我們要新增(Create)的話,我們的method必須使用POST,看後面,他會直接引導至candidates#create。 所以我們在new.html.erb檔內新增以下語法 ```htmlmixed= <h1>Add Candidate</h1> <form action="/candidates" method="POST"> <input type="text" name="abc" id=""> <input type="submit" value="go!"> </form> ``` 按下GO!之後會出現此錯誤訊息 ![](https://i.imgur.com/Vpp4GoB.png) 那就不用多講了... ![](https://i.imgur.com/ITZUOE7.png) 但這次的錯誤訊息與過往的都不一樣,他的意思是rails有內建一個隱藏欄位,目的是因為html是公開的大家都看的到,為了防著有心人士可能會有灌水..等等行為所以才會出現此錯誤告訴你沒有使用AuthenticityToken這個方法 所以我們在表單上再新增一個欄位 ```htmlmixed= <h1>Add Candidate</h1> <form action="/candidates" method="POST"> <input type="hidden" name="authenticity_token" value=" <%= form_authenticity_token %>"> <input type="text" name="abc" id=""> <input type="submit" value="go!"> </form> ``` 在終端機也能看見好像有一串亂碼送給rails,此時rails知道你有使用token這才沒有出問題,而這個亂碼每一次都是隨機不同的 ![](https://i.imgur.com/STKs8Li.png) 但我們也能利用 rails 內建的 form_for語法做出一樣的效果 ```htmlmixed= <%= form_for(Candidate.new) do %> #為了某一個model做一個表單並啟是新增候選人。但Candidate是一個類別要請他做一個東西出來的話要使用.new <% end %> ``` 能發現僅僅一句程式碼就已經幫我們把AuthenticityToken寫出來了 ![](https://i.imgur.com/UdBbpN6.png) 我們再繼續新增表單語法 ```htmlmixed= <%= form_for(Candidate.new) do |form| %> <div> <%= form.label :name %> #label的意思和html的label一樣 <%= form.text_field :name %> </div> <div> <%= form.label :party %> <%= form.text_field :party %> </div> <div> <%= form.label :age %> <%= form.text_field :age %> </div> <div> <%= form.label :politics %> <%= form.text_area :politics %> </div> <%= form.submit %> <% end %> ``` **這邊可以去看一下text_field是什麼東西!?** 但在views裡面其實它屬於比較被動的角色,如果要新增一個物件等等的應該交由controller來做所以可以更改一下程式碼 到controller在new的方法給他一個實體變數 ```ruby= def new @candidate = Condidate.new end ``` 並將views裡的`form_for(Candidate.new)`改成`form_for(@candidate)`即可 雖然重整後按下按鈕還是沒有反應但看終端機能發現有一包params ![](https://i.imgur.com/Rmgvoo0.png) 能發現我們填的東西其實都有在params的參數裡 **老師在純手工打造005這邊有提及到debugger的用法也能回頭參考如何使用這邊就不寫了** 那麼我們到controller的create方法裡寫 ```ruby= def create @candidate = Candidate.new(params[:candidate]) #一個蘿蔔一個坑, 將所有資料對應到正確的地方 if @candidate.save redirect_to '/candidates' #如果正確儲存的話就轉只到'/candidates' else #ng 這邊之後再處理 end end ``` 出現沒看過的錯誤訊息 ![](https://i.imgur.com/7qsfzy1.png) 出現此訊息是因為當我們試圖將整個Hash整包資料透過model寫進資料庫的時候會不知道你這包資料有沒有清洗過,在不知道的情況下就先直接把你擋下來了! 要如何解決這個問題呢? 我們先在程式碼內新增一段告訴rails我們這包東西是被清洗過的! ```ruby= def create clean_params = params.require(:candidate).permit(:name ,:party,:age, :politics) @candidate = Candidate.new(clean_params) if @candidate.save redirect_to '/candidates' else #ng end end ``` require其實和params[:candidate]很像都是要抓資料,permit則是清洗工具,告訴rails這些資料內我只要這幾樣東西其他都不要!!!! 接下來就是要查看我們的檔案是不是真正的被存進去了,到終端機開啟中控台模式(rails c)之後輸入Candidate.all ![](https://i.imgur.com/yKawqtg.png) 就能看見我們剛剛輸入的資料確實被存進去資料表裡,成功! 但直接跳轉到candidates其實感受不到自己真的成功,所以我們利用flash語法出現提示表示存檔成功,且flash只會出現一次!重整後即消失,非常適合我們使用 ```ruby= def create clean_params = params.require(:candidate).permit(:name ,:party,:age, :politics) @candidate = Candidate.new((clean_params) if @candidate.save redirect_to '/candidates' flash[:notice] = "Candidate created!" else #ng end end ``` 但因為之後會時常用到clean_params,所以乾脆直接把他設定為一個叫做 candidate_params的方法,以後呼叫更加方便,且這個方法只有內部使用,外部不會用到,所以可以設定隱私(private),我們來更改一下程式碼 **這邊使用到將method直接丟進Candidate.new當他的參數的用法** ```ruby= def create @candidate = Candidate.new(candidate_params) if @candidate.save redirect_to '/candidates' flash[:notice] = "Candidate created!" else #ng end end private def candidate_params params.require(:candidate).permit(:name ,:party, :age , :politics) end end ``` 接下來處理如果儲存失敗的狀況該怎麼寫 先在Model的candidate.rb檔內給一個規範如果沒有寫name就不能過,使用validates語法 ```ruby= class Candidate < ApplicationRecord validates :name,presence: true end ``` 再回到controller,如果失敗的話就重新回到new的頁面,這邊要注意使用render語法,後面接的:new並不是在呼叫new方法,而是在呼叫new.html.erb檔! render會去搜尋符合的檔案。 ```ruby= class CandidatesController < ApplicationController def index end def new @candidate = Candidate.new end def create @candidate = Candidate.new(candidate_params) if @candidate.save redirect_to '/candidates' flash[:notice] = "Candidate created!" else render :new #引導至 new.html.erb檔 end end ``` 這邊值得注意的是,前面為什麼要用實體變數?效果就在這裡了,我們先到new.html.erb檔看 ```ruby= <%= form_for(@candidate) do |form| %> <div> <%= form.label :name %> <%= form.text_field :name %> </div> <div> <%= form.label :party %> <%= form.text_field :party %> </div> <div> <%= form.label :age %> <%= form.text_field :age %> </div> <div> <%= form.label :politics %> <%= form.text_area :politics %> </div> <%= form.submit %> <% end %> ``` 這邊注意一下,form_for後面接的實體變數`@candidate`,在controller的new以及create方法裡也都有使用到`@candidate`,並不是巧合!而是透過這種方式,就算在render的時候他也會自動抓取@candidate,而且這個實體變數並不是全新的,而是本身就有料的,就是剛剛送過來的那些資料。那麼form_for就會把剛剛那些資料全部保留。 ![](https://i.imgur.com/OTPFoe1.png) 接下來製作index,印出我們資料庫裡所有候選人的名單,先在controller裡新增一段程式碼 ```ruby= def index @candidates = Candidate.all end ``` 取名也要記得,因為這是一個集合,所以變數用複數會比較容易懂 接下來到index.html.erb檔讓名單顯示出來 ```htmlmixed= <table> <tr> <td>name</td> <td>party</td> <td>age</td> <td>politics</td> <td>action</td> </tr> <% @candidates.each do |candidate| %> <tr> <td><%= candidate.name %></td> <td><%= candidate.party %></td> <td><%= candidate.age %></td> <td><%= candidate.politics %></td> <td>action</td> </tr> <% end %> </table> ``` 使用.each 印出每一個欄位,再來檢查目前是否正確無錯誤! ![](https://i.imgur.com/NNJd2IL.png) 但我們怕如果政見太多字的話,會破壞版面,希望我們點擊名稱的時候可以看見內容,這時可以更將name給他超連結,在看作法前,我們先到rails routes查看當我們要展示出內容的時候應該往哪個連結去 ![](https://i.imgur.com/jPdvXnc.png) 我們應該往show的網址過去,依照中間的格式可以得知是/candidates/:id 1. 使用 a href="" ```ruby= <% @candidates.each do |candidate| %> <tr> <td> <a href="/candidates/<%= candidate.id %>"><%= candidate.name %></a> </td> ``` 2. 使用rails的link_to ```ruby= <% @candidates.each do |candidate| %> <tr> <td> <%= link_to candidate.name,candidate_path(candidate.id) %> </td> #<%= link_to candidate.name ,candidate %>也可以 ``` 那麼就會問了`candidate_path(candidate.id)`這個是什麼? 讓我們回到rails routes,看一下prefix其實也是在告訴我們你也能利用這裡的路徑來寫,依照我們的需求 prefix顯示的是`candidate` 所以寫法會是 `candidate_path` 但我們還是需要id,所以在後面銜接一個 `candidate.id` 這邊切記,如果看到prefix有空白其實代表的是同上的意思,且path這個用法比起其他的用法而言更好!未來請多多使用 接下來製作show 讓我們的內容可以在網頁上被看見,一樣到controller給他一個show的方法並且在view裡頭新增一個show.html.erb檔 要讓controller找到我們對應的候選人,才有辦法印出資料這時必須在加一段程式碼 ```ruby= def show Candidate.find_by(id: params[:id]) end ``` 接著再到show 新增程式碼讓內容可以被看見 ```htmlmixed= <h1> <%= @candidate.name %> </h1> <ul> <li>party:<%= @candidate.party %></li> <li>age:<%= @candidate.age %></li> <li>politics:<%= @candidate.politics %></li> </ul> ``` 但如果我們現在在網址上給一個id是不存在在資料庫裡的,網頁依然會出錯,所以我們必須給他一個if判斷式,如果id為true就印出來,false就印出Not found! ```htmlmixed= <% if @candidate %> <h1> <%= @candidate.name %> </h1> <ul> <li>party:<%= @candidate.party %></li> <li>age:<%= @candidate.age %></li> <li>politics:<%= @candidate.politics %></li> </ul> <% else %> <h1>Not found!</h1> <% end %> ``` 還記得我們前面有製作一個如果不打name就會出錯的程式碼嗎? ```ruby= class Candidate < ApplicationRecord validates :name,presence: true end ``` 這邊到目前為止,系統知道發生錯誤了但其實我們在頁面上還看不出來錯誤,頁面還是沒有變化,所以接下來要製作發生錯誤的時候印出的訊息以及使用CSS稍微做一些美化 **這邊老師有使用終端機解釋怎麼出現錯誤的就直接跳過了** 我們這邊會使用到`.errors`以及`@candidate`的語法 ```htmlmixed= <%if @candidate.errors.any?%> <% @candidate.errors.full_messages.each do |message| %> <h3 class="wrong_message"><%= message %></h3> <%end%> <%end%> ``` `.errors`就是在說明`@candidate`是否有錯誤 `.any?`則是問說你裡面有沒有東西啊有的話印true `.full_messages`則是讓錯誤訊息顯示出來。但訊息是以陣列的形式,所以才需要用到`.each`將每個字都印出來。 之後我們在到app/assets/stylesheets/,這邊新增一個css的檔案,之後有關css的檔案統一都在這邊,且你會看見一個application.css的檔案,這個不要看裡面都是註解,其實有兩行程式碼是一定要留下來的!! ```htmlmixed= *= require_tree . #代表他之後會將所有的css都集結成一個檔案 *= require_self #表示在這個檔案下寫的css也都會一併打包 ``` 但我們還是在旁新增一個 candidate.scss的檔案,為何要用scss呢?因為比起css他更加方便,寫起來的速度也更加迅速! 當我們出現錯誤之後的頁面name的格子不再同一行了 ![](https://i.imgur.com/R9qLZXf.png) 檢查網頁原始碼能夠發現我們的name多了一個`class=field_with_errors`的`div` ![](https://i.imgur.com/SPk8A0e.png) 且div為區塊元素才導致換行,這時我們透過css樣式修改之後 ```css= .field_with_errors { display: inline-block; label { color: red; } input[type="text"] { border: 1px solid red; } } .wrong_message { width: 240px; font-size: 20px; background-color: red; color: #fff; line-height: 50px; } ``` 先簡單寫一下css語法目前會像是這樣 ![](https://i.imgur.com/iluKJO0.png) ## 第六步-新增編輯(修改)功能 回到 index.html.erb,在原本的action更改成update,可由rails routes得知到prefix的edit的路徑為 `edit_condidate_path`並且後方同樣需要ID ```htmlmixed= <td> <%= link_to 'update',edit_candidate_path(candidate.id) %> </td> ``` 並且記得到controller和view新增edit的方法與檔案。 那其實edit的畫面與create其實很像,都一樣會有form_for的表單所以可以直接從create直接複製過來。 這邊值得一提的是 ![](https://i.imgur.com/HWxIFun.png) ![](https://i.imgur.com/fAxrouE.png) 目前網站其實只看得懂GET跟POST,看不懂這兩個之外的方法,所以rails很聰明,`<input type="hidden" name="_method" value="patch" />`這句其實就是假裝她有一個PATCH的方法的意思,當回傳到資料庫時他就會知道你有一個_method的方法且是PATCH,就把你當作PATCH的方法吧!那如果使用PATCH的話,會往update的方向過去 而且在表單一樣的情況下,form_for知道你是從資料庫回來的他會猜你應該是要update,所以在form.submit這邊就算沒有更改語法,他也會從create candidate 變成 update candidate。 而且@candidate的關係,裡面的內容一樣會保存。 ![](https://i.imgur.com/HowGTHt.png) 之後便會往update去,所以下意識就要反應到要往controller去新增一個update的方法,那這邊跟新增的方法其實很像,首先要先抓取id才知道要從資料苦抓取誰的資料,並且使用if判斷句 如果成功的話就跳轉到`/candidates`,失敗的話就引導回edit.html.erb ```ruby= def update @candidate = Candidate.find_by(id: params[:id]) if @candidate.update(candidate_params) #update需要有一包更新的東西,這包東西必須被清洗過 redirect_to '/candidates' flash[:notice] = "Candidate updated!" else render :edit #引導至 edit.html.erb檔 end ``` ## 第七步-新增刪除功能 先在index.html.erb在製作一個delete的按鍵,並且回頭看rails routes看路徑,能夠知道是`candidate_path(candidate.id)` 但因為如果只寫這個路徑其實跟GET的路徑是一樣的,如果要讓rails區別你們之間要導向不一樣的地方,必須要讓他知道使用的method不一樣,那如何做呢? 使用`method:'delete'` 能夠讓rails知道你是用delete的方法傳過來的,那麼它將會幫你引導至destroy的方向。 ```htmlmixed= <td> <%= link_to 'update',edit_candidate_path(candidate.id) %> <%= link_to 'delete',candidate_path(candidate.id) ,method: 'delete' %> </td> ``` 然而就會看到老問題,又要請你到controller新增一個destroy的方法,新增後因為要讓資料庫知道你是哪一筆要刪除,所以按照慣例一樣要給他一段程式碼搜尋id並且使用destroy。 ```ruby= def destroy @candidate = Candidate.find_by(id: params[:id]) @candidate.destroy flash[:notice] = "Candidate deleted!" redirect_to '/candidates' end ``` 現在能夠正常執行刪除了但直接按下delete連提示都沒有就直接刪除一筆資料其實使用者體驗很差,所以要再加一段語法讓他出現提醒小視窗,以免誤觸! ```htmlmixed= <td> <%= link_to 'update',edit_candidate_path(candidate.id) %> <%= link_to 'delete',candidate_path(candidate.id) ,method: 'delete' , data: {confirm: 'are you sure?'}%> </td> ``` `confirm` 會出現一個小視窗 那麼到目前為止,表單的新增修改刪除都有了!!!其實基礎也都到這了!完成! --- ## 第八步-製作投票系統 很多新手,像我本人可能做到CRUD結束後,如果想新增其他系統就會卡住了,所以現在來教學該如何製作額外的投票系統吧! 第一步該處理的步驟應該是在頁面上再多新增一個vote的按鈕並且這個按鈕能夠連接到vote的頁面上,但本來的rails routes並沒有vote的路徑,所以我們必須要先手動新增一個路徑在routes裡!先到routes.rb撰寫 用post寫法 ```ruby= Rails.application.routes.draw do resources :candidates post '/candidates/:id/vote', to: 'candidates#vote' end ``` 確實多了一條路徑在routes裡 ![](https://i.imgur.com/4yr7SbD.png) 還有包在resources裡的寫法(推薦使用此寫法) ```ruby= Rails.application.routes.draw do resources :candidates do member do post :vote end collection do post :vote end end end ``` ![](https://i.imgur.com/0YirTWn.png) 這邊分享兩種寫法`member`以及`collection` `member` 除了新增路徑之外,還能夠擴充,仔細看終端機,他在中間還幫我們新增了:id。 `collection` 則是單純的幫我們新增一筆路徑 所以我們現在使用`member`比較恰當。 接下來再回到index更改剛剛vote按鈕的路徑 `<%= link_to 'vote',vote_candidate_path(candidate.id) %>` 重整後發現仍然出現錯誤 ![](https://i.imgur.com/lQNVIi7.png) 其實是在告訴你,這邊是使用get方法,但剛剛我們其實設定的是post所以必須在後面再新增一個方法告訴她我這邊是用post。 ```htmlmixed= <td> <%= link_to 'vote',vote_candidate_path(candidate.id),method: 'post' %> </td> ``` 完成後再到controller新增vote的方法摟! ```ruby= def vote @candidate = Candidate.find_by(id: params[:id]) @candidate.votes = @candidate.votes + 1 @candidate.save flash[:notice] = "Voted!" redirect_to '/candidates' end ``` 因為投票也需要知道要投給哪一位候選人所以一樣要找id並且我們在先前建立表格前就有給一個votes的變數且他為整數,所以可以直接相加。最後轉址。但這邊的` @candidate.votes = @candidate.votes + 1` rails有提供更好的寫法 ```ruby= @candidate.increment(:votes) ``` 接著再回到index給他一個位置顯示票數就大功告成了 ```htmlmixed= <td> <%= candidate.votes %> </td> ``` 但現在的投票系統也沒有辨識身份可以一直續投,至少要知道是誰投的,在哪裡投,時間是什麼時後。所以要建立一個model 先到終端機輸入`rails g model VoteLog candidate:references ip_address` ![](https://i.imgur.com/txn4FDx.png) 且確認db內的檔案沒問題後,具現化表格,`rails db:migrate` 之後再到controller更改一開始我們寫vote的方法,讓我們投票的時候也能記錄在votelog裡 ```ruby= def vote @candidate = Candidate.find_by(id: params[:id]) v = VoteLog.new(candidate: @candidate,ip_address:request.remote_ip) v.save # v = VoteLog.create(candidate: @candidate,ip_addsress:request.remote_ip) 用這種寫法可以省略save flash[:notice] = "Voted!" redirect_to '/candidates' end ``` 當然這段程式語言也能從不同角度撰寫,我們上面是用votelog的角度,以下示範用候選人的角度撰寫 ```ruby= def vote @candidate = Candidate.find_by(id: params[:id]) @candidate.vote_logs.create(ip_address:request.remote_ip) flash[:notice] = "Voted!" redirect_to '/candidates' end ``` ![](https://i.imgur.com/K8j1pno.png) 但你重整後會發現這個寫法會出現錯誤?跟你說找不到vote_log這個方法!原因是因為當初我們雖然有使用references讓vote_log指回candidate但並沒有讓candidate指回votelog,她不知道他要一對一還是一對多,所以必須在candidate的model再新增一行程式碼 ```ruby= class Candidate < ApplicationRecord validates :name,presence: true has_many :vote_logs end ``` 完成之後我們必須更改VOTES的參數,本來是直接抓取@candidate.votes,沒有記錄的投票,現在要抓的是vot_logs裡的次數才對,所以回到index更改一下程式碼 ```ruby= <td> <%= candidate.vote_logs.count %> </td> ``` 加上.count才知道妳總共投了多少票,vote_logs是記錄投你的人以及他的投票時間、ip位置。 但仔細看終端機能夠發現這邊出現了N+1的問題! ![](https://i.imgur.com/ETf1pzM.png) 如果不處理的話效能會越來越差! rails的model有一個特別的設計叫Counter_Cache,意思就是說在投票紀錄被記錄得當下,同時也會把候選人的票數記錄存一份到候選人的欄位,所以之後就不用再count原本的欄位了,因為自己候選人這邊就有一個欄位專門記錄投票。 在專案裡面db裡有一個schema.rb,這個檔案會記載我們這個專案所有的表格與欄位到時候這邊需要幫候選人列表這邊新增一個欄位來記載票數。 到終端機輸入`rails g migration add_count_cache_to_candidate`會幫我們新增一個乾淨的migration檔 ![](https://i.imgur.com/IPGY447.png) 由於我們要新增一個欄位所以可以使用add_column(table_name, column_name, type, options = {})語法在使用rails db:migrate具現化。 ```ruby= class AddCountCacheToCandidate < ActiveRecord::Migration[6.0] def change add_column :candidates, :vote_logs_count, :integer, default: 0 end end ``` 我們在到VotLog的model新增`counter_cache: true`當我寫下這段後,寫入這張票之後同時寫入更新的動作,並且更新到剛剛建立到候選人表單的vot_logs_count這裡來。 並在到index來將剛剛記票的語法再更改為 `candidate.vot_logs_count` ![](https://i.imgur.com/oGcrU8P.png) 再來看看終端機,就能發現資料庫只有查一次就把資料都拉進來了,因為我們只有撈一次候選人的表單,看來N+1的問題真的解決了!!! 但這邊也能將`candidate.vot_logs_count`更改成`candidate.vot_logs.size`,size能夠直接讀取counter_cache。 但進到rails console後能發現我們投票總數好像怪怪的? ![](https://i.imgur.com/gHpAmV5.png) ![](https://i.imgur.com/iOAVb14.png) 為何總數對不上?原來是因為我們counter_cache太晚進場了導致前面的票數沒有被計算 那麼該如何解決? 答案就是`reset_counters` 可以直接在 rails c直接執行但更希望大家在rake執行 我們先在/lib/task裡先建立一個 reset_counter.rake並輸入 ```ruby= namespace :db do #在db裡 desc 'Reset Count Cache' #描述 task :reset_counter => :environment do #引入rails 環境 puts "prepare reset counters" Candidate.all.each do |candidate| Candidate.reset_counters(candidate.id,:vote_logs) end puts "done!" end end ``` 最後在終端機輸入`rails db:reset_counter`就能看見記票正常了! 基本上投票系統已經差不多了,接下來就是要來整理程式碼摟~ ## 第九步-整理code 首先到controller能看見`@candidate = Candidate.find_by(id: params[:id])` 被使用了多次,那其實可以把它定義成一個方法到private然後使用before_action放在最上面,這樣在執行controller時全部的方法都會先自動讀取這段程式碼,由於不是每一個方法都需要,所以使用except將不需要的挑出來!以免發生錯誤,也能使用only,only表示需要的有誰! ```ruby= before_action :find_candidate , except:[ :index , :new , :create] ``` 接下來 ```ruby= flash[:notice] = "Voted!" redirect_to '/candidates' ``` 因為rails不停在更新會挑選幾個最常用的語法加以簡化所以flash其實可以簡化成 ```ruby= redirect_to '/candidates' , notice: "Voted!" ``` 再來,其實rails裡有共用版面的連結,在/layout裡其實就是共用版面的意思,否則其他html檔怎麼都可以使用且都不用寫body等等的呢? 因為都已經在共用版面上寫了。 所以能夠注意到本來的notice我們現在不想要放在上面了其實可以將它放到共用版面`application.html.erb`的body上且可以指簡寫成`<%= notice %>`即可! edit和new的程式碼其實很像,就可以使用共用版面的方式新增一個_form.html.erb,不過記得這個就不能放在/layout了,否則讀取不到資料!將重複的程式碼貼過去! ```htmlmixed= <%if @candidate.errors.any?%> <% @candidate.errors.full_messages.each do |message| %> <h3 class="wrong_message"><%= message %></h3> <%end%> <%end%> <%= form_for(@candidate) do |form| %> <div> <%= form.label :name %> <%= form.text_field :name %> </div> <div> <%= form.label :party %> <%= form.text_field :party %> </div> <div> <%= form.label :age %> <%= form.text_field :age %> </div> <div> <%= form.label :politics %> <%= form.text_area :politics %> </div> <%= form.submit %> <% end %> ``` 那原本的edit以及new的畫面怎麼連接呢? 使用 partial render(局部渲染)的方法,為了要與其他檔案有區隔,被連接的檔案需要再檔名前方增加一個底線(_) 記得在edit以及new的頁面新增一行`<%= render 'form' %>`的程式碼 只是說這樣的整理方法還是有一些問題,畢竟form_for裡面還是有實體變數(@candidate)存在,因為_form.html.erb這個檔案他不能獨立苟活,就像是如果一個網頁,有廣告區塊,這些廣告區塊都要期待說前面的action記得給他實體變數,那當action忘記做的時候就會出錯,所以比較好的方式是讓這個檔案變成被動的角色。所以我們更改一下寫法 edit 以及 new.html.erb ```ruby= <%= render 'form', candidate: @candidate %> #後面是一個hash ``` 傳一個candidate的變數給_form,把實體變數再轉一手給他,所以可以把所有的實體變數,轉為區域變數。 ```htmlmixed= <%if candidate.errors.any?%> <% candidate.errors.full_messages.each do |message| %> <h3 class="wrong_message"><%= message %></h3> <%end%> <%end%> <%= form_for(candidate) do |form| %> <div> <%= form.label :name %> <%= form.text_field :name %> </div> <div> <%= form.label :party %> <%= form.text_field :party %> </div> <div> <%= form.label :age %> <%= form.text_field :age %> </div> <div> <%= form.label :politics %> <%= form.text_area :politics %> </div> <%= form.submit %> <% end %> ``` 簡化程式碼也就告一段落了,重點是這個投票系統到底看不看得懂!?多加練習以後不需要看scanffold也能自己幹出來!!!