Может ли EF автоматически удалять потерянные данные, если родитель не удален?

Для приложения, использующего Code First EF 5 beta, у меня есть:

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

а также

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

Соответствующие операции CRUD выполняются репозиториями, где это необходимо.

В

OnModelCreating(DbModelBuilder modelBuilder)

Я настроил их:

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

Так что если ParentObject удаляется, его дочерние объекты тоже.

Однако, если я бегу:

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

Я получаю исключение:

Операция не выполнена: отношение не может быть изменено, так как одно или несколько свойств внешнего ключа не могут иметь значение NULL. Когда в отношение вносится изменение, для соответствующего свойства внешнего ключа устанавливается нулевое значение. Если внешний ключ не поддерживает нулевые значения, необходимо определить новое отношение, свойству внешнего ключа должно быть назначено другое ненулевое значение или несвязанный объект должен быть удален.

Это имеет смысл, так как определение сущностей включает ограничение внешнего ключа, которое нарушается.

Могу ли я настроить сущность на "очистку себя", когда она станет сиротой, или я должен вручную удалить эти ChildObjectиз контекста (в этом случае с помощью ChildObjectRepository).

7 ответов

Решение

Это на самом деле поддерживается, но только когда вы используете Идентификационные отношения. Сначала он работает с кодом. Вам просто нужно определить сложный ключ для вашего ChildObject содержащий оба Id а также ParentObjectId:

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

Поскольку определение такого ключа удалит соглашение по умолчанию для автоматически увеличенного идентификатора, вы должны переопределить его вручную:

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

Теперь вызов parentObject.ChildObjects.Clear() удаляет зависимые объекты.

Btw. ваше отображение отношений должно использовать WithRequired следовать вашим реальным занятиям, потому что если FK не обнуляем, это не является обязательным:

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

Обновить:

Я нашел способ, при котором не нужно добавлять навигационные свойства от дочерней к родительской сущности или устанавливать сложный ключ.

Он основан на этой статье, которая использует ObjectStateManager найти удаленные объекты.

Со списком ObjectStateEntry в руке, мы можем найти пару EntityKey от каждого, который представляет отношение, которое было удалено.

На данный момент я не смог найти никаких признаков того, какой из них должен быть удален. И вопреки примеру статьи, простой выбор второго приведет к удалению родителя в тех случаях, когда у ребенка есть свойство навигации обратно к родителю. Итак, чтобы исправить это, я отслеживаю, какие типы должны обрабатываться с помощью класса OrphansToHandle,

Модель:

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; }
}

Другие классы:

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; }
}

Оригинальный ответ

Чтобы решить эту проблему без настройки сложного ключа, вы можете переопределить SaveChanges вашей DbContext, но потом использовать ChangeTracker чтобы избежать доступа к базе данных, чтобы найти потерянные объекты.

Сначала добавьте свойство навигации к ChildObject (вы можете сохранить int ParentObjectId свойство, если хотите, оно работает в любом случае):

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; }
}

Затем найдите объекты-сироты, используя 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);
    }
}

Ваша конфигурация становится:

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

Я сделал простой тест скорости итерацию 10.000 раз. С HandleOrphans() для включения потребовалось 1:01.443 мин. для завершения, для него было отключено 0:59.326 мин (для обоих - в среднем три запуска). Тестовый код ниже.

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();
}

В EF Core это можно сделать с помощью Delete Orphans.

Нравится:

dbContext.Children.Clear();

Хотите поделиться другим основным решением.net ef, которое сработало для меня, может быть, кому-то оно пригодится.

У меня была дочерняя таблица с двумя внешними ключами (либо или), поэтому принятое решение у меня не сработало. Основываясь на ответе Маркоса Димитрио, я пришел к следующему:

В моем настраиваемом DbContext:

public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken())
  {
    var modifiedEntities = this.ChangeTracker.Entries().Where(c => c.State == EntityState.Modified);
    foreach (var entityEntry in modifiedEntities)
    {
      if (entityEntry.Entity is ChildObject)
      {
         var fkProperty = entityEntry.Property(nameof(ChildObject.ParentObjectId));
         if (fkProperty.IsModified && fkProperty.CurrentValue == null && fkProperty.OriginalValue != null)
         {
           // Checked if FK was set to NULL
           entityEntry.State = EntityState.Deleted;
         }
      }
    }

    return await base.SaveChangesAsync(cancellationToken);
  }

Это мое общее решение для Entity Framework 6.4.4 без знания конкретной схемы.

Обратите внимание, что я начинаю свой поиск сиротских сущностей из измененных записей сущностей, так как в моем случае я не мог найти ничего, ищущего удаленные записи отношений, как предлагают другие ответы.

Логика, лежащая в основе подхода, заключается в том, что для сущности, удаленной из коллекции требуемых отношений, ее внешний ключ будет обновлен до нулевого значения Entity Framework. Итак, мы ищем все измененные сущности, которые имеют хотя бы одну связь с концом с множественностью «Один», но для внешнего ключа установлено значение null.

Добавьте этот метод в свой подкласс. Вы можете переопределить / методы для автоматического вызова этого метода.

      public void DeleteOrphanEntries()
{
  this.ChangeTracker.DetectChanges();

  var objectContext = ((IObjectContextAdapter)this).ObjectContext;

  var orphanEntityEntries =
    from entry in objectContext.ObjectStateManager.GetObjectStateEntries(EntityState.Modified)
    where !entry.IsRelationship
    let relationshipManager = entry.RelationshipManager
    let orphanRelatedEnds = from relatedEnd in relationshipManager.GetAllRelatedEnds().OfType<EntityReference>()
                            where relatedEnd.EntityKey == null // No foreign key...
                            let associationSet = (AssociationSet)relatedEnd.RelationshipSet
                            let associationEndMembers = from associationSetEnd in associationSet.AssociationSetEnds
                                                        where associationSetEnd.EntitySet != entry.EntitySet // ... not the end pointing to the entry
                                                        select associationSetEnd.CorrespondingAssociationEndMember
                            where associationEndMembers.Any(e => e.RelationshipMultiplicity == RelationshipMultiplicity.One) // ..but foreign key required.
                            select relatedEnd
    where orphanRelatedEnds.Any()
    select entry;

  foreach (var orphanEntityEntry in orphanEntityEntries)
  {
    orphanEntityEntry.Delete();
  }
}

Да. В EF Core работает следующее:

Убедитесь, что вы установили каскадное поведение на Cascade вот так:

      entity.HasOne(d => d.Parent)
                    .WithMany(p => p.Children)
                    .HasForeignKey(d => d.ParentId)
                    .OnDelete(DeleteBehavior.Cascade);

Затем установите Parent свойство быть нулевым во всех дочерних сущностях, которые должны быть удалены следующим образом:

      var childrenToBeRemoved = parent.Children.Where(filter);
foreach(var child in childrenToBeRemoved)
{
    child.Parent = null;
}

Сейчас, context.SaveAsync() следует удалить все осиротевшие дочерние сущности.

Это не то, что автоматически поддерживается EF прямо сейчас. Вы можете сделать это, переопределив SaveChanges в вашем контексте и вручную удалив дочерние объекты, у которых больше нет родителя. Код будет примерно таким:

public override int SaveChanges()
{
    foreach (var bar in Bars.Local.ToList())
    {
        if (bar.Foo == null)
        {
            Bars.Remove(bar);
        }
    }

    return base.SaveChanges();
}
Другие вопросы по тегам