domingo, 18 de junio de 2017

All about Entity Framework migrations


El pasado miércoles día 24 de mayo asistí a un meetup de MadridDotNet (https://www.meetup.com/es-ES/madriddotnet/events/237898951/) en el que Sergio León (@panicoenlaxbox) nos habló en profundidad de Entity Framework 6.x, sobre todo de esas cosas poco conocidas que solo alguien que se ha bregado en las trincheras de EF puede conocer.

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”.

clip_image002
clip_image004

Creamos nuestro contexto en Entity Framework:

clip_image006

Y finalmente una aplicación de consola que muestra por pantalla la información leída, es decir los superhéroes y sus identidades

clip_image008

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.

clip_image010

Con esto ya podemos ejecutar nuestra aplicación. Vemos por pantalla los dos superhéroes, así como las dos identidades secretas de Superman

clip_image012

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!

clip_image014

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

clip_image016

Una vez ejecutado, vemos que en nuestro proyecto ha aparecido la carpeta Migrations, conteniendo un único fichero, con la configuración de las migraciones

clip_image018

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:

clip_image020

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

clip_image022

Debemos actualizar la base de datos, sin crear migración alguna previamente, no hace falta en este modo

clip_image024

Y ahora nuestra aplicación funcionará sin problemas, aunque claro, la columna nueva no tendrá datos.

clip_image026

¿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
Podríamos de nuevo hacer un pequeño cambio, hacer que la nueva columna no admitiese nulos. ¿Qué sucederá?

clip_image028
clip_image030

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:

clip_image031

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


clip_image033

Vemos q una nueva clase de migración ha aparecido. Es la migración inicial, que crea el modelo:

clip_image035

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:

clip_image037
clip_image039

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:

clip_image040

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.

clip_image041

De nuevo, si ejecutamos la aplicación obtendremos un error, ya que la bbdd no coincide con nuestro modelo. Hay que trabajar con migraciones.

clip_image043

Si tratamos de actualizarla mediante update-database como en el caso anterior… no va a funcionar, ya que nuestras migraciones no son automáticas.

clip_image045

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)

clip_image047
clip_image049

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:

  1. Ejecutamos el comando update-database
  2. 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“

clip_image051

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

clip_image053

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

clip_image055

Ahora debemos crear una nueva migración, y aplicarla.
  1. Ejecutamos add-migration NameLenght
  2. 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

clip_image056

Si hubiésemos generado y ejecutado el script, en vez del comando update-database, también tendríamos un error

clip_image057

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

clip_image058

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.

clip_image060

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:

clip_image062
clip_image064

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:

clip_image066
clip_image068

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

clip_image070

¿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). 
En cualto al código, aquí podeis encontrarlo:https://github.com/snavarropino/EF6Migrations

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.











4 comentarios:

  1. Perfecto, ahora sí queda claro, migración automática != migración explícita (o por código)

    En cuanto a conflictos con migraciones en equipos, este link lo explica perfectamente https://msdn.microsoft.com/en-us/library/dn481501(v=vs.113).aspx

    Y respecto a MigrateDatabaseToLatestVersion y que no tenga Seed, es un problema pero se soluciona rápido con un inicializador personalizado, al final el código de Migrate es algo así

    var config = new Migrations.Configuration();
    var migrator = new DbMigrator(config);
    migrator.Update();

    Buen post! :)

    ResponderEliminar
    Respuestas
    1. Gracias Sergio, tomo nota del enlace y del aporte con respecto a la falta del método Seed. Como decia voy a escribir más sobre este tema, de hecho ya estoy preparando el post sobre Ef core :-)

      Eliminar
  2. Yo hace tiempo estuve trabajando y estudiando en estos temas y tuve muchos problemas de rendimiento de primera ejecución ya que la primera vez (o cada vez q el sistema se enfriaba) la comprobación y ejecución (aunque no hubiese migraciones pendientes) ralentizaba mucho el sistema. Al final opté por la ejecución manual porque así la ejecución de la lógica de negocio no era afectada por cosas de despliegue y que solo debería hacerse en ese momento.

    En el sistema estuve tentado de poner una opción en la página web de administración para la ejecución por código de las migraciones para así hacer el despliegue y ejecutarlas desde el propio sistema. Pero ahí me quedé por falta de tiempo. Por ahora ejecutar las migraciones es parte de mi proceso de despliegue.

    Además entiendo que así es y que no tiene sentido llevar un proceso que se debe hacer en despliegue a la ejecución ion porque ahí ralentiza el sistema cada vez que se comprueba, y el impacto en bd bastante grandes o con bastante evolución era muy importante.

    Saludos y muy buen post

    ResponderEliminar
    Respuestas
    1. No me había planteado el tema del rendimiento, voy a investigarlo a ver que encuentro.

      Muchas gracias por el aporte.

      Eliminar