DEV Community

Cover image for Django: mantendo a compatibilidade entre migrations e models
Douglas Silva
Douglas Silva

Posted on

Django: mantendo a compatibilidade entre migrations e models

Ao trabalhar em um projeto desenvolvido com Django, problemas com migrations podem surgir ao longo do tempo, principalmente quando temos inserção de dados, pois tabelas e models mudam ao longo do tempo e garantir a compatibilidade nesse contexto é fundamental para mantermos um bom histórico de migrations.

Cenário

No projeto hipotético existe um model chamado Profile que tem como objetivo manter as configurações padrões para perfis dentro do sistema:

# models.py

from django.db import models

class Profile(models.Model):
    name = models.CharField(max_length=64)
    description = models.CharField(max_length=256)

Enter fullscreen mode Exit fullscreen mode

Por ser um model que só interessa a parte interna do sistema, uma migration adiciona alguns perfis inicialmente na aplicação:

# 0003_insert_data_profile.py

from django.db import migrations, models
from application.models import Profile

def insert_data(apps, schema_editor):
    Profile.objects.create(name='Manager', description='Perfil de acesso superior')
    Profile.objects.create(name='Visitor', description='Perfil de visita, para acesso temporário')

class Migration(migrations.Migration):

    dependencies = [('migrations', '0002_profile')]

    operations = [
        migrations.RunPython(insert_data),
    ]
Enter fullscreen mode Exit fullscreen mode

Até esse ponto tudo bem. Se colocarmos em produção tudo ira ocorrer bem e caso um outro desenvolvedor pegue o código, e execute as migrations tudo continuará funcionando.

O Problema

Depois de um determinado tempo, foi requisitado que se adicionasse uma nova coluna em Profile chamada is_active quer irá determinar se um Profile está ou não ativo:

# models.py

from django.db import models

class Profile(models.Model):
    name = models.CharField(max_length=64)
    description = models.CharField(max_length=256)
    is_active = models.Boolean(default=True)
Enter fullscreen mode Exit fullscreen mode

Consequentemente uma migration será criada para adicionar essa nova coluna em nosso banco de dados:

#0004_profile_is_active.py

from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('application', '0003_insert_data_profile'),
    ]

    operations = [
        migrations.AddField(
            model_name='profile',
            name='is_active',
            field=models.BooleanField(default=True),
        ),
    ]
Enter fullscreen mode Exit fullscreen mode

Se pegarmos esses novos arquivos e depois enviarmos para nosso ambiente de staging ou produção e executarmos as migrations, nosso processo continuará funcionando normalmente, pois apenas as novas migrations serão executadas. Mas e se alguém pegar o projeto e executar as migrations a partir do ínicio, como acontece quando iniciamos nossa base de desenvolvimento ou quando executamos um suite de testes que rodam todas nossas migrations?

Imagem com erro do processo de migration

Temos o erro acima! E por que isso acontece?

Quando criamos inserção de dados em nossas migrations e nos referenciamos ao model de forma direta, ao executar as migrations, ele pega todas as colunas existentes dentro do model e as usa para criar um insert. Nesse caso a coluna is_active ainda não existe no nosso banco de dados, vela é criado apenas na migration 0004_profile_is_active.py

Esse erro aparace tanto para colunas novas, colunas que são removidas ao longo do tempo e colunas que em algum momento foram renomeadas. E como podemos assegurar que nossas migrations que insere dados não fique dependendo de alterações manuais nos arquivos anteriores toda vez que alterarmos um campo dentro de uma model?

A Solução

Parte da solução segue a lógica de acompanhar o model durante suas modificações dentro das migrations! Ou seja e se pudéssemos não nos referenciar ao model Profile que está no módulo models, mas sim para para um model que está sendo modificado durante o tempo de execução e respeitar a linha do tempo?

Toda vez que usamos RunPython ele passa na função chamada dois argumentos: uma instância de django.apps.registry.Apps e uma instância de SchemaEditor.

def insert_data(apps, schema_editor):
    Profile.objects.create(name='Manager', description='Perfil de acesso superior')
Enter fullscreen mode Exit fullscreen mode

E o que importa nesse caso é o apps, que contém um histórico dos model correspondente as mudanças ocorridas em tempo de execução e que nos fornece o método apps.get_model() que recupera um model.

Analisando o método apps.get_models() podemos ver que ele pode receber três parâmetros:

  • app_label: irá receber o nome do app onde está nossos models, que no nosso exemplo chama-se application.
  • model_name: recebe o nome do model que queremos acessar que é o Profile.
  • required_ready: quando recebe o valor False ele pega o model propriamente dito e não o que estã sendo alterado durante as migrations

Logo, para nossas migrations voltarem a funcionar novamente precisamos alterar como iremos nos referenciar a variável Profile:

# 0003_insert_data_profile.py

from django.db import migrations, models

def insert_data(apps, schema_editor):
    Profile = apps.get_model('application', 'Profile')

    Profile.objects.create(name='Manager', description='Perfil de acesso superior')
    Profile.objects.create(name='Visitor', description='Perfil de visita, para acesso temporário')


class Migration(migrations.Migration):

    dependencies = [('migrations', '0002_profile')]

    operations = [
        migrations.RunPython(insert_data),
    ]
Enter fullscreen mode Exit fullscreen mode

Nesse momento estamos recuperando do contexto histórico um model Profile que possui apenas os campos name e description. Depois da migration 0004_profile_is_active.py, caso seja necessário adicionar novos dados poderiamos criar um novo Profile com campo is_active, pois tanto o model Profile quanto o banco de dados já possuem o campo/coluna is_active:

# 0005_insert__more_data_profile.py

from django.db import migrations, models

def insert_data(apps, schema_editor):
    Profile = apps.get_model('application', 'Profile')

    Profile.objects.create(
        name='Guest', 
        description='Perfil de acesso que não precisa ser identificado',
        is_active=False
    )


class Migration(migrations.Migration):

    dependencies = [
        ('application', '0004_profile_is_active'),
    ]

    operations = [
        migrations.RunPython(insert_data),
    ]
Enter fullscreen mode Exit fullscreen mode

E como podemos ver que esse contexto histórico de fato existe e que determinado model está recendo novos campos ao longo do processo de migrations? Imprimindo todos os fields de um model utilizando o Profile._meta.get_fields(), e para isso podemos usar a função print().

# 0003_insert_data_profile.py

from django.db import migrations, models

def insert_data(apps, schema_editor):
    Profile = apps.get_model('application', 'Profile')
    print(Profile._meta.get_fields())

    Profile.objects.create(name='Manager', description='Perfil de acesso superior')
    Profile.objects.create(name='Visitor', description='Perfil de visita, para acesso temporário')


class Migration(migrations.Migration):

    dependencies = [('migrations', '0002_profile')]

    operations = [
        migrations.RunPython(insert_data),
    ]
Enter fullscreen mode Exit fullscreen mode
#0005_insert_more_data_profile

from django.db import migrations, models

def insert_data(apps, schema_editor):
    Profile = apps.get_model('application', 'Profile')
    print(Profile._meta.get_fields())

    Profile.objects.create(
        name='Guest', 
        description='Perfil de acesso que não precisa ser identificado',
        is_active=False
    )


class Migration(migrations.Migration):

    dependencies = [
        ('application', '0004_profile_is_active'),
    ]

    operations = [
        migrations.RunPython(insert_data),
    ]
Enter fullscreen mode Exit fullscreen mode

E teremos o seguinte resultado:

Applying application.0001_initial... OK
  Applying application.0002_profile... OK
  Applying application.0003_insert_data_profile...(
  <django.db.models.fields.BigAutoField: id>, 
  <django.db.models.fields.CharField: name>, 
  <django.db.models.fields.CharField: description>)
 OK
  Applying application.0004_profile_is_active... OK
  Applying application.0005_insert_data_profile...(
  <django.db.models.fields.BigAutoField: id>, 
  <django.db.models.fields.CharField: name>, 
  <django.db.models.fields.CharField: description>, 
  <django.db.models.fields.BooleanField: is_active>)
 OK
Enter fullscreen mode Exit fullscreen mode

Interessante né?

E sim, você pode alterar migrations antigas que faz o processo de inserção de dados, se referenciando o model usando o apps.get_model() e manter a devida compatibilidade.

Você pode ler mais sobre migrations operations no site do projeto Django

Caso tenha alguma dúvida, deixe-a na caixa de comentários ou pode entrar em contato através de uma das minha redes.

Top comments (0)