En el post de la semana pasada, veíamos como aplicar un flujo de migraciones de base de datos utilizando Entity Framework Code Migrations y realizamos algunas operaciones sencillas de migración.

Continuando nuestra exploración de este framework, hoy vamos a ver dos escenarios a los que nos efrenteramos habitualmente, deshacer migraciones una vez aplicadas y realizar migraciones cuando ya tenemos datos en la base de datos.

Deshaciendo migraciones

Durante el proceso de desarrollo, podemos crear una migración y al probar nuestro código darnos cuenta que hemos olvidado algún campo o propiedad en la misma. Si no hemos aplicado la migración a la base de datos, en el post de la semana pasada veíamos como podíamos volver a generar la migración.

Sin embargo, en muchas ocasiones ya habremos aplicado la migración a nuestra base de datos de desarrollo, por lo que no nos bastará con volver a generarla ya que esta migración ya se encuentra aplicada!

Como ejemplo vamos a aplicar una nueva migración, para lo cual añadiremos dos nuevas clases a nuestro modelo, BillingAddress y DeliveryAddress que asociaremos a la entidad Client. Si no has seguido el post anterior, recuerda que puedes obtener el código del mismo desde GitHub.

public abstract class Address
{
     public int Id { get; set; }
     public string AddressName { get; set; }
     public string City { get; set; }
     public string State { get; set; }
}

public class BillingAddress : Address
{

}

