简体   繁体   中英

Entity Framework hard cascade delete

I have a SQLite DB mapped with Entity Framework. There are 2 tables : Collections (1:n) Albums.

When I delete a collection, all related albums have to be deleted as well. I use CollectionRepo.Delete(collection); to achieve that. It uses the following code :

public int Delete(Collection entity)
{
    Context.Entry(entity).State = EntityState.Deleted;
    return Context.SaveChanges();
}

The problem is: when I execute this code, Context.SaveChanges(); give me an exception:

The operation failed: The relationship could not be changed because one or more of the foreign-key properties is non-nullable. When a change is made to a relationship, the related foreign-key property is set to a null value. If the foreign-key does not support null values, a new relationship must be defined, the foreign-key property must be assigned another non-null value, or the unrelated object must be deleted.

It seems that Entity Framework wants to null on the foreign keys instead of deleting the entries. But this is absolutely not what I want because an album makes no sense without a parent (in my use case at least).

I could obviously manualy delete the albums first and then delete the empty collection but it seems to me a bit tricky. First, it seems to me that EF should be smart enough to do it on it's own to simplify the code and second, what if I have dozens of relations to collections and albums, I would end up with quite a big, hard to maintain, code base.


Collection Class

public class Collection
{
    [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public long Id { get; set; }

    public virtual List<Album> Albums { get; set; } = new List<Album>();
}

Album class

public class Album
{
    [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public long Id { get; set; }

    [Required]
    [ForeignKey("Collection")]
    public long CollectionId { get; set; }

    public virtual Collection Collection { get; set; }
}

DbContext child class

public class DataEntities : DbContext
{
    public virtual DbSet<Collection> Collections { get; set; }
    public virtual DbSet<Album> Albums { get; set; }

    public DataEntities() : base("name=Connection")
    {
        Configuration.ProxyCreationEnabled = false;
    }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Album>()
            .HasRequired(a => a.Collection)
            .WithMany(c => c.Albums)
            .HasForeignKey(a => a.CollectionId)
            .WillCascadeOnDelete(true);
        modelBuilder.Entity<Collection>()
            .HasMany(c => c.Albums)
            .WithRequired(a => a.Collection)
            .WillCascadeOnDelete(true);
    }
}

Applying detached object graph modifications has always been unclear in EF. This is one of the cases where it fails without a good reason.

Assuming the Collection entity passed to the Delete method has Albums collection populated (at least this is how I was able to reproduce the exception). The line

Context.Entry(entity).State = EntityState.Deleted;

does two things: attaches entity and all Album objects from the entity.Albums to the context, marks entity as Deleted , and (note!) the Album objects as Modified . This leads to incorrect behavior when you call SaveChanges , and at the end generates the exception in question.

There are two ways (workarounds) to fix this incorrect behavior.

The first one is to replace the above line with

Context.Collections.Attach(entity);
Context.Collections.Remove(entity);

The effect is similar to the described above, with the importand difference that now the related Album objects arte marked as Deleted , which allows successfully executing the SaveChanges .

The drawback is that now the SaveChanges issues a DELETE command for each Album before the command for deleting the Collection , which is inefficient and doesn't make much sense since the cascade delete would handle that perfectly inside the database.

The second option is to keep the code as is, but clear the related collection before attaching the entity:

entity.Albums = null;
Context.Entry(entity).State = EntityState.Deleted;

This allows successfully executing SaveChanges and it generates a single DELETE command only for the entity.

The drawback is that you need to write additional code and not forget any child collection which supports cascade delete, and not doing that for collections that need cascade update (ie with optional relation which requires updating the FK field with NULL ).

The choice is yours.

Per your comments, you're mapping to a pre-existing database (EF did not generate it). CascadeOnDelete only affects the generation of the database. If the database doesn't have CascadeOnDelete configured on the table, then EF will be confused when it attempts to delete and Sqlite doesn't comply.

Also you have the mapping for the foreign key as non-nullable and required (redundant by the way) but in the database the foreign key is nullable. EF assumes that it's not valid because of what you told it.

If you fix your mapping (remove the required annotation from the CollectionID property and change its type to int? instead of just int you should fix your problem. Actually change the mapping in the DbContext class from HasRequired to HasOptional...from there it should work.

Either that or change the table definitions on your database itself.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM