sábado, 15 de junio de 2013

Eliminación en cascada en Code First

En un anterior post vimos las relaciones en Code First, pero algo que podría merecer su propio post es el manejo de la eliminación en cascada en el modelo y base de datos.

Será a través de ejemplos como intentemos mostrar los distintos escenarios posibles.

El siguiente código creará una relación entre las tablas Productos y LineasPedido con la eliminación en cascada habilitada, puesto que la propiedad ProductoId es requerida en LineaPedido, es decir, no puede existir una LineaPedido sin un Producto asociado.

[Table("Productos")]

class Producto

{

    public int ProductoId { get; set; }

    public string Descripcion { get; set; }

    public ICollection<LineaPedido> LineasPedido { get; set; }

}

 

[Table("LineasPedido")]

class LineaPedido

{

    public int LineaPedidoId { get; set; }

    public int Unidades { get; set; }

    public int ProductoId { get; set; }

    public Producto Producto { get; set; }

}

Si hacemos opcional la propiedad ProductoId en LineaPedido, ahora la relación no tendrá habilitada la eliminación en cascada puesto que una LineaPedido podría existir sin un Producto, esto es que el campo LineasPedido.ProductoId admitirá nulos en la base de datos:

[Table("LineasPedido")]

class LineaPedido

{

    public int LineaPedidoId { get; set; }

    public int Unidades { get; set; }

    public int? ProductoId { get; set; }

    public Producto Producto { get; set; }

}

Lo cierto es que este es el comportamiento predeterminado de Code First para inferir si la relación tiene o no habilitada la eliminación en cascada, si la propiedad de navegación en la tabla dependiente es requerida, entonces la cascada estará habilitada, sino no.

Cuando Code First habilita la eliminación en cascada, lleva a cabo 2 tareas:

·         Eliminación en cascada para datos en memoria.

·         Eliminación en cascada como restricción en la base de datos.

Si por algún motivo queremos sobrescribir la convención, podremos hacerlo a través de Fluent API (no es posible hacerlo con DataAnnotations). Por ejemplo (y volviendo a asumir que ProductoId es una propiedad requerida en LineaPedido) podemos deshabilitar la eliminación en cascada con el siguiente código:

modelBuilder.Entity<Producto>().

    HasMany(p => p.LineasPedido).

    WithRequired(p => p.Producto).

    WillCascadeOnDelete(false);

Aquí vemos como, de nuevo, es necesario definir la relación entera entre Producto y LineaPedido para configurar llamar finalmente al método WillCascadeOnDelete.

Lógicamente, ahora será nuestra la responsabilidad eliminar LineasPedido cuando se elimine un Producto. En la situación actual: una propiedad de navegación requerida en una tabla dependiente pero sin estar habilitada la eliminación en cascada, cuando borremos un registro desde la tabla principal (Producto), Code First intentará establecer a null el campo LineasPedido.ProductoId y como esto no está permitido obtendremos un error:

var producto = new Producto()

    {

        Descripcion = "Producto 1"

    };

context.Productos.Add(producto);

var lineaPedido = new LineaPedido()

    {

        Producto = producto,

        Unidades = 1

    };

context.LineasPedido.Add(lineaPedido);

context.SaveChanges();

 

context.Productos.Remove(producto);

// No se puede dejar huerfana la línea de pedido

context.SaveChanges();

clip_image001

Un caso especial en el que no está soportado la eliminación en cascada es cuando existen múltiples rutas de eliminación en cascada a una misma tabla desde distintas relaciones.

Imaginemos por ejemplo el siguiente escenario (basado en hechos reales):

·         Una empresa puede tener asociados varios centros comerciales.

·         Un centro comercial puede tener asociados varias tiendas.

·         Una empresa puede tener asociados varios tipos de tienda.

·         Un tipo de tienda puede tener asociados varias tiendas.

Lo cierto es que el escenario parece sencillo… ¡manos a la obra!

[Table("Empresas")]

class Empresa

{

    public int EmpresaId { get; set; }

    public ICollection<CentroComercial> CentrosComerciales { get; set; }

}

 

[Table("CentrosComerciales")]

class CentroComercial

{

    public int CentroComercialId { get; set; }

    public int EmpresaId { get; set; }

    public Empresa Empresa { get; set; }

    public ICollection<Tienda> Tiendas { get; set; }

}

 

[Table("Tiendas")]

class Tienda

{

    public int TiendaId { get; set; }

    public int CentroComercialId { get; set; }

    public CentroComercial CentroComercial { get; set; }

    public int TipoTiendaId { get; set; }

    public TipoTienda TipoTienda { get; set; }

}

 

[Table("TiposTienda")]

class TipoTienda

{

    public int TipoTiendaId { get; set; }

    public int EmpresaId { get; set; }

    public Empresa Empresa { get; set; }

    public ICollection<Tienda> Tiendas { get; set; }

 

}

Sin embargo, al intentar crear la base de datos obtenemos el siguiente error:

clip_image002

El problema está que una tienda puede verse eliminada en cascada a través de dos distintas rutas: Empresas o Tipos de tienda. Bastaría con deshabilitar la eliminación en cascada para alguna de las rutas y la generación de la base datos funcionaría ya correctamente. Por ejemplo, deshabilitaremos la eliminación en cascada para la ruta de tipos de tienda.

modelBuilder.Entity<TipoTienda>().

    HasMany(p => p.Tiendas).

    WithRequired(p => p.TipoTienda).

    WillCascadeOnDelete(false);

Nada más y espero que te haya servido de ayuda este post.

Un saludo!

 

1 comentario:

  1. Gracias a tu post me has abierto los ojos, no conseguia encontrar la forma de solucionar este problema.

    ResponderEliminar