Django: mantendo a compatibilidade entre migrations e models
Tags: django, python, migrations, models
Published: 2021-04-30 13:11:04

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:

```python
# models.py

from django.db import models

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

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

```python
# 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),
    ]
```

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:

```python
# 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)
```

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

```python
#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),
        ),
    ]
```

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](https://dogusky.netlify.app/images/blog/erros_migration.png "Erro no 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**](https://docs.djangoproject.com/en/3.2/ref/schema-editor/).

```python
def insert_data(apps, schema_editor):
    Profile.objects.create(name='Manager', description='Perfil de acesso superior')
```
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()`](https://docs.djangoproject.com/en/3.2/ref/applications/#django.apps.apps.get_model) que recupera  um *model*.

Analisando o método [`apps.get_models()`](https://docs.djangoproject.com/en/3.0/ref/applications/#django.apps.apps.get_model) 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**:

```python
# 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),
    ]
```

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**:

```python
# 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),
    ]
```

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

```python
# 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),
    ]
```

```python
#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),
    ]
```

E teremos o seguinte resultado:

```bash
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
```

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*](https://docs.djangoproject.com/en/3.2/ref/migration-operations/#runpython) 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.