[v] xdite rails 第二週 開發實作

商品加到購物車

  • 為什麼要有購物車的設計?不能直接把商品跟使用者做連結嗎(用使用者當container)?是為了session還是為了將來在shopping cart之外,還能實作出 buy later, wish list……等分類做準備?

[不確定] 可以把商品掛 user 身上結帳,但有幾個特性提一下:

  1. 這樣無法記錄購物車資訊,也就無法做後續的數據分析及利用。
  2. 理論上可以把 user 購物欄位做在 user model ,但實務上不這麼做,因為 user model 會變得很複雜。你有一台用 hash 的形式裝要買的商品和數量的購物車,比如 { xxx: 2, aaa: 1 },就要生一個欄位,不現實。
  3. 會建議購物車、稍後購買、心願清單......等,有各自的model。
  • [提問] 實務上,要有購物車才能產生類似訂單記錄/訂單快照的功能?

pass

  • [提問] 假如每個人只能有一台購物車,每一次結帳完就把購物車清空,那似乎也不會多出什麼欄位,而且 hash 一樣可以做出訂單快照,不是嗎?

pass

  • def current_cart 中的 @current_cart 是什麼?哪裡來的?
class ApplicationController < ActionController::Base
 
  ......
  
  def current_cart
    @current_cart ||= find_cart
  end

  def find_cart
    cart = Cart.find_by(id: session[:cart_id])
  
    unless cart.present?
    cart = Cart.create
  end

  session[:cart_id] = cart.id
  return cart
end

我們先來拆解各個元素:

def current_cart
  @current_cart
end

把 def current_cart 改寫成這樣來理解,當我們第一次跑 def current_cart 時,因為沒有 @current_cart,所有程式會自動去建立 @current_cart 這個 instance variable。

def current_cart
  @current_cart ||= find_cart
end

a ||= b 的意思是,假如 a = nil, 就會先被賦予初始值 b
所有上面 @current_cart ||= find_cart 的意思是,假如 @current_cart = nil, 就賦予 find_cart 得到的值。

......
  session[:cart_id] = cart.id
  cart
end

"Ruby methods ALWAYS return the evaluated result of the last line of the expression unless an explicit return comes before it." 所以 find_cart 最後一行的 cart, 實際上是 return cart 的作用。沒有最後一行的 cart, 就會 return session[:cart_id] = cart.id 給 @current_cart, 程式就會出錯。

整個流程就是:

  1. 執行 current_cart
  2. 沒有 @current_cart, 所以建立 @current_cart
  3. 建立之後,@current_cart = nil, 所以執行 find_cart
  4. find_cart 最後 return cart 的值,所以把值吐給 @current_cart
  • def add_to_cart 中的 current_cart 是個 method,為什麼可以直接被當成物件來使用,後面接上 .add_product_to_cart(@product) 變成 current_cart.add_product_to_cart(@product)?
def add(a, b)
    sum = a + b 
end

add(3, 5) 的值是 8,add(3, 5) + 3 = 11;那我們也可以 assign 給它一個 variable, 比如說 total = add(3, 5), total + 3 一樣是 11。回到原來的問題,current_cart 的值就是 @current_cart, 所以可以直接拿來用。

  • session[:cart_id] = cart.id,究竟 session hash 有哪些 keys 呢?

在檔案裡面插入 byebug,然後在 rails server 中可以看到所有的 session hash 的內容。artstore 的 session 沒有 cart_id 這個 key, 我們直接 session[:cart_id] = cart.id 就可以在 session hash 中新增這組 key-value pair.

  • 為什麼 product controller 有一個 def add_to_cart 後,還要在 cart model 有一個 def add_product_to_cart(product)

可能其他地方還會用到 add_product_to_cart(product) method, 所以拆出來。

  • ci.product = product 是筆誤?還是 ci.product_id = product.id 的另一種寫法,屬於 Rails convention? 還是說這兩種是不同的兩件事,但殊途同歸?
def add_product_to_cart(product)
  ci = cart_items.build
  ci.product = product
  ci.save
end

也可以寫成

def  add_product_to_cart(product)
  items << product
end

