Migrações de bases de dados com mssql-django

Este artigo explica como o sistema de migração do Django funciona com o SQL Server através do mssql-django backend e documenta os casos limites conhecidos.

Criar e aplicar migrações

O fluxo de trabalho de migração do Django funciona da mesma forma com o SQL Server que com outras bases de dados:

  1. Gerar migrações a partir de alterações de modelo:

    python manage.py makemigrations myapp
    
  2. Revise os ficheiros de migração gerados em <app>/migrations/.

  3. Aplicar migrações à base de dados:

    python manage.py migrate myapp
    
  4. Verifique o estado da migração:

    python manage.py showmigrations myapp
    

Configuração inicial do projeto

Quando configuras um novo projeto Django com SQL Server, executa migrações para criar as tabelas integradas do Django (autenticação, sessões, administração):

python manage.py migrate

Este comando cria todas as tabelas exigidas pelas aplicações listadas em INSTALLED_APPS.

SQL personalizado em migrações

Use migrations.RunSQL para executar instruções SQL brutas durante migrações. Esta abordagem é útil para criar procedimentos armazenados, triggers ou outros objetos específicos do SQL Server:

from django.db import migrations

class Migration(migrations.Migration):

    dependencies = [
        ("myapp", "0001_initial"),
    ]

    operations = [
        migrations.RunSQL(
            sql="CREATE INDEX IX_myapp_product_name ON myapp_product (name);",
            reverse_sql="DROP INDEX IX_myapp_product_name ON myapp_product;",
        ),
    ]

Casos de transição conhecidos

As seguintes operações de migração requerem soluções alternativas quando o destino é o SQL Server.

Alteração do AutoField

Alterar um campo de modelo de ou para AutoField no momento da migração não é suportado. O SQL Server não permite adicionar ou remover a IDENTITY propriedade de uma coluna existente.

Solução alternativa: Crie um novo modelo com o tipo de campo desejado. Migra dados da tabela antiga para a nova tabela e depois remove a tabela antiga.

Renomear campo ou modelo com restrições de chave estrangeira

Renomear um campo ou modelo que tenha restrições de chave estrangeira pode falhar. O SQL Server exige eliminar e recriar restrições FK durante as operações de renomeação.

Solução alternativa: Use migrations.SeparateDatabaseAndState para eliminar a restrição FK, renomear a coluna e recriar a restrição, enquanto diz ao Django para atualizar o estado do modelo. O exemplo seguinte renomeia a product chave estrangeira num Order modelo para item:

from django.db import migrations

class Migration(migrations.Migration):

    dependencies = [
        ("myapp", "0002_previous"),
    ]

    operations = [
        migrations.SeparateDatabaseAndState(
            database_operations=[
                migrations.RunSQL(
                    sql="ALTER TABLE myapp_order DROP CONSTRAINT FK_order_product;",
                    reverse_sql="ALTER TABLE myapp_order ADD CONSTRAINT FK_order_product FOREIGN KEY (product_id) REFERENCES myapp_product(id);",
                ),
                migrations.RunSQL(
                    sql="EXECUTE sp_rename 'myapp_order.product_id', 'item_id', 'COLUMN';",
                    reverse_sql="EXECUTE sp_rename 'myapp_order.item_id', 'product_id', 'COLUMN';",
                ),
                migrations.RunSQL(
                    sql="ALTER TABLE myapp_order ADD CONSTRAINT FK_order_item FOREIGN KEY (item_id) REFERENCES myapp_product(id);",
                    reverse_sql="ALTER TABLE myapp_order DROP CONSTRAINT FK_order_item;",
                ),
            ],
            state_operations=[
                migrations.RenameField(
                    model_name="order",
                    old_name="product",
                    new_name="item",
                ),
            ],
        ),
    ]

Consulta o nome real da restrição na tua base de dados antes de executar este código T-SQL. O Django gera nomes de restrições que incluem um hash curto, por isso o nome no seu esquema não corresponde ao marcador de posição mostrado aqui.

Migrações das abóboras

Depois de muitas migrações se acumularem, pode consolidá-las num número menor de ficheiros:

python manage.py squashmigrations myapp 0001 0010

Dica

Teste sempre migrações comprimidas contra uma base de dados nova para garantir que produzem o esquema correto.

