Ruby on Rails CRUD

候選人票選系統純手工打造

基本新增修改刪除介紹

.使用vscode編輯

.參考資料: 為你自己學Ruby On Rails

00 開始之前

step 1. 使用指定Rails版本生成專案:

rails _版本_ new 專案名

% rails _6.1.5_ new hello_rails

step 2. 進到該資料夾 cd hello_rails git版控初始化 git init

step 3. 第一次commit git add . git commit -m"init commit"

step 4. 確定環境正常運作 rails s 進到localhost:3000確認畫面

01處理Route,新增(建立)路徑

step 1. 找到Route檔 confing -> routes.rb (或是使用ctrl+p搜尋)

step 2. 做出候選人相關資源/路徑 在routes.rb檔案裡輸入

1
resources :candidates

rails routes可查看路徑對照表 可看到做出的八條路徑,對應到七個action

candidates#index
candidates#create
candidates#new
candidates#edit
candidates#show
candidates#update
candidates#update
candidates#destroy

若只想做出特定路徑,如index跟show,示範如下

1
resources :candidates, only: [:index, :show]

step 3.新增controllers
app -> controllers 按右鍵新增檔案

candidates_controller.rb

接著,在檔案裡面輸入

1
2
3
4
5
class CandidatesController < ApplicationController

    def index
    end
end

step 4. 新增view
app -> views 按右鍵新增資料夾

candidates

在candidates裡再新增檔案

index.html.erb

輸入

1
<h1>Hi</h1>

這時,就可以在

localhost:3000/candidates

看到剛剛輸入的Hi了

step3-step4手工打造的部分,可使用指令

% rails g controller candidates

02 新增Model

step 1.使用指令新增model

要先想一下Candidate的Model要有哪些欄位及其對應的資料型態

Model不等於資料庫/資料表,是一個抽象層的概念

這個過程如果要純手工,會涉及到需手動建立migration檔來描述這個資料表要長什麼樣子,需要寫一些語法,對現階段的我們來說會有點吃力,建議直接使用指令

% rails g model Candidate name:string party:string age:integer politics:text votes:integer

如果是文字型態(string),可省略不寫

% rails g model Candidate name party  age:integer politics:text votes:integer

這個指令會幫你做兩件事

  1. 建立candidate這個model
  2. 會根據你給它的欄位,建位一個migration(主要目的)

慣例:如果model叫Candidate(大寫單數),資料表migrate中的表格table就會是candidates(小寫複數)

step 2.描述檔具現化

% rails db:migrate

會在 db 這個資料夾建立檔案 檔案名為:

development.sqlite3

在rails預設使用的資料庫就是sqlite,它是一種檔案型的資料庫,效能不好,但簡單易用,以練習來說還算堪用,在我們這次開發過程中所有資料都會寫到這裡。

在config/database.yml裡的adapter可以確認本次專案所使用的資料庫系統

在不同的環境(開發、測試),會存放在不同的資料庫

例如
開發:

database:db/development.sqlite3

測試:

database:db/test.sqlite3

03 新增候選人表單

step 1. 確認表單路徑

% rails routes

找到

candidates/new

step 2. 寫入連結
回到00章節中 step4 裡在candidates建立的index,在裡面寫入

1
<a href="/candidates/new">Add Candidate </a>

就可以在

localhost:3000/candidates

頁面看到我們設定的超連結了。但這時點下去這個超連結,應該會出現在錯誤訊息,因為我們還沒有做出相對應的action給它。

step 3.定義 new action
回到

candidate_controller.rb

定義 new action

1
2
def new
end

step 4. 做出 new 的view

在views -> candidates 新增檔案

new.html.erb

輸入

1
<h1>Add Candidate</h1>

就可以在localhost:1313/candidates/new看到這行新增履歷的文字了

03-1 手刻建立表單

step 1. 建立 form 表單,使用POST方法
new.html.erb 檔裡寫入

1
2
3
4
<form action="candidates(要去什麼地方)" method="POST(用什麼方式送)">
    <input type="text" name="abc">
    <input type="submit" value="go!">
</form>

這邊要稍微記一下(或是可以參考路徑對照表),在我們從candidates/new這邊,用POST方法往candidates送,那接著就會要找create這個action

candidate#create

step 2.定義create action
回到

candidate_controller.rb

定義 create action

1
2
def create
end

