Gestão de transações no mssql-django

Este artigo explica como configurar o tratamento de transações e níveis de isolamento para aplicações Django usando o mssql-django backend com o SQL Server.

Comportamento padrão

Por defeito, o Django opera em modo de autocommit. Cada consulta à base de dados executa a sua própria transação e é confirmada imediatamente. Pode alterar este comportamento usando a AUTOCOMMIT definição ou a API de gestão de transações do Django.

Configuração de AUTOCOMMIT

Defina AUTOCOMMIT para False na configuração da sua base de dados para desativar o modo de 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

Desativar o autocommit significa que tem de confirmar ou anular explicitamente as transações. A maioria das aplicações Django deixa o autocommit ativado e utiliza transaction.atomic() para operações específicas.

Utilize transaction.atomic()

Envolva as operações transaction.atomic() da base de dados para garantir que são executadas numa única transação:

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

Se ocorrer alguma exceção dentro de um atomic() bloco, toda a transação é revertida.

Transações aninhadas

O Django suporta blocos aninhados atomic() através dos pontos de restauro do 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

Níveis de isolamento de transações

Configure o nível de isolamento da transação usando a isolation_level opção na configuração da sua base de dados:

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

Níveis de isolamento suportados

Nível de isolamento Description
READ UNCOMMITTED Permite leituras sujas. Menor isolamento, maior concorrência.
READ COMMITTED SQL Server predefinido. Previne leituras sujas.
REPEATABLE READ Impede leituras sujas e não repetíveis.
SNAPSHOT Utiliza versionamento de linhas para leituras consistentes sem bloqueios. Requer que o isolamento de snapshots ao nível da base de dados esteja ativado.
SERIALIZABLE Isolamento máximo. Previne leituras fantasma.

Ativar o isolamento SNAPSHOT

Para usar SNAPSHOT isolamento, ative-o primeiro na base de dados:

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

Depois configure-o em 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",
        },
    },
}

Use o decorador @transaction.atomic

Aplicar transações a todas as funções de visualização:

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

Leitura de dados sem bloqueio (equivalente a NOLOCK)

Uma solicitação frequente é consultar o SQL Server com a sugestão NOLOCK ou o nível de isolamento READ UNCOMMITTED para evitar bloqueios em tabelas com muita atividade. O ORM do Django não gera sugestões de tabela, mas tem duas opções.

Opção 1: Definir READ UNCOMMITTED por ligação

Defina o nível de isolamento como READ UNCOMMITTED num alias dedicado para uma base de dados só de leitura, para que se aplique a todas as consultas nessa ligação:

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

Depois, encaminhar as consultas para o 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)

Opção 2: Usar SQL bruto com NOLOCK

Para consultas específicas sobre tabelas específicas, utilize SQL em bruto com a diretiva de tabela 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

Ambos READ UNCOMMITTED e NOLOCK permitem leituras sujas, o que significa que as consultas podem devolver dados de transações não comprometidas. Use estas técnicas apenas para relatórios ou consultas analíticas onde não é necessária consistência absoluta.

Opção 3: Usar o isolamento SNAPSHOT em alternativa

SNAPSHOT O isolamento proporciona leituras consistentes sem bloqueios e sem leituras sujas. É a alternativa recomendada a NOLOCK para a maioria das cargas de trabalho:

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

SNAPSHOT requer configuração ao nível da base de dados. Veja Ativar isolamento SNAPSHOT.

Bloqueio ao nível da linha com select_for_update()

O select_for_update() do Django é totalmente suportado pelo back-end mssql-django. O SQL Server faz isto usando diretivas de tabela em vez da cláusula FOR UPDATE usada por outras bases de dados.

Utilização básica

from django.db import transaction

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

O backend gera: SELECT ... FROM [myapp_product] WITH (ROWLOCK, UPDLOCK) WHERE ...

NOWAIT e SKIP LOCKED

Os nowait parâmetros e skip_locked são ambos suportados:

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]
Parâmetro Indicação para tabelas no SQL Server
Predefinição WITH (ROWLOCK, UPDLOCK)
nowait=True WITH (NOWAIT, ROWLOCK, UPDLOCK)
skip_locked=True WITH (ROWLOCK, UPDLOCK, READPAST)

Note

select_for_update() deve ser usado dentro de um transaction.atomic() bloco. O Django gera um erro se o chamar fora de uma transação.

Diferenças em relação ao PostgreSQL

  • O of parâmetro (select_for_update(of=(...))) não é suportado. O backend gera NotSupportedError se lho passares.
  • O SQL Server utiliza sugestões ao nível da tabela (UPDLOCK) em vez de cláusulas ao nível da linha FOR UPDATE. Em situações de alta contenção, a escalada de bloqueios pode fazer com que mais linhas ou páginas fiquem bloqueadas do que o alvo. Utilize o nível de isolamento SNAPSHOT se precisar de leituras sem bloqueio a par de escritas com bloqueio.