Manajemen transaksi di mssql-django

Artikel ini menjelaskan cara mengonfigurasi penanganan transaksi dan tingkat isolasi untuk aplikasi Django menggunakan mssql-django backend dengan SQL Server.

Perilaku bawaan

Secara default, Django beroperasi dalam mode autocommit. Setiap kueri basis data berjalan dalam transaksinya masing-masing dan langsung dikomit. Anda dapat mengubah perilaku ini menggunakan AUTOCOMMIT pengaturan atau API manajemen transaksi Django.

Pengaturan AUTOCOMMIT

Atur AUTOCOMMIT ke False dalam konfigurasi database Anda untuk menonaktifkan mode autocommit:

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

Menonaktifkan autocommit berarti Anda harus secara eksplisit menerapkan atau membatalkan transaksi. Sebagian besar aplikasi Django membiarkan autocommit diaktifkan dan digunakan transaction.atomic() untuk operasi tertentu.

Gunakan transaction.atomic()

Bungkus operasi database dalam transaction.atomic() agar dieksekusi dalam satu transaksi:

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

Jika ada pengecualian yang terjadi di dalam atomic() blok, seluruh transaksi akan digulung balik.

Transaksi berlapis

Django mendukung blok berlapis atomic() melalui titik penyimpanan 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

Tingkat isolasi transaksi

Konfigurasikan tingkat isolasi transaksi menggunakan isolation_level opsi dalam konfigurasi database Anda:

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

Tingkat isolasi yang didukung

Tingkat isolasi Description
READ UNCOMMITTED Memungkinkan bacaan kotor. Isolasi terendah, konkurensi tertinggi.
READ COMMITTED Bawaan SQL Server. Mencegah pembacaan kotor.
REPEATABLE READ Mencegah pembacaan kotor dan tidak dapat diulang.
SNAPSHOT Menggunakan pembuatan versi baris untuk pembacaan konsisten tanpa pemblokiran. Memerlukan isolasi rekam jepret tingkat database untuk diaktifkan.
SERIALIZABLE Isolasi tertinggi. Mencegah pembacaan hantu.

Mengaktifkan isolasi SNAPSHOT

Untuk menggunakan SNAPSHOT isolasi, aktifkan pada database terlebih dahulu:

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

Kemudian konfigurasikan di 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",
        },
    },
}

Gunakan dekorator @transaction.atomic

Terapkan transaksi ke seluruh fungsi tampilan:

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

Membaca data tanpa pemblokiran (setara NOLOCK)

Permintaan umum adalah mengkueri SQL Server dengan NOLOCK petunjuk atau READ UNCOMMITTED isolasi untuk menghindari pemblokiran pada tabel yang sibuk. ORM Django tidak menghasilkan petunjuk tabel, tetapi Anda memiliki dua opsi.

Opsi 1: Tetapkan READ UNCOMMITTED untuk setiap koneksi

Atur tingkat isolasi ke READ UNCOMMITTED pada alias database baca-saja khusus untuk menerapkannya ke semua kueri pada koneksi tersebut:

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

Kemudian rutekan kueri ke 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)

Opsi 2: Gunakan SQL mentah dengan NOLOCK

Untuk kueri yang ditargetkan pada tabel tertentu, gunakan SQL mentah dengan NOLOCK petunjuk tabel:

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

Perhatian

Baik READ UNCOMMITTED maupun NOLOCK mengizinkan dirty read, yang berarti kueri dapat mengembalikan data dari transaksi yang belum dikomitkan. Gunakan teknik ini hanya untuk kueri pelaporan atau analitik di mana konsistensi absolut tidak diperlukan.

Opsi 3: Gunakan isolasi SNAPSHOT sebagai gantinya

SNAPSHOT isolasi menyediakan bacaan yang konsisten tanpa memblokir dan tanpa bacaan kotor. Ini adalah alternatif yang direkomendasikan untuk NOLOCK bagi sebagian besar beban kerja:

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

SNAPSHOT memerlukan konfigurasi pada tingkat basis data. Lihat Mengaktifkan isolasi SNAPSHOT.

Penguncian tingkat baris dengan select_for_update()

select_for_update() Django didukung sepenuhnya oleh backend mssql-django. SQL Server menerapkan ini menggunakan petunjuk tabel alih-alih klausul FOR UPDATE yang digunakan oleh basis data lain.

Penggunaan dasar

from django.db import transaction

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

Backend menghasilkan: SELECT ... FROM [myapp_product] WITH (ROWLOCK, UPDLOCK) WHERE ...

NOWAIT dan SKIP TERKUNCI

Parameter nowait dan skip_locked keduanya didukung:

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

Note

select_for_update() harus digunakan di dalam blok transaction.atomic(). Django menghasilkan error jika Anda memanggilnya di luar transaksi.

Perbedaan dari PostgreSQL

  • Parameter of (select_for_update(of=(...))) tidak didukung. Backend memunculkan NotSupportedError jika Anda meneruskannya.
  • SQL Server menggunakan petunjuk tingkat tabel (UPDLOCK) alih-alih klausa tingkat FOR UPDATE baris. Dalam kondisi persaingan akses yang tinggi, eskalasi kunci dapat menyebabkan lebih banyak baris atau halaman terkunci daripada yang Anda maksudkan. SNAPSHOT Gunakan tingkat isolasi jika Anda memerlukan bacaan non-pemblokiran bersama tulisan terkunci.