En el anterior post, Una base de datos por desarrollador, sí en serio!, comentaba por encima como existían frameworks que nos podían ayudar con la tarea de versionar nuestras bases de datos y evitarnos escribir tediosas consultas DML.

Gorka Armentia comentaba que buscaba un tutorial sobre como utilizar uno de ellos, Entity Framework Migrations y como lo prometido es deuda aquí esta esta primera parte del mismo.

El concepto de migración

Cada vez que realizamos un cambio en nuestro modelo de base de datos, deberíamos generar un script que refleje los cambios realizados en la base de datos. Además, para poder llevar un control de los cambios, de igual modo que hacemos con el códigodeberíamos anotar en la base de datos la revisión de la misma. De esta forma podemos saber en todo momento, en que versión se encuentra cada base de datos de cada uno de los entornos que dispongamos y aplicar sólo los cambios necesarios.

Cada script de cambios y su número de revisión correspondiente, conforman una migración. Y para ser correctos del todo, deberíamos de incluir también la opción de deshacer cada uno de nuestros cambios en la misma, en caso de que algo no fuera tal y como queremos.

Por tanto, escribir un script de migracinó que gestione el versionado automáticamente y permita incrementar o reducir el número de revisión fácilmente, puede resultar una tarea compleja si la hacemos solo con SQL. Por ello podemos apoyarnos en frameworks que nos faciliten la vida un poco.

El flujo de trabajo

Lo más importante a la hora de utilizar cualquier framework de migraciones es acostumbrarse a utilizar un correcto flujo de trabajo. Independientemente del framework, el flujo será igual para todos:

Flujo de Trabajo EF Migrations

Flujo de Trabajo EF Migrations

Es importante seguir de forma disciplinada este flujo, sino podemos encontrarnos con dificultades.

Nuestra primera migración con Entity Framework

Iniciamos un nuevo proyecto, utilizando el modo Code First de Entity Framework, creamos un modelo y contexto como los siguientes:

