跳過前往內容 跳過前往搜尋

Active Record 交易

交易的保護性區塊,其中,SQL 陳述式只有全部成功執行,才能變成永久的區塊。最經典的範例就是兩個帳戶之間的轉帳:只有在提款成功,才有存款,反之亦然。交易強制執行資料庫完整性,並保護資料免於程式錯誤或資料庫發生中斷。因此,基本上,每當你有多個陳述式必須一起執行,否則就都不執行時,你應該都要使用交易區塊。

例如

ActiveRecord::Base.transaction do
  david.withdrawal(100)
  mary.deposit(100)
end

以下範例將從大衛身上取錢,將錢給瑪麗,但前提是:withdrawaldeposit 都沒有引發例外狀況。例外狀況會強制執行 ROLLBACK,讓資料庫回復到交易開始前的狀態。但請注意,物件不會將其執行個體資料回復到交易前的狀態。

單一交易中的不同 Active Record 類別

雖然 transaction 類別方法是由某個 Active Record 類別呼叫的,不過交易區塊內的物件不一定都是該類別的執行個體。這是因為交易是根據資料庫連線來執行,而不是根據模型執行。

在以下範例中,balance 記錄會以交易方式儲存,即使 transaction 是由 Account 類別呼叫的

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

transaction 方法也可以作為模型執行個體方法使用。例如,你也可以這麼做

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

Transactions 無法跨資料庫連線

交易的作用是在單一資料庫連線上。如果你有多個類別特定資料庫,交易無法保護它們之間的互動。一種解決方法是在模型變更的每個類別上開始執行交易

Student.transaction do
  Course.transaction do
    course.enroll(student)
    student.units += course.units
  end
end

這是一個很差的解決方案,但完全散發的交易並非 Active Record 的範圍。

savedestroy 會自動封裝在交易中

不論是 #save#destroy,都會封裝在一個交易中,確保你在驗證或回呼中執行的任何動作都會在受保護的情形下發生。因此,你可以使用驗證來檢查交易依賴的值,或在回呼中引發例外狀況來執行回滾,包括 after_* 回呼。

如此一來,你的連線外部就不會看到資料庫的變更,直到操作完成為止。例如,如果你嘗試在 after_save 中更新搜尋引擎的索引,索引器將看不到更新的記錄。after_commit 回呼是唯一一個會觸發的,讓人知道更新已提交。請見下列內容。

Exception 處理和回滾

另外,請記住,交易區塊內引發的例外狀況會被傳播(觸發 ROLLBACK 之後),因此你應該隨時準備好捕捉應用程式程式碼中的例外狀況。

一個例外是 ActiveRecord::Rollback 例外,它會在引發時触发 ROLLBACK,但不會被事务區塊重新引發。任何其他例外都將被重新引發。

警告:不应在事务區塊中捕获 ActiveRecord::StatementInvalid 例外。 ActiveRecord::StatementInvalid 例外表示在数据库级别發生了错误,例如唯一約束被違反時。在某些数据库系统(例如 PostgreSQL)中,事务中的数据库错误会导致整個事务在從頭開始重新開始之前不可用。以下是一個演示此問題的示例

# Suppose that we have a Number model with a unique column called 'i'.
Number.transaction do
  Number.create(i: 0)
  begin
    # This will raise a unique constraint error...
    Number.create(i: 0)
  rescue ActiveRecord::StatementInvalid
    # ...which we ignore.
  end

  # On PostgreSQL, the transaction is now unusable. The following
  # statement will cause a PostgreSQL error, even though the unique
  # constraint is no longer violated:
  Number.create(i: 1)
  # => "PG::Error: ERROR:  current transaction is aborted, commands
  #     ignored until end of transaction block"
end

如果發生 ActiveRecord::StatementInvalid,應重新啟動整個事务。

嵌套事务

transaction 调用可以嵌套。默認情況下,這使嵌套事务區塊中的所有数据库语句成為父事務的一部分。例如,以下行為可能是令人驚訝的

User.transaction do
  User.create(username: 'Kotori')
  User.transaction do
    User.create(username: 'Nemu')
    raise ActiveRecord::Rollback
  end
end

創建了“Kotori”和“Nemu”。原因是嵌套區塊中的 ActiveRecord::Rollback 例外沒有發出 ROLLBACK。由于這些例外是在事务區塊中捕獲的,因此父區塊看不到它,并且實際事务會提交。

為了獲得嵌套事务的 ROLLBACK,你可以通過傳遞 requires_new: true 來請求一個真正的子事务。如果出現任何問題,数据库會回滾到子事务開始時,而不會回滾父事务。如果我們將它添加到前面的示例

User.transaction do
  User.create(username: 'Kotori')
  User.transaction(requires_new: true) do
    User.create(username: 'Nemu')
    raise ActiveRecord::Rollback
  end
end

只創建了“Kotori”。

大多數数据库不支持真正的嵌套事务。在撰寫本文時,我們所知的唯一支持真正嵌套事务的数据库是 MS-SQL。由於這個原因,Active Record 使用保存點模擬嵌套事务。請參閱 dev.mysql.com/doc/refman/en/savepoint.html 了解有關保存點的更多信息。

回調

有兩種與提交和回滾事務相關的回調類型: after_commitafter_rollback

after_commit 回調在事务中保存或銷毀的每條記錄在事務提交後立即調用。 after_rollback 回調在事務或保存點回滾後立即調用事务中保存或銷毀的每條記錄。

這些回調對於與其他系統交互非常有用,因為你可以保證只有在数据库處於永久狀態時才執行回調。例如, after_commit 是在掛接清除緩存中的好地方,因為在事務中清除緩存可能會在数据库更新之前觸發緩存重新生成。

注意: Callbacks 按過濾器對每個回調進行去重。

嘗試使用相同的過濾器定義多個回調將導致只運行一個回調。

例如

after_commit :do_something
after_commit :do_something # only the last one will be called

這也適用於 after_*_commit 回調的所有變體。

after_commit :do_something
after_create_commit :do_something
after_save_commit :do_something

建議使用 on: 選項來指定何時運行回調。

after_commit :do_something, on: [:create, :update]

這等效於使用 after_create_commitafter_update_commit,但不會進行去重。

注意事項

如果你在 MySQL,切勿在通過儲存點模擬的嵌套交易區段中使用資料定義語言 (DDL) 作業。亦即,不要在此類區段中執行類似「CREATE TABLE」的語句。原因在於 MySQL 會在執行 DDL 作業時自動釋放所有儲存點。當交易完成並嘗試釋放它先前建立的儲存點時,將發生資料庫錯誤,因為儲存點已自動釋放。下例展示了這個問題

Model.lease_connection.transaction do                           # BEGIN
  Model.lease_connection.transaction(requires_new: true) do     # CREATE SAVEPOINT active_record_1
    Model.lease_connection.create_table(...)                    # active_record_1 now automatically released
  end                                                     # RELEASE SAVEPOINT active_record_1
                                                          # ^^^^ BOOM! database error!
end

請注意「TRUNCATE」也是 MySQL DDL 語法!

方法
A
C
S
T

實例公開方法

after_commit(*args, &block)

這個回呼會在記錄建立、更新或銷毀之後呼叫。

你可以指定回呼只會透過 :on 選項由特定動作觸發

after_commit :do_foo, on: :create
after_commit :do_bar, on: :update
after_commit :do_baz, on: :destroy

after_commit :do_foo_bar, on: [:create, :update]
after_commit :do_bar_baz, on: [:update, :destroy]
# File activerecord/lib/active_record/transactions.rb, line 266
def after_commit(*args, &block)
  set_options_for_callbacks!(args, prepend_option)
  set_callback(:commit, :after, *args, &block)
end

after_create_commit(*args, &block)

after_commit :hook, on: :create 的捷徑。

# File activerecord/lib/active_record/transactions.rb, line 278
def after_create_commit(*args, &block)
  set_options_for_callbacks!(args, on: :create, **prepend_option)
  set_callback(:commit, :after, *args, &block)
end

after_destroy_commit(*args, &block)

after_commit :hook, on: :destroy 的捷徑。

# File activerecord/lib/active_record/transactions.rb, line 290
def after_destroy_commit(*args, &block)
  set_options_for_callbacks!(args, on: :destroy, **prepend_option)
  set_callback(:commit, :after, *args, &block)
end

after_rollback(*args, &block)

這個回呼會在建立、更新或銷毀回滾後呼叫。

請查看 after_commit 文件以取得選項。

# File activerecord/lib/active_record/transactions.rb, line 298
def after_rollback(*args, &block)
  set_options_for_callbacks!(args, prepend_option)
  set_callback(:rollback, :after, *args, &block)
end

after_save_commit(*args, &block)

after_commit :hook, on: [ :create, :update ] 的捷徑。

# File activerecord/lib/active_record/transactions.rb, line 272
def after_save_commit(*args, &block)
  set_options_for_callbacks!(args, on: [ :create, :update ], **prepend_option)
  set_callback(:commit, :after, *args, &block)
end

after_update_commit(*args, &block)

after_commit :hook, on: :update 的捷徑。

# File activerecord/lib/active_record/transactions.rb, line 284
def after_update_commit(*args, &block)
  set_options_for_callbacks!(args, on: :update, **prepend_option)
  set_callback(:commit, :after, *args, &block)
end

current_transaction()

傳回目前交易狀態的表示方式,可以是頂層交易、儲存點或沒有交易。

無論是否有交易目前處於作用中,都會永遠傳回一個物件。若要檢查交易是否已開啟,請使用 current_transaction.open?

請參閱 ActiveRecord::Transaction 文件以了解詳細的行為。

# File activerecord/lib/active_record/transactions.rb, line 245
def current_transaction
  connection_pool.active_connection&.current_transaction&.user_transaction || Transaction::NULL_TRANSACTION
end

set_callback(name, *filter_list, &block)

ActiveSupport::Callbacks::ClassMethods#set_callback 相似,但提供 after_commitafter_rollback 回呼中所提供的選項支援。

# File activerecord/lib/active_record/transactions.rb, line 305
def set_callback(name, *filter_list, &block)
  options = filter_list.extract_options!
  filter_list << options

  if name.in?([:commit, :rollback]) && options[:on]
    fire_on = Array(options[:on])
    assert_valid_transaction_action(fire_on)
    options[:if] = [
      -> { transaction_include_any_action?(fire_on) },
      *options[:if]
    ]
  end


  super(name, *filter_list, &block)
end

transaction(**options, &block)

# File activerecord/lib/active_record/transactions.rb, line 232
def transaction(**options, &block)
  with_connection do |connection|
    connection.transaction(**options, &block)
  end
end