mssql-django でのトランザクション管理

この記事では、SQL Serverで mssql-django バックエンドを使用して Django アプリケーションのトランザクション処理と分離レベルを構成する方法について説明します。

既定の動作

既定では、Django は自動コミット モードで動作します。 各データベース クエリは、独自のトランザクションで実行され、すぐにコミットされます。 この動作は、 AUTOCOMMIT 設定または Django のトランザクション管理 API を使用して変更できます。

AUTOCOMMIT 設定

自動コミット モードを無効にするには、 AUTOCOMMIT をデータベース構成で False に設定します。

DATABASES = {
    "default": {
        "ENGINE": "mssql",
        "NAME": "<your-database>",
        "USER": "<your-username>",
        "PASSWORD": "<your-password>",
        "HOST": "<your-server>",
        "PORT": "1433",
        "AUTOCOMMIT": False,
        "OPTIONS": {
            "driver": "ODBC Driver 18 for SQL Server",
        },
    },
}

Note

自動コミットを無効にすると、トランザクションを明示的にコミットまたはロールバックする必要があります。 ほとんどの Django アプリケーションでは、自動コミットが有効なままになり、特定の操作に transaction.atomic() が使用されます。

transaction.atomic() を使用する

データベース操作を transaction.atomic() でラップして、1 つのトランザクションで確実に実行されるようにします。

from django.db import transaction
from myapp.models import Account

def transfer_funds(from_account_id, to_account_id, amount):
    with transaction.atomic():
        sender = Account.objects.select_for_update().get(pk=from_account_id)
        receiver = Account.objects.select_for_update().get(pk=to_account_id)

        sender.balance -= amount
        receiver.balance += amount

        sender.save()
        receiver.save()

atomic() ブロック内で例外が発生した場合、トランザクション全体がロールバックされます。

入れ子構造のトランザクション

Django は、SQL Serverのセーブポイントを介して入れ子になったatomic() ブロックをサポートします。

from django.db import transaction

with transaction.atomic():
    # Outer transaction
    Product.objects.create(name="Widget A", price=9.99)

    try:
        with transaction.atomic():
            # Inner savepoint
            Product.objects.create(name="Widget B", price=14.99)
            raise ValueError("Simulated error")
    except ValueError:
        pass  # Inner savepoint is rolled back, outer continues

    # Widget A is committed, Widget B is not

トランザクション分離レベル

データベース構成の isolation_level オプションを使用して、トランザクション分離レベルを構成します。

DATABASES = {
    "default": {
        "ENGINE": "mssql",
        "NAME": "<your-database>",
        "USER": "<your-username>",
        "PASSWORD": "<your-password>",
        "HOST": "<your-server>",
        "PORT": "1433",
        "OPTIONS": {
            "driver": "ODBC Driver 18 for SQL Server",
            "isolation_level": "READ COMMITTED",
        },
    },
}

サポートされている分離レベル

分離レベル 説明
READ UNCOMMITTED ダーティ リードを許可します。 分離が最も低く、コンカレンシーが最も高い。
READ COMMITTED SQL Server の既定値 ダーティリードを防止します。
REPEATABLE READ ダーティ読み取りと繰り返し不可能な読み取りを防止します。
SNAPSHOT ブロックせずに一貫性のある読み取りに行のバージョン管理を使用します。 データベース レベルのスナップショット分離を有効にする必要があります。
SERIALIZABLE 最高の絶縁性。 ファントム読み取りを防止します。

SNAPSHOT 分離を有効にする

SNAPSHOT分離を使用するには、最初にデータベースで有効にします。

ALTER DATABASE [<your-database>]
SET ALLOW_SNAPSHOT_ISOLATION ON;

次に、 settings.pyで構成します。

DATABASES = {
    "default": {
        "ENGINE": "mssql",
        "NAME": "<your-database>",
        "USER": "<your-username>",
        "PASSWORD": "<your-password>",
        "HOST": "<your-server>",
        "PORT": "1433",
        "OPTIONS": {
            "driver": "ODBC Driver 18 for SQL Server",
            "isolation_level": "SNAPSHOT",
        },
    },
}

@transaction.atomic デコレーターを使用する

ビュー関数全体にトランザクションを適用します。

from django.db import transaction
from django.http import JsonResponse

@transaction.atomic
def create_order(request):
    # All database operations in this view run in a single transaction
    order = Order.objects.create(customer_id=request.user.id)
    for item in request.POST.getlist("items"):
        OrderItem.objects.create(order=order, product_id=item)
    return JsonResponse({"order_id": order.pk})

ブロックせずにデータを読み取る (NOLOCK に相当)

一般的な要求は、ビジー状態のテーブルでブロックされないように、NOLOCK ヒントまたはREAD UNCOMMITTED分離を使用してSQL Serverにクエリを実行することです。 Django の ORM ではテーブル ヒントは生成されませんが、2 つのオプションがあります。

オプション 1: 接続ごとに READ UNCOMMITTED を設定する

分離レベルを専用の読み取り専用データベース エイリアスで READ UNCOMMITTED に設定し、その接続のすべてのクエリに適用します。

DATABASES = {
    "default": {
        "ENGINE": "mssql",
        "NAME": "<your-database>",
        "HOST": "<your-server>",
        "PORT": "1433",
        "OPTIONS": {
            "driver": "ODBC Driver 18 for SQL Server",
        },
    },
    "read_uncommitted": {
        "ENGINE": "mssql",
        "NAME": "<your-database>",
        "HOST": "<your-server>",
        "PORT": "1433",
        "OPTIONS": {
            "driver": "ODBC Driver 18 for SQL Server",
            "isolation_level": "READ UNCOMMITTED",
        },
    },
}

次に、クエリを read_uncommitted エイリアスにルーティングします。

# Read with NOLOCK-equivalent behavior
products = Product.objects.using("read_uncommitted").filter(active=True)

# Writes still go through the default connection
Product.objects.create(name="Widget", price=9.99)

オプション 2: NOLOCK で生 SQL を使用する

特定のテーブルに対する対象クエリの場合は、 NOLOCK テーブル ヒントで生 SQL を使用します。

from django.db import connection

with connection.cursor() as cursor:
    cursor.execute("SELECT id, name, price FROM myapp_product WITH (NOLOCK) WHERE active = %s", [1])
    rows = cursor.fetchall()

注意事項

READ UNCOMMITTEDNOLOCKの両方でダーティ読み取りが許可されます。つまり、クエリはコミットされていないトランザクションからデータを返すことができます。 これらの手法は、絶対整合性が必要ないレポートまたは分析クエリにのみ使用します。

オプション 3: 代わりに SNAPSHOT 分離を使用する

SNAPSHOT 分離レベルは、ブロックせず、ダーティ リードも発生しない一貫した読み取りを提供します。 これは、ほとんどのワークロードの NOLOCK に代わる推奨される代替手段です。

DATABASES = {
    "default": {
        "ENGINE": "mssql",
        "NAME": "<your-database>",
        "HOST": "<your-server>",
        "PORT": "1433",
        "OPTIONS": {
            "driver": "ODBC Driver 18 for SQL Server",
            "isolation_level": "SNAPSHOT",
        },
    },
}

SNAPSHOT には、データベース レベルの構成が必要です。 スナップショット分離の有効化を参照してください。

select_for_update() を使用した行レベルのロック

Django の select_for_update() は、 mssql-django バックエンドで完全にサポートされています。 SQL Serverは、他のデータベースで使用されるFOR UPDATE句ではなく、テーブル ヒントを使用してこれを実装します。

基本的な使用方法

from django.db import transaction

with transaction.atomic():
    product = Product.objects.select_for_update().get(pk=1)
    product.stock -= 1
    product.save()

バックエンドは次を生成します。 SELECT ... FROM [myapp_product] WITH (ROWLOCK, UPDLOCK) WHERE ...

NOWAIT と SKIP LOCKED

nowaitパラメーターとskip_locked パラメーターの両方がサポートされています。

from django.db import transaction

# Raise DatabaseError immediately if the row is already locked
with transaction.atomic():
    product = Product.objects.select_for_update(nowait=True).get(pk=1)

# Skip rows that are locked by other transactions
with transaction.atomic():
    available = Product.objects.select_for_update(skip_locked=True).filter(
        reserved=False
    )[:10]
Parameter SQL Server テーブル ヒント
デフォルト WITH (ROWLOCK, UPDLOCK)
nowait=True WITH (NOWAIT, ROWLOCK, UPDLOCK)
skip_locked=True WITH (ROWLOCK, UPDLOCK, READPAST)

Note

select_for_update() は、 transaction.atomic() ブロック内で使用する必要があります。 トランザクションの外部で呼び出すと、Django によってエラーが発生します。

PostgreSQL との違い

  • of パラメーター (select_for_update(of=(...))) はサポートされていません。 それを渡すと、バックエンドは NotSupportedError を発生させます。
  • SQL Serverは、行レベルのUPDLOCK句ではなく、テーブル レベルのヒント (FOR UPDATE) を使用します。 競合が激しい場合、ロック エスカレーションによって、意図したよりも多くの行またはページがロックされる可能性があります。 ロックされた書き込みとともに非ブロッキング読み取りが必要な場合は、 SNAPSHOT 分離レベルを使用します。