iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 9
0
Modern Web

關於 Ruby on Rails,我想說的是系列 第 21

[Day 21] 交易 transaction

  • 分享至 

  • xImage
  •  

Transaction(交易):所有資料操作都成功時才寫入資料庫。

這個技巧在一連串的資料操作特別有用,比如說虛擬貨幣的交易所,要從虛擬錢包A轉0.1顆ETH(以太幣)到虛擬錢包B,這筆交易一定是A扣掉0.1顆ETH,B增加0.1顆。
不能讓A成功扣掉0.1顆ETH後,B要增加0.1顆卻失敗了,那0.1顆ETH就消失被交易所幹走了。所以這筆交易就是匯入跟匯出都要成立,以Rails來說就長這樣:

Account.transaction do
  account1.withdraw(0.1)
  account2.deposit(0.1)
end

transaction作為Account的class_method,後面的block內執行的操作,都要成功才會一次commit到資料庫,只要有一個動作失敗,就會rollback。

不同Model也可以

可以在一個交易中包含不同Active Record的類別或物件,這是因為交易是以資料庫連線為範圍,而不是個別Model:

Customer.transaction do
  Customer.create!(name: 'Bat Man') # 使用create!
  Car.create!
end

注意這裡使用create!而不是create,使用create!才會拋出例外讓交易失敗。

transaction也可以作用在instance

balance.transaction do
  balance.save!
  account.save!
end

callback life_cycle vs. transaction

建立一個TodoList Model 及資料表

class TodoList < ApplicationRecord
  before_validation->{puts"before_validation is called"}
  after_validation->{puts"after_validation is called"}
  before_save->{puts"before_save is called"}
  before_update->{puts"before_update is called"}
  before_create->{puts"before_create is called"}
  after_create->{puts"after_create is called"}
  after_update->{puts"after_update is called"}
  after_save->{puts"after_save is called"}
  after_commit->{puts"after_commit is called"}
end

在console下

TodoList.new.save

   (0.1ms)  BEGIN              # transaction開始
before_validation is called
after_validation is called
before_save is called
before_create is called
  TodoList Create (45.9ms)  INSERT INTO "todo_lists" ("created_at", "updated_at") VALUES ($1, $2) RETURNING "id"  [["created_at", "2019-10-14 16:11:48.201360"], ["updated_at", "2019-10-14 16:11:48.201360"]]
after_create is called
after_save is called
   (40.2ms)  COMMIT            # transaction結束
after_commit is called
 => true 

可以看到callback除了after_commit之外,都包在BEGINCOMMIT之內,其實BEGIN就是transaction開始,COMMIT是transaction結束,只要其中任何一個callback失敗,這次save就不會成立。


使用after_commit取代after_save

如果想在transaction 完成savedestroy後進行執行某個動作,不能像一般的Model使用after_save,因為這是DB可能還沒被更新,after_save會失效,必須使用after_commit取代after_save

transaction 失敗時用 after_rollback

如果想要transaction裡面savedestroy失敗時執行callback,可以使用after_rollback callback。

transaction可以加快資料處理

transaction 讓SQL一次執行的特性,可以加快資料處理的速度
參考文章:
https://kakas.github.io/Rails/比較各種方式在-Rails-DB-中寫入大筆資料的速度/

原本從csv檔一行行讀取出資料後,將每一筆資料存到DB

def perform
  Pokemon.delete_all
  CSV.foreach(csv_path, headers: true) do |row|
    Pokemon.create(
      name:     row["Name"],
      location: row["Location"],
      level:    row["Level"].to_i
    )
  end
end

SQL會長這樣:

 (0.1ms)  begin transaction
SQL (0.3ms)  INSERT INTO "pokemons" ("name", "location", "level", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?)  [["name", "Meowth"], ["location", "Cerulean City"], ["level", 44], ["created_at", 2016-07-22 08:00:54 UTC], ["updated_at", 2016-07-22 08:00:54 UTC]]
 (0.8ms)  commit transaction
 (0.1ms)  begin transaction
SQL (0.3ms)  INSERT INTO "pokemons" ("name", "location", "level", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?)  [["name", "Bulbasaur"], ["location", "Verdanturf Town"], ["level", 33], ["created_at", 2016-07-22 08:00:54 UTC], ["updated_at", 2016-07-22 08:00:54 UTC]]
 (0.9ms)  commit transaction
...

可以看到每儲存一筆資料就要做一次transaction

使用transaction

將剛才的程式碼包在transaction

def perform
  Pokemon.delete_all
  ActiveRecord::Base.transaction do
    CSV.foreach(csv_path, headers: true) do |row|
      Pokemon.create(
        name:     row["Name"],
        location: row["Location"],
        level:    row["Level"].to_i
      )
    end
  end
end

SQL會長這樣:

(0.1ms)  begin transaction
SQL (0.3ms)  INSERT INTO "pokemons" ("name", "location", "level", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?)  [["name", "Meowth"], ["location", "Cerulean City"], ["level", 44], ["created_at", 2016-07-22 08:00:54 UTC], ["updated_at", 2016-07-22 08:00:54 UTC]]
SQL (0.1ms)  INSERT INTO "pokemons" ("name", "location", "level", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?)  [["name", "Bulbasaur"], ["location", "Verdanturf Town"], ["level", 33], ["created_at", 2016-07-22 08:00:54 UTC], ["updated_at", 2016-07-22 08:00:54 UTC]]
 (0.6ms)  commit transaction
...

只要一次transaction就完成,頓時輕鬆許多。

不過大量匯入也有適合的Gemactiverecord-import可以使用,讓SQL語法更優化,有興趣的可以閱讀原文。

小結

今天了解transaction跟callback的特性,還學到處理大量資料的方法,我的想法是每次文章都盡可能帶入多點知識,讓願意看文章的人不虛此行。
雖然別人的鐵人賽都到終點了,對我來說還有九天要努力,明年的鐵人賽一定要完賽。/images/emoticon/emoticon08.gif


上一篇
# [Day 20] 多對多關聯及多型關聯
下一篇
[Day 22] ActiveHash 內建資料
系列文
關於 Ruby on Rails,我想說的是23
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言