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。
可以在一個交易中包含不同Active Record的類別或物件,這是因為交易是以資料庫連線為範圍,而不是個別Model:
Customer.transaction do
Customer.create!(name: 'Bat Man') # 使用create!
Car.create!
end
注意這裡使用create!
而不是create
,使用create!
才會拋出例外讓交易失敗。
balance.transaction do
balance.save!
account.save!
end
建立一個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
之外,都包在BEGIN
跟COMMIT
之內,其實BEGIN
就是transaction開始,COMMIT
是transaction結束,只要其中任何一個callback失敗,這次save就不會成立。
after_commit
取代after_save
如果想在transaction 完成save
或destroy
後進行執行某個動作,不能像一般的Model使用after_save
,因為這是DB可能還沒被更新,after_save會失效,必須使用after_commit
取代after_save
如果想要transaction
裡面save
或destroy
失敗時執行callback,可以使用after_rollback
callback。
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
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的特性,還學到處理大量資料的方法,我的想法是每次文章都盡可能帶入多點知識,讓願意看文章的人不虛此行。
雖然別人的鐵人賽都到終點了,對我來說還有九天要努力,明年的鐵人賽一定要完賽。