不是筆誤,是殊途同歸。ci.product_id = product.id 只會處理 product_id 欄位,ci.product = product, 如果欄位是一樣的,所有的值就都會傳過去,包括product_id。

  • [提問] 上面的第一種寫法是把 product 存進 cart_item 的一個 local variable ci 之中,為什麼第二種寫法卻是把 product 存進 items?
  • cart_id 跟 user 是什麼時候建立起關聯的?

======= 原本的思路 =======

[以下待解]

先看下面這篇了解 cookies & sessions
https://rocodev.gitbooks.io/rails-102/content/chapter2-rails/cookies-and-session.html

開始講解:def add_to_cart中有一段內容是current_cart.add_product_to_cart(@product)。其中,current_cart 是要找出一台 cart 給 user,也就是以 cart_id 和 user 建立關聯為目標。

def current_cart
  @current_cart ||= find_cart
end

def current_cart 的口語是,去找 @current_cart 的值,如果找不到,代表 user 沒有 cart,那就去找一台給他,也就是進入def find_cart的階段。def find_cart 的口語是,用 session[:cart_id] 來找 user 的 cart,如果沒有找到,代表 user 沒有 cart,沒有就去創一台,最後,把這一台 cart 的 session[:cart_id] 給予 cart.id 的值。

由上可知,cart_id 跟 user 是在 session[:cart_id] = cart.id 時建立起關聯的。

[以上不確定,解完下面問題再回去寫好寫滿!]

======= 助教的說法 =======

[不確定] cart_id ↔ session[:cart_id] ↔ user 但因為 session 不持久,所以無法用 session 來連接起 cart_id 跟 user 的永久關係。

  • 如果 user 拿到一台 cart.id = 1 的 cart, 後來可能清掉 cookies 還是 session 之類的,總之,破壞掉 user 跟 cart_id 的關聯,後來再去找一台 cart 給 user, 請問一樣是找來 cart.id = 1 的 cart, 還是給一台新的 cart? 如果是重新給一台,那 cart.id = 1 的 cart 到哪去了?

重新給一台新的 cart,原本的 cart 作廢。以消費者的角度來說,這些作廢的 carts 永遠看不到,也沒任何用處;但以管理者的角度來說,留著這些廢棄 carts 可以做資料分析,比如哪些類型的人容易將商品放入 cart 卻沒有結帳。

  • session 是專門用來存暫時性、非永久性(比如 cart_id, login, log out......等)的值嗎?

是的

  • session[:cart_id] & 跟 user 有關聯的 cart_id 有什麼差別?

[不確定] user 跟 cart_id 是沒關聯的,[這段開始不確定] 硬要說就是 session[:cart_id] 跟 cart_id 有關,而 user 又跟 session 有關。但 session 會過期,所以用 session 來連接 user 和 cart_id 是不穩固的。

  • [提問] 到底 user 跟 session[:cart_id] 有什麼關係?或者說,user 跟 session 有什麼關係?有關係的話是 devise 自動建立起關係的嗎?可不可以說,使用到 session 的 user 必然是 current_user?
  • item 跟 cart_item 的差別在哪?比如說 def add_to_cart 中,if !current_cart.items.include?(@product) 我會認為是要寫成 if !current_cart.cart_items.include?(@product),有什麼地方理解錯了?

item 經過has_many :items, through: :cart_items, source: :product後,是 product 的同義詞,cart_item 則是在 cart 中的那些 items。

  • 為什麼要這樣分呢?

http://rails.ruby.tw/association_basics.html 2.4

  • Cart model 中,
class Cart < ActiveRecord::Base
  has_many :cart_items, dependent: :destroy
  has_many :items, through: :cart_items, source: :product
end

為什麼 cart 要有 cart_item 又要有 item(product)? cart_items table 不就已經可以對到 cart 有哪些 items(products) 了嗎?

這是多對多的寫法,一定要這樣寫。如果沒有 item, cart has_many products 的結果是,每一種 product 只能對到一台 cart.
http://guides.rubyonrails.org/association_basics.html 2.4

  • 拆解 has_many :items, through: :cart_items, source: :product
  1. items可以自由命名,增加易讀性。
  2. cart_items table 裡面有 art_id, product_id 兩個欄位,所有可以用它們來連結 cart 或是 product。
  3. 自由命名完,程式不知道 items 是什麼,所以後面補充說明 items 透過 cart_items table 連往 product model。
  4. 因此 items 可以想成是 products 的同義詞,也就是理解為 cart has_many products
  5. 同理,如果改成 has_many :items, through: :cart_items, source: :cart,那 items 就變成了 carts 的同義詞。cart_items 會拿呼叫他的 model 的 id 去 cart 查資料,不過前提是 cart_item 裡面要有 cart_id 欄位。
  • routes.rb 裡面有 post :add_to_cart
