Správa transakcí v mssql-django

Tento článek vysvětluje, jak nakonfigurovat úrovně zpracování transakcí a izolace pro aplikace Django pomocí back-endu mssql-django s SQL Server.

Výchozí chování

Ve výchozím nastavení django funguje v režimu automatickéhocommitu. Každý databázový dotaz běží ve své vlastní transakci a je potvrzen okamžitě. Toto chování můžete změnit pomocí AUTOCOMMIT nastavení nebo rozhraní API pro správu transakcí Django.

Nastavení AUTOCOMMIT

Nastavte v konfiguraci databáze AUTOCOMMIT na False, aby se vypnul režim automatického potvrzování:

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

Vypnutí automatického potvrzování znamená, že transakce musíte explicitně potvrdit nebo vrátit zpět. Většina aplikací Django ponechá funkci autocommit povolenou a používá transaction.atomic() pro konkrétní operace.

Použijte transaction.atomic()

Zabalte databázové operace transaction.atomic() , aby se zajistilo, že se provádějí v jedné transakci:

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

Pokud dojde k jakékoli výjimce atomic() uvnitř bloku, celá transakce se vrátí zpět.

Vnořené transakce

Django podporuje vnořené atomic() bloky pomocí savepointů SQL Serveru:

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

Úrovně izolace transakcí

Nakonfigurujte úroveň izolace transakcí pomocí isolation_level možnosti v konfiguraci databáze:

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

Podporované úrovně izolace

Úroveň izolace Description
READ UNCOMMITTED Umožňuje špinavé čtení. Nejnižší izolace, nejvyšší souběžnost.
READ COMMITTED SQL Server výchozí. Zabraňuje špinavým čtením.
REPEATABLE READ Zabraňuje špinavému a neopakovatelnému čtení.
SNAPSHOT Používá verzování řádků pro konzistentní čtení bez blokování. Vyžaduje, aby byla povolená izolace snímků na úrovni databáze.
SERIALIZABLE Nejvyšší izolace. Zabraňuje fantomovým čtením.

Povolení izolace SNAPSHOT

Pokud chcete použít SNAPSHOT izolaci, povolte ji nejprve v databázi:

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

Potom ho nakonfigurujte v 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",
        },
    },
}

Použijte dekorátor @transaction.atomic

Použití transakcí na celé funkce zobrazení:

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

Čtení dat bez blokování (ekvivalent NOLOCK)

Běžným požadavkem je spouštět dotazy na SQL Server s hintem NOLOCK nebo s izolací READ UNCOMMITTED, aby se zabránilo blokování při práci s vytíženými tabulkami. ORM Djanga negeneruje hinty tabulek, ale máte dvě možnosti.

Možnost 1: Nastavení READ UNCOMMITTED pro každé připojení

Nastavte úroveň izolace na READ UNCOMMITTED ve vyhrazeném aliasu databáze jen pro čtení, aby se použila na všechny dotazy v tomto připojení:

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

Potom směrujte dotazy na 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)

Možnost 2: Použití nezpracovaných SQL s NOLOCK

U cílových dotazů na konkrétní tabulky použijte nezpracovaný SQL s nápovědou k NOLOCK tabulce:

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

Caution

READ UNCOMMITTED i NOLOCK umožňují špinavá čtení, což znamená, že dotazy mohou vracet data z nepotvrzených transakcí. Tyto techniky použijte pouze pro dotazy pro vytváření sestav nebo analýzy, u kterých není vyžadována absolutní konzistence.

Možnost 3: Místo toho použijte izolaci SNAPSHOT.

SNAPSHOT izolace poskytuje konzistentní čtení bez blokování a bez zašpiněných čtení. Je to doporučená alternativa NOLOCK pro většinu úloh:

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

SNAPSHOT vyžaduje konfiguraci na úrovni databáze. Viz Povolit izolaci SNAPSHOT.

Uzamčení na úrovni řádků pomocí select_for_update()

Djangův select_for_update() je plně podporován backendem mssql-django. SQL Server to implementuje pomocí hintů tabulky místo klauzule FOR UPDATE, kterou používají jiné databáze.

Základní použití

from django.db import transaction

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

Back-end vygeneruje: SELECT ... FROM [myapp_product] WITH (ROWLOCK, UPDLOCK) WHERE ...

NOWAIT a SKIP LOCKED

Parametry nowait a skip_locked jsou podporovány:

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 Nápověda k tabulce SQL Serveru
Default WITH (ROWLOCK, UPDLOCK)
nowait=True WITH (NOWAIT, ROWLOCK, UPDLOCK)
skip_locked=True WITH (ROWLOCK, UPDLOCK, READPAST)

Note

select_for_update() musí být použit uvnitř transaction.atomic() bloku. Django vyvolá chybu, pokud je zavoláte mimo transakci.

Rozdíly oproti PostgreSQL

  • Parametr of (select_for_update(of=(...))) není podporován. Backend vyvolá NotSupportedError, pokud jej předáte.
  • SQL Server místo klauzulí na úrovni UPDLOCK řádků používá nápovědy na úrovni tabulky (FOR UPDATE). Při vysoké míře kolizí může eskalace zámků způsobit uzamčení více řádků nebo stránek, než jste zamýšleli. SNAPSHOT Úroveň izolace použijte, pokud potřebujete neblokující čtení spolu s uzamčenými zápisy.