step 3. 認識rails預設的保護機制
為避免票務或其他相關設計的灌水問題,在使用form表單要往某個地方送的時候,rails會要求要有 authenticiy token。我們可以用以下的 ruby 方法<%= form_authenticity_token %>讓每次載入頁面時,會生出一段authenticity token。按下送出時這段token就會跟著表單一起送到 action。目的是要透過由我們發出去的 authenticity token 來確認用戶是從我們網站進來的,進而達到避免有心人士使用自己寫的程式來不停送出(票數)灌水。

1
<input type="hidden" name="authenticity_token" value="<%= form_authenticity_token %>">

03-2 使用form_for小幫手

在rails裡,有個叫form helper的小幫手,使用它來幫助我們建立表單。就不用像03-1過程這麼繁瑣。

step 1. 使用form_for
form for 是為了某個model建立表單, 語法如下form_for(model) ,套用在我們這次的實作,因為Candidate是一個class,所以我們在後面加上.new,讓它變成一個model。並把它定義在candidates_controller.rbnew裡。並給它一個實體變數(@),view才能拿的到。

(之所以要把model定義在controller裡,是因為在 MVC 結構裡,view 的角色就是單純把東西印出來,不要做產生物件或邏輯運算之類的事,屬於被動角色)

1
2
3
def new
  @candidate = Candidate.new
end

回到candidates/new,把定義好的實體變數接在form_for裡

<%= form_for(@candidate) %>

form_for 發現後面接的是全新的model的時候,會幫你長出相對應的路徑、方法包含token全部寫完。

step 2. 搭配 ruby 的 程式碼區塊(Block)
搭配 ruby的程式碼區塊(Block)do end加上小幫手的.text_field方法來產生欄位,及.label方法來做出標籤

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<%= form_for(@candidate) do |form| %>

<%= form.label :name %>
<%= form.text_field :name %>

<%= form.label :party %>
<%= form.text_field :party %>

<%= form.label :age %>
<%= form.text_field :age %>

<%= form.label :politics %>
<%= form.text_area :politics %>

<%= form.submit %>
<% end %>

03-3 create action

step 1. 得到parameters
params 是一個方法會回傳 ActionController::Parameters 物件,在實務上會像一個 hash。所以我們可以透過params來取得parameters。
在 03-2 把表單建立好,按下送出之後,在 log 中可以發現一包參數(parameters),裡面包含token、欄位所填的資料 ,我們就可以透過params這個hash,使用candidate這個key來取得相對應的一包hash(value)

1
params[:candidate]

丟給Candidate.new這個model。接著使用(@)實體變數建立物件,叫這個物件存檔(save)。如果成功,就把頁面導向候選人列表頁。 如果失敗,使用 render 這個方法,借 new 的頁面重新渲染。render :new

1
2
3
4
5
6
7
8
9
def create
    @candidate = Candidate.new(params[:candidate])

    if @candidate.save
        redirect_to '/cnadidates'
    else
        render :new
    end
end

補充說明 1:hash是什麼?
hash是由key、value所組成的資料,設計者只要根據Key值就可以取得相對應的資料。
如何建立hash?
1.使用hash類別,new一個給他。
user = Hash.new
2.使用大括號
user = { name: ‘Judy’, age:8 }

補充說明 2:製造巧合
在 new 及 create 取了一樣的實體變數名字,讓render :new 可以順利在空中抓取資料並透過form_for讓值擺放在相對應的欄位。

def new
    @candidate = Candidate.new
end
def create
    @candidate = Candidate.new(candidate_params)
end

step 2. 清洗params
當我們試圖要把整包hash的資料透過 model 寫進資料庫的時候,model會發現這包網路上抓下的東西還沒清洗(過濾檢查)過(使用者可以很輕易的在頁面上編輯加欄位)。預設會擋下來。 使用 require 這個方法,抓取candidate並使用permit只允許部分欄位過來。

1
params.require(:candidate).permit(:name, :party, :age, :politics)

接著,設定一個變數名稱給它,或是知道我們後面會很常再使用它,可以直接定義方法。再把它放進Candidate.new這個model。
第一個方式如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def create
    clean_params = params.require(:candidate).permit(:name, :party, :age, :politics)

    @candidate = Candidate.new(clean_params)

    if @candidate.save
        redirect_to '/cnadidates'
    else
        render :new
    end
end

