データベースのトランザクション

Django では、データベースのトランザクションをコントロールする方法が提供されています。

データベースのトランザクションを管理する

Django のデフォルトのトランザクションの動作

Django のデフォルトの動作は、オートコミットモードで実行することです。トランザクションがアクティブでない限り、各クエリは即座にデータベースにコミットされます。詳しくは下記を参照してください

Django は、自動的にトランザクションやセーブポイントを使い、特に delete()update() クエリにおいて、複数のクエリを要求する ORM 操作の信頼性を担保します。

Django の TestCase クラスは、パフォーマンス向上のため、各テストをトランザクションでラップします。

HTTP リクエストにトランザクションを結びつける

ウェブ上でトランザクションを扱う一般的な方法は、各リクエストをトランザクションでラップすることです。この動作を有効化したい各データベースの設定で、ATOMIC_REQUESTSTrue にセットしてください。

これは、次のように動作します。まず、ビュー関数を呼び出す前に、Django はトランザクションを開始します。レスポンスが問題なく生成された場合は、Django はトランザクションをコミットします。もしビューが例外を生成した場合は、Django はトランザクションをロールバックします。

ビューのコード (通常は atomic() コンテキストマネージャー) の中で、セーブポイントを使ったサブトランザクションを扱うことができます。ただし、ビューの最後では、すべての変更がコミットされるか、何もコミットされないかのどちらかです。

警告

このトランザクションモデルは簡潔ではありますが、トラフィックが増加するときには非効率となります。全てのビューでトランザクションを扱うとオーバーヘッドが増加します。パフォーマンスへの影響は、アプリケーションのクエリパターンと、どれだけうまくデータベースがロッキングを扱うかに依存します。

リクエストごとのトランザクションとストリーミングレスポンス

ビューが StreamingHttpResponse を返すとき、レスポンスの内容読み出しが内容を生成するためのコードを実行することがあります。ビューはすでに返されているので、このコードはトランザクションの外で走ります。

一般的に言って、ストリーミングレスポンスが生成されている間はデータベースに書き込みすることは推奨されません。レスポンスを送信開始した後にエラーを扱う適切な方法が存在しないからです。

実際には、この機能はすべてのビュー関数を以下で説明する atomic() デコレータでラップします。

あなたのビューの実行だけがトランザクションで閉じられることに注意してください。ミドルウェアはトランザクションの外で実行し、テンプレートレスポンスのレンダリングを実行します。

ATOMIC_REQUESTS が有効な場合、ビューがトランザクション内で実行するのを防ぐことができます。

non_atomic_requests(using=None)

このデコレータは、与えられたビューのために ATOMIC_REQUESTS を無効化します。

from django.db import transaction


@transaction.non_atomic_requests
def my_view(request):
    do_stuff()


@transaction.non_atomic_requests(using="other")
def my_other_view(request):
    do_stuff_on_the_other_database()

ビュー自身に適用される場合にのみ作動します。

明示的にトランザクションをコントロールする

Django は、データベーストランザクションをコントロールするための一つの API を提供しています。

atomic(using=None, savepoint=True, durable=False)

Atomicityは、データベーストランザクションの定義プロパティです。atomic は、データベースの atomicity が保証されるコードブロックを作成することを可能にします。 コードブロックが正常に完了すると、変更はデータベースにコミットされます。 例外がある場合、変更はロールバックされます。

atomic のブロックはネスト可能です。この場合、内側のブロックが成功裏に完了しても、その効果は後で外側のブロックで例外が発生した場合にロールバック可能となっています。

アトミックなブロックを常に一番外側のアトミックなブロックにすることで、そのブロックがエラーにならずに終了したときにデータベースの変更がコミットされるようにすると便利なことがあります。これは耐久性 (durability) と呼ばれ、 durable=True を設定することで実現できます。もし atomic ブロックが他のブロックの中に入れ子になっている場合、 RuntimeError が発生します。

atomic は、decorator として使用することも:

from django.db import transaction


@transaction.atomic
def viewfunc(request):
    # This code executes inside a transaction.
    do_stuff()

