实体框架渴望加载加载一切

时间:2014-05-14 08:25:21

标签: entity-framework repository-pattern entity-framework-6 eager-loading

我们在基于Web的应用程序中使用Entity Framework + Repository Pattern来获取数据库。由于我们的业务复杂,我们的模型有时会变得复杂,这会导致Entity Framework急切加载系统出现奇怪的行为。

请想象我们这样的真实模特。我们有桌子,桌子上的盒子,可放在桌子上或盒子里的铅笔盒以及可放在桌子上,盒子里或铅笔盒里的铅笔。 我们在这样的应用程序中对此进行了建模。

public class Table
{
    public int TableID{ get; set; }
    public virtual ICollection<Box> Boxes{ get; set; }
    public virtual ICollection<PencilCases> PencilCases{ get; set; }
    public virtual ICollection<Pencils> Pencils{ get; set; }
}

public class Box
{
    public int BoxID{ get; set; }
    public int TableID{ get; set; }
    [ForeignKey("TableID")]
    public virtual Table Table{ get; set; }
    public virtual ICollection<PencilCases> PencilCases{ get; set; }
    public virtual ICollection<Pencils> Pencils{ get; set; }
}

public class PencilCases
{
    public int PencilCaseID{ get; set; }
    public int? BoxID{ get; set; }
    public int TableID{ get; set; }
    [ForeignKey("TableID")]
    public virtual Table Table{ get; set; }
    [ForeignKey("BoxID")]
    public virtual Box Box{ get; set; }
    public virtual ICollection<Pencils> Pencils{ get; set; }
}

public class Pencils
{
    public int PencilID{ get; set; }
    public int? PencilCaseID{ get; set; }
    public int? BoxID{ get; set; }
    public int TableID{ get; set; }
    [ForeignKey("TableID")]
    public virtual Table Table{ get; set; }
    [ForeignKey("BoxID")]
    public virtual Box Box{ get; set; }
    [ForeignKey("PencilCaseID")]
    public virtual PencilCase PancelCase{ get; set; }
}

我们的存储库模式实现与本教程http://www.asp.net/mvc/tutorials/getting-started-with-ef-5-using-mvc-4/implementing-the-repository-and-unit-of-work-patterns-in-an-asp-net-mvc-application类似 所以我们这样调用get方法。

var tables = unitOfWork.TableRepository.Get(includeProperties: "Boxes, PencilCases, Boxes.Pencils");

所以问题是结果与我的期望大不相同;我希望只提取BoxesPencilCasesBoxes.Pencils个集合,但是从数据库中提取的所有Pencil实体都包括PencilsPencilCases.PencilsBoxes.PencilCases.Pencils。这种递归提取会导致OutOfMemoryException,因为数据量很大。

我无法理解为什么Entity Framework会获取除Boxes.Pencils之外的所有Pencils。我还尝试使用Expression而不是Query Path指定包含列表,但结果没有改变。

1 个答案:

答案 0 :(得分:1)

首先关闭 - 我自己对EF很新,所以如果以下内容不是100%准确,请原谅。但是,我几天前就处理过这个完全相同的问题,所以希望这会有所帮助。

问题在于,当EF加载特定实体时,它会将该实体添加到它出现的数据模型的每个部分 - 而不仅仅是显式加载的部分。

这意味着PencilBoxes.Pencils的{​​{1}}中的每个Table.Pencils都会自动解析,即使您没有明确要求它

这个事实本身并不存在问题,甚至可以在用户驱动的MVC应用程序中提供帮助。

当一切都出错时,当您尝试通过数据实体进行任何处理时,例如尝试将自我递归数据实体映射到业务模型尝试将自递归数据实体转换为JSON / XML

现在,有几种解决方案可以解决这个问题:

实现一个映射器/编码器,用于散列/记住每个对象,只添加一次:

这个问题是它可能导致一些难以预测的结果,特别是当你想要/需要多个地方的对象时。此外,散列和比较每个对象可能会很昂贵。

实现可配置为忽略某些属性的映射器/编码器

相对简单 - 如果您可以指定您根本不想映射或编码Pencil,则您不会遇到任何问题。当然,如果你不警惕指定被忽略的属性,那么你可能仍会遇到堆栈溢出。

实现具有可指定递归深度的映射器/编码器

这是一个非常简单且相当不错的解决方案 - 只需在全局或每个类型的基础上设置递归深度的硬限制,并且您不会再有任何堆栈溢出。缺点是你仍然会得到你不想要的元素,从而得到一个不必要的臃肿的回归对象。

实施自定义业务实体

这可能是最好的解决方案 - 只需创建一个新的业务实体,并删除违规的导航属性。主要缺点是它需要您为不同目的创建不同的业务实体。

这是一个例子:

// Removed Pencils
public class BusinessTable
{
    public int TableID{ get; set; }
    public IEnumerable<Box> Boxes{ get; set; }
    public IEnumerable<PencilCases> PencilCases{ get; set; }
}

// Removed Table & PencilCases
public class BusinessBox
{
    public int BoxID{ get; set; }
    public int TableID{ get; set; }
    public IEnumerable<Pencils> Pencils{ get; set; }
}

// Removed Table & Box & Pencils
public class BusinessPencilCases
{
    public int PencilCaseID{ get; set; }
    public int? BoxID{ get; set; }
    public int TableID{ get; set; }
}

// Removed Table, Box, PencilCase
public class BusinessPencils
{
    public int PencilID{ get; set; }
    public int? PencilCaseID{ get; set; }
    public int? BoxID{ get; set; }
    public int TableID{ get; set; }
}

现在,当您将数据实体映射到这组业务实体时,您将不会再遇到任何错误。

对于映射方面,有两个解决方案:手动执行/使用映射工厂Example of Model FactoryValueInjecterAutoMapper - 后两个是可用的NuGet包。

对于AutoMapper: 我没有使用AutoMapper,但您必须创建一个如下所示的配置文件:

Mapper.CreateMap<Table, BusinessTable>();
Mapper.CreateMap<Box, BusinessBox>();
Mapper.CreateMap<PencilCases, BusinessPencilCases>();
Mapper.CreateMap<Pencils, BusinessPencils>();

然后在你的查询中:

var tables = unitOfWork.TableRepository.Get(includeProperties: "Boxes, PencilCases, Boxes.Pencils");
var result = Mapper.Map<IEnumerable<Table>, IEnumerable<BusinessTable>>(tables);

或者

var tables = unitOfWork.TableRepository.Get(includeProperties: "Boxes, PencilCases, Boxes.Pencils").Project().To<IEnumerable<BusinessTable>;

有关AutoMapper的更多信息(如何设置配置文件):https://github.com/AutoMapper/AutoMapper/wiki/Getting-started

ValueInjecter

var tables = unitOfWork.TableRepository.Get(includeProperties: "Boxes, PencilCases, Boxes.Pencils");
var result = new List<BusinessTable>().InjectFrom(tables);

或者:

var tables = unitOfWork.TableRepository.Get(includeProperties: "Boxes, PencilCases, Boxes.Pencils");
var result = tables.Select(x => new BusinessTable.InjectFrom(x).Cast<BusinessTable>());

查看其他ValueInjecter注入也是值得的,例如SmartConventionInjectionDeep CloningUseful InjectionsORM with ValueInjecter指南。

我也为我自己的项目做了几次注射,你可以找到它On my Github

例如,使用MaxDepthCloneInjector,您可以提供(属性名称,最大递归深度)的字典,它只会映射字典中包含的值,并且只会映射到指定的级别。

另外两条建议:

  • 如果您希望查询更加自由,则应考虑使用Query Expression Syntax来满足一些更复杂的需求。在这个答案中,还有一些很好的信息:How to limit number of related data with Include
  • 如果您计划运行包含示例中的导航属性的查询: STICK WITH EAGER LOADING 。像Lazy Loading中的查询会导致N + 1问题。根据经验:
    • 如果您不需要立即使用整个结果集,请使用延迟加载,例如,如果您正在开发基于用户与应用程序交互的数据要求自然扩展的应用程序。
    • 如果您需要立即使用整个结果集,请使用Eager Loading,例如在Web Api或需要使用整个实体的应用程序中。

祝你好运, 菲利克斯