jueves, 2 de mayo de 2013

Rompiendo el hielo con Code First Migrations

Como continuación del post Rompiendo el hielo con Code First, ahora le toca el turno a las migraciones de Code First.

Con el tiempo nuestro modelo evolucionará, eso está claro. Todo modelo cambia y el nuestro no será una excepción. Cuando esto ocurra, el esquema de bd actual ya no será válido para persistir el modelo y requerirá que apliquemos cambios en la db para adecuarlo al nuevo modelo. Para validar que el modelo y el esquema de bd están a nivel, Code First utiliza la tabla __MigrationHistory. Esta tabla conoce cuál fue el último modelo aplicado a la base de datos y puede comparar a éste con el modelo actual que queremos ejecutar.

Para poder ver esto de forma más clara basta con ver cuál es el contenido de esta tabla cuando la base de datos es creada por primera vez por Code First. Se está guardado un único registro con el modelo inicial que sirvió para crear automáticamente la base de datos.

Imaginemos un modelo con una sola entidad llamada Cliente con las propiedades ClienteId y Nombre. El registro inicial será similar a este:

clip_image002

El campo Model almacena los metadatos del EDM utilizado para generar la base de datos. Si quieres verlo más gráficamente te invito a que leas el post Can I get decode an EntityFramework Model from a specified migration? y pruebes a descomprimir este dato y guardar el resultado en un fichero .edmx. Podrás ver que es perfectamente válido y que no hay diferencia entre este .edmx y cualquier otro que pudieras haber creado con Database First o Model First.

clip_image003

Viendo esto también comprendemos porque no funciona poner manualmente a nivel la base de datos con el nuevo modelo. La comparación siempre se realizará entre modelos (el guardado y el nuevo que quiere persistir) y nada tiene que ver aquí el esquema de la base de datos.

En este punto, el único medio disponible para evolucionar la base de datos junto al modelo consiste en la utilización de las migraciones de Code First (lógicamente no tengo en cuenta la táctica de eliminar y recrear la base de datos, que ni es migración ni es ná).

Una migración podría definirse como el conjunto de cambios a realizar en el esquema de la base de datos para que sea acorde al modelo.

Las migraciones pueden ser de 2 tipos (no excluyentes):

  • Automáticas.
  • Basadas en código.

Migraciones automáticas

Para habilitar las migraciones automáticas tenemos que ejecutar el comando Enable-Migrations –EnableAutomaticMigrations en la consola de NuGet (recordando que como proyecto predeterminado en la consola tenemos que seleccionar el que contenga nuestra clase de contexto). Como resultado del comando, se agregará al proyecto una carpeta Migrations con un fichero llamado Configuration.cs y con la propiedad AutomaticMigrationsEnabled establecida a true.

clip_image004

A partir de este momento, cada vez que hagamos un cambio en el modelo y queramos poner a nivel la base de datos, tendremos que ejecutar el comando Update-Database en la consola de NuGet. Aquí “cambio” lo podemos traducir como uno o muchos “cambios”. Este comando llevará a cabo 2 tareas:

  • Ejecuta las sentencias SQL necesarias para poner a nivel el esquema de la base de datos.
  • Insertar una nueva fila en __MigrationHistory con el nombre Timestamp_AutomaticMigration.

Ahora Code First ya sabe que el último modelo aplicado a la base de datos no es Timestamp_InitialCreate sino Timestamp_AutomaticMigration.

clip_image006

A partir de aquí la operativa es siempre la misma: un cambio en el modelo vendrá siempre acompañado del comando Update-Database.

Además de poder utilizar el comando Update-Database, también podemos automatizar la migración a través de código (porque lo de ejecutar NuGet en un entorno de producción, yo no lo veo claro…). Esto lo podemos conseguir, bien con el inicializador MigrateDatabaseToLatestVersion o bien a través de código puro y duro.

Utilizar el nuevo inicializador es muy sencillo:

Database.SetInitializer(

    new MigrateDatabaseToLatestVersion

        <MyContext, Migrations.Configuration>());

Si queremos tener más control sobre cuando sucede la migración, podemos utilizar esta otra forma que tomo prestada del post de Javier Torrecilla DbMigration y Code First donde ejecutamos la migración automática a través de código.

var config = new Migrations.Configuration();

var migrator = new DbMigrator(config);

migrator.Update();

Ambos métodos funcionarán si la base de datos no existe y el único cambia será que el nombre de la migración inicial pasará a llamarse Timestamp_AutomaticMigration en vez de Timestamp_InitialCreate.

