Cómo crear migraciones de datos con Django

Hoy corregimos un problema de carga de datos en una de nuestras aplicaciones en producción realizadas con Django y nos pareció buena idea mencionar cómo lo resolvimos.

Django incluye un sistema de migraciones que permite modificar la estructura de la base de datos de forma reproducible y consistente. Cada cambio que hacemos en los modelos, como agregar o modificar campos, generan migraciones automáticas para propagar los cambios sobre la base de datos.

Sin embargo, las migraciones no solamente sirven para eso. En este artículo queremos mostrar un tipo muy particular de migraciones: las migraciones de datos.

Un caso real

En una de las aplicaciones que estamos realizando tenemos un recurso llamado Paquete, que contiene información sobre un pedido de desbloqueo de un equipo del programa Conectar Igualdad.

El problema que encontramos es que algunos campos de la base de datos destinados a almacenar un número Hexadecimal tenía errores de carga.

Por ejemplo, donde claramente el valor debería ser un número hexadecimal como A2F nos encontramos con un número equivalente como 000A2F.

Los números habían sido cargado por usuarios de forma masiva, así que teníamos cientos de registros como estos, donde los ceros a la izquierda del número nos impedía localizar esos recursos fácilmente.

Así se veían algunos de los registros que presentaban este problema.

Así que usamos una migración personalizada de datos para resolverlo, de forma tal que los cambios no solamente arreglen el problema en el sistema en producción, sino que además se pueda reproducir de forma local y forme parte del repositorio de código.

Creando la migración de datos para corregir el problema

Para crear un migración de este tipo en Django hay que ejecutar el siguiente comando, especificando el nombre de la aplicación y de la migración:

python manage.py makemigrations APP --empty -n NOMBRE  

En nuestro caso, como la aplicación se llama escuelas, y estamos usando pipenv, el comando debería quedar así:

pipenv run "python manage.py makemigrations escuelas --empty -n corregir_valores_hexadecimales_con_ceros_a_izquierda"  

En pantalla nos debería aparecer el nombre del archivo generado:

pipenv run "python manage.py makemigrations escuelas etc...  
Loading .env environment variables…  
Migrations for 'escuelas':  
  escuelas/migrations/0054_corregir_valores_hexadecimales_con_ceros_a_izquierda.py:

El archivo en este punto no tiene casi nada de contenido, solamente una estructura inicial y una descripción de la migración que la precede:

Ahora bien, para nuestro caso, tendremos que crear una función para corregir los datos Hexadecimales y agregar una referencia a la lista de operaciones:

# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2018-05-08 00:36
from __future__ import unicode_literals

from django.db import migrations

def corregir_valores_hexa(apps, schema_editor):  
    Paquete = apps.get_model("escuelas", "Paquete")
    for paquete in Paquete.objects.all():
        try:
            paquete.ma_hexa = hex(int(paquete.ma_hexa, 16))[2:]
            paquete.save()
        except (ValueError, TypeError) as error:
            print("Imposible corregir el paquete id={}".format(paquete.id))


class Migration(migrations.Migration):

    dependencies = [
        ('escuelas', '0053_paquete_zip_devolucion'),
    ]

    operations = [
        migrations.RunPython(corregir_valores_hexa, migrations.RunPython.noop),
    ]

Luego, para ejecutar la migración podemos ejecutar el comando:

pipenv run "python manage.py migrate  

O incluso ejecutar la migración de forma explícita usando el identificador numérico:

pipenv run "python manage.py migrate escuelas 0054"  

Esta última forma de ejecutar la migración es bastante útil cuando queremos revertir y volver a ejecutar la migración varias veces. Por ejemplo, si queres ejecutar varias veces la migración 0054 deberías revertirla ejecutando primero la migración anterior, la 0053:

pipenv run "python manage.py migrate escuelas 0053"  
pipenv run "python manage.py migrate escuelas 0054"  

y con esto, corregimos los datos correctamente:

Conclusión

Django es una fuente de soluciones y workflows muy sólida, con varios proyectos productivos sobre sus hombros.

Da gusto encontrarse con una herramienta que va más allá de las necesidades iniciales y tiene pensada una solución para este caso de problemas tan delicado ambientes de producción.

Las migraciones son solo una parte muy pequeña de estas convenciones y soluciones compartidas por la comunidad de Django. Sin embargo, nos resultan de tanta utilidad que quisimos compartirlas aquí.