context manager として使用することも可能です:

from django.db import transaction


def viewfunc(request):
    # This code executes in autocommit mode (Django's default).
    do_stuff()

    with transaction.atomic():
        # This code executes inside a transaction.
        do_more_stuff()

try/except ブロック内で atomic をラップすると、integrity error を自然な形で処理できます:

from django.db import IntegrityError, transaction


@transaction.atomic
def viewfunc(request):
    create_parent()

    try:
        with transaction.atomic():
            generate_relationships()
    except IntegrityError:
        handle_exception()

    add_children()

この例では、 generate_relationships() が整合性制約を破ってデータベースエラーを起こしたとしても、 add_children() でクエリを実行でき、 create_parent() での変更はそのまま同じトランザクションにバインドされます。 generate_relationships() 内での試行は、handle_exception() が呼び出された時点ですでに安全にロールバックされていることに留意してください。したがって、必要に応じてデータベース上で例外ハンドラを使用できます。

atomic の内部で例外をキャッチしない!

atomic ブロックの処理を終える際、Django は通常の終了なのか例外を伴うのかを見て、コミットするかロールバックするかを決定します。atomic ブロック内で例外をキャッチしてハンドする場合、Django に対して問題が発生したことを隠すことになります。これは予期しない動作の原因となります。

この挙動は、DatabaseError およびそのサブクラス (IntegrityError など) に対する懸念となります。こうしたエラーが起きた場合、Djangoは atomic ブロックの最後にロー路バックを実施します。ロールバックが発生する前にデータベースクエリを実行しようとすると、Django は TransactionManagementError を送出します。ORM 関連のシグナルハンドラが例外を送出した際にも同様の挙動となります。

データベースエラーをキャッチする正しい方法は、上記で示した通りの atomic ブロックです。必要に応じてさらに atomic ブロックを追加してください。このパターンはもう一つのメリットがあります: 例外が発生した場合にどの操作がロールバックされるかを限定できることです。

素の SQL クエリによって発生した例外をキャッチした場合、Django の挙動は決まっておらず、データベースに依存します。

トランザクションをロールバックする際、アプリの状態を手動で戻す必要がある場合があります。

トランザクションのロールバックが発生しても、モデルのフィールドの値は元に戻りません。これは、手動で元のフィールドの値を復元しない限り、一貫性のないモデルの状態につながる可能性があります。

たとえば、active フィールドを持つ MyModel が与えられた場合、以下のコードはトランザクション内で activeTrue への更新が失敗した場合に、最後の if obj.active チェックで正しい値が使用されることを保証します:

from django.db import DatabaseError, transaction

obj = MyModel(active=False)
obj.active = True
try:
    with transaction.atomic():
        obj.save()
except DatabaseError:
    obj.active = False

if obj.active:
    ...

これはキャッシュやグローバル変数など、アプリの状態を保持する他のメカニズムにも当てはまります。たとえば、コードがオブジェクトを保存した後にキャッシュ内のデータを積極的に更新する場合、トランザクションが実際にコミットされるまでキャッシュの改変を延期するために、代わりに transaction.on_commit() を使用することを推奨します。

アトミックを保証するために、atomic はいくつかの API を無効にします。アトミックな atomic ブロック内で、データベースコネクションのコミット、ロールバック、または自動コミットの状態を変更しようとすると、例外が発生します。

atomicusing 引数を取り、データベース名を指定します。この引数が与えられない場合、 Django は "default" データベースを使用します。

以下の通り、Djangoのトランザクション管理コードは、:

  • 一番外側のアトミックブロックに入るときにトランザクションを開きます;
  • 内側のアトミックブロックに入るときにセーブポイントを作成します;
  • 内側のブロックを抜けるときに、セーブポイントを解放するかロールバックします;
  • 一番外側のブロックを抜けるときに、トランザクションをコミットするかロールバック します。

引数 savepointFalse に設定することで、内部ブロックのセーブポイン トの作成を無効にできます。例外が発生した場合、 Django はセーブポイントがあれば最初の親ブロックを、なければ一番外側のブロックを終了するときにロールバックを行います。原子性は外側のトランザクションで保証されたままです。このオプションは、セーブポイントのオーバーヘッドが目立つ場合にのみ使うべきです。このオプションには、上述のエラー処理を壊してしまうという欠点があります。