Quizás el ejemplo de código anterior no te compile porque la clase Configuration (la que agregó automáticamente el comando Enable-Migrations – EnableAutomaticMigrations) se crea internal y sealed. Si lo necesitas, puedes cambiar el modificador de acceso a public sin ningún problema.

Otro dato a tener en cuenta es que las migraciones automáticas sólo funcionan si no hay una posible pérdida de datos. Si por ejemplo tu modelo cambia porque eliminas una propiedad, verás de forma predeterminada la siguiente excepción:

clip_image007

Esto se puede evitar si establecemos a true la propiedad AutomaticMigrationDataLossAllowed en el constructor de la clase Configuration (ahora bien, lo que pase a partir de ese momento ya no es responsabilidad mía).

Por último, hay disponible un método Seed en la clase Configuration (dando igual si vamos por migraciones automáticas o por código) que se ejecutará cada vez que nuestro modelo se actualice (cabe recorrdar que se ejecutará sólo una vez y después de aplicar todas las migraciones pendientes). Es un lugar idóneo para inicializar datos que se requieran con la subida de versión.

Migraciones basadas en código

Con esta solución lo que vamos a conseguir es tener más el control de las migraciones a realizar. Es decir, pasamos de conducir con piloto automático a pilotar nosotros mismos. De hecho, la práctica habitual es utilizar este tipo de migraciones porque aunque las automáticas pintan bien sobre el papel, después y con un programa real en producción son del todo inservibles, por ejemplo un simple cambio en el nombre de una propiedad del modelo implicaría perder todos los datos de la columna sin posibilidad de poder renombrarla y salvaguardar así los datos.

Para habilitar las migraciones basadas en código tendremos que ejecutar el comando Enable-Migrations.

Aunque ahora tendremos igualmente la carpeta Migrations y el fichero Configuration.cs, esta vez la propiedad AutomaticMigrationsEnabled está establecida a false. Además tenemos un nuevo fichero Timestamp_InitialCreate.cs que representa una migración para la configuración inicial de la base de datos.

clip_image008

    public partial class InitialCreate : DbMigration

    {

        public override void Up()

        {

            CreateTable(

                "dbo.Clientes",

                c => new

                    {

                        ClienteId = c.Int(nullable: false, identity: true),

                        Nombre = c.String(),

                    })

                .PrimaryKey(t => t.ClienteId);

           

        }

       

        public override void Down()

        {

            DropTable("dbo.Clientes");

        }

    }

Con las migraciones basadas en código, cada clase de migración tiene 2 métodos: Up y Down. El método Up contiene los comandos necesarios para actualizar la base de datos y el método Down permite revertir los cambios realizados por Up. Como vemos, ahora nuestra migración tiene un nombre con el formato <timestamp>_NombreMigracion (según Add-Migration) y además sus métodos Up y Down nos permiten personalizar la migración (que es justamente uno de los principales beneficios de las migraciones basadas en código). Aquí cabe mencionar que <timestamp> sólo tiene el efecto de ordenar las migraciones cuando tengan que aplicarse.

Los métodos disponibles en las migraciones son todos aquellos disponibles en la clase DbMigration (de la que hereda una migración). Los métodos están totalmente orientados a manipular el esquema físico de la base de datos. Por ejemplo tenemos disponibles AddColumn, AlterColumn, CreateTable, DropTable, etc.

Un método muy interesante si hablamos de personalizar la migración es el método Sql que permite ejecutar directamente cualquier sentencia SQL.

Con este tipo de migraciones, cuando hagamos un cambio en el modelo no bastará simplemente con llamar a Update-Database sino que tendremos que agregar una migración antes con el comando Add-Migration <NombreMigracion>. Al agregar una migración se creará un nuevo fichero Timestamp_NombreMigracion.cs con el código necesario en Up y Down para actualizar o revertir la base de datos. Una vez creada la migración ya sí podremos ejecutar el comando Update-Database, utilizar el inicializador MigrateDatabaseToLatestVersion o actualizar a la última versión a través de código (como hicimos para el caso de las migraciones automáticas).

Como “tip” te diré que si utilizamos kebab-case (o como quiera que se llame esta notación) en el nombre de la migración será convertido a snake_case en el nombre de la clase generada, aunque en cualquier caso, nos referiremos a las migraciones en los comandos de PowerShell con el nombre original.

