トランザクション¶
データベースを操作する場合、操作をグループ化して、1つが失敗した場合に最新の安全な状態に戻ることができるようにする必要があることがよくあります。この解決策は、**トランザクションパラダイム**で説明されており、ACIDプロパティ(原子性、一貫性、独立性、耐久性)[^ACID]を満たすために、ほとんどのデータベースエンジンによって実装されています。
これを踏まえ、次の例を示します。
2つの口座(それぞれ名前と金額で表されます)があります。
db = get_bank_db
create_account db, "John", amount: 100
create_account db, "Sarah", amount: 100
ある時点で、ある口座から別の口座に送金が行われます。たとえば、*JohnがSarahに50ドルを送金する*という場合です。
2つの口座(それぞれ名前と金額で表されます)があります。
deposit db, "Sarah", 50
withdraw db, "John", 50
いずれかの操作が失敗した場合、最終状態が一貫性のないものになる可能性があることに注意することが重要です。そのため、**2つの操作**(預金と引き出し)を**1つの操作**として実行する必要があります。そして、エラーが発生した場合、その1つの操作が実行されなかったかのように時間を巻き戻したいと考えています。
db = get_bank_db
create_account db, "John", amount: 100
create_account db, "Sarah", amount: 100
db.transaction do |tx|
cnn = tx.connection
transfer_amount = 1000
deposit cnn, "Sarah", transfer_amount
withdraw cnn, "John", transfer_amount
end
上記の例では、Database#transaction
メソッドを呼び出すだけでトランザクションを開始します(database
オブジェクトの取得方法はget_bank_db
メソッドにカプセル化されており、このドキュメントの範囲外です)。block
はトランザクションの本体です。block
が(エラーなしで)実行されると、データベースに変更を永続化するために**暗黙的なコミット**が最終的に実行されます。いずれかの操作によって例外がスローされると、**暗黙的なロールバック**が実行され、データベースがトランザクション開始前の状態に戻ります。
例外処理とロールバック¶
前述のように、例外がスローされると**暗黙的なロールバック**が実行されます。そして、その例外は私たちによってレスキューされる可能性があることに言及する価値があります。
db = get_bank_db
create_account db, "John", amount: 100
create_account db, "Sarah", amount: 100
begin
db.transaction do |tx|
cnn = tx.connection
transfer_amount = 1000
deposit(cnn, "Sarah", transfer_amount)
# John does not have enough money in his account!
withdraw(cnn, "John", transfer_amount)
end
rescue ex
puts "Transfer has been rolled back due to: #{ex}"
end
トランザクションの本体内で例外を発生させることもできます。
db = get_bank_db
create_account db, "John", amount: 100
create_account db, "Sarah", amount: 100
begin
db.transaction do |tx|
cnn = tx.connection
transfer_amount = 50
deposit(cnn, "Sarah", transfer_amount)
withdraw(cnn, "John", transfer_amount)
raise Exception.new "Because ..."
end
rescue ex
puts "Transfer has been rolled back due to: #{ex}"
end
前の例と同様に、例外によりトランザクションがロールバックされ、その後私たちによってレスキューされます。
動作が異なるexception
が1つあります。DB::Rollback
がブロック内で発生した場合、暗黙的なロールバックは行われますが、例外はブロックの外では発生しません。
db = get_bank_db
create_account db, "John", amount: 100
create_account db, "Sarah", amount: 100
begin
db.transaction do |tx|
cnn = tx.connection
transfer_amount = 50
deposit(cnn, "Sarah", transfer_amount)
withdraw(cnn, "John", transfer_amount)
# rollback exception
raise DB::Rollback.new
end
rescue ex
# ex is never a DB::Rollback
end
明示的なコミットとロールバック¶
これまでのすべての例では、ロールバックは**暗黙的**ですが、トランザクションにロールバックするように指示することもできます。
db = get_bank_db
create_account db, "John", amount: 100
create_account db, "Sarah", amount: 100
begin
db.transaction do |tx|
cnn = tx.connection
transfer_amount = 50
deposit(cnn, "Sarah", transfer_amount)
withdraw(cnn, "John", transfer_amount)
tx.rollback
puts "Rolling Back the changes!"
end
rescue ex
# Notice that no exception is used in this case.
end
そして、commit
メソッドを使用することもできます。
db = get_bank_db
db.transaction do |tx|
cnn = tx.connection
transfer_amount = 50
deposit(cnn, "Sarah", transfer_amount)
withdraw(cnn, "John", transfer_amount)
tx.commit
end
注記
commit
またはrollback
が使用された後、トランザクションは使用できなくなります。接続は開いたままですが、ステートメントは終了したトランザクションのコンテキスト外で実行されます。
ネストされたトランザクション¶
名前が示すように、ネストされたトランザクションは、別のトランザクションのスコープ内で作成されたトランザクションです。ここに例を示します。
db = get_bank_db
create_account db, "John", amount: 100
create_account db, "Sarah", amount: 100
create_account db, "Jack", amount: 0
begin
db.transaction do |outer_tx|
outer_cnn = outer_tx.connection
transfer_amount = 50
deposit(outer_cnn, "Sarah", transfer_amount)
withdraw(outer_cnn, "John", transfer_amount)
outer_tx.transaction do |inner_tx|
inner_cnn = inner_tx.connection
# John => 50 (pending commit)
# Sarah => 150 (pending commit)
# Jack => 0
another_transfer_amount = 150
deposit(inner_cnn, "Jack", another_transfer_amount)
withdraw(inner_cnn, "Sarah", another_transfer_amount)
end
end
rescue ex
puts "Exception raised due to: #{ex}"
end
上記の例からのいくつかの観察結果:inner_tx
は、outer_tx
がコミットを保留しているにもかかわらず、更新された値で動作します。outer_tx
とinner_tx
で使用される接続は、**同じ接続**です。これは、inner_tx
が作成時にouter_tx
から接続を継承するためです。
ネストされたトランザクションのロールバック¶
すでに見たように、ロールバックはいつでも(例外によって、またはrollback
メッセージを明示的に送信することによって)実行できます。
そのため、**外部トランザクションに配置された例外によって実行されるロールバック**の例を示します。
db = get_bank_db
create_account db, "John", amount: 100
create_account db, "Sarah", amount: 100
create_account db, "Jack", amount: 0
begin
db.transaction do |outer_tx|
outer_cnn = outer_tx.connection
transfer_amount = 50
deposit(outer_cnn, "Sarah", transfer_amount)
withdraw(outer_cnn, "John", transfer_amount)
outer_tx.transaction do |inner_tx|
inner_cnn = inner_tx.connection
# John => 50 (pending commit)
# Sarah => 150 (pending commit)
# Jack => 0
another_transfer_amount = 150
deposit(inner_cnn, "Jack", another_transfer_amount)
withdraw(inner_cnn, "Sarah", another_transfer_amount)
end
raise Exception.new("Rollback all the things!")
end
rescue ex
puts "Exception raised due to: #{ex}"
end
outer_tx
ブロックで行われたロールバックは、inner_tx
ブロック内のものも含め、すべての変更をロールバックしました(**明示的な**ロールバックを使用した場合も同じです)。
**内部トランザクションブロックの例外によってロールバックが実行された場合**、outer_tx
内のものも含め、すべての変更がロールバックされます。
db = get_bank_db
create_account db, "John", amount: 100
create_account db, "Sarah", amount: 100
create_account db, "Jack", amount: 0
begin
db.transaction do |outer_tx|
outer_cnn = outer_tx.connection
transfer_amount = 50
deposit(outer_cnn, "Sarah", transfer_amount)
withdraw(outer_cnn, "John", transfer_amount)
outer_tx.transaction do |inner_tx|
inner_cnn = inner_tx.connection
# John => 50 (pending commit)
# Sarah => 150 (pending commit)
# Jack => 0
another_transfer_amount = 150
deposit(inner_cnn, "Jack", another_transfer_amount)
withdraw(inner_cnn, "Sarah", another_transfer_amount)
raise Exception.new("Rollback all the things!")
end
end
rescue ex
puts "Exception raised due to: #{ex}"
end
inner-transaction
の変更をロールバックしながら、outer-transaction
の変更を保持する方法があります。inner_tx
オブジェクトでrollback
を使用します。これにより、**内部トランザクションのみ**がロールバックされます。ここに例を示します。
db = get_bank_db
create_account db, "John", amount: 100
create_account db, "Sarah", amount: 100
create_account db, "Jack", amount: 0
begin
db.transaction do |outer_tx|
outer_cnn = outer_tx.connection
transfer_amount = 50
deposit(outer_cnn, "Sarah", transfer_amount)
withdraw(outer_cnn, "John", transfer_amount)
outer_tx.transaction do |inner_tx|
inner_cnn = inner_tx.connection
# John => 50 (pending commit)
# Sarah => 150 (pending commit)
# Jack => 0
another_transfer_amount = 150
deposit(inner_cnn, "Jack", another_transfer_amount)
withdraw(inner_cnn, "Sarah", another_transfer_amount)
inner_tx.rollback
end
end
rescue ex
puts "Exception raised due to: #{ex}"
end
inner-transaction
ブロックでDB::Rollback
例外が発生した場合も同じです。
db = get_bank_db
create_account db, "John", amount: 100
create_account db, "Sarah", amount: 100
create_account db, "Jack", amount: 0
begin
db.transaction do |outer_tx|
outer_cnn = outer_tx.connection
transfer_amount = 50
deposit(outer_cnn, "Sarah", transfer_amount)
withdraw(outer_cnn, "John", transfer_amount)
outer_tx.transaction do |inner_tx|
inner_cnn = inner_tx.connection
# John => 50 (pending commit)
# Sarah => 150 (pending commit)
# Jack => 0
another_transfer_amount = 150
deposit(inner_cnn, "Jack", another_transfer_amount)
withdraw(inner_cnn, "Sarah", another_transfer_amount)
# Rollback exception
raise DB::Rollback.new
end
end
rescue ex
puts "Exception raised due to: #{ex}"
end
[^ACID]: Theo Haerder and Andreas Reuter. 1983. Principles of transaction-oriented database recovery. ACM Comput. Surv. 15, 4 (December 1983), 287-317. DOI=http://dx.doi.org/10.1145/289.291