オートコミットがオフの場合は atomic を使用できます。一番外側のブロックであっても、セーブポイントだけを使用します。

パフォーマンスに関する注意事項

オープン中のトランザクションは、データベース・サーバーにとってパフォーマンス・コストとなります。このオーバヘッドを最小にするために、トランザクションはできるだけ短くしてください。これは特に、 Django のリクエスト/レスポンスサイクル以外の、長時間実行するプロセスで atomic() を使っている場合に重要です。

自動コミット

なぜ Django は自動コミットを使うのか

標準SQLでは、SQLクエリはそれぞれトランザクションを開始します。そのようなトランザクションは、明示的にコミットするかロールバックしなければなりません。

これはアプリケーション開発者にとっては必ずしも便利ではありません。この問題を軽減するために、ほとんどのデータベースは自動コミットモードを提供しています。オートコミットが有効でトランザクションがアクティブでない場合、各 SQL クエリは独自のトランザクションに包まれます。言い換えると、それぞれのクエリがトランザクションを開始するだけでなく、クエリが成功したかどうかに応じて、トランザクションも自動的にコミットまたはロールバックされます。

PEP 249, Python Database API Specification v2.0 では、オートコミッ トは初期状態ではオフになっている必要があります。Django はこのデフォルトを上書きし、自動コミットをオンにします。

これを避けるために、 トランザクション管理 を無効にすることもできますが、推奨されません。

トランザクション管理を無効化する

データベースの設定で AUTOCOMMITFalse にすると、Django のトランザクション管理を完全に無効にできます。こうすると、 Django は自動コミットを有効にせず、コミットも行いません。基礎となるデータベースライブラリの通常の動作になります。

この場合、Django やサードパーティのライブラリで開始されたトランザクションであっても、全てのトランザクションを明示的にコミットする必要があります。したがって、これは独自のトランザクション制御ミドルウェアを実行したい場合や、本当に変わったことをしたい場合に最適な方法です。

コミット後にアクションを実行する

現在のデータベーストランザクションに関連するアクションを実行する必要があるが、トランザクションが正常にコミットされた場合にだけ実行したいことがあります。たとえば、バックグラウンドタスク、メール通知、キャッシュの無効化などが考えられます。

on_commit() では、オープンしたトランザクションが正常にコミットされた後に実行されるコールバックを登録できます:

on_commit(func, using=None, robust=False)

on_commit(): に関数または callable (呼び出し可能オブジェクト) を渡します:

from django.db import transaction


def send_welcome_email(): ...


transaction.on_commit(send_welcome_email)

コールバックには引数は渡されませんが、 functools.partial() で渡すことができます:

from functools import partial

for user in users:
    transaction.on_commit(partial(send_invite_email, user=user))

コールバックはオープンしたトランザクションが正常にコミットされた後に呼び出されます。トランザクションがロールバックされた場合(通常は atomic() ブロックで例外を発生させた場合)、コールバックは破棄され、呼び出されることはありません。

トランザクションがオープンされていない状態で on_commit() を呼び出すと、コールバックはすぐに実行されます。

失敗する可能性のあるコールバックを登録すると便利なことがあります。 robust=True を渡すと、現在のコールバックが例外をスローしても、次のコールバックを実行できます。Python の Exception クラスに由来するエラーはすべて捕捉され、 django.db.backends.base ロガーに記録されます。

on_commit() で登録されたコールバックをテストするには TestCase.captureOnCommitCallbacks() を使用します。

Changed in Django 4.2:

引数 robust が追加されました。

セーブポイント

セーブポイント(ネストされた atomic() ブロック)は正しく処理されます。つまり、(入れ子になった atomic() ブロック内の) セーブポイントの後に登録された on_commit() callable は、外側のトランザクションがコミットされた後に呼び出されます:

with transaction.atomic():  # Outer atomic, start a new transaction
    transaction.on_commit(foo)

    with transaction.atomic():  # Inner atomic block, create a savepoint
        transaction.on_commit(bar)

# foo() and then bar() will be called when leaving the outermost block

一方、(例外が発生して)セーブポイントがロールバックされると、内部の callable は呼び出されません:

with transaction.atomic():  # Outer atomic, start a new transaction
    transaction.on_commit(foo)

    try:
        with transaction.atomic():  # Inner atomic block, create a savepoint
            transaction.on_commit(bar)
            raise SomeError()  # Raising an exception - abort the savepoint
    except SomeError:
        pass

# foo() will be called, but not bar()

実行順

トランザクションの On-commit 関数は、登録された順に実行されます。

例外のハンドリング

あるトランザクション内で robust=False で登録された on-commit 関数が例外を発生させた場合、同じトランザクション内でそれ以降に登録された関数は実行されません。これは on_commit() なしで関数を順番に実行した場合と同じ動作です。

Changed in Django 4.2:

引数 robust が追加されました。

実行タイミング

コールバックはコミットが成功した 後に 実行されるので、コールバックに失敗してもトランザクションがロールバックされることはありません。コールバックはトランザクションの成功時に条件付きで実行されますが、トランザクションの 一部 ではありません。想定される使用例(メール通知、バックグラウンドタスクなど)では、これで問題ないでしょう。もしそうでなければ(フォローアップアクションが非常にクリティカルで、その失敗がトランザクション自体の失敗を意味するのであれば)、 on_commit() フックを使うべきではありません。その代わりに、 psycopg Two-Phase Commit protocol supportoptional Two-Phase Commit Extensions in the Python DB-API specification のような two-phase commit が必要かもしれません。

コールバックは、コミット後のコネクションでオートコミットがリストアされるまで実行されません (そうしないと、コールバックで実行されたクエリが暗黙のトランザクションをオープンしてしまい、コネクションが自動コミットモードに戻るのを防いでしまうからです)。

自動コミットモードで atomic() ブロックの外にある場合、関数はコミット時ではなく即座に実行されます。

on-commit 関数は autocommit modeatomic() (または ATOMIC_REQUESTS) トランザクション API でのみ動作します。オートコミットが無効でアトミックブロック内にいない時に on_commit() を呼び出すとエラーになります。

テストでの使用

Django の TestCase クラスは各テストをトランザクションでラップし、テストの分離を行うために、各テストの後にそのトランザクションをロールバックします。つまり、実際にトランザクションがコミットされることはなく、 on_commit() コールバックが実行されることはありません。

この制限を克服するには TestCase.captureOnCommitCallbacks() を使用します。これは on_commit() コールバックをリストにキャプチャし、それに対してアサーションを行ったり、それらを呼び出してトランザクションのコミットをエミュレートしたりすることができます。

この制限を克服するもう一つの方法は、 TestCase の代わりに TransactionTestCase を使うことです。これにより、トランザクションがコミットされ、コールバックが実行されます。しかし、 TransactionTestCase はテストとテストの間にデータベースを flush するので、 分離されている TestCase よりも大幅に遅くなります。

なぜロールバックフックがないのですか?

ロールバックフックはコミットフックよりもロバストに実装するのが難しいです。

たとえば、プロセスが正常にシャットダウンする機会なく終了してデータベースコネクションが切断された場合、ロールバックフックは実行されないでしょう。

アトミックブロック(トランザクション)中に何かを行い、トランザクションが失敗した場合にそれを元に戻すのではなく、on_commit() を使ってトランザクションが成功するまで最初の処理を遅らせるのです。そもそもやっていないことを元に戻すのはとても簡単です!

低レベルAPI

警告

可能であれば、常に atomic() を選択してください。各データベースの特異性を考慮し、不正な操作を防ぐことができます。

低レベルのAPIは、独自のトランザクション管理を実装する場合にだけ有用です。

自動コミット

Django は django.db.transaction モジュールで、各データベースコネクションの自動コミット状態を管理する API を提供しています。

get_autocommit(using=None)
set_autocommit(autocommit, using=None)

