[Entity Framework] ON DELETE CASCADE por defecto nunca más

Voy a ser muy conciso: Entity Framework aplica ON DELETE CASCADE por defecto a las relaciones requeridas. Esto es una mala decisión para un framework de uso general, porque a) muchos desarrolladores no lo saben, lo que puede conducir a acciones peligrosas, y b) es fácil olvidarse de desactivarlo. Sí, desactivarlo, porque esta mala decisión tiene una cara positiva: es muy fácil de quitar, sólo una línea para retirar una convención.

Por suerte, usar SQL Compact Edition como BD durante las primeras etapas de un desarrollo me suele avisar de este olvido. Otro día podemos hablar más de esta técnica. La cuestión hoy es ¿cómo me avisa? Muy sencillo: porque hay patrones de ON DELETE CASCADE que SqlCe no soporta, y da un error, aunque no muy descriptivo. Vamos a verlo con un sencillo ejemplo.

Montar una aplicación EF + SqlCe en 5 pasos

Vamos a crear un modelo con 3 entidades: Cliente, Ticket (que tiene un cliente) y Pago (que tiene un Cliente y un Ticket). Sí, ya sé que es redundante y no es 3NF, pero así tiene que ser. Además, todas esas relaciones serán requeridas.

1 Crear un nuevo proyecto de consola (también puede ser WPF o MVC, lo que más os guste).

2 Añadir el paquete de nuget EntityFramework.SqlServerCompact (¿Que no sabéis? Pues no sigáis leyendo. Yo sólo quiero lectores vagos, como buenos informáticos).

3 Crear el modelo. Como buenos vagos, podéis copiar y pegar esto (aunque he metido un compiler error… sólo por fastidiar a los vagos, que me caen mal):

public class DemoDb : DbContext {
    public DbSet<Cliente> Clientes { get; set; }
    public DbSet<Ticket> Tickets { get; set; }
    public DbSet<Pago> Pagos { get; set; }
}

public class Cliente {
    public int Id { get; set; }
    public string Nombre { get; set; }
}

public class Ticket {
    public int Id { get; set; }
    public string Numero { get; set; }

    public int ClienteId { get; set; }
    public Cliente Cliente { get; set; }

    public ICollection<Pago> Pagos { get; set; }
}

public class Pago {
    public int Id { get; set; }
    public DateTimo Fecha { get; set; }
    public decimal Importe { get; set; }

    public int ClienteId { get; set; }
    public Cliente Cliente { get; set; }
    
    public int TicketId { get; set; }
    public Ticket Ticket { get; set; }
}

(Por cierto, el artículo no es de WinRT, pero ¿a que los números de los pasos son muy Metro?)

4  Añadimos algo de código al Main para que conecte con (y cree) la base de datos:

static void Main(string[] args) {
    using(var db = new DemoDb()) {
        Console.WriteLine("Hay {0} clientes", db.Clientes.Count());
    }
}

5 El paso final: Ejecutar. EF creará una nueva base de datos TuNamespace.DemoDb.sdf… pero se producirá un error:

The referential relationship will result in a cyclical reference that is not allowed. [ Constraint name = FK_Pagoes_Tickets_TicketId ]

Dejando a un lado el Pagoes (aunque en realidad a mí no me importa que las tablas se llamen así. Total, eso sólo lo ven los DBA), ¿alguien entiende este error? “Cyclical reference” ¿dónde? Evidentemente no hay ningún ciclo, pero lo que le pasa a SqlCe es que no soporta todos los ON DELETE CASCADE que estamos definiendo. Como dijimos, cada una de las relaciones requeridas de antes es por defecto ON DELETE CASCADE, y SqlCe 4.0 no soporta los dos caminos de eliminación que se están generando: porque si eliminamos un cliente, los pagos se intentarán eliminar por 2 caminos:

Cliente –> Pago

Cliente –> Ticket –> Pago

No le doy mayor importancia a esta decisión en el diseño de SqlCe 4.0 (que sí podría tenerla), y reitero que esto sólo sucede en SqlCe, es decir, cualquier SQL Server desde el Express en adelante no tienen esta restricción ni por lo tanto daría este error: crearía la base de datos sin problemas. Sin problemas pero con los peligrosos ON DELETE CASCADE. ¿Qué pasa si un usuario avanzado (léase el programador) elimina un cliente? Que todos sus tickets y pagos se borran. Sin más aviso. Y sin vaselina.

Solución

La solución para ambos problemas (a saber, para los despistados, uno, poder generar la base de datos en SqlCe, y dos, protegernos de eliminaciones accidentales de datos en el resto de motores) es eliminar la convención de que cada relación requerida sea ON DELETE CASCADE. Esta convención es una de las muchas que vienen activadas por defecto en nuestro modelo de DbContext, y quitarla es tan sencillo como añadir esto (en nuestro caso en la clase DemoDb):

protected override void OnModelCreating(DbModelBuilder modelBuilder) {
    modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>();
}

Si ahora volvemos a ejecutar el proyecto, SqlCe creará la base de datos sin más error. Bueno, no, porque antes falló a mitad de su creación por lo que antes hay que eliminar ese archivo (por defecto, SqlCe crea su base de datos en BinDebug con extensión .sdf).

De ahora en adelante, un intento de eliminar un cliente que tenga Tickets y/o Pagos dará un error. Y si queremos activar ON DELETE CASCADE para alguna relación en particular, después de sopesarlo y con todas sus consecuencias, podemos hacerlo con la API fluida de definición del modelo, método WillCascadeOnDelete.

Conclusión

Otro día podemos hablar de la conveniencia de usar SqlCe en las etapas más tempranas del desarrollo de una aplicación. A mí, entre otras cosas, me sirve como recordatorio de eliminar esta convención. También podéis poneros una alarma en el móvil, pero recordad que “OneToManyCascadeDeleteConvention is evil”.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *