DjangoBrothers BLOG ✍️

2020/11/07

このエントリーをはてなブックマークに追加
Django データベース

【Django】トランザクションの設定方法

バージョン

  • Python 3.8.4
  • Django 3.1.2
  • sqlite3 3.32.2

Djangoでトランザクションの設定

Djangoではデフォルトで、各クエリが即座にデータベースにコミットされます。

例えばですが以下のようなView関数があったとします。あるUserオブジェクトから別のUserオブジェクトに「送金処理」をする例です。やることとしては、送金元Userの残高を減らし、送金先のUserの残高を同じ分だけ増やす処理です。

views.py

def send_money(request, sender_id, reciever_id, amount):
    """送金処理をする関数
    送金元ユーザーの残高を減算する
    送金先ユーザーの残高を加算する
    """

    # 処理1(送金元ユーザーの残高を減算する)
    sender = User.objects.get(id=sender_id)
    sender.balance -= amount
    sender.save()  # commitされる

    # 処理2(送金先ユーザーの残高を加算する)
    reciever = User.objects.get(id=reciever_id)
    reciever.balance += amount
    reciever.save()  # commitされる

この例だと、sender.save()が実行されたときに処理1がコミットされ、次にreciever.save()で処理2がコミットされます。

仮にreciever_idで送金先の対象ユーザーを取得できなかった場合、処理2では例外が発生しreciever.save()は実行されません。その時、「処理1はDBに反映されていて、処理2はDBに反映されていない状況」になります。

つまり、「senderの残高だけが減っていて誰の残高も増えていない」という整合性の取れない事態となります。

これを防ぐために、処理1と処理2を1つのトランザクションにまとめます。

View関数の処理全体を1つのトランザクションとしてまとめたい場合は、settings.pyのDATABASESATOMIC_REQUESTSを追加します。

settings.py

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
        'ATOMIC_REQUESTS': True,  # 追加
    }
}

ATOMIC_REQUESTSをTrueにすると、1リクエストの開始〜終了までが1つのトランザクションになります。View関数が呼ばれる前にトランザクションが開始され、最後まで問題が発生しなければコミットされて、途中で例外が発生すればトランザクションはロールバックされます。

今回の例だと、処理2で例外が発生した場合はロールバックされて処理1、2は共になかったことになります

ATOMIC_REQUESTSの機能を適用したくないView関数がある場合は、@transaction.non_atomic_requestsをつけるとよいです。

views.py

from django.db import transaction

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

ATOMIC_REQUESTSを使うやり方は、View関数の処理だけがトランザクションとしてまとめられることになります。ミドルウェアや自作コマンドの処理は対象外なので別途対応が必要です。

ATOMIC_REQUESTSではプロジェクト全体に設定が適用されますが、各処理ごとに個別に設定したい場合は@transaction.atomicが使えます。

@transaction.atomicの使用例

from django.db import transaction

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

同様のことをwith文を使った書き方でもできます。

with文を使ってトランザクションを作る

from django.db import transaction

def viewfunc(request):
    do_stuff()  # このコードは即時コミットされる

    with transaction.atomic():
        # このブロックの中は1つのトランザクションとして扱われる
        do_more_stuff()

try-except文でatomicを使うと、Integrityエラーが自然な形でハンドリングできます。

try-exceptでatomicを使う

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()で例外が発生した場合でもハンドリングされているので、create_parent()add_children()は実行・コミットされます。

注意点として、with transaction.atomic():の中では例外をキャッチしないようにします。 atomicブロックの中で例外が発生したかどうかで、コミットするかロールバックするかが決まるため、例外をブロックの中でハンドリングしてしまうと例外は発生しなかったものとして、ロールバックはされません。

また、以下のようにロールバックが発生してDBに保存されなかったフィールドの値については自分で戻す必要があります。

views.py

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:
    ...

参照