第二個方式,把它單獨定義一個方法,讓它可以被重複使用,因為不需被外部存取,可以加入 private 註記,變成私有方法。是一個比較好的作法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15

 def create
    @candidate = Candidate.new(candidate_params)
    
    if @candidate.save
        redirect_to '/candidates'
    else
        render :new
    end
  end

private
def candidate_params
    params.require(:candidate).permit(:name, :party, :age, :politics)
end

step 3. 確認存取的資料 在rails consolerails c輸入

Candidate.all

就可以印出目前Candidate這個表格裡的所有候選人

step 4. 使用者提示 flash flash 是快閃訊息的意思,只要印在畫面一次之後,就會消失。
flash 本質上就是一個hash。key的慣例上會使用notice,後面再接上要給的訊息。寫的方式如下:

1
flash[:notice] = "Candidate created"

接著,就在要印出這個flash的html頁面寫入

1
<%= flash[:notice]%>

03-4 加入驗證

在 app -> models 裡的 candidate.rb 檔,根據我們的驗證條件寫入 validates

1
2
3
calss Candidate < ApplicationRecord
    validates :name, present:true
end

03-5 撈出候選人列表

我們已經順利把候選人寫入資料庫了,現在要把資料撈出來。在candidates_controller.rb這裡透過 Candidate 這個 model 的類別方法 all Candidate.all

1
2
3
def index
    @candidates = Candidate.all
end

並且在index.html.erb這個檔案裡用table,搭配使用ruby的迴圈each把候選人資料一筆一筆印出來

View-helper小幫手link_to

1
<%= link_to '要秀出來的字樣', '要去的位置'%>

第二種寫法,使用路徑 Prefix 提供的名稱加上 _path

1
<%= link_to '要秀出來的字樣', new_cnadidate_path %>

第二種寫法的優點

  1. 拼錯就會馬上噴錯
  2. 之後更改路徑名稱方便

所以盡量以第二種寫法為主。

03-7 show action

step 1. 重覆之前在controller定義action的方式定義show action。注意不要放在private底下。

1
2
def show
end

step 2. 做出view show.html.erb

step 3. 透過params拿id欄位回來用
透過params拿id欄位回來,再透過modle的find_by方法找某候選人的資料

1
@candidate = Candidate.find_by(id: params[:id])

step 4. 在show.html.erb做列表,把find_by 找到的資料印出來,會根據不同的id而有不同的結果。

step 5. 加上 if 判斷 如果候選人有資料,就印出來,反之,印出no record

1
2
3
4
5
6
7
<% if  @candidate %>
    #印出資料

<% else %>
    <h1> No Record Found </h1>

<% end %>

補充說明
<%= %> 與 <% %>
兩者的差別在於需不需要於畫面輸出,有加=的會印出來。

03-8 沒有通過驗證的錯誤訊息

step 1.透過any?來詢問有沒有任何錯誤訊息

.errors.any?

這時,對照03-3的save,存檔過程中要是沒有通過驗證,就可以在

.errors.any?

得到true的結果
再使用full_messages來得到錯誤訊息

.errors.full_messages

一樣搭配.each的方法來逐個印出資訊

1
2
3
4
5
6
7
<% if  candidate.errors.any? %>

<ul>
    <% candidate.errors.full_messages.each  do |message| %>
    <li><%= message %></li>
    <% end  %>
</ul>

補充說明 可以透過.method的方法來確認有什麼method可以使用

step2. 修正版面
因為驗證沒有過而render :new 回來後,版面會因為被多了一層div(field_with_errors)包裏而跑掉,我們要怎麼處理呢?
來到 app -> assets -> stylesheets 資料夾裡按右鍵開一個檔案candidate.scss。這個資料夾裡的所有css檔案可以透過同個資料夾的application.css(打包描述檔)裡的這個描述

*= require_tree .

打包回來。

接著,就可以在candidate.scss編輯想要的效果。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
.field_with_errors{
    display: inline-block;


    input[type="text"]{
        border-color: red;
        border-width: 1px;
    }

    label {
        color: red;
    }

} 

04 修改(edit)

一樣使用前面提到的link_to來寫。
step 1. 確認路徑

/candidates/:id/edit

step 2. 確認Prefix

edit_candidate

step 3. 在index.html.erb寫入

1
<%= link_to 'update', edit_candidate_path(candidate.id) %>

