Подтвердить что ты не робот

Может ли 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();

Итак, если a ParentObject удален, его объекты ChildObject тоже.

Однако, если я запустил:

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

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

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

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

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

4b9b3361

Ответ 1

Фактически поддерживается, но только при использовании Идентификация отношения. Он также работает с кодом. Вам просто нужно определить сложный ключ для 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();

Ответ 2

Обновить:

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

Он основан на этой статье, которая использует 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 a plain class, sometimes it 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 объекты, используя 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();
}

Ответ 3

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

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

В моем пользовательском 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);
  }

Ответ 4

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

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

    return base.SaveChanges();
}