Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can EF automatically delete data that is orphaned, where the parent is not deleted?

Tags:

For an application using Code First EF 5 beta I have:

public class ParentObject {     public int Id {get; set;}     public virtual List<ChildObject> ChildObjects {get; set;}     //Other members } 

and

public class ChildObject {     public int Id {get; set;}     public int ParentObjectId {get; set;}     //Other members } 

The relevant CRUD operations are performed by repositories, where necessary.

In

OnModelCreating(DbModelBuilder modelBuilder) 

I have set them up:

modelBuilder.Entity<ParentObject>().HasMany(p => p.ChildObjects)             .WithOptional()             .HasForeignKey(c => c.ParentObjectId)             .WillCascadeOnDelete(); 

So if a ParentObject is deleted, its ChildObjects are too.

However, if I run:

parentObject.ChildObjects.Clear(); _parentObjectRepository.SaveChanges(); //this repository uses the context 

I get the 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.

This makes sense as the definition of the entities includes the foreign key constraint which is being broken.

Can I configure the entity to "clear itself up" when it gets orphaned or must I manually remove these ChildObjects from the context (in this case using a ChildObjectRepository).

like image 812
StuperUser Avatar asked May 31 '12 14:05

StuperUser


People also ask

How soft delete is implemented in Entity Framework?

The Soft Delete feature allows you to flag entities as deleted (Soft Delete) instead of deleting them physically (Hard Delete). The soft delete feature can be achieved by using the 'IEFSoftDelete' interface. By default, this interface is always added to the manager.

How do I enable cascade delete in Entity Framework?

Cascade delete automatically deletes dependent records or sets null to ForeignKey columns when the parent record is deleted in the database. Cascade delete is enabled by default in Entity Framework for all types of relationships such as one-to-one, one-to-many and many-to-many.

What is Cascade delete in EF core?

Cascade delete allows the deletion of a row to trigger the deletion of related rows automatically. EF Core covers a closely related concept and implements several different delete behaviors and allows for the configuration of the delete behaviors of individual relationships.

Which method is used to let the DbContext know an entity should be deleted?

DbContext. Remove Method (Microsoft.


2 Answers

It is actually supported but only when you use Identifying relation. It works with code first as well. You just need to define complex key for your ChildObject containing both Id and ParentObjectId:

modelBuilder.Entity<ChildObject>()             .HasKey(c => new {c.Id, c.ParentObjectId}); 

Because defining such key will remove default convention for auto incremented Id you must redefine it manually:

modelBuilder.Entity<ChildObject>()             .Property(c => c.Id)             .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity); 

Now calling to parentObject.ChildObjects.Clear() deletes dependent objects.

Btw. your relation mapping should use WithRequired to follow your real classes because if FK is not nullable, it is not optional:

modelBuilder.Entity<ParentObject>().HasMany(p => p.ChildObjects)             .WithRequired()             .HasForeignKey(c => c.ParentObjectId)             .WillCascadeOnDelete(); 
like image 85
Ladislav Mrnka Avatar answered Oct 20 '22 04:10

Ladislav Mrnka


Update:

I found a way that doesn't need to add navigational properties from the child to the parent entity or to set up a complex key.

It's based on this article which uses the ObjectStateManager to find the deleted entities.

With a list ObjectStateEntry in hand, we can find a pair of EntityKey from each, which represents the relationship that was deleted.

At this point, I couldn't find any indication of which one had to be deleted. And contrary to the article's example, simply picking the second one would get the parent deleted in cases where the child had a navigation property back to the parent. So, in order to fix that, I track which types should be handled with the class OrphansToHandle.

The Model:

public class ParentObject {     public int Id { get; set; }     public virtual ICollection<ChildObject> ChildObjects { get; set; }      public ParentObject()     {         ChildObjects = new List<ChildObject>();     } }  public class ChildObject {     public int Id { get; set; } } 

The other classes:

public class MyContext : DbContext {     private readonly OrphansToHandle OrphansToHandle;      public DbSet<ParentObject> ParentObject { get; set; }      public MyContext()     {         OrphansToHandle = new OrphansToHandle();         OrphansToHandle.Add<ChildObject, ParentObject>();     }      public override int SaveChanges()     {         HandleOrphans();         return base.SaveChanges();     }      private void HandleOrphans()     {         var objectContext = ((IObjectContextAdapter)this).ObjectContext;          objectContext.DetectChanges();          var deletedThings = objectContext.ObjectStateManager.GetObjectStateEntries(EntityState.Deleted).ToList();          foreach (var deletedThing in deletedThings)         {             if (deletedThing.IsRelationship)             {                 var entityToDelete = IdentifyEntityToDelete(objectContext, deletedThing);                  if (entityToDelete != null)                 {                     objectContext.DeleteObject(entityToDelete);                 }             }         }     }      private object IdentifyEntityToDelete(ObjectContext objectContext, ObjectStateEntry deletedThing)     {         // The order is not guaranteed, we have to find which one has to be deleted         var entityKeyOne = objectContext.GetObjectByKey((EntityKey)deletedThing.OriginalValues[0]);         var entityKeyTwo = objectContext.GetObjectByKey((EntityKey)deletedThing.OriginalValues[1]);          foreach (var item in OrphansToHandle.List)         {             if (IsInstanceOf(entityKeyOne, item.ChildToDelete) && IsInstanceOf(entityKeyTwo, item.Parent))             {                 return entityKeyOne;             }             if (IsInstanceOf(entityKeyOne, item.Parent) && IsInstanceOf(entityKeyTwo, item.ChildToDelete))             {                 return entityKeyTwo;             }         }          return null;     }      private bool IsInstanceOf(object obj, Type type)     {         // Sometimes it's a plain class, sometimes it's a DynamicProxy, we check for both.         return             type == obj.GetType() ||             (                 obj.GetType().Namespace == "System.Data.Entity.DynamicProxies" &&                 type == obj.GetType().BaseType             );     } }  public class OrphansToHandle {     public IList<EntityPairDto> List { get; private set; }      public OrphansToHandle()     {         List = new List<EntityPairDto>();     }      public void Add<TChildObjectToDelete, TParentObject>()     {         List.Add(new EntityPairDto() { ChildToDelete = typeof(TChildObjectToDelete), Parent = typeof(TParentObject) });     } }  public class EntityPairDto {     public Type ChildToDelete { get; set; }     public Type Parent { get; set; } } 

Original Answer

To solve this problem without setting up a complex key, you can override the SaveChanges of your DbContext, but then use ChangeTracker to avoid accessing the database in order to find orphan objects.

First add a navigation property to the ChildObject (you can keep int ParentObjectId property if you want, it works either way):

public class ParentObject {     public int Id { get; set; }     public virtual List<ChildObject> ChildObjects { get; set; } }  public class ChildObject {     public int Id { get; set; }     public virtual ParentObject ParentObject { get; set; } } 

Then look for orphan objects using ChangeTracker:

public class MyContext : DbContext {     //...     public override int SaveChanges()     {         HandleOrphans();         return base.SaveChanges();     }      private void HandleOrphans()     {         var orphanedEntities =             ChangeTracker.Entries()             .Where(x => x.Entity.GetType().BaseType == typeof(ChildObject))             .Select(x => ((ChildObject)x.Entity))             .Where(x => x.ParentObject == null)             .ToList();          Set<ChildObject>().RemoveRange(orphanedEntities);     } } 

Your configuration becomes:

modelBuilder.Entity<ParentObject>().HasMany(p => p.ChildObjects)             .WithRequired(c => c.ParentObject)             .WillCascadeOnDelete(); 

I did a simple speed test iterating 10.000 times. With HandleOrphans() enabled it took 1:01.443 min to complete, with it disabled it was 0:59.326 min (both are an average of three runs). Test code below.

using (var context = new MyContext()) {     var parentObject = context.ParentObject.Find(1);     parentObject.ChildObjects.Add(new ChildObject());     context.SaveChanges(); }  using (var context = new MyContext()) {     var parentObject = context.ParentObject.Find(1);     parentObject.ChildObjects.Clear();     context.SaveChanges(); } 
like image 33
Marcos Dimitrio Avatar answered Oct 20 '22 05:10

Marcos Dimitrio