C# 实体框架 - 优化/最佳实践
Posted
技术标签:
【中文标题】C# 实体框架 - 优化/最佳实践【英文标题】:C# Entity Framework - Optimization/Best Practices 【发布时间】:2021-04-30 13:34:19 【问题描述】:我有一个具有以下结构的实体框架数据库:
“Lots”是零件的命名集合
“部件”是单独的部件
“站”是零件的结果
“工具”是站的结果
“测量”是工具的结果
对于一个给定的批次,大约。 53,000 个零件,在我的测试用例中有 6 个关联行,总行数为 318,001(很多 + 1)
我的问题是如何通过这种类型的数据关联来提高 Entity Framework 的性能。
删除批次(使用 mysql 的 CASCADE 删除)大约需要。 12 秒。 提取批次数据大约需要。 2分钟
我正在使用以下内容来提取批次信息:
this.Context.Database.CommandTimeout = 2700;
Lot foundLot = EntitiesContext.Lots.Where(x => x.ID == lotID).FirstOrDefault();
this.Context.Database.CommandTimeout = 180;
我正在使用 Entity Framework Code First 中的迁移
Database.SetInitializer(new MigrateDatabaseToLatestVersion<GenInspContext, Migrations.Configuration>());
当我创建表时,我将 cascadeDelete 设置为 true
.ForeignKey("dbo.Lots", t => t.LotID, cascadeDelete: true)
比起删除数据的 12 秒,我更关心提取数据所需的时间。
【问题讨论】:
这是什么意思? “站”是零件的结果 【参考方案1】:延迟加载是开发人员使用 Entity Framework 遇到的第一个,可以说是最大的性能障碍。确保应用程序正常运行而不是抛出 NullReferenceException
s 是一项很棒的功能,但如果您不考虑它,它绝对是性能杀手。
语句如下:
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;
之所以这样做(而不是JOIN
ing Part on Station)是因为迭代涉及每个 Part 的 Station 集合。这会导致对 each 部分的 Stations 查询。如果每个部分有1-5个站。随着这些站被序列化,每个站的引用反过来会导致更多的查询,依此类推。您可以在应用程序运行时使用分析器观察 SQL 垃圾邮件的扩散。
快速解决方法是利用急切加载。这会将初始查询中的SELECT
垃圾邮件替换为JOIN
s。这可以使加载数据比依赖延迟加载更快。与等待 EF 组合和启动数万个查询的几分钟相比,具有各种内部和外部联接的查询可能需要几秒钟。但是,这里的考虑是您仍在从服务器加载潜在的垃圾数据。 EF 必须编写查询,将其发送到 DB 执行,DB Server 需要为所有结果分配空间,然后将这些结果通过网络传输回应用程序服务器,然后应用程序服务器必须为结果图,可能会将其序列化到客户端。随着应用程序的发展和新的关系被添加到现有实体中,这也会使地雷潜伏在您的应用程序中。一个新的关系被添加到图表的某个地方,在您知道之前,在完全未触及的区域(例如搜索)中突然出现了看似无关的性能问题,因为序列化程序现在正在拾取并加载那些未急切加载的实体。 (因为应用程序的这些区域不需要显示该信息。)
更好的解决方案是在读取要计算或序列化到客户端的数据时利用投影,并保存加载实体及其相关详细信息(如有必要)以用于更新操作等操作。使用 Select
或 Automapper 的 ProjectTo
方法的投影允许您编写填充安全、POCO 以进行序列化或匿名类型以检查和使用的查询,这将导致更远、更远、更快的查询和更小的数据负载发送过来电线。这样,查询只会加载它需要的信息,并且根据新关系对数据模型的更改永远不会污染现有的查询。那些现有的查询可以忽略新表,仅在实际需要考虑新数据时才更改。这种随时间变化的地雷效应是我提出的最大论据之一,即从不将实体发送到 Web 客户端。 (除了浪费时间/负载发送客户端不需要看到的数据,以及如果您从客户端接受实体返回潜在的安全问题。)
【讨论】:
最初我为此使用 JOIN 语句,但结果却比这种方式慢。我不确定为什么。我实际上已经下载了一个 nuget 包,我相信它被称为“ZExpressions”,它可以转换 Join 语句并为 EntityFramework 进一步优化它。您是否有推荐的方法来对这些表进行连接,您认为这比尝试仅在选择查询中选择批次信息要快?但是,您对它执行每个单独的选择是正确的。我注意到在为正在运行的进程列表拉取 MySQL 工作台时。 我们从不更新很多信息,一旦运行了一个批次,它就保持不变。所以我不需要专注于能够存储连接。 准备好的声明对此有帮助吗?如果我使用数据库迁移创建了一个准备好的语句,然后稍后调用它,这对速度有好处吗? 这个问题的答案将取决于需要如何使用数据。一次从相关表中提取数十万行是非常重要的。问题是“为什么”需要加载数据以及是否可以更有选择地或增量地完成。如果不需要编辑数据,那么拉取它 /wAsNoTracking
也是有益的,因为 DbContext 不会保留对检索到的预期数据更改的实体的引用。
另一个常见的陷阱是当您拥有相关数据并希望对相关集进行选择性处理时。例如想要加载一个批次,并且只加载符合特定标准的零件。 EF 可以支持一些基本的全局过滤,例如软删除 IsActive 或租赁,但除此之外,Lot.Parts 将始终获取该批次的 all 部分。在这些情况下,您可能希望使用 Select
来检索 Lot,忽略 Lot.Parts,然后选择 FilteredParts = x.Parts.Where(p => ...)
之类的东西作为所选匿名类型的一部分。以上是关于C# 实体框架 - 优化/最佳实践的主要内容,如果未能解决你的问题,请参考以下文章