实体框架急切加载会加载所有内容

4
我们在基于Web的应用程序中使用Entity Framework +存储库模式来获取数据库。由于业务复杂,我们的模型有时会变得非常复杂,这会导致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。我还尝试使用表达式来指定包含列表,而不是查询路径,但结果没有改变。

1
你有检查过EF在SQL上运行的查询吗?使用SQL Profiler来监视问题所在。每当EF出现任何奇怪的行为时,我总是这样做。 - mesut
1
为什么你在多个实体中都有PencilCasesPencils?你应该通过箱子->笔盒->铅笔的方式获取例如桌子上的铅笔。你的模型有许多冗余的外键。这可能会导致你得到的行为,因为EF正在通过关系修复在子实体中建立所有这些关联。 - Gert Arnold
1
@GertArnold,我们有多个实体的铅笔盒和铅笔,因为桌子上的铅笔与盒子里的铅笔不同,而且与在盒子中的PencilCase中的铅笔也不同。我知道这很复杂,但我们的真实模型就是这样。 - bahadir arslan
1
Boxes.Pencils 中的铅笔也会在其他 Pencils 集合中找到。这并不一定意味着其他集合已经完全加载,但 EF 会尽可能建立关联。如果数据量很大,可能会导致 OOME。但你应该检查执行的查询语句。也许某个地方触发了延迟加载。 - Gert Arnold
我同意Gert的建议,尝试关闭懒加载或者移除所有虚拟关键字,这样EF就无法懒加载额外的内容。所以在你的上下文构造函数中,加入以下代码:this.Configuration.LazyLoadingEnabled = false; - Casey Sebben
我同意Gert Arnold的观点,即“在更深的地方”您需要铅笔,并且这些将自动修复在更高的Table集合Pencils中。我认为这不是什么大问题,尽管您可能会遇到OOME。我不知道您的系统、EF等的内存大小。您可以考虑使用Skip和Take进行分页。 - Youp Bernoulli
1个回答

1
首先,我自己对EF比较新,如果以下内容不是100%准确,请原谅。然而,我几天前刚好处理了这个完全相同的问题,希望这会有所帮助。
问题在于当EF加载特定实体时,它将该实体添加到数据模型中的每个部分 - 而不仅仅是显式加载的部分。
这意味着,即使您没有明确请求,Boxes.Pencils中的每个Pencil也将被自动解析为Table.Pencils的ICollection中的每个Pencil
单独看,这个事实并不会产生问题,甚至可以在用户驱动的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; }
}

现在,当你将数据实体映射到这组商业实体时,就不会再出现错误了。
对于映射方面,有两种解决方案:手动操作/使用映射工厂模型工厂示例ValueInjecterAutoMapper——后两者都是可用的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 Injections以及一个使用ValueInjecter的ORM指南

我还为自己的项目制作了一些注入,可能对你有用,你可以在我的Github上找到它们。

使用MaxDepthCloneInjector,你可以提供一个字典(属性名称、最大递归深度),它只会映射字典中包含的值,并且仅限于指定的层级。
另外两个建议:
  • 如果您想在查询方面拥有更多自由度,可以考虑使用 查询表达式语法来满足您更复杂的需求。此外,在SO上的这个答案中还有一些很好的信息:如何使用Include限制相关数据的数量
  • 如果您计划运行包含导航属性的查询(例如您示例中的查询):请坚持使用Eager Loading。在Lazy Loading中进行此类查询会导致N + 1问题。作为经验法则:
    • 如果您不需要立即获得整个结果集,则使用Lazy Loading,例如,如果您正在开发一个应用程序,数据需求会根据用户与应用程序的交互而自然扩展。
    • 如果您需要立即获得整个结果集,则使用Eager Loading,例如在Web Api或需要完整实体的应用程序中。

祝好运,Felix


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