Вопрос по entity-framework, c# – Может ли EF автоматически удалять потерянные данные, если родитель не удален?

14

Для приложения, использующего 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

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

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.

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

Могу ли я настроить объект так, чтобы он «очищался»? когда он осиротел или я должен вручную удалить этиChildObjects из контекста (в этом случае используя ChildObjectRepository).

К счастью, команда EFknows about this и, скорее всего, предложит встроенное решение, которое не требует изменения внутренних PinnyM

Ваш Ответ

3   ответа
-1

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

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

    return base.SaveChanges();
}
Error: User Rate Limit Exceeded
Error: User Rate Limit Exceeded
30

Это на самом деле поддерживается, но только когда вы используетеИдентифицирующая связь, Сначала он работает с кодом. Вам просто нужно определить сложный ключ для вашего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();
Error: User Rate Limit Exceeded StuperUser
Error: User Rate Limit Exceeded
Error: User Rate Limit Exceeded
Error: User Rate Limit Exceeded
Error: User Rate Limit Exceeded
4

Update:

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

Это основано на этомстатья который использует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; }
}

Original Answer

Чтобы решить эту проблему без настройки сложного ключа, вы можете переопределить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();
}

Похожие вопросы