補充說明
若Prefix的欄位是空白的,則對應的是前一個欄位

04-1 edit action

step 1. 重覆之前在controller定義action的方式定義edit action。注意不要放在private底下。並且一樣透過params拿id欄位回來,再透過modle的find_by方法找某號候選人的資料

1
2
3
def edit
    @candidate = Candidate.find_by(id: params[:id])
end

step 2. 做出view edit.html.erb

step 3. 做一個表單
(可用new.html.erb的內容來套用)

step 4. PATCH / UPDATE 接下來在瀏覽器按下update後,可以在log裡看到form_for幫我們產出的其中一個input,有patch這個方法。

1
<input type="hidden" name="_method" value="patch">

這是因為目前瀏覽器還沒有支援到那麼多的方法(動詞),所以rails用這個方式"假裝"patch(實際上是post)

由路徑對照表可以看到,如果是用PATCH的方法對/cnadidates/:id這個路徑去送的話,會對應到candidates#update這個方法。

補充說明 1.
rails會根據那個modle是不是全新的來判斷(猜測)form.submit是要使用create還是update
全新的物件 -> create
從資料庫撈出來的 -> update

補充說明 2.
(1) 用GET的方法對/cnadidates/:id這個路徑去送的話,會找到candidates#show這個方法
(2) 用PUT的方法對/cnadidates/:id這個路徑去送的話,會找到candidates#update這個方法
(3) 用DELETE的方法對/cnadidates/:id這個路徑去送的話,會找到candidates#destroy這個方法

04-2 update action

step 1. 重覆之前在controller定義action的方式定義edit action。注意不要放在private底下。並且一樣透過params拿id欄位回來,再透過modle的find_by方法找某號候選人的資料

1
2
3
def update
    @candidate = Candidate.find_by(id: params[:id])
end

step 2. 類似 create action 的作法,並且失敗的話,借edit的頁面來渲染

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def update
    @candidate = Candidate.find_by(id: params[:id])

    if @candidate.update(candidate_params)
        flash[:notice] = "Candidate updated!"
        redirect_to '/cnadidates'
    else
        render :edit
    end
end

05 刪除

step 1. 確認路徑

/candidates/:id

step 2. 確認Prefix

candidate

(沒寫的話的,為同上)

step 3. 在index.html.erb寫入

1
<%= link_to 'delete', candidate_path(candidate.id) %>

step 4. 做出跟update的區別 到目前為止,因為跟update一樣路徑都是在/candidates/:id 所以要在 link_to 裡加幾個參數,讓它不會往show送

1
<%= link_to 'delete', candidate_path(candidate.id), method: 'delete' %>

在檢視原始碼的時候,就會發現多出data-mathod="delete" 如此,在頁面上按下delete的時候,就會對該路徑使用delete這個動詞,接著往/cnadidates/:id這個路徑去送,會找到candidates#destroy這個方法

05-1 destroy action

step 1. 重覆之前在controller定義action的方式定義edit action。注意不要放在private底下。並且一樣透過params拿id欄位回來,再透過modle的find_by方法找某號候選人的資料

1
2
3
4
5
6
7
def destroy
    @candidate = Candidate.find_by(id: params[:id])
    @candidate.destroy

    flash[:notice] = "Candidate deleted!"
    redirect_to '/candidates'
end

step 2. 防呆機制 在index.html.erb裡的link_to加上確認data: {confirm:""}

1
<%= link_to 'delete', candidate_path(candidate.id), method: 'delete', data: { confirm: 'are you sure?' } %>

如此,便可以在刪除資料的時候,跳出確認視窗

06 錯誤訊息

錯誤訊息的解答通常都在訊息裡

錯誤訊息:missing a template 代表缺少MVC裡的V(view),需要在view裡建相對應的 html 給它

錯誤訊息:Migrations are pending 代表還有一個migrations還沒處理,執行rails db:migrate就可以解決

錯誤訊息:UnKnown action 代表缺少某個action,需要在 controller 裡定義

錯誤訊息:Routing Error. No route matches 找不到路徑(有可能是還沒建或是路徑打錯字)

錯誤訊息:InvalidAuthenticity Token 無有效的驗證

錯誤訊息:ForbiddenAttributesError 還沒清洗params(還不是白名單)

rails設計哲學之一就是慣例優於設定

updatedupdated2022-04-242022-04-24