public class Client
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Address { get; set; }
    public string City { get; set; }
    public string State { get; set; }
}
public class ClientsMapping 
        : EntityTypeConfiguration
{
        public ClientsMapping()
        {
            this.ToTable("Clients");

            this.HasKey(x =>; x.Id);
            this.Property(x =>; x.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
            this.Property(x =>; x.Name).IsRequired().HasMaxLength(40);
        }
}
public class TinyERPSampleContext
        : DbContext
{
     public DbSet Clients { get; set; }

     protected override void OnModelCreating(DbModelBuilder modelBuilder)
     {
          modelBuilder.Configurations.Add(new ClientsMapping());
     }
}

Una vez creado este modelo podemos aplicar nuestra primera migración. Una de las ventajas de Entity Framework Code Migrations frente a otros frameworks es su integración con Visual Studio. Abrimos la consola del Package Manager (Ctrl + Q, “Package”), nos aseguramos que tenemos seleccionado el proyecto en el que esta nuestro contexto, y escribimos “Enable-Migrations“, tras lo cual creamos nuestra primera migración con “Add-Migration Initial“:

Nuestra primera migración

El comando Enable-Migration, nos habrá creado una carpeta en el proyecto seleccionado “Migrations“, que contendrá una clase Configuration donde podemos configurar el comportamiento de las migraciones:

internal sealed class Configuration : DbMigrationsConfiguration
    {
        public Configuration()
        {
            AutomaticMigrationsEnabled = false;
        }

        protected override void Seed(EFMigrationsSample.TinyERPSampleContext context)
        {
            //  This method will be called after migrating to the latest version.

            //  You can use the DbSet;.AddOrUpdate() helper extension method 
            //  to avoid creating duplicate seed data. E.g.
            //
            //    context.People.AddOrUpdate(
            //      p => p.FullName,
            //      new Person { FullName = "Andrew Peters" },
            //      new Person { FullName = "Brice Lambson" },
            //      new Person { FullName = "Rowan Miller" }
            //    );
            //
        }
    }

Así mismo, el comando Add-Migration Initial, nos habrá creado una migración con la fecha, hora y el nombre que le dimos, en mi caso “201212210817274_Initial“:

 public partial class Initial : DbMigration
    {
        public override void Up()
        {
            CreateTable(
                "dbo.Clients",
                c => new
                    {
                        Id = c.Int(nullable: false, identity: true),
                        Name = c.String(nullable: false, maxLength: 40),
                        Address = c.String(),
                        City = c.String(maxLength: 75),
                        State = c.String(),
                    })
                .PrimaryKey(t => t.Id);

        }

        public override void Down()
        {
            DropTable("dbo.Clients");
        }
    }

Podemos fijarnos como el generador de código ha creado la migración en base a la información que tenía sobre la entidad en el Mapping, y ha configurado la clave primaria Id así como las restricciones de tamaño en la propiedad Name. También habrá creado un método Down para deshacer esta migración.

Si en este momento queremos añadir más información que el generador de código no ha podido deducir y no podemos incluirla en el Mapping  como es el caso de una restricción unique en la columna City, podemos hacerlo:

CreateTable(
                "dbo.Clients",
                c => new
                    {
                        Id = c.Int(nullable: false, identity: true),
                        Name = c.String(nullable: false, maxLength: 40),
                        Address = c.String(),
                        City = c.String(maxLength: 75),
                        State = c.String(),
                    })
                .PrimaryKey(t => t.Id)
                .Index(x => x.City, unique: true);

Aplicando los cambios a la base de datos

Para aplicar estos cambios a la base de datos, desde la misma ventana del Package Manager Console escribimos: “Update-Database

Actualizando la base de datos

Actualizando la base de datos

Por defecto, Entity Framework utilizara la base de datos configurada en la cadena de conexión predeterminada del contexto, y espera que las migraciones estén en el mismo ensamblado que el contexto, si este no es nuestro caso siempre podemos utilizar los parámetros del comando que encontraremos aquí.

Snapshots ¿qué?

Supongamos que en este momento, nos damos cuenta que se nos ha olvidado añadir una columna a la entidad.

Tenemos que tener en cuenta que cada migración graba un snapshot, con la información del estado de nuestro modelo  en el archivo .resx que acompaña a la misma, por lo que, en cuanto modifiquemos nuestro modelo, Entity Framework detectará que el modelo ha cambiado y al hacer un nuevo Add-Migration, nos generará una nueva migración.

Pero si no queremos generar una nueva migración, porque todavía no hemos actualizado la base de datos, por ejemplo, podemos actualizar el snapshot actual utilizando Add-Migration seguido del nombre completo de nuestra migración.

Dado que esto podemos hacer con todas las migraciones, excepto con la inicial, primero añadimos una nueva entidad Order a nuestro modelo y su migración correspondiente:

public partial class Orders : DbMigration
    {
        public override void Up()
        {
            CreateTable(
                "dbo.Orders",
                c => new
                    {
                        Id = c.Int(nullable: false, identity: true),
                        Code = c.String(),
                        Address = c.String(),
                        SubTotal = c.Decimal(nullable: false, precision: 10, scale: 2),
                        Total = c.Decimal(nullable: false, precision: 18, scale: 2),
                    })
                .PrimaryKey(t => t.Id);

        }

        public override void Down()
        {
            DropTable("dbo.Orders");
        }
    }

Nos damos cuenta que se nos ha olvidado añadir la relación entre la entidad Orders y la entidad Client, como todavía no hemos actualizado la base de datos podemos regenerar el código de la migración para que incluya estos cambios, una vez cambiemos el modelo. Además como en este caso no tenemos modificaciones realizadas a la migración podemos usar el parámetro -force para que el generador de código reemplaze el código de la migración.

Si no usamos este parámetro el generador de código sólo actualiza el snapshot de la migración, dejándonos el código de la migración sin modificar, por lo que tendríamos que añadir los cambios manualmente nosotros:

Add-Migration 201212211031421_Orders -force

Y como resultado la migración para la entidad Order se ha modificado, incluyendo ahora la columna ClientId y la ForeignKey correspondiente:

 public override void Up()
        {
            CreateTable(
                "dbo.Orders",
                c => new
                    {
                        Id = c.Int(nullable: false, identity: true),
                        Code = c.String(),
                        Address = c.String(),
                        SubTotal = c.Decimal(nullable: false, precision: 10, scale: 2),
                        Total = c.Decimal(nullable: false, precision: 18, scale: 2),
                        ClientId = c.Int(nullable: false),
                    })
                .PrimaryKey(t => t.Id)
                .ForeignKey("dbo.Clients", t => t.ClientId, cascadeDelete: true)
                .Index(t => t.ClientId);

        }

        public override void Down()
        {
            DropIndex("dbo.Orders", new[] { "ClientId" });
            DropForeignKey("dbo.Orders", "ClientId", "dbo.Clients");
            DropTable("dbo.Orders");
        }

En resumen

Hoy hemos visto como crear una migración sencilla y como entender el concepto de snapshot y lo que implica en nuestro flujo de creación – actualización de los cambios en el modelo.

La próxima semana seguiremos con la Parte II donde os hablaré de cómo deshacer una migración y trabajar con migraciones que tienen en cuenta los datos existentes.

Mientras tanto tenéis los ejemplos de este post en GitHub, podéis seguir cada paso revisando el historial de commits.

Hasta la próxima! Disfrutad de las fiestas y sobre todo no os acerquéis al ordenador! 😉