ADZ 學習筆記

Ruby/Rails, Startup, Life

Rails 筆記 - activerecord 實作 (一) - transaction

| Comments

上一個 rails 專案是一個線上報名系統,有各 table 的 CRUD、審核流程、資料驗證、有的動作還會一連串附帶好幾個 query,activerecord 提供的 callbacksvalidations 的設計看來是想在 model layer 就處理掉部分商業邏輯資料驗證,然後把操作流程丟到 controller 裡實踐,但真正實作前還是花了點時間測試,這次分享的就是測試後的結果!

在講 activerecord 的交易機制前,先提提 transaction 解決的兩個主要問題:

1. db 故障造成的資料不一致問題

假設有兩張一對一的關聯的資料表 users & profiles,當 users 被 create 資料時要順便 create profile,但如果遇到 create user 後,剛好突然某個新來的 SI 把 db reboot、或地震、海嘯造成 db 中斷,此時,原本應該一致的資料就會得不一致了,所以 transcation 提供了一種 all or nothing 的概念,把所有 query 包在一個 transaction 中,只有全部執行完成、或者全部都沒有。

2. Race condition

另一種情況要先拉出某筆資料做驗證,在決定下一步的動作,比如 users 表裡有 credit (點數),可以買購 products,訂單存在 orders,交易的流程如下:

  1. 檢查 user credit 是否大於購物車的總金額 (是的話才進行下一步)
  2. 將購物車的訂單明細寫入 orders
  3. 扣除 user 的 credit

但如果使用者同時發出 reqeust 兩次,兩個 request 同時執行第一步,拉到的 user credit 都 > 購物車總金額,就 pass 到第二步了,訂單也就成立兩次,user credit 也就被扣兩次,但這是錯誤的。

於是 transaction 提供一種可以 lock 住某筆資料 (user) 的功能,在這個 transaction 未結束前,若另外一個 transaction select 這筆資料,會在排在 未結束的 transaction 的後面,讓 transactions 之間產生順序性,而不是 query 之間的順序,詳細 mysql 的 race condition 可參考 google 大神

使用 activerecord 實作 transaction

開 table 如果有用 migration 的話預設使用 mysql innodb engine 儲存,如果不是的話要注意 MyISAM 是無法支援交易的 (就算使用不會出錯、但實際上是沒有交易功能的)

範例一

student.rb
class Student < ActiveRecord::Base
  has_one :profile  
end
profile.rb
class Profile < ActiveRecord::Base
    belongs_to :student
end
student_contorller.rb
  def create
    begin
      ActiveRecord::Base.transaction do
        # 這裡使用 save! 是為了當 valid? 不過時丟出 exception 讓整個 transaction rollback

        @student = Student.new(student_params)
        @student.save!(student_prams)
        @user.build_profile.save! # build_profile 是建立 has_one 時自動產生的 function

      end
      redirect_to @student, notice: '新增學員成功'
    rescue
      render action: 'new'
    end

P.S begin & rescue 像是 php & java 中的 try catch

範例二 (使用 callback 簡化 controller)

經過測試後發現在操作 activerecord save, create, update, destroy 動作時,本身就是一個完整的 transaction 而且會包住 validation & callbacks。

student.rb
class Student < ActiveRecord::Base
  has_one :profile  
  after_create :build_student_profile
  
  protected
    def build_student_profile
      self.build_profile.save!
    end
end
student_contorller.rb
  def create
    @student = Student.new(student_params)
    if @student.valid?
        @student.save
      redirect_to @student, notice: '新增學員成功'
    else
      render action: 'new'
    end
  end

以前用 PHP 寫過最複雜的 transaction query + validate 就超過百行,而且 if, else nested 結構包得超深,但透過這種方式大大把提升了程式碼的可讀性,只能說 Rails 的 ORM 設計的真不錯。

Comments

comments powered by Disqus