paint-brush
Python: Django トランザクションの仕組みを詳しく見る@mta
134 測定値 新しい歴史

Python: Django トランザクションの仕組みを詳しく見る

Michael T. Andemeskel9m2025/03/02
Read on Terminal Reader

長すぎる; 読むには

Django の transaction.atomic() を使用すると、トランザクションを作成してアトミック操作 (すべて実行され、一緒に保存されるか失敗するかが保証される DB 操作) を実現できます。Django は、基盤となる DB のトランザクション メカニズムを使用してトランザクションを作成し、トランザクション内のコードを実行し、エラーが発生しない場合はトランザクションを DB にコミットします。
featured image - Python: Django トランザクションの仕組みを詳しく見る
Michael T. Andemeskel HackerNoon profile picture
0-item


コンテンツ

  • TLDR — Django トランザクション ロジックの概要
  • ロジック図
  • トランザクションとATOMIC操作
  • Django のトランザクション
  • Django 自動コミットモード — 自動トランザクションをオフにする
  • 出典

TLDR — Django トランザクション ロジックの概要

これは、PSQL または他の同様の SQL (SQLite、MySQL など) ベースのデータベース (DB) で Django を実行するアプリ向けです。トランザクションに関する基本的なルールは、他の DB には適用されない可能性があります。内部的には、Django のトランザクション管理コードは次の処理を行います (これは Django のドキュメントからコピーしたもので、説明のために独自の箇条書きを付けています (下にスクロールしてください))。


  • 最も外側のアトミック ブロックに入るときにトランザクションを開きます。
    • DB への接続を取得または作成し、それをトランザクションとして設定します。


  • DB 操作が発生したときにロックを要求して取得します。
    • すべての DB 操作にはロックが必要です。一部のロックは緩く、他の接続が同じリソースで操作を実行できるようにしますが、他のロックはより厳密で、排他的であり、読み取りと書き込みの両方の操作をブロックします。レコードの作成、更新、削除、およびテーブルと列の変更では、より厳密なロックが取得され、他の接続による操作の実行がブロックされます。これらのロックは、トランザクションの終了まで保持されます。


  • 内部のアトミック ブロックに入るときにセーブポイントを作成します。
    • これは、たとえばテーブルにレコードを挿入するときに自動的に実行されます。一部のモデル操作は、自動的にアトミック ブロックにラップされます。

    • これは、atomic() でラップされた関数を呼び出すか、with ステートメントで atomic() を使用することによって手動で実行できます (コンテキスト マネージャーとして)。

    • セーブポイントは外部トランザクションと同じ接続を使用します。セーブポイントの Django コードを見ると、別のトランザクションを作成するための単なる再帰呼び出しであることがわかります。

    • セーブポイントはセーブポイント内にネストすることができ、スタックがセーブポイントを追跡します。


  • 内部ブロックを終了するときにセーブポイントを解放またはロールバックします。
    • 挿入後、変更をコミットしてトランザクションの残りの部分(他の接続ではない)で使用できるようにするか、変更をロールバックすることによってセーブポイントが削除されます。


  • DB に触れないコードに遭遇した場合、そのコードは通常どおり実行されます。
    • DB 以外のロジックが実行されている間も、DB のロックは保持されます。たとえば、トランザクションの本体で HTTP リクエストを送信すると、そのリクエストが送信され、応答が返されるまでロックは保持されます。Django は、DB 操作の実行を停止してもロックを解放しません。外部トランザクションが終了するまでロックを保持します。

    • ネストされたトランザクションで取得されたロックも、外部のトランザクションが終了するか、ネストされたトランザクションがロールバックされるまで保持されます。


  • RuntimeError が発生した場合、または DB 外部でエラーが発生した場合 (DB 操作とは関係ありません)。
    • トランザクションは停止され、ロールバックされ、ロックが解除されます。

    • ネストされたトランザクションでエラーが発生した場合、外側のトランザクションは影響を受けません。ロールバックは発生しますが、外側のトランザクションは中断されることなく続行できます (発生したエラーがキャッチされ、処理される限り)。

    • トランザクションはエラーをキャッチしませんが、変更をロールバックするタイミングを知るためにトランザクションに依存しています。したがって、DB 操作を try/except でラップするのではなく、トランザクションでラップしてから、そのトランザクションを try/except でラップします。そうすることで、DB 操作が失敗し、外部のトランザクションに影響を与えることなく自動的にロールバックされます。


  • 最も外側のブロックを終了するときに、トランザクションをコミットまたはロールバックします。
    • 最後に、トランザクション全体がコミットされ、変更がすべての DB ユーザー/接続で利用できるようになります。または、トランザクションがロールバックされ、DB はトランザクションが発生したときと同じ状態のままになります。