resources :products do
    member do
      post :add_to_cart
    end
  end

products/show.html.erb 中有 method: :post

<%= link_to "加入購物車", add_to_cart_product_path(@product), method: :post, class: "btn btn-lg btn-danger" %>

兩個地方都必須宣告使用說明 http verb 嗎?

是的。預設是 get

  • post :to_admin 為什麼不是用 put?

post 和 put 在這都可以

  • sean 的提問: 助教請問一下在結帳按鈕的地方這邊用 post 有什麼特別的用意嗎 post :checkout <%= link_to("確認結賬", checkout_carts_path, method: :post 因為也沒有要特別 post 什麼資料到下一個頁面是否能用 get 就可以了呢?

小蟹:
要用 POST,因為 http method :post 的語意就是要建立新的資料,在這邊會產生訂單。
你的疑問是我們沒有 post 資料,那是因為資料都已經記在購物車了,但我們仍然有新增資料的行為。
雖然改成 GET 也會動,但是這樣並不好。

sean 的回應:
我以為是要post資料過去才要用post,因為表單是在checkout才會送出去的,所以這樣也算是個慣例嗎?

小蟹:
是的,還有現在沒有不代表以後不會有
基本上要建立資料就要送 POST
要改變資料就要送 PUT 或 PATCH
這跟 RESTful 無關了,這算是 http 的 spec
所以是 RESTful 去遵循 http spec 的規範

  • current_user 是 devise gem 提供的 helper

  • @current_cart ||= find_cart

if @current_cart (= true) -> @current_cart; if @current_cart = false || nil -> find_cart

  • convention: 比如某個物件 item,如果單數,可以寫作 build_item,但如果是複數,得寫成 items.build。

  • 1:51:30

如果 session[:cart_id] 是 nil, 用 find(id: session[:cart_id]) 會出現 ActiveRecord not found, 然後就中斷了,用 find_by(id: session[:cart_id]) 會回傳 nil 或 false, 表示沒這個 cart, 而不會程式中斷。

  • slide 2-2 16/16 => 7:20
  • 17:48 不要在 view 裡面撈 count, 而是用 helper 的原因

在 view 裡直接撈 DB 的值,不會被 cache 住,也難以維護,建議用 helper 來做這件事。原因是直接寫在view裡的話,可讀性會比較差,別人要猜這個代碼是做什麼的;但如果是用 helper 包起來,我們可以給 helper 一個清楚表達代碼用意的名稱,放進去 view 時可以直接看懂這段代碼在做什麼,別人不需要用猜的,比較好維護。另外一個好處就是 view 會比較乾淨好讀,也是比較好維護。

購物車的商品明細

  • 算數字的都寫成 helper 幫助維護。

  • 49:10 怎樣寫 helper

  • 下方是一個寫成 helper 的例子

def render_cart_items_count(cart)
   cart.cart_items.count
end

建立訂單 ( 寄送資訊 )

  • 為什麼要寫 class_name?

1:21:00 Rails convention, 比如說 has_one info 時,就會去找有沒有info model(同名的model), 但我們沒這個 model, 所以要註明 class_name 是 OrderInfo. 而前面 has_many :XXX 實際上可以任意命名,後面再指向正確的 class 就好了。

  • [提問] 那為什麼不直接寫 has_many :orderinfo 就好了?如果 info 撞名了怎麼辦?

可以直接寫成 has_many :orderinfo。寫成 has_many :info 後面再宣告 class_name 是 OrderInfo 的方法,是為了增加易讀性,order.info 比 order.orderinfo 好讀。

  • 下面這樣是否就可以避免前面的命名撞名? order has_many :info, class_name :OrderInfo => order.info user has_many :info, class_name :UserInfo => user.info

可以用這種方式避免撞名沒錯!比如上方問題中舉的例子,只要我們後面的 class_name 都宣告清楚,

order has_many :info, class_name :OrderInfo    
user  has_many :info, class_name :UserInfo
xxx   has_many :info, class_name :XxxInfo
......
...

就可以維持像下面這樣,

order.info
user.info
xxx.info
......
...

風格一致又好讀的寫法

  • .build.new 的差異

[錯誤] rails 的 .build 是 ruby 的 .new 的 alias, which means .build 在 ruby 中不能用。
http://anxgang.logdown.com/posts/712225-note-rails-method-new-build-create-and-save-the-difference
http://vinhboy.com/blog/2009/01/15/rails-new-vs-build/

建立訂單 ( 生成訂單 )

app/models/order.rb
class Order < ActiveRecord::Base
  belongs_to :user

  has_many :items, class_name: "OrderItem", dependent: :destroy
  has_one  :info,  class_name: "OrderInfo", dependent: :destroy

  accepts_nested_attributes_for :info


# 第一段

  def build_item_cache_from_cart(cart)
    cart.items.each do |cart_item|                             
      item = items.build                       
      item.product_name = cart_item.title
      item.quantity = 1
      item.price = cart_item.price
      item.save
    end
  end


# 第二段

  def calculate_total!(cart)
    self.total = cart.total_price
    self.save
  end
end

第一段:
假設 has_many :items, class_name: "OrderItem" 外還有 has_many :items, class_name: "CartItem",那麼要怎麼判斷 item = items.build 中的 items 是哪一個 items?

第二段:什麼時候用 self ?

建立訂單 ( 顯示訂單內容 )

  • add_index 加索引,好處是方便查找,效率高。但如果什麼東西都加索引,會導致索引肥大而降低效率。

將訂單結帳

  • 為什麼 pay_with_credit_card 是用 get 不是 post? 是因為要 get 智付寶的支付頁面嗎?
resources :orders do
  member do
    get :pay_with_credit_card
  end
end
  • <% if !@order.is_paid? %> 中 .is_paid? 怎麼來的?

Rails magic. is_paid 的屬性是 boolean, 所以可以直接後面加 "?" 來增加易讀性。

訂單狀態

  • :is_paid => true, 為什麼還要有一個:aasm_state => “paid”?

aasm_state 欄位是 aasm state machine 預設會去讀取的欄位,用來判斷現在整個訂購流程在哪個狀態,而讓狀態機能順利運作。

  • [提問] 所以,更該問的是,可不可以將結帳鈕和 aasm 做結合,結帳時直接把 aasm_state 改成 paid, 然後拿掉 :is_paid 欄位?

xdite:
我完全不建議這樣做,這樣你會遇到付款然後退貨的狀況,金流查詢會亂掉。

  • 關於 state machine / aasm

State machine 三個主要元素是 event, state, transition, 以下表當參考,就很容易理解了。

class Job
  include AASM
  
  aasm do
    state :sleeping, :initial => true
    state :running
    state :cleaning
    
    event :run do
      transitions :from => :sleeping, :to => :running
    end
    
    event :clean do
      transitions :from => :running, :to => :cleaning
    end
    
    event :sleep do
      transitions :from => [:running, :cleaning], :to => :sleeping
    end
  end
  
end

State machine 本身不內建 states, events, transitions, 但是只要照著它規定好的語法來寫 states, events, transitions, 就能正常運作。aasm 是一種 state machine, aasm_state 欄位是 aasm 預設會抓的欄位,也可以自定義後加上參數指定。

  • 拆解 event :make_payment, after_commit: :pay! do

aasm 會把 event 生成 method,這段等於是先跑 make_payment 這個 method, 等 DB 欄位 commit 之後(after_commit),再跑 pay! 這個 method。

  • [提問] 所以 before_action 是指在跑 actions 之前,先執行 XXX 嗎?

是的,但 before_action 是 controller 的操作,而 after_commit 是 model 的操作。

  • 什麼是 ActiveRecord Callbacks?

http://guides.rubyonrails.org/active_record_callbacks.html

  • [待整理] 了解rails g migration 的 rails magic, 比如什麼寫法會在 migration 檔自動產生欄位......等。

http://edgeguides.rubyonrails.org/active_record_migrations.html

2 Creating a Migration

If the migration name is of the form "AddXXXToYYY" or "RemoveXXXFromYYY" and is followed by a list of column names and types then a migration containing the appropriate add_column and remove_column statements will be created.