Transaktionsverwaltung in mssql-django

In diesem Artikel wird erläutert, wie Transaktionsbehandlungs- und Isolationsebenen für Django-Anwendungen mithilfe des mssql-django Back-Ends mit SQL Server konfiguriert werden.

Standardverhalten

Standardmäßig wird Django im Autocommit-Modus ausgeführt. Jede Datenbankabfrage wird in einer eigenen Transaktion ausgeführt und sofort zugesichert. Sie können dieses Verhalten mithilfe der Einstellung oder der AUTOCOMMIT Transaktionsverwaltungs-API von Django ändern.

AUTOCOMMIT-Einstellung

Setzen Sie AUTOCOMMIT in Ihrer Datenbankkonfiguration auf False, um den Autocommit-Modus zu deaktivieren:

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

Das Deaktivieren von Autocommit bedeutet, dass Sie Transaktionen explizit committen oder zurückrollen müssen. Die meisten Django-Anwendungen lassen autocommit aktiviert und verwenden transaction.atomic() für bestimmte Vorgänge.

Verwenden Sie `transaction.atomic()`

Datenbankvorgänge in transaction.atomic() einschließen, um sicherzustellen, dass sie in einer einzigen Transaktion ausgeführt werden:

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()

Wenn eine Ausnahme innerhalb eines atomic() Blocks auftritt, wird die gesamte Transaktion zurückgesetzt.

Geschachtelte Transaktionen

Django unterstützt geschachtelte atomic() Blöcke mithilfe der Sicherungspunkte des SQL Servers:

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

Transaktionsisolationsstufen

Konfigurieren Sie die Transaktionsisolationsstufe mithilfe der isolation_level Option in der Datenbankkonfiguration:

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",
        },
    },
}

Unterstützte Isolationsstufen

Isolationsstufe Beschreibung
READ UNCOMMITTED Ermöglicht Dirty Reads. Niedrigste Isolation, höchste Parallelität.
READ COMMITTED SQL Server Standardeinstellung. Verhindert schmutzige Lesevorgänge.
REPEATABLE READ Verhindert schmutzige und nicht wiederholbare Lesevorgänge.
SNAPSHOT Verwendet die Zeilenversionsverwaltung für konsistente Lesevorgänge ohne Blockierung. Erfordert, dass die Momentaufnahmeisolation auf Datenbankebene aktiviert ist.
SERIALIZABLE Höchste Isolation. Verhindert Phantomlesevorgänge.

Aktivieren der SNAPSHOT-Isolation

Um SNAPSHOT Isolation zu verwenden, aktivieren Sie sie zunächst für die Datenbank:

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

Konfigurieren Sie sie dann in 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",
        },
    },
}

Verwenden Sie den @transaction.atomic-Dekorator

Transaktionen auf gesamte Ansichtsfunktionen anwenden:

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})

Lesen von Daten ohne Blockierung (NOLOCK-Entsprechung)

Eine häufige Anforderung ist es, SQL Server mit dem Hinweis NOLOCK oder der Isolationsstufe READ UNCOMMITTED abzufragen, um Blockierungen bei stark ausgelasteten Tabellen zu vermeiden. Djangos ORM generiert keine Tabellenhinweise, sie haben jedoch zwei Optionen.

Option 1: Festlegen von READ UNCOMMITTED pro Verbindung

Legen Sie die Isolationsstufe für einen dedizierten schreibgeschützten Datenbankalias auf READ UNCOMMITTED fest, damit sie auf alle Abfragen angewendet wird, die über diese Verbindung ausgeführt werden:

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",
        },
    },
}

Leiten Sie dann Abfragen an den read_uncommitted Alias weiter:

# 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)

Option 2: Verwenden von rohem SQL mit NOLOCK

Verwenden Sie für gezielte Abfragen für bestimmte Tabellen unformatierte SQL mit dem NOLOCK Tabellenhinweis:

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()

Vorsicht

Sowohl READ UNCOMMITTED als auch NOLOCK erlauben Dirty Reads, das heißt, dass Abfragen Daten aus nicht festgeschriebenen Transaktionen zurückgeben können. Verwenden Sie diese Techniken nur für Berichte oder Analyseabfragen, bei denen absolute Konsistenz nicht erforderlich ist.

Option 3: Verwenden Sie stattdessen die SNAPSHOT-Isolation

SNAPSHOT Isolation bietet konsistente Lesevorgänge ohne Blockierung und ohne schmutzige Lesevorgänge. Es ist die empfohlene Alternative zu NOLOCK für die meisten Workloads:

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

SNAPSHOT erfordert konfiguration auf Datenbankebene. Siehe "SNAPSHOT-Isolation aktivieren".

Zeilenbasierte Sperrung mit select_for_update()

Djangos select_for_update() wird vollständig vom mssql-django Back-End unterstützt. SQL Server implementiert dies mithilfe von Tabellenhinweisen anstelle der von anderen Datenbanken verwendeten FOR UPDATE-Klausel.

Grundlegende Nutzung

from django.db import transaction

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

Das Back-End generiert Folgendes: SELECT ... FROM [myapp_product] WITH (ROWLOCK, UPDLOCK) WHERE ...

NOWAIT und SKIP LOCKED

Die Parameter nowait und skip_locked werden beide unterstützt:

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-Tabellenhinweis
Vorgabe WITH (ROWLOCK, UPDLOCK)
nowait=True WITH (NOWAIT, ROWLOCK, UPDLOCK)
skip_locked=True WITH (ROWLOCK, UPDLOCK, READPAST)

Note

select_for_update() muss innerhalb eines transaction.atomic() Blocks verwendet werden. Django löst einen Fehler aus, wenn Sie ihn außerhalb einer Transaktion aufrufen.

Unterschiede von PostgreSQL

  • Der of Parameter (select_for_update(of=(...))) wird nicht unterstützt. Das Backend gibt NotSupportedError aus, wenn Sie es übergeben.
  • SQL Server verwendet Hinweise auf Tabellenebene (UPDLOCK) anstelle von Klauseln auf ZeilenebeneFOR UPDATE. Bei hoher Sperrenkonkurrenz kann die Sperreskalierung dazu führen, dass mehr Zeilen oder Seiten gesperrt werden, als Sie beabsichtigt haben. Verwenden Sie die SNAPSHOT Isolationsstufe, wenn Sie nicht blockierende Lesevorgänge zusammen mit gesperrten Schreibvorgängen benötigen.