Si hemos agregado una migración y nos damos cuenta de que faltaba algo y todavía no hemos ejecutado Update-Database, podemos simplemente eliminar la migración y volver a crearla (eliminar es eliminar del proyecto los ficheros generados por la migración) o bien volver a llamar a Add-Migration <MismoNombreMigracion> con –Force y así reemplazar la migración. Si por el contrario ya hemos ejecutado Update-Database, primero tendremos que deshacer la migración aplicada con Update-Database –TargetMigration <NombreMigraciónAnterior>.

Mezclar migraciones automáticas con migraciones basadas en código

Aunque no parece un escenario probable, podríamos (yo no lo hago) tener activadas las migraciones automáticas (AutomaticMigrationsEnabled a true) y además agregar migraciones basadas en código con el comando Add-Migration. En este caso, cuando ejecutemos el comando Update-Database lo que pasará es que primero se ejecutarás las migraciones basadas en código y después se ejecutará la migración automática.

Más comandos

Los comandos disponibles en la consola de NuGet en lo relativo a migraciones son los siguientes:

  • Enable-Migrations
  • Add-Migration
  • Update-Database
  • Get-Migrations

Puede consultar una referencia completa en este post EF Migrations Command Reference

Además de los comandos anteriormente mostrados, todos ellos tienen disponibles bastantes parámetros. Por ejemplo, podemos volver a una migración en concreto con el comando Update-Database –TargetMigration <NombreMigracion>, y aquí “volver” podría significar aplicar (Up) o revertir (Down).

Trabajar contra una base de datos existente

En este supuesto la clave está en decidir si quieres o no utilizar la tabla __MigrationHistory.

Con __MigrationHistory:

  • Podrás utilizar migraciones.
  • Tu modelo validará contra el esquema de base de datos.
  • Tendrás que poder crearla en la base de datos y además tendrás que saber crearla con contenido adecuado (más adelante en el post veremos cómo).

Sin __MigrationHistory:

  • No podrás utilizar migraciones.
  • Tu modelo no validará contra el esquema de base de datos, luego podrías tener por ejemplo una clase Cliente con los campos ClienteId, Nombre y Descripción y tener en base de datos una tabla con sólo los campos ClienteId y Nombre. Nadie se quejará, nadie te avisará (Code First no puede validar tu modelo) así que si hay un error lo sabrás en tiempo de ejecución.
  • Podrás poner a nivel “manualmente” la base de datos con tu modelo sin tener que preocuparte de los intríngulis de __MigrationHistory.

Como ves la decisión depende del contexto.

Trabajar sin __MigrationHistory es fácil, un poco de ingenieria inversa con Entity Framework Power Tools y a correr! Puedes ver un tutorial en MSDN Code First to an Existing Database.

Por otro lado, querer generar la tabla __MigrationHistory con contenido adecuado tiene su miga. En el siguiente post se explica paso a paso Using EF 4.3 Code First Migrations with an Existing Database. A grandes rasgos hay que hacer lo siguiente:

  • Habilitar las migraciones por código con Enable-Migrations.
  • Añadir una migración con Add-Migration InitialCreate, aquí el nombre InitialCreate no es obligado, podría ser cualquier otro, yo lo llamo así por convención.
  • Comentar el código de los métodos Up y Down de la migración.
  • Actualizar la base de datos con Update-Database, que lo único que hará será crear la tabla __MigrationHistory con un registro para la migración anteriormente creada (lógicamente aquí se asume que el modelo de nuestro código es válido para el esquema actual de la base de datos).

Completados los anteriores pasos, sólo queda trabajar con normalidad como si nada hubiera pasado. Después de haber trabajo un poco más con Code First y Migrations, es más sencillo generar __MigrationHistory simplemente habilitando las migraciones y después agregando una migración con el parámetro –IgnoreChanges, por ejemplo Add-Migration Inicial –IgnoreChanges.

Otro tip que podría resultar interesante es saber cómo hacer que la tabla __MigrationHistory no sea marcada como tabla de sistema. En mi caso aún no he encontrado ningún problema con ello, pero si tienes la necesidad de que la tabla no sea de sistema, puedes visitar el siguiente enlace Code First Migrations: Making __MigrationHistory not a system table.

También algunas recomendaciones que me parecen todas bastantes acertadas en Tips for Entity Framework Migrations.

Para terminar, sólo desear que entre el post de Rompiendo el hielo con Code First y éste, te hayan servido para ver a Code First con mejores ojos.

Un saludo!

1 comentario: