EF 急切加载导航属性问题

4

我正在使用EF6和通用存储库模式。最近,我试图在单个步骤中删除复合实体时遇到了问题。这里是一个简化的场景:

public class Parent
{
    public int Id { get; set; }
    public string Name { get; set; }
    public virtual ICollection<Child> Children { get; set; }
}
public class Child
{
    public int Id { get; set; }
    public string Name { get; set; }

    [ForeignKey("Parent")]
    public int ParentId { get; set; }

    public virtual Parent Parent { get; set; }
}

如果要删除与相关子项的父实体,我会采用以下方法:

public virtual T GetById(int id)
{
    return this.DBSet.Find(id);
}
public virtual void Delete(T entity)
{
    DbEntityEntry entry = this.Context.Entry(entity);

    if (entry.State != EntityState.Deleted)
    {
        entry.State = EntityState.Deleted;
    }
    else
    {
        this.DBSet.Attach(entity);
        this.DBSet.Remove(entity);
    }
}

首先,我通过ID查找父对象,然后将其传递给删除方法以将其状态更改为已删除。最后,context.SaveChanges()提交了删除操作。

这个方法很好用。由于我在Children上启用了级联删除,因此find方法只会检索Parent对象,删除操作也可以正常进行。

但是,当我在Child类中添加了另一个属性时:

[ForeignKey("Gender")]
public int GenderId { get; set; }

public virtual Gender Gender { get; set; }

出于某种原因,EF在Parent.Find()方法上开始拉取相关的Children。 因此我遇到以下错误:
操作失败:无法更改关系,因为一个或多个外键属性是非空的。当对关系进行更改时,相关的外键属性将设置为null值。如果外键不支持null值,则必须定义新关系,将外键属性分配给另一个非null值,或删除不相关的对象。
即使恢复更改(删除Gender属性),问题仍然存在。我不能理解这种奇怪的行为!!
我想做的就是删除Parent对象以及Children。虽然有一些解决方案,但没有一个真正符合我的目的:
1.将LazyLoading更改为false——this.Configuration.LazyLoadingEnabled = false; 这个方法有效,但在我的实际应用程序中,我需要该属性为true。 2.先迭代所有子项并删除它们,然后再删除父项。这似乎是一个解决方法,但很冗长。 3.使用Remove()而不仅仅是将EntityState更改为Deleted。 我需要跟踪审计更改,所以EntityState在这里有帮助。
有人能解释一下为什么即使我没有使用它们,EF也会加载相关的实体吗?

是的,我做了。我删除了数据库并重新创建了它。 - Amanvir Mundra
1
你需要先加载实体吗?你可以通过创建一个新实例,设置ID,将其附加到上下文中,然后调用Delete来删除它。 - Sefe
1
撤销更改让我感到怀疑。你确定这不是由于你的调试器检查“Children”而导致加载行吗? - Rob
似乎如果没有看到调用“Delete()”的代码,我们无法真正回答为什么相关实体正在被加载。当然,因为“Delete()”本身也不知道这一点(除非变得更加复杂),这就显示了为什么应该使用“Remove()”而不是摆弄“EntityState”的原因。 - Marc L.
1
@Rob 谢谢你。我感觉自己像个白痴。原来是调试器在加载示例应用程序中的属性:/但在我的真实应用程序中问题不同。你的评论给了我一个新的方向去调查。谢谢。似乎在我的真实应用程序中,上下文在请求之间没有被销毁。有问题的实体在以前的请求中完全加载到上下文中,而删除请求似乎实际上是从缓存或其他地方拉取的。我会添加一个答案来详细说明。 - Amanvir Mundra
显示剩余5条评论
1个回答

2

看起来问题与上下文的生命周期有关。我正在使用工作单元,并使用ninject将其注入到我的服务层中。

kernel.Bind<IUnitOfWork>().To<UnitOfWork>().InRequestScope();

UnitOWork 类实现了 IDisposable 接口。
public bool DeleteView(int viewId)
    {
        // This is a workaround. It seems ninject is not disposing the context. 
        // Because of that all the info (navigation properties) of a newly created view is presisted in the context.
        // Hence you get a referential key error when you try to delete a composite object.
        using (var context = new ApplicationDbContext())
        {
            var repo = new GenericRepository<CustomView>(context);
            var view = repo.GetById(viewId);
            repo.Delete(view);
            context.SaveChanges();
        }
        //var model = _unitOfWork.CustomViews.GetById(viewId);
        //_unitOfWork.CustomViews.Delete(model);
        //_unitOfWork.Save();

        return true;
    }

注释掉的代码会抛出错误,而未注释的代码(使用块)可以正常工作。在此调用之前,控制器方法加载了CustomView实体(与Parent具有类似结构的子列表)。随后的用户操作可能会触发删除该视图。
我认为这与上下文没有被处理有关。也许这与Ninject或UnitOfWork有关,我还没有能够确定。GetById()可能从上下文缓存中拉取整个实体。
但上述变通方法对我有用。只是把它放在这里,希望能帮助到某人。

使用Autofac - 更成熟且易于理解的解决方案,用于生命周期、作用域、释放和工作单元(每个请求)。 - Matt Kocaj
换句话说,整篇文章都是错误的,与EF没有任何关系,但涉及到模式、注入等。我认为一旦赏金过期,这个问题应该被删除,因为它只会误导人,我不认为它能帮助任何人。 - Ivan Stoev

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接