Adicionando campo unique em um model que já possui dados

08/07/2020 - 4 min de leitura

DJANGO

Meses atrás eu escrevi um post sobre a importância de ter dois identificadores de modelos no Django. A ideia do post é muito boa, mas existe um problema: e se eu quiser adicionar o UUID em um modelo que possui dados cadastrados?

Você pode conferir o post aqui.

Para quem não sabe, quando você adiciona um campo não nulo em um modelo que possui dados no Django, você tem que definir um valor padrão para ele popular as linhas do BD. E basicamente esse processo pode ser de duas formas: você define o parâmetro default no campo do model, ou já define o valor padrão quando está rodando a migration.

Porém, quando você define algum valor padrão para os registros, irá ocorrer uma exceção com o campo único (vai repetir a mesma uuid para todos os dados, entende?).

Para resolver esse problema, você tem que fazer basicamente 3 passos:

  • Migrar o campo com a opção null=True ao invés de unique=True;
  • De alguma forma, percorrer todos os registros daquele model e adicionar um valor de uuid único para todos;
  • Atualizar o campo para unique=True ao invés de null=True.

Mas e aí? Você vai ter que fazer migrate, rodar o shell, digitar um script para geração de uuid e fazer migrate novamente?

Teoricamente, sim! Mas o Django é isso aqui 👉🏽❤️ para seus devs. Existe uma maneira mais "profissional" de resolvermos esse problema, sem precisar executar shell. Só com migrations.

Mãos no código

Digamos que temos este modelo:

from django.db import models


class Student(models.Model):
    name = models.CharField('aluno', max_length=255)

E agora queremos adicionar mais um identificador (uuid):

import uuid

from django.db import models


class Student(models.Model):
    uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
    name = models.CharField('aluno', max_length=255)

É isso que gostaríamos, né?

Pois bem, agora faremos 3 makemigrations:

python manage.py makemigrations
python manage.py makemigrations nome_app --empty
python manage.py makemigrations nome_app --empty

Vou explicar:

  • A primeira migração vai preparar o BD para adicionar o campo uuid único;
  • As duas migrações seguintes são vazias. Feitas especialmente para a app onde está o model Student.

Digamos que essas linhas resultaram na migração 003, 004 e 005, na pasta meu_app/migrations/.

O que temos que fazer agora? Seguir os 3 passos informados no início do post.

Primeiro, vamos na primeira migração: a 003.

Copiamos o conteúdo de operations dela e jogamos na última migração. Porém, vamos alterar um detalhe: ao invés de ser uma operação de AddField, será uma de AlterField (pois, quando essa migração for executada, já existirá o campo de uuid).

O conteúdo final, fica mais ou menos assim:

# arquivo de migration 005_algum_nome.py

from django.db import migrations, models
import uuid

class Migration(migrations.Migration):

    dependencies = [
        ('nomeapp', '0004_nome_migracao'),
    ]

    operations = [
        migrations.AlterField(
            model_name='student',
            name='uuid',
            field=models.UUIDField(default=uuid.uuid4, unique=True),
        ),
    ]

** Não esqueça de conferir a(s) dependência(s) e imports. A migração 5 depende da anterior e do uuid 😘

** Se você quiser, pode colocar um nome para sua migrations, definindo o que ela faz. Nomes das migrations 👉🏽 003_nome.py

Pronto! O terceiro passo já foi concluído. Agora vamos fazer o primeiro. No arquivo 003 (minha primeira migração), vamos alterar a opção unique=True, para null=True.

O código final ficará mais ou menos assim:

class Migration(migrations.Migration):

    dependencies = [
        ('nomeapp', '0002_alguma_coisa'),
    ]

    operations = [
        migrations.AddField(
            model_name='student',
            name='uuid',
            field=models.UUIDField(default=uuid.uuid4, null=True),
        ),
    ]

Beleza! Essa migração cria um campo uuid que pode ser nulo.

Concluímos a parte 1 e 3 do passo a passo. Agora precisamos, de alguma forma, gerar esses UUIDs no BD.

Para quem não sabe, podemos rodar código Python nas oprações de migrations através dos comandos RunPython ou RunSQL.

Então, podemos fazer uma função geradora de UUID e colocar pra rodar numa migration.

No segundo arquivo (004. Onde ficou a minha primeira migração --empty) coloque algo mais ou menos assim:

import uuid

from django.db import migrations


def gen_uuid(apps, schema_editor):
    MyModel = apps.get_model('nomeapp', 'Student')
    for row in MyModel.objects.all():
        row.uuid = uuid.uuid4()
        row.save(update_fields=['uuid'])


class Migration(migrations.Migration):

    dependencies = [
        ('nomeapp', '0003_add_uuid_field'),
    ]

    operations = [
        migrations.RunPython(gen_uuid, reverse_code=migrations.RunPython.noop),
    ]

Basicamente ele vai gerar um uuid para cada linha salva no BD, através da função gen_uuid.

** Detalhe para a importação do uuid.

** Se você fizer alguma merda, o Django nos oferece a possibilidade de reverter a migração através da flag reverse_code (que basicamente tem que fazer o reverso. Se a gen_uuid gera a UUID, a reverse_code deverá apagar a UUID). Você pode criar a sua propria função e passar como referência pro reverse_code.

** Essa opção de reverse_code é opcional. Se você não vai precisar reverter o código, não precisa colocar. Você quem decide.

Legal! Com isso, é só executar o comando migrate e ser feliz.

Dependendo da quantidade de dados/linhas que serão afetadas no BD, esse processo pode demorar um pouco. Isso ocorre porque os BDs que suportam DDL fazem suas alterações dentro de uma transação. Caso você queira otimizar isso, recomendo que olhe a documentação do Django. Que aliás, foi de onde eu tirei a ideia e exemplos deste post.

Foto de capa by Caspar Camille Rubin on Unsplash

Compartilhe

Twitter