Transactiebeheer in mssql-django

In dit artikel wordt uitgelegd hoe u transactieafhandelings- en isolatieniveaus configureert voor Django-toepassingen met behulp van de mssql-django back-end met SQL Server.

Standaardgedrag

Django werkt standaard in de autocommit-modus. Elke databasequery wordt uitgevoerd in een eigen transactie en onmiddellijk vastgelegd. U kunt dit gedrag wijzigen met behulp van de AUTOCOMMIT instelling of de API voor transactiebeheer van Django.

AUTOCOMMIT-instelling

Stel AUTOCOMMIT in op False in uw databaseconfiguratie om de autocommitmodus uit te schakelen:

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

Als u autotoewijzing uitschakelt, moet u expliciet transacties doorvoeren of terugdraaien. De meeste Django-toepassingen laten autocommit ingeschakeld en gebruiken transaction.atomic() voor specifieke bewerkingen.

Transaction.atomic() gebruiken

Verpak databasebewerkingen in transaction.atomic() zodat ze in één transactie worden uitgevoerd:

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

Als er een uitzondering optreedt binnen een atomic() blok, wordt de hele transactie teruggedraaid.

Geneste transacties

Django ondersteunt geneste atomic() blokken via de savepoints van SQL Server:

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

Isolatieniveaus voor transacties

Configureer het niveau van transactieisolatie met behulp van de isolation_level optie in uw databaseconfiguratie:

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

Ondersteunde isolatieniveaus

Isolatieniveau Description
READ UNCOMMITTED Staat vuile leesbewerkingen toe. Laagste isolatie, hoogste gelijktijdigheid.
READ COMMITTED SQL Server standaardinstelling. Voorkomt vuile leesbewerkingen.
REPEATABLE READ Voorkomt vuile en niet-herhaalbare leesbewerkingen.
SNAPSHOT Maakt gebruik van rijversies voor consistente leesacties zonder blokkeringen. Vereist dat momentopname-isolatie op databaseniveau is ingeschakeld.
SERIALIZABLE Hoogste isolatie. Voorkomt fantoomlezingen.

Momentopname-isolatie inschakelen

Als u isolatie wilt gebruiken SNAPSHOT , schakelt u deze eerst in op de database:

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

Configureer deze vervolgens 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",
        },
    },
}

Gebruik de @transaction.atomic decorator

Transacties toepassen op volledige weergavefuncties:

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

Gegevens lezen zonder te blokkeren (NOLOCK-equivalent)

Een veelvoorkomend verzoek is om SQL Server te bevragen met de hint NOLOCK of de isolatie-instelling READ UNCOMMITTED om blokkeringen op drukbezette tabellen te voorkomen. Django's ORM genereert geen tabelhints, maar u hebt twee opties.

Optie 1: READ UNCOMMITTED per verbinding instellen

Stel het isolatieniveau READ UNCOMMITTED in op een toegewezen alleen-lezen-databasealias om deze toe te passen op alle query's op die verbinding:

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

Routeer vervolgens query's naar de read_uncommitted alias:

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

Optie 2: onbewerkte SQL gebruiken met NOLOCK

Voor gerichte query's op specifieke tabellen gebruikt u onbewerkte SQL met de NOLOCK tabelhint:

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

Waarschuwing

Zowel READ UNCOMMITTED als NOLOCK vuile leesbewerkingen toestaan, wat betekent dat query's gegevens van niet-doorgevoerde transacties kunnen retourneren. Gebruik deze technieken alleen voor rapportage- of analysequery's waarbij geen absolute consistentie is vereist.

Optie 3: Gebruik in plaats daarvan SNAPSHOT-isolatie

SNAPSHOT isolatie biedt consistente leesbewerkingen zonder blokkeren en zonder vuile leesbewerkingen. Het is het aanbevolen alternatief voor NOLOCK voor de meeste 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 vereist configuratie op databaseniveau. Zie SNAPSHOT-isolatie inschakelen.

Vergrendeling op rijniveau met select_for_update()

Django's select_for_update() wordt volledig ondersteund door de mssql-django back-end. SQL Server implementeert dit met behulp van tabelhints in plaats van de FOR UPDATE component die door andere databases wordt gebruikt.

Basaal gebruik

from django.db import transaction

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

De back-end genereert: SELECT ... FROM [myapp_product] WITH (ROWLOCK, UPDLOCK) WHERE ...

NOWAIT en SKIP LOCKED

De nowait parameters en skip_locked parameters worden beide ondersteund:

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 Hint voor SQL Server-tabel
Default WITH (ROWLOCK, UPDLOCK)
nowait=True WITH (NOWAIT, ROWLOCK, UPDLOCK)
skip_locked=True WITH (ROWLOCK, UPDLOCK, READPAST)

Note

select_for_update() moet in een transaction.atomic() blok worden gebruikt. Django genereert een fout als u deze buiten een transactie aanroept.

Verschillen met PostgreSQL

  • De of parameter (select_for_update(of=(...))) wordt niet ondersteund. De back-end geeft NotSupportedError terug als u die doorgeeft.
  • SQL Server gebruikt hints op tabelniveau (UPDLOCK) in plaats van componenten op rijniveauFOR UPDATE. Bij een hoog conflict kan escalatie van vergrendeling ertoe leiden dat meer rijen of pagina's worden vergrendeld dan waarop u zich richt. Gebruik het SNAPSHOT isolatieniveau als u niet-blokkerende leesbewerkingen naast vergrendelde schrijfbewerkingen nodig hebt.