コンテンツ
これは、PSQL または他の同様の SQL (SQLite、MySQL など) ベースのデータベース (DB) で Django を実行するアプリ向けです。トランザクションに関する基本的なルールは、他の DB には適用されない可能性があります。内部的には、Django のトランザクション管理コードは次の処理を行います (これは Django のドキュメントからコピーしたもので、説明のために独自の箇条書きを付けています (下にスクロールしてください))。
DB への接続を取得または作成し、それをトランザクションとして設定します。
すべての DB 操作にはロックが必要です。一部のロックは緩く、他の接続が同じリソースで操作を実行できるようにしますが、他のロックはより厳密で、排他的であり、読み取りと書き込みの両方の操作をブロックします。レコードの作成、更新、削除、およびテーブルと列の変更では、より厳密なロックが取得され、他の接続による操作の実行がブロックされます。これらのロックは、トランザクションの終了まで保持されます。
これは、たとえばテーブルにレコードを挿入するときに自動的に実行されます。一部のモデル操作は、自動的にアトミック ブロックにラップされます。
これは、atomic() でラップされた関数を呼び出すか、with ステートメントで atomic() を使用することによって手動で実行できます (コンテキスト マネージャーとして)。
セーブポイントは外部トランザクションと同じ接続を使用します。セーブポイントの Django コードを見ると、別のトランザクションを作成するための単なる再帰呼び出しであることがわかります。
セーブポイントはセーブポイント内にネストすることができ、スタックがセーブポイントを追跡します。
挿入後、変更をコミットしてトランザクションの残りの部分(他の接続ではない)で使用できるようにするか、変更をロールバックすることによってセーブポイントが削除されます。
DB 以外のロジックが実行されている間も、DB のロックは保持されます。たとえば、トランザクションの本体で HTTP リクエストを送信すると、そのリクエストが送信され、応答が返されるまでロックは保持されます。Django は、DB 操作の実行を停止してもロックを解放しません。外部トランザクションが終了するまでロックを保持します。
ネストされたトランザクションで取得されたロックも、外部のトランザクションが終了するか、ネストされたトランザクションがロールバックされるまで保持されます。
トランザクションは停止され、ロールバックされ、ロックが解除されます。
ネストされたトランザクションでエラーが発生した場合、外側のトランザクションは影響を受けません。ロールバックは発生しますが、外側のトランザクションは中断されることなく続行できます (発生したエラーがキャッチされ、処理される限り)。
トランザクションはエラーをキャッチしませんが、変更をロールバックするタイミングを知るためにトランザクションに依存しています。したがって、DB 操作を try/except でラップするのではなく、トランザクションでラップしてから、そのトランザクションを try/except でラップします。そうすることで、DB 操作が失敗し、外部のトランザクションに影響を与えることなく自動的にロールバックされます。
最後に、トランザクション全体がコミットされ、変更がすべての DB ユーザー/接続で利用できるようになります。または、トランザクションがロールバックされ、DB はトランザクションが発生したときと同じ状態のままになります。
次のブログでは、ロック不足による停止を回避するためにトランザクションに何を入れるべきか、何を入れるべきでないかについて書きます。
トランザクションは、 ACID DB (PSQL、SQL、最新のMongoDBなど) をATOMIC (ACID の A) にするためのメカニズムです。つまり、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;
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 が発生します。
ネストされたトランザクションについて覚えておくべきこと:
DATABASES = { 'default': { # True by default 'AUTOCOMMIT': False, # ...rest of configs } }
デフォルトでは、Django は自動コミットモードで実行されます。つまり、すべての DB 操作はトランザクションでラップされ、ユーザーが明示的にトランザクションで操作をラップしない限り、すぐに実行されます (コミットされるため、自動コミット)。これにより、基盤となる DB のトランザクション ロジックが難読化されます。PSQL などの一部の DB は、すべての操作をトランザクションで自動的にラップしますが、他の DB はそうしません。Django がこれを自動的に実行してくれるなら、基盤となる DB ロジックについて心配する必要はありません。
自動コミット モードをオフにすることもできますが、そうすると DB に依存して操作を管理し、操作が ATOMIC であることを確認することになり、開発者にとってリスクが高く不便です。API を作成するときに、どの書き込み操作がトランザクション内にあるかを判断し、部分的にではなく全体的に書き込まれるようにする必要があるとしたら、それは楽しいことではありません。
ただし、DB 上の接続数と実行中の操作の順序がわかっている場合は、自動コミット モードをオフにする必要があります。これらの操作の競合を解消する方法がわかれば、自動コミット モードをオフにして効率を上げることができます。
欠点は、データ破損のリスクが高いことです。それだけの価値はありませんが、私が思いつかないようなユースケースがあるかもしれません。