Cómo versionar tu base de datos con "migraciones" en node.js y python
TLDR; Así como tu aplicativo o solución tiene versiones, la base de datos también debe tener versiones, las cuales pueden actualizarse a una versión mayor a través de "upgrades" o ser revertidas a versiones anteriores con "downgrades". Esto asegura que tu aplicativo pueda ser usado en diferentes etapas de su evolución de acuerdo a los requerimientos con integraciones o actualizaciones del cliente. En este post hablamos sobre qué son "migraciones" de una base de datos y construimos 3 ejemplos para aplicativos en Node.js con Sequelize, Node.js con TypeORM y Flask con SQLAlchemy y Alembic.
Enlaces a documentaciones oficiales:
- Documentación de Sequelize
- Documentación de TypeORM
- Documentación de Alembic
- Documentación de Flask-Migrate
También te dejo este enlace donde se encuentran los ejemplos de es Post en GitHub: Migraciones con Node.js y Python.
Entonces, ¿Qué son las "migraciones" en una base de datos?
De manera muy corta y sencilla, las migraciones en una base de datos es el versionamiento del esquema, o de las tablas o de la relación entre entidades (ERD -Entity Relationship Diagram-), de acuerdo con la evolución de tu aplicativo.
Ejemplo de un aplicativo web de correspondencia
En un ejemplo práctico: imagina que has desarrollado un aplicativo web para el seguimiento de envío de correspondencia física, como sobres y paquetes, que consta de un frontend, un backend y una base de datos.
Este aplicativo tiene varias funciones y características, sin embargo, vamos a enfocarnos que un usuario final tiene la posibilidad de ver el próximo envío a recibir en una interfaz que le muestra desde donde viene, a donde se va a entregar y qué día será entregando, incluyendo una aproximación en la parte del día (mañana, tarde o noche).
El aplicativo es perfecto, cumple con todas las solicitudes de los stake-holders o interesados y tus jefes están sumamente contentos con el trabajo realizado.
Los usuarios finales deciden invertir para añadirle una segunda serie de funcionalidades y características a tu aplicativo, entre las cuales, está la posibilidad de indicar el peso total del paquete próximo a recibir porque los clientes quieren saber si es algo muy grande que deben manejar en la entrega.
Bien, el cambio no es difícil, es solo añadir una columna nueva en la base de datos, actualizar el esquema o clase en la lógica del backend y habilitar un campo en la interfaz del frontend para introducir esta información.
Tu plan para llevar a cabo esta actualización, y la de las demás funcionalidades y características, es actualizar los repositorios, por lo cual ya tus aplicativos frontend y backend tienen la nueva lógica, y para tu base de datos, decides que entrarás al servidor de producción y harás este cambio manualmente con un ALTER TABLE pedidos ADD peso_kg DECIMAL (16, 4)
.
Como podrás notar, esto de entrar a los servidores en producción es un ¡no no no! Existen muchos riesgos y peligros, créeme. Por más experimentado que uno sea, siempre va a ocurrir el error al momento de alterar o modificar algo en una base de datos en producción (por desgracia, aprendí esto a las malas).
La "migración" de la base de datos en tu aplicativo de correspondencia
Para resolver este inconveniente de actualización del esquema de la base de datos, creamos o "generamos" migraciones que se encarguen de alterar, añadir o eliminar objetos de tu base de datos de acuerdo al requerimiento de las nuevas versiones.
Pero recuerda esto super importante: así como en tu control de versiones en GitHub, GitLab, BitBucket, Azure Devops, o el servicio que estés usando, debe existir tanto una forma de subir o mejorar tu versión de base de datos ("upgrade"), como una forma de bajar o degradar la versión ("downgrade").
Entonces vamos a crear 2 métodos o funciones que hagan exactamente esto mismo:
- La función de
upgrade
se encarga mejorar la base de datos desde la versión v1.0 a la v2.0. - La función de
downgrade
se encarga de degradar la base de datos desde la versión v2.0 a la v.10.
Puede suceder el caso de que un error inesperado afecta producción y la mejor solución es revertir todos los cambios y retornar a una versión anterior. En nuestro ejemplo sería regresar a la versión v1.0, por lo tanto es importante tener la función downgrade
disponible.
Así como hacemos git revert
para revertir cambios en el repositorio y retornarlo a un estado anterior, lo mismo logramos con el método downgrade
.
Listo, de esta forma logras varios objetivos en tu aplicativo:
- Manejar tu base de datos con un control de versiones.
- Hacer actualizaciones de forma programada con cada salida o liberación de nueva versión de tu aplicativo.
- Tener un camino o vía para retornar los cambios en cualquier punto de tu aplicativo.
Si, a pesar de que es mayor trabajo al momento de desarrollar tu aplicativo, es la mejor forma de tener un control y trazabilidad completa de tu esquema de base de datos.
Ahora, ¿Hay algunos ejemplos prácticos que podamos usar en Node.js o Python?
Si, para esta pregunta te voy a presentar 2 ejemplos en Node.js, uno usando la librería Sequelize y otro usando la librería TypeORM. El último ejemplo lo haremos en Python con la librería Alembic.
De nuevo, todo el código referente a estos ejemplos lo encuentras en Migraciones con Node.js y Python
Aplicativo en Node.js (JavaScript) usando Sequelize
En este primer ejemplo creamos una aplicación desde cero en Node.js usando las librerias dotenv, express, pg, pg-hstore y sequelize. Como dependencias de desarrollador usamos las librerias de nodemon y sequelize-cli.
Como vamos a usar dotenv, creamos un archivo .env
el cual tendrá la variable DATABASE_CONNECTION_URI = postgres://usuario:contraseña@localhost:5432/migrations_sequelize
apuntando a la base de datos.
Creamos el archivo .sequelizerc
para indicar los paths hacia los distintos archivos de configuración. En este ejemplo indicamos donde se encuentra el archivo config.js
.
En el archivo config.js
indicamos el objeto "development" para nuestras variables en el ambiente de desarrollo e indicamos los valores de url
y dialect
para nuestra base de datos.
Creamos el directorio migrations
e indicamos nuestras migraciones creando documentos con el formato <timestamp>-<nombre-migracion>.js
. Y en este archivo definimos las funciones up
y down
a ser exportadas donde está la lógica de subida de versión y bajada de versión de la base de datos.
Otra forma de crear este archivo es a través del uso de sequelize-cli
, cuya herramienta nos permite usar el comando npx sequelize-cli model:generate --name MyModel
el cual creará un archivo con el nombre <timestamp>-create-user.js
en la carpeta migrations
.
Nota: más información acerca de esta librería la encuentras en el portal de Sequelize Migrations.
Aplicativo en Node.js (TypeScript) usando TypeORM
En este segundo ejemplo creamos una aplicación desde cero en Node.js usando las librerias dotenv, express, pg, reflect-metadata y typeorm. Como dependencias de desarrollador usamos las librerias de @types/express, @types/node, nodemon, ts-node y typescript.
Con el uso de dotenv vamos a crear un archivo .env
con la variable DATABASE_CONNECTION_URI = postgres://usuario:contraseña@localhost:5432/migrations_typeorm
apuntando a la base de datos.
Creamos un archivo llamado data-source.ts
donde usaremos la clase DataSource
de typeorm. Adicionalmente importaremos todas las clases que representen un modelo de datos de las entidades del proyecto. Finalizamos el archivo exportando nuestro objeto AppDataSource
con la configuración de nuestra fuente de datos.
Al ser un proyecto en TypeScript, también se debe configurar el archivo tsconfig.json
, el cuál dependerá de cada proyecto o de tus preferencias en este framework.
Y por último creamos nuestros archivos de migraciones, los cuales llevan un formato parecido al de Sequelize: <timestamp>-<NombreMigracion>.ts
(nótese el uso del formato Pascal Case).
Otra forma de crear este archivo es a través del cli usando el comando typeorm migration:create ./path-to-migrations-dir/PostRefactoring
, el cual creará el archivo con el nombre de migración PostRefactoring
usando el timestamp actual, para que puedas modificarlo a tu gusto.
Nota: más información acerca de esta librería la encuentras en el portal de TypeORM Migrations.
Aplicativo en Flask (Python) usando SQLAlchemy y Alembic
Y para nuestro último ejemplo creamos una aplicación desde cero en Flask usando las librerías Flask, SQLAlchemy, Flask-SQLAlchemy, Alembic, Flask-Migrate y psycopg2-binary.
En este aplicativo también podemos usar librerías para el uso de variables de entorno como lo es python-dotenv, sin embargo, en nuestro ejemplo nos saltamos esta parte. De todas maneras recuerda mantener la seguridad de tu aplicativo usando variables de entorno y secretos para las credenciales de los aplicativos. También para el caso de Flask vamos a crear un archivo config.py
donde exportamos la clase Config con las variables SQLALCHEMY_DATABASE_URI = 'postgresql://postgres:password@localhost:5432/migrations_alembic'
y SQLALCHEMY_TRACK_MODIFICATIONS = False
.
El uso de Alembic para las migraciones tiene una lógica distinta a las de Sequelize y TypeORM: las migraciones no se crean a mano sino a través de cli (aunque éstas últimas también tienen la tienen opción, en Alembic es el método preferido). Entonces, en este caso usaremos el comando flask db init
para inicializar nuestro sistema de migraciones, para luego usar el comando flask db migrate -m "Initial migration."
y por último el comando flask db upgrade
. En estos 3 pasos hemos inicializado la carpeta de migraciones (en el archivo app.py le hemos indicado al objeto migrate
que usaremos el nombre de migraciones
como carpeta).
En Alembic las migraciones van en la carpeta versions
y no van enumeradas de menor a mayor a través del uso de timestamp o numeración con en las otras librerias, sino que se asigna una cadena hexadecimal de 12 carácteres o un "12 digit hex code", y el orden de las versiones se indica a través de las variables revision
y down_revision
dentro de cada archivo (pudiendo complicar a simple vista el orden de las migraciones para nosotros los humanos).
Dentro de cada archivo se encuentra el código a ejecutar para subir o bajar de versión.
Y listo, usamos el comando flask db upgrade
para actualizar nuestra base de datos para tener nuestro proyecto en Flask al día.
Nota: más información acerca de esta librería la encuentras en el portal de Flask-Migrate usando Alembic.
Conclusiones
Las migraciones de bases de datos son esenciales para mantener la integridad y la funcionalidad de una aplicación en crecimiento. Así como el código de una aplicación evoluciona, también lo debe hacer la estructura de su base de datos. Al implementar un enfoque estructurado para las migraciones, nos podemos asegurar que las actualizaciones y mejoras se realicen de manera controlada y reproducible, minimizando el riesgo de errores en producción. Esto es especialmente crucial en entornos donde la precisión y la consistencia son fundamentales, como en sistemas que manejan grandes volúmenes de datos o que tienen requisitos regulatorios estrictos.
Incorporar tanto las funciones de "upgrade" como de "downgrade" en el proceso de migración no solo permite mejorar la base de datos, sino también revertir cambios en caso de errores, así como también el uso de herramientas como Sequelize, TypeORM y Alembic hace que la creación y gestión de migraciones sea más accesible y menos propensa a fallas.
Y finalmente, adoptar un enfoque proactivo en la gestión de migraciones refuerza el compromiso de un equipo de desarrollo con la calidad y la estabilidad del software. A medida que las aplicaciones crecen en complejidad y alcance, la capacidad de gestionar cambios en la base de datos de manera eficaz se convierte en un diferenciador clave.