atomicusing 引数を取り、データベース名を指定します。この引数が与えられない場合、 Django は "default" データベースを使用します。

オートコミットは初期状態ではオンになっています。オフにした場合、元に戻すのはあなたの責任です。

自動コミットをオフにすると、データベースアダプタのデフォルトの動作になり、 Django は助けてくれません。この動作は PEP 249 で指定されていますが、アダプタの実装は互いに一貫性があるとは限りません。使っているアダプタのドキュメントをよく読んでください。

自動コミットをオンに戻す前に、通常は commit() または rollback() を発行して、トランザクションがアクティブでないことを確認する必要があります。

Django は atomic() ブロックがアクティブなときに自動コミットをオフにすることを拒否します。

トランザクション

トランザクションはアトミックなデータベースクエリの集合です。たとえプログラムがクラッシュしても、データベースはすべての変更が適用されるか、あるいはまったく適用されないかを保証します。

Django はトランザクションを開始する API を提供していません。トランザクションを開始するために期待される方法は、 set_autocommit() で自動コミットを無効にすることです。

トランザクションに入ったら、 commit() でそれまでの変更を適用するか、 rollback() で取り消すかを選択できます。これらの関数は django.db.transaction で定義されています。

commit(using=None)
rollback(using=None)

atomicusing 引数を取り、データベース名を指定します。この引数が与えられない場合、 Django は "default" データベースを使用します。

Django は atomic() ブロックがアクティブなとき、コミットやロールバックを拒否します。

セーブポイント

セーブポイントはトランザクション内のマーカーで、トランザクション全体ではなくトランザクションの一部をロールバックできます。セーブポイントはSQLite、PostgreSQL、Oracle、MySQL(InnoDBストレージエンジンを使用している場合)のバックエンドで使用できます。他のバックエンドはセーブポイント関数を提供しますが、空の操作です。

Django のデフォルトの動作である autocommit を使っている場合、セーブポイントは特に役に立ちません。しかし、いったん atomic() でトランザクションをオープンすると、コミットやロールバックを待つ一連のデータベース操作が構築されます。ロールバックを行うと、トランザクション全体がロールバックされます。セーブポイントは、 transaction.rollback() によって実行される完全なロールバックではなく、きめ細かいロールバックを実行する機能を提供します。

atomic() デコレータがネストされている場合、部分的なコミットやロールバックを可能にするためにセーブポイントを作成します。以下に説明する関数よりも atomic() を使用することを強く推奨しますが、これらはまだパブリック API の一部であり、非推奨とする予定はありません。

これらの関数はそれぞれ using 引数を取り、その引数には動作が適用されるデータベースの名前を指定します。 引数に using が指定されていない場合は、 "default" データベースが使用されます。

セーブポイントは django.db.transaction の3つの関数で制御されます:

savepoint(using=None)

新しいセーブポイントを作成します。これはトランザクションの中で "良い" 状態であることが分かっている点をマークします。セーブポイントID (sid) を返します。

savepoint_commit(sid, using=None)

セーブポイント sid を解放します。セーブポイントが作成されてから行われた変更は、トランザクションの一部となります。

savepoint_rollback(sid, using=None)

トランザクションをセーブポイント sid にロールバックします。

これらの関数は、セーブポイントがサポートされていない場合やデータベースが自動コミットモードの場合は何もしません。

さらに、ユーティリティ関数があります:

clean_savepoints(using=None)

一意のセーブポイントIDを生成するためのカウンタをリセットします。

以下に、セーブポイントの使い方の例を示します:

from django.db import transaction


# open a transaction
@transaction.atomic
def viewfunc(request):
    a.save()
    # transaction now contains a.save()

    sid = transaction.savepoint()

    b.save()
    # transaction now contains a.save() and b.save()

    if want_to_keep_b:
        transaction.savepoint_commit(sid)
        # open transaction still contains a.save() and b.save()
    else:
        transaction.savepoint_rollback(sid)
        # open transaction now contains only a.save()

