En la charla surgió un interesante debate sobre las migraciones automáticas, con diversas opiniones y referencias, entre otras a un comentario de Luis Ruiz en un reciente video de PlainTv (https://www.youtube.com/watch?v=D38K7URXKZg&t=45s ): nadie debe activar las migraciones automáticas en producción.
¿Qué significa migraciones automáticas? No quedo del todo claro el día del meetup.
Pues bien, pretendo con este post aclarar un poco las cosas, ya que creo que hay cierta confusión, que parte de que en general pensamos que hay dos modos de tratar las migraciones: manuales y automáticas. Sin embargo, la realidad es que hay 3 modos de funcionamiento, vamos a verlo:
- Primer modo: Migraciones automáticas que se aplican con el comando Update-Database, sin que previamente sea necesaria la creación explicita de la migración
Las conseguimos mediante un flag específico que debemos indicar cuando habilitamos las migraciones:
enable-migrations -EnableAutomaticMigrations
- Segundo modo: Migraciones por código, o “explícitas”, aplicadas bien con el comando Update-Database bien con la ejecución de un script en la base de datos
- Tercer modo: Migraciones por código, o “explícitas”, aplicadas de modo automático con un inicializador concreto de bases de datos
Aviso a navegantes: entiendo que estáis familiarizados con las migraciones de Entity Framework 6.x, no pretendo que este post sea una introducción a las mismas, sino una “discusión” sobre las opciones que tenemos y cuando usar cada una.
El ejemplo:
Vamos a trabajar sobre un ejemplo, lo más simple posible, que nos ayude a entender bien las opciones disponibles.
Partimos de dos entidades, superhéroes e identidades secretas, que se relacionarán de un modo “one to many”.
Creamos nuestro contexto en Entity Framework:
Y finalmente una aplicación de consola que muestra por pantalla la información leída, es decir los superhéroes y sus identidades
Si ejecutamos, vamos a ver como entity framework crea la base de datos, usando LocalDb, pero claro, no tenemos datos, así que vamos a crear un inicializador para poblar nuestra bbdd. Vamos a heredar de la estrategia por defecto, que crea la bbdd si no existe.
Con esto ya podemos ejecutar nuestra aplicación. Vemos por pantalla los dos superhéroes, así como las dos identidades secretas de Superman
Como era de esperar la bbdd ha sido creada ya que no existía previamente, y los datos insertados. Lo que llama la atención es la existencia de la tabla de migraciones… ¡pero si no las hemos habilitado aun!
Parece ser que Entity Framework crea la tabla en dos situaciones: cuando se habilitan las migraciones (hecho que aún no ha sucedido) o cuando Entity Framework crea la base de datos, lo que sí ha sucedido debido a nuestro inicializador. Aquí hay más detalles:
https://blog.oneunicorn.com/2012/02/27/code-first-migrations-making-__migrationhistory-not-a-system-table/
Con este punto de partida, vamos a empezar a trabajar con migraciones... en los 3 casos que estamos estudiando.
1. Migraciones automáticas
Lo primero inicializarlas con el comando
enable-migrations -EnableAutomaticMigrations
Una vez ejecutado, vemos que en nuestro proyecto ha aparecido la carpeta Migrations, conteniendo un único fichero, con la configuración de las migraciones
El fichero tan solo indica que trabajamos con migraciones automáticas.
Ahora vamos a modificar alguna de nuestras entidades… por ejemplo para añadir una nueva propiedad a la entidad de superhéroes. Añadiremos un booleano que nos indique si el superhéroe es de DC Comics:
Y ejecutamos (nótese que no hemos creado una migración a mano) obtendremos una excepción indicando que nuestro modelo no corresponde con la bbdd… lo esperado
Debemos actualizar la base de datos, sin crear migración alguna previamente, no hace falta en este modo
Y ahora nuestra aplicación funcionará sin problemas, aunque claro, la columna nueva no tendrá datos.
¿Cuáles son los problemas de esta aproximación?
- Falta de control. No sabemos que se está haciendo, lo que genera cierta inseguridad.
- Debemos ejecutar el comando update-database contra las bases de datos en todos los entornos, en staging, en producción etc. Este comando permite que le pasemos una cadena de conexión, pero no parece una gran idea, y seguro que habrá sitios donde no nos dejarán conectar nuestro equipo a ciertas bases de datos.
- He escuchado que podemos tener pérdidas de información, aunque no lo tengo tan claro. Vamos a verlo
Como veis somos avisados de la posible pérdida de información… y nos dice que hay que hacer para forzar la situación, hagámoslo:
Tampoco, el servidor arroja un error, ya que no sabe qué valor meter en la nueva columna que no admite nulos, y por tanto pone un NULO lo que provoca el error.
Lo bueno: ¡No se borran filas de mi base de datos!
Por mi parte esta opción (migraciones automáticas) no debería contemplarse para proyectos destinados a producción, pero si podría ser válido para pruebas de concepto, pequeños desarrollos personales etc etc…
2. Migraciones por código (o explicitas)
De nuevo partimos de nuestro modelo inicial, dos entidades, superhéroes e identidades secretas, que se relacionarán de un modo “one to many”. También tendremos un contexto, nuestro inicializador que creará la base de datos en caso de no existir, rellenando además datos de prueba… y finalmente nuestra aplicación de consola.
Esta vez activamos las migraciones del siguiente modo:
enable-migrations
Vemos q una nueva clase de migración ha aparecido. Es la migración inicial, que crea el modelo:
Y de hecho podemos ver que la tabla de migraciones ha aparecido en la base de datos, con una fila que incluye esta primera migración:
También es interesante hacer notar que la clase que configura las migraciones difiere un poco del caso anterior, ahora se indica que las migraciones automáticas están deshabilitadas:
Y realizamos una modificación en nuestro modelo, de nuevo metemos una nueva propiedad a la entidad superhéroe, como en el caso anterior incluimos un booleano, admitiendo nulos, para indicar si el superhéroe es de DC Comics.
De nuevo, si ejecutamos la aplicación obtendremos un error, ya que la bbdd no coincide con nuestro modelo. Hay que trabajar con migraciones.
Si tratamos de actualizarla mediante update-database como en el caso anterior… no va a funcionar, ya que nuestras migraciones no son automáticas.
Es necesario crear una migración, y esta vez debemos hacer nosotros con el comando add-migration. Esto generará un nuevo archivo de migración, una clase C# que podemos examinar (punto a favor, ganamos en confianza)
Una vez hecho esto si ejecuto la aplicación sigo obteniendo un error, ya que hay que aplicar la migración explícitamente. Tenemos dos opciones:
- Ejecutamos el comando update-database
- Generamos un script de migración y lo lanzamos a la base de datos, mediante el comando update-database -script
Esta vez voy a seguir la segunda opción, así que ejecutaré “update-database -script“
Me parece interesante el log que vemos en la package manager console: dice que está aplicando migraciones explícitas… es un buen nombre
Por otro lado, podemos ver que el script hace lo esperado: añadir una nueva columna a la bbdd y actualizar la tabla de migraciones
Si lo ejecutamos en nuestra bbdd conseguiremos actualizarla, y a partir de ese momento nuestra aplicación volverá a funcionar
De nuevo nuestra nueva columna no tiene datos… lo esperado ya que es nulable, y las filas existentes antes de meter la columna tendrán este dato a NULL
Siguiendo la secuencia del primer caso, nos preguntamos: ¿Qué sucederá si incluimos una migración que implique posible pérdida de datos?
Vamos a hacerlo: reduciremos la longitud máxima de una columna de texto
Ahora debemos crear una nueva migración, y aplicarla.
- Ejecutamos add-migration NameLenght
- Ejecutamos el comando update-database
Después de hacerlo vemos que el comando update-database falla, ya que tengo en la bbdd datos con longitud mayor a la nueva longitud máxima
Sin embargo, no hemos recibido ninguna clase de aviso anta la posible pérdida de datos, como sucedía con las migraciones automáticas
Si hubiésemos generado y ejecutado el script, en vez del comando update-database, también tendríamos un error
Como vemos no hay ningún mecanismo que nos avise de la posible pérdida de información, pero sin embargo Entity Framework no hace “trampas” para sortear posibles errores, lo que es en cierto modo tranquilizador.
¿Cuáles son los problemas de esta aproximación?
- El hecho de tener que actualizar la bbdd a mano puede generar cierta sobrecarga de trabajo en equipos de trabajo que realicen muchas migraciones
- Las migraciones deben ejecutarse en orden, por lo que en caso de que tengamos varias sin aplicar, debemos averiguar y seguir el orden correcto, cosa que no sucede con migraciones automáticas
Considero que esta opción es adecuada para proyectos cuyo destino es ser puestos en producción, ya que nos da el control que necesitamos, y nos garantiza que las migraciones se aplicarán cuando nosotros queramos. Por cierto, en producción el método adecuado es la ejecución del script SQL generado por el comando update-database -script
3. Migraciones por código (o explicitas) ejecutadas automáticamente con un inicializador
Se trata de migraciones de código explicitas, como en el caso anterior, paro van a ser aplicadas directamente en la base de datos en tiempo de ejecución, cuando EF detecte que hay una migración sin aplicar.
De nuevo partimos de nuestro ejemplo, lo ejecutamos con lo que la base de datos y los datos de prueba son creados, y finalmente procedemos a activar las migraciones en este tercer modo de funcionamiento. ¿Cómo lo hacemos?
- Primero habilitamos las migraciones: Enable-migrations
- Después debemos modificar nuestro inicializador, para indicar que las aplique automáticamente
Un detalle a tener en cuenta es que este inicializador no nos va a permitir rellenar información inicial con el método Seed, como hemos hecho en los dos casos anteriores. Vamos a mover esta creación inicial a la clase Configuration de las migraciones, que también nos permiten esto.
Lo siguiente será realizar la primera modificación en nuestro modelo, y crear la migración pertinente. Vamos por tercera vez a incluir una propiedad booleana a la entidad de superhéroes, indicando que admitimos nulos. Seguidamente creamos nuestra migración:
Y sin aplicarla compilamos y ejecutamos nuestra aplicación, que funciona ya que nuestra migración es ejecutada por nosotros.
Tratando de nuevo de generar una migración que implicase pérdida de información, vamos a hacer que la propiedad que indica si un superhéroe es de DC no admita nulos. Tenemos varias filas en la bbdd con dicha propiedad a NULL así que o bien se borran las filas o bien la migración no puede aplicarse.
Cambiamos el modelo y generamos migración:
Hecho esto podemos ejecutar, a ver q sucede: Excepción, que indica que la columna no admite Nulos. De nuevo la migración no se aplica y afortunadamente no hay perdida de datos
¿Qué opinión me merece este modelo?
Creo que aúna las bondades de los dos modelos anteriores, ejecuta automáticamente las migraciones, ahorrándonos trabajo, pero somos nosotros quienes las creamos explícitamente, pudiendo verificar que la migración hace lo que queremos (viendo el código C# generado).
Sin duda es la opción que recomendaría para la mayoría de los casos.
Resumen:
Como hemos visto hay 3 modelos posibles, que recomiendo elegir de acuerdo a las siguientes premisas:
- Si estamos trabajando en una PoC, prueba o similar empezaría con migraciones automáticas, que solo cambiaría a manuales (también llamadas por código o explívitas) en caso de necesidad.
- Sin embargo, para un proyecto destinado a producción elegiría migraciones explicitas, de entrada ejecutadas automáticamente mediante un inicializador (el caso 3 de este artículo). Tan solo en caso de querer ser muy estrictos con la actualización de la bbdd y querer el 100% del control me iría por la opción 2 (migraciones explicitas ejecutadas manualmente).
Siguientes pasos:
Me gustaría escribir más acerca de este tema, ya que hay una cuarta opción que quiero explorar: migraciones automáticas aplicadas automáticamente con un inicializador. No tengo referencias de ella, pero quiero ver que sucede.
Además quisiera revisar todo esto sobre Entity Framework Core, y finalmente, quisiera escribir en profundidad acerca de los posibles problemas y conflictos que suceden al trabajar en grupo con migraciones.
Hasta la próxima, espero que esto sea de utilidad.