mssql-django의 트랜잭션 관리

이 문서에서는 SQL Server 백 엔드를 사용하여 mssql-django Django 애플리케이션에 대한 트랜잭션 처리 및 격리 수준을 구성하는 방법을 설명합니다.

기본 동작

기본적으로 Django는 자동 커밋 모드에서 작동합니다. 각 데이터베이스 쿼리는 자체 트랜잭션에서 실행되며 즉시 커밋됩니다. 설정 또는 Django의 트랜잭션 관리 API를 AUTOCOMMIT 사용하여 이 동작을 변경할 수 있습니다.

AUTOCOMMIT 설정

데이터베이스 구성에서 AUTOCOMMITFalse(으)로 설정하여 자동 커밋 모드를 비활성화합니다:

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

메모

자동 커밋을 사용하지 않도록 설정하면 트랜잭션을 명시적으로 커밋하거나 롤백해야 합니다. 대부분의 Django 애플리케이션은 자동 커밋을 활성화한 상태로 두고, 특정 작업에는 transaction.atomic()를 사용합니다.

transaction.atomic() 사용

데이터베이스 작업을 transaction.atomic() 래핑하여 단일 트랜잭션에서 실행되도록 합니다.

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은 테이블 힌트를 생성하지 않지만 두 가지 옵션이 있습니다.

옵션 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 사용

특정 테이블의 대상 쿼리의 경우 테이블 힌트와 함께 원시 SQL을 NOLOCK 사용합니다.

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 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 에는 데이터베이스 수준 구성이 필요합니다. 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 잠김

nowaitskip_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]
매개 변수 SQL Server 테이블 힌트
기본값 WITH (ROWLOCK, UPDLOCK)
nowait=True WITH (NOWAIT, ROWLOCK, UPDLOCK)
skip_locked=True WITH (ROWLOCK, UPDLOCK, READPAST)

메모

select_for_update()transaction.atomic() 블록 내부에서 사용해야 합니다. 트랜잭션 외부에서 호출하면 Django에서 오류가 발생합니다.

PostgreSQL의 차이점

  • of 매개 변수(select_for_update(of=(...)))는 지원되지 않습니다. 이를 전달하면 백엔드가 NotSupportedError를 발생시킵니다.
  • SQL Server는 행 수준 FOR UPDATE 절 대신 테이블 수준 힌트(UPDLOCK)를 사용합니다. 경합이 심한 경우 잠금 에스컬레이션으로 인해 원래 의도한 것보다 더 많은 행이나 페이지가 잠길 수 있습니다. 잠긴 쓰기 작업과 동시에 비차단 읽기가 필요한 경우 SNAPSHOT 격리 수준을 사용합니다.