public class DeliveryAddress : Address
{

}
public AddressesMapping()
{
     this.Map(x => x.ToTable("AddressBook"));
     this.Map(x => x.Requires("AddressType").HasValue("Delivery"));
     this.Map(x => x.Requires("AddressType").HasValue("Billing"));

    this.HasKey(x => x.Id);
    this.Property(x => x.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
    this.Property(x => x.AddressName).IsRequired();
    this.Property(x => x.City).IsRequired();
    this.Property(x => x.State).IsRequired();
}

Tras ejecutar el commando Add-Migration AddressBook obtenemos una migración que mapea ambas entidades del modelo a la tabla AdressBook, y usando el modelo TPH (Table Per Hierarchy), utiliza la columna AddressType para seleccionar la entidad a crear:

public partial class AddressBook : DbMigration
{
     public override void Up()
     {
         CreateTable(
             "dbo.AddressBook",
             c => new
                 {
                     Id = c.Int(nullable: false, identity: true),
                     AddressName = c.String(nullable: false),
                     City = c.String(nullable: false),
                     State = c.String(nullable: false),
                     AddressType = c.String(nullable: false, maxLength: 128),
                 })
             .PrimaryKey(t => t.Id);

        }

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

Tras hacer Update-Database nos damos cuenta que hemos olvidado incluir la asociación con la entidad Client. Podemos solucionarlo añadiendo una nueva migración, pero si queremos mantener nuestro historial de migraciones limpio, es posible que nuestra primera reacción sea eliminar la migración anterior.

Sin embargo, esto nos dejará la base de datos en un estado bloqueado, donde no podremos migrar hacia delante ni atrás. La solución que nos permite mantener el historial de migraciones limpio y continuar añadiendo nuevas migraciones es deshacer la migración antes de eliminarla:

Update-Database -TargetMigration Orders

En este caso estamos especificando que la migración objetivo sea Orders, porque queremos deshacer sólo la última de las migraciones. Sin embargo -TargetMigration acepta cualquier migración que hayamos creado, y revertirá los cambios aplicando el método Down de cada una de las migraciones en el orden correcto.

La tabla MigrationHistory

Pero, cuál es la razón por la que no podemos simplemente eliminar la migración? La misma por la que Entity Framework sabe precisamente cuando una migración esta aplicada, la tabla MigrationHistory.

Cada vez que aplicamos una migración Entity Framework añade un registro a esta tabla, anotando el código de la migración e información sobre el estado del modelo en el momento de aplicar la migración:

Tabla Migration History

Tabla Migration History

Por tanto, si simplemente eliminamos la migración, la tabla MigrationHistory seguirá manteniendo una referencia a la migración, lo que nos bloqueará de aplicar nuevas migraciones. Si en algún momento borramos la migración por error y no podemos recuperarla, podemos borrar el registro de la tabla migraciones, teniendo que deshacer los cambios manualmente, para poder aplicar migraciones de nuevo.

Sin embargo, es recomendable acostumbrarse a deshacer una migración antes de eliminarla o modificarla, si ya la hemos aplicado a la base de datos.

Aplicando migraciones con datos

El método Seed

Si queremos añadir datos una vez ha terminado nuestra migración, podemos utilizar el método Seed, en la clase Configuration:

 protected override void Seed(EFMigrationsSample.TinyERPSampleContext context)
{
       context.Clients.AddOrUpdate(
          new Client()
          {
              Name = "Client 1",
              City = "Bilbao",
              Address = "Carolina 3, 4-D",
              State = "Vizcaya"
           },
           new Client()
           {
              Name = "Client 2",
              City = "Eibar",
              Address = "Pantxineta 4, 5-A",
              State = "Eibar"
            });
}

El comando Seed, se ejecuta siempre tras haber aplicado, o no, las migraciones. En este caso la tabla Clients contendrá los datos especificados tras realizar Update-Database:

Datos rellenados con Seed

Datos rellenados con Seed

El problema con el método Seed

Dado que el método Seed se ejecuta siempre al final de aplicar las migraciones, puede no ser el lugar más idóneo para insertar nuestros datos si:

  • El destino es una base de datos de producción por lo que no podemos modificar los datos existentes y no tenemos forma fiable de detectar si estos han cambiado, AddOrUpdate actualizará los datos siempre.
  • Sólo necesitamos crear los datos en una migración especifica, en el futuro no tenemos que añadir más veces estos datos.
  • Si nuestro modelo cambia, tendremos que mantener y evolucionar el método Seed, en modelos complejos esto puede resultar bastante costoso en el tiempo.
  • Con Seed no podemos realizar operaciones en los datos dentro de una migración, previamente a realizar otros pasos.

Realizando operaciones con datos dentro de una migración

En el ejemplo de migración anterior habíamos dejado pendiente asociar la nueva tabla AddressBook a la tabla Clients. Dado que ya hemos llenado la tabla Clients, tenemos ciertos datos de direcciones que queremos mantener y migrar a la nueva tabla AddressBook. Sin embargo, como veíamos, el método Seed no nos sirve en esta situación.

En estos escenarios Entity Framework nos permite ejecutar consultas SQL directamente dentro de una migración. Para ver su funcionamiento primero cambiemos el modelo para que incluya los cambios que queremos, asociando las entidades DeliveryAddress y BillingAddress a la entidad Client:

public class Client
{
    public int Id { get; set; }
    public virtual IEnumerable Orders { get; set; }
    public int BillingAddressId { get; set; }
    public virtual BillingAddress BillingAddress { get; set; }
    public int DeliveryAddressId { get; set; }
    public virtual DeliveryAddress DeliveryAddress { get; set; }
}

public abstract class Address
{
    public int Id { get; set; }
    public string AddressName { get; set; }
    public string City { get; set; }
    public string State { get; set; }
    public virtual IEnumerable Clients { get; set; }
}

Y agregamos la migración correspondiente con Add-Migration:

 public partial class ClientAddressesAssociation : DbMigration
 {
    public override void Up()
    {
         AddColumn("dbo.Clients", "BillingAddressId", c => c.Int(nullable: false));
         AddColumn("dbo.Clients", "DeliveryAddressId", c => c.Int(nullable: false));
         AddForeignKey("dbo.Clients", "BillingAddressId", "dbo.AddressBook", "Id", cascadeDelete: true);
         AddForeignKey("dbo.Clients", "DeliveryAddressId", "dbo.AddressBook", "Id", cascadeDelete: true);
         CreateIndex("dbo.Clients", "BillingAddressId");
         CreateIndex("dbo.Clients", "DeliveryAddressId");
         DropColumn("dbo.Clients", "Name");
         DropColumn("dbo.Clients", "Address");
         DropColumn("dbo.Clients", "City");
         DropColumn("dbo.Clients", "State");
    }

    public override void Down()
    {
         AddColumn("dbo.Clients", "State", c => c.String());
         AddColumn("dbo.Clients", "City", c => c.String());
         AddColumn("dbo.Clients", "Address", c => c.String());
         AddColumn("dbo.Clients", "Name", c => c.String(nullable: false, maxLength: 40));
         DropIndex("dbo.Clients", new[] { "DeliveryAddressId" });
         DropIndex("dbo.Clients", new[] { "BillingAddressId" });
         DropForeignKey("dbo.Clients", "DeliveryAddressId", "dbo.AddressBook");
         DropForeignKey("dbo.Clients", "BillingAddressId", "dbo.AddressBook");
         DropColumn("dbo.Clients", "DeliveryAddressId");
         DropColumn("dbo.Clients", "BillingAddressId");
     }
}

Si ejecutamos la migración mediante Update-Database, en este momento no nos funcionará dado que la tabla Clients tiene datos y por tanto la relación obligatoria entre Clients y AddressBook no se cumplirá:

The ALTER TABLE statement conflicted with the FOREIGN KEY constraint "FK_dbo.Clients_dbo.AddressBook_BillingAddressId". The conflict occurred in database "EFMigrationsSample.TinyERPSampleContext", table "dbo.AddressBook", column 'Id'.

Es en este momento cuando necesitamos modificar la migración para que tenga en cuenta los datos existentes y se ejecute correctamente. Como además queremos conservar las direcciones actuales, necesitaremos de dos script sql que se ejecuten justo antes de crear la relación y eliminar las columnas:

WITH uniqueaddresses (Address, City, State) AS 
	(SELECT Address, City, State 
		FROM (SELECT *, ROW_NUMBER() OVER (PARTITION BY City, Name ORDER BY City, Name) AS Ocurrences 
		FROM Clients) Clients_RANK
	 WHERE Ocurrences = 1)
INSERT INTO AddressBook (AddressName, City, State, AddressType)
SELECT Address, City, State, 'Delivery'
FROM uniqueaddresses
UNION
SELECT Address, City, State, 'Billing'
FROM uniqueaddresses
UPDATE Clients
SET DeliveryAddressId = A.Id
FROM AddressBook AS A JOIN Clients AS C
ON A.City = C.City AND A.AddressName = C.Address
WHERE A.AddressType = 'Delivery'

UPDATE Clients
SET BillingAddressId = A.Id
FROM AddressBook AS A JOIN Clients AS C
ON A.City = C.City AND A.AddressName = C.Address
WHERE A.AddressType = 'Billing'

El primer script busca direcciones únicas en la tabla Clients y las inserta como direcciones de tipo Billing y tipo Delivery, mientras que el segundo script actualiza las relaciones entre Clients y Addresses con las direcciones correspondientes.

Entity Framework nos proporciona el método Sql para ejecutar instrucciones SQL durante una migración, este método recibe un parámetro String, pero como nuestras instrucciones Sql son largas incluirlas directamente en la migración sería poco claro.

Como ya hemos comentado cada migración incluye un archivo .resx donde Entity Framework almacena un snapshot del estado de nuestro modelo. Podemos utilizar este archivo .resx para almacenar información adicional asociada a la migración. Otra ventaja de utilizar el archivo de recursos de la migración, es que no tendremos que deducir la ruta a los scripts sql, algo que en diferentes entornos puede resultar complicado.

Editando el archivo Resx

Una vez hemos añadido los script sql al archivo de recursos, estamos listos para utilizarlos en nuestra migración.

Es importante recordar llamar a cada script en el lugar adecuado de la migración, de tal forma que primero se ejecute el script que rellena los datos en AddressBook y una vez se han creado las columnas de relación en la tabla Clients, se ejecute el script que asocia cada Client con su Address. Podemos obtener acceso a los recursos de la migración actual a través de la propiedad Resources:

 public override void Up()
 {
       Sql(this.Resources.GetString("CreateAddressData"));

       AddColumn("dbo.Clients", "BillingAddressId", c => c.Int(nullable: false));
       AddColumn("dbo.Clients", "DeliveryAddressId", c => c.Int(nullable: false));

       Sql(this.Resources.GetString("MigrateClientAddresses"));

       AddForeignKey("dbo.Clients", "BillingAddressId", "dbo.AddressBook", "Id", cascadeDelete: false);
       AddForeignKey("dbo.Clients", "DeliveryAddressId", "dbo.AddressBook", "Id", cascadeDelete: false);

       CreateIndex("dbo.Clients", "BillingAddressId");
       CreateIndex("dbo.Clients", "DeliveryAddressId");

       DropIndex("dbo.Clients", new [] { "City" });
       DropColumn("dbo.Clients", "Name");
       DropColumn("dbo.Clients", "Address");
       DropColumn("dbo.Clients", "City");
       DropColumn("dbo.Clients", "State");
}

Por último hemos modificado el parámetro cascadeDelete a false en las claves foráneas dado que sino se crearían referencias circulares. Con estos cambios la migración ya ejecuta y actualiza los datos correctamente. Si ya se ha aplicado la migración, este proceso, a diferencia de con el método Seed, no se ejecutará más veces.

En resumen

En esta parte 2 hemos visto un uso más avanzado, pero muy común, de las migraciones con Entity Framework. Estamos ya listos para enfrentarnos a la mayoría de escenarios que nos encontraremos durante la vida de un proyecto.

Para la próxima parte nos quedaría conocer como aplicar automáticamente nuestras migraciones y como podemos hacer más sencillas algunas de las tareas utilizando los mecanismos de extensión que nos proporciona Entity Framework. Pero eso será otra historia.

Disfrutad de las fiestas y empezad muy bien el año! Feliz 2013!