セーブポイントは部分的なロールバックを行うことで、データベースエラーから回復するために使用できます。atomic() ブロックの中でこの処理を行っている場合、ブロック全体がロールバックされます!これを防ぐために、以下の関数でロールバックの動作を制御できます。

get_rollback(using=None)
set_rollback(rollback, using=None)

ロールバックフラグを True に設定すると、最も内側のアトミックブロックを終了するときにロールバックします。これは例外を発生させずにロールバックを行うのに便利です。

これを False に設定すると、このようなロールバックを防ぐことができます。これを行う前に、トランザクションを現在のアトミックブロック内の既知のセーブポイントまでロールバックしたことを確認してください!そうしないと、アトミック性が壊れてしまい、データの破損が発生する可能性があります。

データベース固有の注意事項

SQLite におけるセーブポイント

SQLiteはセーブポイントをサポートしていますが、 sqlite3 モジュールの設計に欠陥があるため、セーブポイントはほとんど使えません。

自動コミットが有効な場合、セーブポイントは意味を成しません。これが無効な場合、 sqlite3 はセーブポイントステートメントの前に暗黙的にコミットします。(実際には SELECTINSERTUPDATEDELETEREPLACE 以外のステートメントの前にコミットします)。このバグには2つの影響があります:

  • セーブポイントの低レベルのAPIはトランザクションの中、つまり atomic() ブロックの中でしか使えません。
  • 自動コミットがオフになっている場合、atomic() を使用することはできません。

MySQL におけるトランザクション

MySQL を使用している場合、テーブルがトランザクションをサポートしているかどうかは MySQL のバージョンと使用しているテーブルタイプに依存します。(「テーブルタイプ」とは、「InnoDB」や「MyISAM」のようなものを意味します。) MySQLのトランザクションに関する特殊性はこの記事の範囲外ですが、MySQLのサイトには information on MySQL transactions があります。

MySQL がトランザクションをサポートしていない場合、 Django は常に自動コミットモードで動作します。MySQL がトランザクションをサポートしている場合、 Django はこの文書で説明されているようにトランザクションを処理します。

PostgreSQLトランザクション内での例外ハンドリング

注釈

このセクションは、独自のトランザクション管理を実装している場合にだけ関係があります。この問題は Django のデフォルトモードでは発生せず、 atomic() が自動的に処理します。

トランザクション内で、PostgreSQLカーソルの呼び出しが例外(通常は IntegrityError )を発生させた場合、同じトランザクション内の後続のSQLはすべて "current transaction is aborted, queries ignored until end of transaction block" というエラーで失敗します。 save() の基本的な使用では例外を発生させる可能性は低いですが、一意なフィールドを持つオブジェクトの保存、 force_insert/force_update フラグを使用した保存、カスタムSQLの実行など、より高度な使用パターンでは例外が発生する可能性があります。

この種のエラーから回復する方法はいくつかあります。

トランザクションのロールバック

一つ目の選択肢は、トランザクション全体をロールバックすることです。たとえば:

a.save()  # Succeeds, but may be undone by transaction rollback
try:
    b.save()  # Could throw exception
except IntegrityError:
    transaction.rollback()
c.save()  # Succeeds, but a.save() may have been undone

transaction.rollback() を呼び出すと、トランザクション全体がロールバックされます。コミットされていないデータベース操作はすべて失われます。この例では、a.save() による変更は、その操作自体がエラーを発生させなかったにもかかわらず、失われることになります。

セーブポイントのロールバック

セーブポイント を使用すると、ロールバックの範囲を制御できます。失敗する可能性のあるデータベース操作を実行する前に、セーブポイントを設定または更新できます。こうすることで、操作に失敗した場合にトランザクション全体ではなく、問題のある操作だけをロールバックできます。たとえば、次のようにします:

a.save()  # Succeeds, and never undone by savepoint rollback
sid = transaction.savepoint()
try:
    b.save()  # Could throw exception
    transaction.savepoint_commit(sid)
except IntegrityError:
    transaction.savepoint_rollback(sid)
c.save()  # Succeeds, and a.save() is never undone

この例では、 b.save() が例外を発生させても、 a.save() は元に戻りません。

Back to Top