2017年5月14日 星期日

第三次上五倍紅寶石rails課 5/14

service

要使用別的功能的時候可以把method定義在service裡面
不過這邊其實我沒作筆記囧
參考一下

routes

後面接path的話可以方便改變網址,而且不用特別改link_to裡面的path

  resources :candidates, path: 'users'
        Prefix Verb   URI Pattern               Controller#Action
          root GET    /                         welcome#index
    candidates GET    /users(.:format)          candidates#index
               POST   /users(.:format)          candidates#create
 new_candidate GET    /users/new(.:format)      candidates#new
edit_candidate GET    /users/:id/edit(.:format) candidates#edit
     candidate GET    /users/:id(.:format)      candidates#show
               PATCH  /users/:id(.:format)      candidates#update
               PUT    /users/:id(.:format)      candidates#update
               DELETE /users/:id(.:format)      candidates#destroy

:id

其實我最一開始不知道什麼時候才要在path後面加東西,後來才瞭解到網址後面有id的都需要,其實寫candidate.id也是一樣的,但因為rails會幫忙把id抽出來,所以寫短一點的就可以了
  <% @candidates.each do |candidate| %>
    <li>
      <%= link_to candidate.name, candidate_path(candidate) %>
      <%= link_to '編輯', edit_candidate_path(candidate) %>
      <%= link_to '刪除', candidate_path(candidate), method: :delete, data: {confirm: '刪除嗎??'} %>
      <%= candidate.party %>
      <%= candidate.age %>
      <%= candidate.politics %>
    </li>
  <% end %>
candidate_path(candidate)可以只寫candidate就好,只把物件給rails也會自己判斷,但我個人會因為搞不清楚我在幹嘛,所以會寫完整的。(編輯的path不能改)
實際上html會變成下面這樣,都會變成data-*, 所以method其實放在data裡面也可以(只是也沒必要),或者想要另外傳一些資料進去的時候也可以寫在data裡面
<a data-confirm="刪除嗎??" rel="nofollow" data-method="delete" href="/candidates/1">刪除</a>

path

因為下面那幾個的path是共用的,所以刪除資料的時候記得後面method要說是delete,不然會跑到show,這也是我當初常用錯的東西
     candidate GET    /users/:id(.:format)      candidates#show
               PATCH  /users/:id(.:format)      candidates#update
               PUT    /users/:id(.:format)      candidates#update
               DELETE /users/:id(.:format)      candidates#destroy

find / find_by

Candidate.find_by(id: 1)
Candidate.find(1)
上面這兩個找得到id的時候得到的結果是一模一樣的,但找不到的時候的情形會不一樣,看你喜歡nil,還是喜歡處理例外
會回傳nil
Candidate.find_by(id: 999)
丟給你ActiveRecord::RecordNotFound: Couldn’t find Candidate with ‘id’={:id=>999}
Candidate.find(999)

edit的form

和new是可以共用的,而且畫面上會出現已經填好的資料(驚嚇),因為rails會去判斷你丟給form_for的是新的物件還是從資料庫抓出來的
(不過前提是要new和edit的變數名稱要取一樣的)
然後生出來的html長這樣,雖然method是寫post,不過因為是要更新資料,所以會偷偷幫你模擬用patch(瀏覽器實際上沒有支援patch,所以都是用模擬的)
<form class="edit_candidate" id="edit_candidate_1" action="/candidates/1" accept-charset="UTF-8" method="post"><input name="utf8" type="hidden" value="✓"><input type="hidden" name="_method" value="patch">

partial

盡量不要在partial裡用實體變數

比方說最一開市像下面這樣子寫
# new / edit
<%= render 'form' %>
#_form.html.erb
<%= form_for @candidate do |f| %>
  姓名: <%= f.text_field :name %><br/>
  政黨: <%= f.text_field :party %><br/>
  年紀: <%= f.text_field :age %><br/>
  政見: <%= f.text_area :politics %><br/>
  <%= f.submit %>
<% end %>
雖然上面那樣也會動,因為他會自己去抓@candidate,但是如果controller裡面變數名稱不一樣就炸了,而且這樣子寫比較不好瞭解到底是在幹嘛。
比較清楚一點的方法是自己丟變數給他
#view
<%= render partial: 'form', locals: { candidate: @candidate } %>
#也可以這樣就好
<%= render 'form', candidate: @candidate %>
#_form.html.erb可以拿掉@了
<%= form_for candidate do |f| %>
  姓名: <%= f.text_field :name %><br/>
  政黨: <%= f.text_field :party %><br/>
  年紀: <%= f.text_field :age %><br/>
  政見: <%= f.text_area :politics %><br/>
  <%= f.submit %>
<% end %>