Colunas geradas (colunas calculadas)

O backend mssql-django suporta o GeneratedField do Django (Django 5.0 e versões posteriores), que mapeia para colunas computadas do SQL Server.

Colunas geradas armazenadas (PERSISTENTES)

Uma coluna armazenada gerada é fisicamente escrita no disco e atualizada quando as colunas de origem mudam:

from django.db import models
from django.db.models import F

class Product(models.Model):
    price = models.DecimalField(max_digits=10, decimal_places=2)
    tax_rate = models.DecimalField(max_digits=5, decimal_places=4)
    total_price = models.GeneratedField(
        expression=F("price") * (1 + F("tax_rate")),
        output_field=models.DecimalField(max_digits=10, decimal_places=2),
        db_persist=True,
    )

Isto gera: total_price AS ([price] * (1 + [tax_rate])) PERSISTED.

Colunas geradas virtualmente

Uma coluna virtual gerada é calculada em tempo de consulta e não consome armazenamento:

from django.db import models
from django.db.models import F, Value
from django.db.models.functions import Concat

class Employee(models.Model):
    first_name = models.CharField(max_length=50)
    last_name = models.CharField(max_length=50)
    full_name = models.GeneratedField(
        expression=Concat(F("first_name"), Value(" "), F("last_name")),
        output_field=models.CharField(max_length=101),
        db_persist=False,
    )

Note

O SQL Server restringe índices em colunas computadas não persistentes. Usa db_persist=True se precisares de indexar a coluna gerada.

Comentários de tabela e coluna

O mssql-django backend é compatível com a funcionalidade db_comment do Django (Django 4.2 e versões posteriores). Os comentários são armazenados como MS_Description propriedades estendidas no objeto SQL Server.

Comentários nas tabelas

class AuditLog(models.Model):
    action = models.CharField(max_length=50)
    timestamp = models.DateTimeField(auto_now_add=True)

    class Meta:
        db_table_comment = "Tracks user actions for compliance auditing."

Comentários da coluna

class Measurement(models.Model):
    value = models.FloatField(db_comment="Sensor reading in Celsius")
    recorded_at = models.DateTimeField(db_comment="UTC timestamp from the data logger")

Os comentários são visíveis no SQL Server Management Studio, nas propriedades de colunas/tabelas e via sys.extended_properties.

Chaves primárias compostas

O Django 5.2 introduziu CompositePrimaryKey. O mssql-django backend tem suporte parcial para chaves primárias compostas, mas alguns casos de teste Django ainda estão excluídos. Valide as migrações e consultas com chaves compostas na sua aplicação antes de as adotar em produção.

  • inspectdb não gera corretamente as chaves primárias compostas. Defina-os manualmente após a inspeção.
  • As consultas de tuplas não são suportadas. O backend decompõe as comparações de chaves compostas em condições individuais para cada coluna.
  • A comparação de tuplas com subconsultas requer o Django 5.2.4 e versões posteriores.
  • Algumas operações de migração ainda têm exclusões conhecidas. Consulte Limitações e funcionalidades não suportadas no mssql-django para o estado atual.
from django.db import models
from django.db.models import CompositePrimaryKey

class OrderItem(models.Model):
    pk = CompositePrimaryKey("order_id", "product_id")
    order = models.ForeignKey("Order", on_delete=models.CASCADE)
    product = models.ForeignKey("Product", on_delete=models.CASCADE)
    quantity = models.IntegerField()

IDENTITY_INSERT tratamento

Quando insere valores explícitos num AutoField (por exemplo, restaurando dados de um backup com IDs específicos), o backend envolve automaticamente o inserto emSET IDENTITY_INSERT ON / SET IDENTITY_INSERT OFF . Não é necessário SQL manual.

# The backend handles IDENTITY_INSERT automatically
Product.objects.create(id=42, name="Restored Widget", price=9.99)

Note

O SQL Server permite que apenas uma tabela por sessão tenha IDENTITY_INSERT ON de cada vez. Se inserires IDs explícitos em múltiplas tabelas num único bloco atomic(), o backend processa a alternância por instrução. No entanto, as sessões simultâneas que também utilizam IDENTITY_INSERT na mesma tabela podem entrar em conflito.