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)
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),
]
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)
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),
),
]
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?
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')
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),
]
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),
]
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),
]
#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:
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 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)