那如果是下面這種partial要拿掉@呢
<% @candidates.each do |candidate| %>
  <li>
    <%= link_to candidate.name, candidate_path(candidate) %>
    <%= link_to '編輯', edit_candidate_path(candidate) %>
    <%= link_to '刪除', candidate_path(candidate), method: :delete, data: {confirm: '刪除嗎??'} %>
  </li>
<% end %>
那render就改成這樣,然後上面把@拿掉就好了
  <%= render 'candidate', candidate: @candidates %>
但是這樣只是複製code貼上到另一個地方沒什麼意義,其實有辦法讓你連<% @candidates.each do |candidate| %>都不用寫
<%= render partial: 'candidate', collection: @candidates %>

#或者下面這樣,但因為精簡過頭我會不知道我在幹嘛,所以我不愛,記得第一次看到的時候整個滿頭問號

<%= render @candidates %>
#裡面一定要是單數candidate
<li>
  <%= link_to candidate.name, candidate_path(candidate) %>
  <%= link_to '編輯', edit_candidate_path(candidate) %>
  <%= link_to '刪除', candidate_path(candidate), method: :delete, data: {confirm: '刪除嗎??'} %>
</li>

view helper

所有的helper其實都會被載入,那如果不同helper裡面有同樣名稱的method會發生什麼事?
雖然大家都會被載進來,但是畢竟載入還是有先後順序的,而順序是照英文字母的排列a..z,所以放在檔名開頭字母順序比較後面的那個method會被執行

自訂action member / collection

有member和collection兩種方法,差別是前者會幫你帶id進來,後者不會
Rails.application.routes.draw do
  root 'welcome#index'

  resources :candidates do 
    collection do 
      post :vote1
    end

    #post :vote1, on: :member 其實只有一個的話這樣也可以寫
    member do 
      post :vote2
    end
  end
  # post "/candidates/:id/vote", to: 'candidates#vote'
end
#結果
vote1_candidates POST   /candidates/vote1(.:format)     candidates#vote1
 vote2_candidate POST   /candidates/:id/vote2(.:format) candidates#vote2

關聯

下面兩種寫法都可以設好Foreign Key,但我偶爾references會忘記加s=.=
另關聯其實是在model裡面做的,並不是在表格裡面做
#這兩個是一樣的
rails g model VoteLog ip_address candidate_id:integer
rails g model VoteLog ip_address candidate:references
如果是用下面那種寫法的話會幫你加以下的東西
class VoteLog < ApplicationRecord
  belongs_to :candidate
end

has_many

慣例上has_many後面會用複數,但其實用單數也可以正常跑(但是打一串完全無關的字是不行的),因為他會去找model名字或是類別,只是就會用動,還是照慣例走吧
#其實都會動
has_many :vote_logs
has_many :vote_log

counter cache

http://railsbook.tw/chapters/14-crud-part-2.html#step14-counter-cache
一般來說想要找出每一個candidate的vote_log的時候會需要去一筆筆的sql count查詢,像下面這樣
  Candidate Load (0.2ms)  SELECT "candidates".* FROM "candidates"
   (0.3ms)  SELECT COUNT(*) FROM "vote_logs" WHERE "vote_logs"."candidate_id" = ?  [["candidate_id", 5]]
   (0.3ms)  SELECT COUNT(*) FROM "vote_logs" WHERE "vote_logs"."candidate_id" = ?  [["candidate_id", 6]]
   (0.7ms)  SELECT COUNT(*) FROM "vote_logs" WHERE "vote_logs"."candidate_id" = ?  [["candidate_id", 7]]
   (0.1ms)  SELECT COUNT(*) FROM "vote_logs" WHERE "vote_logs"."candidate_id" = ?  [["candidate_id", 8]]
效能可能會比較不好,這時候可以用counter cache
先在Candadite新增一個欄位叫vote_logs_count
class AddCounter < ActiveRecord::Migration[5.0]
  def change
    add_column :candidates, :vote_logs_count, :integer, default: 0
  end
end
rails db:migrate之後在model裡面改成下面這樣

class VoteLog < ApplicationRecord
  belongs_to :candidate, counter_cache: true
end
然後在view裡面把count改成size,重新整理會發現sql變成只需要下面這樣
  Candidate Load (0.5ms)  SELECT "candidates".* FROM "candidates"

reset_counter

但會發現之前的票數不見了?!
可以直接進rails console去用,也可以寫個rake來執行
namespace :db do 
  desc "重置counter cache"
  task :reset_counter => :environment do 
    Candidate.all.each do |candidate|
      Candidate.reset_counters(candidate.id, :vote_logs)
    end
  end
end
執行之後票數就會正常了。
Written with StackEdit.

沒有留言:

張貼留言