次のブログでは、ロック不足による停止を回避するためにトランザクションに何を入れるべきか、何を入れるべきでないかについて書きます。

ロジック図

Django トランザクションのロジック図 — 完全版

トランザクションとATOMIC操作

トランザクションは、 ACID DB (PSQL、SQL、最新のMongoDBなど) をATOMIC (ACID の A) にするためのメカニズムです。つまり、DB へのすべての書き込みは、どんなに複雑であっても、単一の操作として実行されます。書き込みが成功すると、DB へのすべての変更が保持され、すべての接続で同時に利用できるようになります (部分的な書き込みはありません)。書き込みが失敗すると、すべての変更がロールバックされます。この場合も、部分的な変更はありません。


トランザクションでは次のルールが保証されます。

  • DB 内のデータはトランザクションの途中で変更されません。
  • トランザクションが完了するまで、他の接続ではトランザクションによって行われた変更を確認できません。
  • DB には新しいデータがすべて含まれるか、まったく含まれないかのいずれかになります。


これらのルールにより、DB のユーザーは複雑な操作をまとめて 1 つの操作として実行できます。たとえば、ある銀行口座から別の銀行口座への振替を実行する場合、トランザクションがなければ、この 2 つの書き込み操作の途中で引き出しや口座閉鎖操作が行われ、振替が無効になる可能性があります。トランザクションにより、ユーザーは何らかの形でトラフィックを制御できます。トランザクションの進行中は、他のすべての競合する操作をブロックできます。


操作は、テーブルと行に対する一連のロックによってブロックされます。PSQL やその他の SQL バリアントでは、トランザクションは BEGIN で作成され、次に select/insert/delete/alter などの操作が実行されるとロックが取得され、トランザクションは COMMIT または ROLLBACK で終了します。ロックは COMMIT または ROLLBACK が実行されると解除されます。幸い、Django では、これらの 3 つのステートメントを使用せずにトランザクションを作成できます (ただし、ロックについては引き続き考慮する必要があります。これについては次の投稿で詳しく説明します)。


 -- Start a new transaction BEGIN; SELECT … INSERT INTO … UPDATE … DELETE FROM … -- Save the changes COMMIT;


  • 選択により、読み取られる行の更新をブロックするロックが取得されます。
  • 挿入は、作成される行の更新をブロックするロックを取得します。
  • 更新では、更新される行の更新をブロックするロックが取得されます。
  • 削除では、削除される行の更新をブロックするロックが取得されます。
  • 明示的にエラーをチェックして ROLLBACK を呼び出す必要はありません。トランザクションがこれを実行します。
  • そこにテーブル変更操作があった場合、テーブルのすべての読み取りと書き込みがブロックされます (これについては次のブログで詳しく説明します)。

Django のトランザクション

from django.db import transaction from app.core.models import Accounts, Fee, NotificationJob def do_migration(): overdrawn_accounts = Accounts.objects.filter(type='overdrawn') for acount in overdrawn_accounts: create_notification(acount) @transaction.atomic def create_notification(acount: Accounts): # $5 fee for overdraft - 500 because we never store money as float!!! recall = Fee.objects.create(acount=acount, description='Fee for overdraft', amount=500) NotificationJob.objects.create(recall=recall, notification_type='all') acount.status = 'awaiting_payment' acount.save() def do_migration2(): overdawn_account = Accounts.objects.filter(type='overdrawn') for account in overdawn_account: with transaction.atomic(): recall = Fee.objects.create(acount=account, description='Fee for overdraft', amount=500) NotificationJob.objects.create(recall=recall, notification_type='all') account.status = 'awaiting_payment' account.save()


Django は、自動コミット モードで実行すると、すべての DB 操作をトランザクションに自動的にラップします (詳細は後述)。 明示的なトランザクションは、atomic() デコレータまたはコンテキスト マネージャ (atomic() を使用) を使用して作成できます。atomic() を使用すると、個々の DB 操作はトランザクションにラップされず、DB にすぐにコミットされません (操作は実行されますが、DB への変更は他のユーザーには表示されません)。代わりに、関数全体がトランザクション ブロックにラップされ、最後にコミットされます (エラーが発生しない場合)。つまり、COMMIT が実行されます。


エラーがある場合、DB の変更はロールバックされます (ROLLBACK が実行されます)。トランザクションが最後までロックを保持するのは、このためです。テーブルのロックを解除し、別の接続でテーブルを変更したり読み取ったりして、変更または読み取った内容を強制的にロールバックさせられるような事態は避けたいからです。


 ANGRY_CUSTOMER_THRESHOLD = 3 @transaction.atomic def create_notification(acount: Accounts): recall = Fee.objects.create(acount=acount, description='Fee for overdraft', amount=500) NotificationJob.objects.create(recall=recall, notification_type='all') try: # when this completes successfully, the changes will be available to the outer transaction with transaction.atomic(): owner = acount.owner fees = Fee.objects.filter(owner=owner).count() if fees >= ANGRY_CUSTOMER_THRESHOLD: for fee in fees: fee.amount = 0 fee.save() owner.status = 'angry' owner.save() # as long as we catch the error, the outer transaction will not be rolled back except Exception as e: logger.error(f'Error while removings fees for account {acount.id}: {e}') acount.status = 'awaiting_payment' acount.save()


また、atomic() でラップされた別の関数を呼び出すか、トランザクション内でコンテキスト マネージャーを使用することで、トランザクションをネストすることもできます。これにより、トランザクションの残りの部分に影響を与えずにリスクのある操作を試行できるセーブポイントを作成できます。内部トランザクションが検出されると、セーブポイントが作成され、内部トランザクションが実行され、内部トランザクションが失敗した場合は、データがセーブポイントにロールバックされ、外部トランザクションが続行されます。外部トランザクションが失敗した場合は、すべての内部トランザクションが外部トランザクションとともにロールバックされます。トランザクションのネストは、atomic() で persistent=True を設定することで防止できます。これにより、内部トランザクションが検出されると、トランザクションで RuntimeError が発生します。


ネストされたトランザクションについて覚えておくべきこと:

  • ネストされたトランザクションは、ネストされたトランザクションがロールバックされるか、外部のトランザクションがコミットまたはロールバックされない限り、ロックを解除しません。
  • ネストされたトランザクションは DB エラーをキャッチしません。エラーの原因となったトランザクション内の操作を単にロールバックするだけです。外部のトランザクションを続行するには、DB エラーをキャッチする必要があります。
  • ネストされたトランザクションで行われた変更は、外側のトランザクションと後続のネストされたトランザクションで利用できます。

Django 自動コミット モード — 自動トランザクションをオフにする

DATABASES = { 'default': { # True by default 'AUTOCOMMIT': False, # ...rest of configs } }

デフォルトでは、Django は自動コミットモードで実行されます。つまり、すべての DB 操作はトランザクションでラップされ、ユーザーが明示的にトランザクションで操作をラップしない限り、すぐに実行されます (コミットされるため、自動コミット)。これにより、基盤となる DB のトランザクション ロジックが難読化されます。PSQL などの一部の DB は、すべての操作をトランザクションで自動的にラップしますが、他の DB はそうしません。Django がこれを自動的に実行してくれるなら、基盤となる DB ロジックについて心配する必要はありません。


自動コミット モードをオフにすることもできますが、そうすると DB に依存して操作を管理し、操作が ATOMIC であることを確認することになり、開発者にとってリスクが高く不便です。API を作成するときに、どの書き込み操作がトランザクション内にあるかを判断し、部分的にではなく全体的に書き込まれるようにする必要があるとしたら、それは楽しいことではありません。


ただし、DB 上の接続数と実行中の操作の順序がわかっている場合は、自動コミット モードをオフにする必要があります。これらの操作の競合を解消する方法がわかれば、自動コミット モードをオフにして効率を上げることができます。

  • すべての操作から BEGIN と COMMIT/ROLLBACK の 2 つのコマンドを削除します。ただし、ほとんどの DB はトランザクション内のすべての操作を自動的にラップするため、これは無意味な場合があります。
  • ロックは長時間保持されません。トランザクション全体が終了するまで待機する場合と比べて、操作が終了するとすぐに保持されているロックが解除されます。
  • デッドロックの可能性は低くなります。デッドロックは、2 つのトランザクションが、もう一方のトランザクションが保持しているロックを必要とする場合に発生します。この場合、両方のトランザクションは、もう一方のトランザクションが終了するまで待機しますが、どちらも終了できません。ただし、デッドロックは依然として発生する可能性があります。一部の DB 操作では、複数の書き込みが行われます。たとえば、インデックスがある列を更新すると、列とインデックスが更新されます (少なくとも 2 回の書き込み)。別の接続が影響を受ける行または列を操作しようとすると、デッドロックが発生する可能性があります。


欠点は、データ破損のリスクが高いことです。それだけの価値はありませんが、私が思いつかないようなユースケースがあるかもしれません。

出典