Gerenciamento de transações no mssql-django

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

Comportamento padrão

Por padrão, o Django opera no modo de confirmação automática. Cada consulta de banco de dados é executada em sua própria transação e é confirmada imediatamente. Você pode alterar esse comportamento usando a configuração ou a AUTOCOMMIT API de gerenciamento de transações do Django.

Configuração de AUTOCOMMIT

Defina AUTOCOMMIT como False na configuração do seu banco de dados para desativar o modo de confirmação automática:

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

Desabilitar a confirmação automática significa que você deve confirmar ou reverter transações explicitamente. A maioria das aplicações Django mantém o autocommit ativado e usa transaction.atomic() para operações específicas.

Use transaction.atomic()

Encapsule as operações de transaction.atomic() banco de dados para garantir que elas sejam executadas em uma ú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 será revertida.

Transações aninhadas

O Django dá suporte a blocos aninhados atomic() por meio dos pontos de salvamento 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ção

Configure o nível de isolamento da transação usando a opção isolation_level na configuração do banco 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 Descrição
READ UNCOMMITTED Permite leituras sujas. Isolamento mais baixo, maior simultaneidade.
READ COMMITTED Padrão do SQL Server. Impede leituras sujas.
REPEATABLE READ Impede leituras sujas e não repetíveis.
SNAPSHOT Usa o controle de versão de linhas para leituras consistentes sem bloqueios. Requer que o isolamento de instantâneo no nível do banco de dados seja habilitado.
SERIALIZABLE Isolamento mais alto. Impede leituras fantasma.

Habilitar o isolamento de instantâneo

Para usar o isolamento SNAPSHOT, ative-o primeiro no banco de dados:

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

Em seguida, 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 funções de exibição inteiras:

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

Ler dados sem bloqueio (equivalente a NOLOCK)

Uma solicitação comum é consultar o SQL Server com a dica 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 você tem duas opções.

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

Defina o nível de isolamento como READ UNCOMMITTED em um alias dedicado de banco de dados somente leitura, para que ele seja aplicado a todas as consultas nessa conexã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",
        },
    },
}

Em seguida, encaminhe consultas para o alias 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)

Opção 2: Usar SQL bruto com NOLOCK

Para consultas direcionadas a tabelas específicas, use SQL puro com a dica 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()

Cuidado

Ambas READ UNCOMMITTED e NOLOCK permitem leituras sujas, o que significa que as consultas podem retornar dados de transações não confirmadas. Use essas técnicas apenas para consultas de relatório ou análise em que a consistência absoluta não é necessária.

Opção 3: Usar o isolamento SNAPSHOT em vez disso

SNAPSHOT O isolamento fornece leituras consistentes sem bloqueio e sem leituras sujas. É a alternativa recomendada para NOLOCK na 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 a configuração no nível do banco de dados. Consulte Habilitar isolamento SNAPSHOT.

Bloqueio em nível de linha com select_for_update()

O select_for_update() do Django tem suporte completo do backend mssql-django. O SQL Server implementa isso usando dicas de tabela em vez da cláusula FOR UPDATE usada por outros bancos de dados.

Uso Básico

from django.db import transaction

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

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

NOWAIT e SKIP LOCKED

Há suporte para os parâmetros nowait e skip_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]
Parâmetro Hint de tabela do SQL Server
Default 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 você chamá-lo fora de uma transação.

Diferenças do PostgreSQL

  • O of parâmetro (select_for_update(of=(...))) não tem suporte. O backend gera NotSupportedError se você o passar.
  • O SQL Server usa instruções em nível de tabela (UPDLOCK) em vez de cláusulas FOR UPDATE em nível de linha. Sob alta contenção, o escalonamento de bloqueios pode fazer com que mais linhas ou páginas sejam bloqueadas do que você pretendia bloquear. Use o SNAPSHOT nível de isolamento se precisar de leituras sem bloqueio junto com gravações bloqueadas.