延迟加载是开发人员使用 Entity Framework 遇到的第一个,可以说是最大的性能障碍。确保应用程序正常运行而不是抛出 NullReferenceExceptions 是一项很棒的功能,但如果您不考虑它,它绝对是性能杀手。
语句如下:
Lot foundLot = EntitiesContext.Lots.Where(x => x.ID == lotID).FirstOrDefault();
...看起来很无辜,它们是完全无害的实体的信息。这包括执行诸如序列化 Lot 以发送到 Web 客户端之类的操作。
默认情况下,每当 EF 加载包含 virtual 对其他实体的引用的实体时,除非您明确禁用延迟加载代理,否则 EF 将创建将跟踪这些相关实体的实体的代理。如果引用不是预先加载的并且代码尝试访问它,实体将检查它的 DbContext 是否仍然可用,并发出一个新的查询来获取被触摸的实体。
将此行为与序列化程序结合起来,从性能的角度来看,这会导致各种地狱。例如,如果一个 Lot 有一个 Parts 集合,每个 Part 都有一个 Stations 集合,我们加载 1 个 Lot,我们会得到:
SELECT * FROM Lot WHERE LotId = 1;
够无辜的。但是,如果我们的序列化程序或代码触及 Parts 集合,则会运行以下查询:
SELECT * FROM Parts WHERE LotId = 1;
现在加载了 100 个零件......不过,没有什么可真正关心的。然而,对象模型越嵌套,它很快就会变得越糟糕。在序列化程序的情况下,它将开始迭代每个部分,这涉及到 Stations 集合。现在便便开始撞击风扇:
SELECT * FROM Stations WHERE PartId = 1;
SELECT * FROM Stations WHERE PartId = 2;
SELECT * FROM Stations WHERE PartId = 3;
SELECT * FROM Stations WHERE PartId = 4;
SELECT * FROM Stations WHERE PartId = 5;
....
SELECT * FROM Stations WHERE PartId = 100;
之所以这样做(而不是JOINing Part on Station)是因为迭代涉及每个 Part 的 Station 集合。这会导致对 each 部分的 Stations 查询。如果每个部分有1-5个站。随着这些站被序列化,每个站的引用反过来会导致更多的查询,依此类推。您可以在应用程序运行时使用分析器观察 SQL 垃圾邮件的扩散。
快速解决方法是利用急切加载。这会将初始查询中的SELECT 垃圾邮件替换为JOINs。这可以使加载数据比依赖延迟加载更快。与等待 EF 组合和启动数万个查询的几分钟相比,具有各种内部和外部联接的查询可能需要几秒钟。但是,这里的考虑是您仍在从服务器加载潜在的垃圾数据。 EF 必须编写查询,将其发送到 DB 执行,DB Server 需要为所有结果分配空间,然后将这些结果通过网络传输回应用程序服务器,然后应用程序服务器必须为结果图,可能会将其序列化到客户端。随着应用程序的发展和新的关系被添加到现有实体中,这也会使地雷潜伏在您的应用程序中。一个新的关系被添加到图表的某个地方,在您知道之前,在完全未触及的区域(例如搜索)中突然出现了看似无关的性能问题,因为序列化程序现在正在拾取并加载那些未急切加载的实体。 (因为应用程序的这些区域不需要显示该信息。)
更好的解决方案是在读取要计算或序列化到客户端的数据时利用投影,并保存加载实体及其相关详细信息(如有必要)以用于更新操作等操作。使用 Select 或 Automapper 的 ProjectTo 方法的投影允许您编写填充安全、POCO 以进行序列化或匿名类型以检查和使用的查询,这将导致更远、更远、更快的查询和更小的数据负载发送过来电线。这样,查询只会加载它需要的信息,并且根据新关系对数据模型的更改永远不会污染现有的查询。那些现有的查询可以忽略新表,仅在实际需要考虑新数据时才更改。这种随时间变化的地雷效应是我提出的最大论据之一,即从不将实体发送到 Web 客户端。 (除了浪费时间/负载发送客户端不需要看到的数据,以及如果您从客户端接受实体返回潜在的安全问题。)