如果您通过“连接表”指的是具有与记录相关的外键的关系模型,那么 Entity Framework 专门设计用于处理此问题。为了获得最佳性能,您希望确保 EF 具有映射的实体之间的关系,然后在读取数据时利用 Select 进行投影,以确保您在任何给定时间只读取所需的数据。
我不建议将 Sprocs 与 EF 一起使用。它确实支持它们,但坦率地说,我发现映射关系要灵活得多。
视图可以很好地用于复杂数据模型的只读表示。它不适用于您要插入/更新数据的情况。
EF 中的显式连接也是一种选择,但应谨慎使用。映射关系并在表达式中引用这些关系很容易阅读和使用。当我看到开发人员在 LINQ 中编写连接时,他们可能只是在使用 ADO,它错过了 EF 提供的灵活性和强大功能。
我从新接触 EF 的开发人员那里看到的常见性能缺陷:
- 查询的数据过多
- 查询数据过于频繁
- 被延迟加载绊倒
- 使用
ToList 修复 EF 错误
- 应该排队的大查询
- 过度使用异步
查询太多数据:这包括像急切加载整个实体图(相关数据)和加载实体时所有代码想要做的就是获取计数、检查是否存在行等。了解IQueryable 和如何利用它来执行计数、存在检查 (Any) 和其他操作,而不是获取实体来进行这些检查。例如,当搜索结果页面等视图仅显示少数字段时,将实体作为数据容器传递给视图是一个常见问题。利用Select 填充简单的视图模型可以加快查询速度并减少资源使用。此外,不考虑潜在的大结果集可能会影响性能。搜索应始终限制返回的总行数并利用分页。只有在执行更新时才真正需要加载完整的实体图。
查询过于频繁:这包括严重依赖延迟加载来加载相关数据,以及低效的编码做法,例如循环加载单个实体。这通常会导致开发人员做一些愚蠢的事情,比如实现缓存,这会导致内存问题和陈旧数据错误。缓存应该只用于预计不会发生变化的静态数据。相反,如果您需要多个实体,请在单个查询中加载数据集。例如,当您创建一个订单时,可能有 10 个订单行引用一个产品,而不是遍历每个订单行并加载单个产品以与该行关联,执行 1 个查询以加载 10 个(或更少)不同的通过 ID 识别产品。
被延迟加载绊倒:我看到人们遇到的最大的无法解释的性能损失可能是延迟加载调用,无论是在他们不期望的代码中,还是由于序列化实体。序列化程序将触及实体上的每个属性,因此每个延迟加载属性都会在未急切加载时触发查询。在为搜索结果加载实体时,这可能会导致针对返回的每个搜索结果触发许多单独的查询。基本上,永远不要返回实体,这包括永远不要在视图模型中放置对实体的引用。
使用ToList 修复 EF 错误:当开发人员有一个查询,他们希望在其代码中使用函数或未映射的属性来进行格式化或计算时,就会发生这种情况。 EF6 将抛出表达式无法转换为 SQL 的错误,因此快速而肮脏的解决方案是在使用该函数调用 Where/Select 之前调用 ToList。这样做的结果是,它可能导致执行的查询加载所有数据,或者在应用条件之前加载比预期更多的数据。在 EF6 将引发错误的情况下,EFCore 将自动实现此部分查询。虽然它仍然可以应用部分过滤,但不能保证您的查询可以有效地对数据库运行 SELECT * FROM 类型的查询。在您进行测试运行时观察 EF 生成的 SQL,并注意性能指标。
应该排队的大查询。非常繁重的查询,例如报告和非常开放的搜索(即文本搜索等)应该考虑使用后台进程和队列来防止同时启动太多请求。需要接触大量记录的查询应该针对同步的只读副本执行,或者如果没有副本则考虑脏读,前提是这些对于正在报告的数据是实用的。涉及大量行的查询会导致并等待大量锁,此外还会导致来自数据库服务器的大量 I/O 请求。队列有助于确保其中很多不会立即启动。这些运行的越多,它们相互干扰(和其他查询)就越多,从而加剧了性能问题。
过度使用async。 EF 支持async 操作,以帮助在运行潜在的繁重查询时提高 Web 服务器等响应速度。不幸的是,许多开发人员认为这会使查询“更快”,或者它们应该保持一致并在任何地方使用异步操作。它没有,他们也不应该。异步查询名义上比同步查询慢,并且应该用于需要花费更长时间的查询,从而释放 Web 服务器以在等待时为其他请求提供服务。 async 不是性能提升,在调查性能问题时不应该是默认建议。在退出查询之前消除所有上述情况,只需要花费更长的时间。
要真正关注 EF 和性能问题,熟悉 SQL 分析会大有帮助。这将捕获 EF 执行的所有查询,您可以查找任何奇怪的额外查询或“繁重”查询。 (大量的行命中或缓慢的执行时间)即使测试看起来足够快,也应该检查具有高行“接触”的查询。如果其中一些被同时启动,或者在负载下运行而其他操作竞争锁,这些可能会在生产中出现问题。
编辑:对于订单和订单类型示例:
例如,正如您所说,我们有一个 Order 表和一个 OrderType 表,每个表都有自己的 DBSet。现在我们想加入它们并在 EF 中从中获取一个新模型,以在我们的 LINQ 表达式 (OrderJoinTable) 中引用,这怎么可能请说明您如何在 EF 中创建此模型?
我们有一个指向 Order 表的 Order 实体和一个指向 OrderType 表的 OrderType 实体。在您的示例中,您有 Order 和 OrderType 的 DbSet。我会假设 Order 表包含 OrderType 的 FK,OrderTypeId
初始实体:
public class Order
{
[Key]
public int OrderId { get; set; }
public int OrderTypeId { get; set; }
public string OrderNumber { get; set; }
/* Other order fields... */
}
public class OrderType
{
[Key]
public int OrderTypeId { get; set; }
public string Name { get; set; }
}
在 DbContext 中有两个 DbSet:
public DbSet<Order> Orders { get; set; }
public DbSet<OrderType> OrderTypes { get; set; }
根据您的问题,您想要填充类似订单列表的内容,其中显示订单类型的名称而不仅仅是它们的键,因此“加入”两者。是的,这当然可以使用 EF,但它在某种程度上破坏了使用 ORM 而不是普通的旧 ADO 的全部好处。使用 EF,我们设置了实体之间的关系(导航属性),以便我们可以自动解析此信息。回顾实体声明:
public class Order
{
[Key]
public int OrderId { get; set; }
public int OrderTypeId { get; set; }
public string OrderNumber { get; set; }
/* Other order fields... */
[ForeignKey("OrderTypeId")]
public virtual OrderType OrderType { get; set; }
}
通过设置此导航属性,EF 拥有构建查询所需的一切,该查询可在您查询订单时返回订单类型详细信息。您仍然可能有一个 OrderType DbSet,因为我们要查找订单类型以分配给查找列表并与订单相关联;但是,对于像 OrderLines 这样仅作为订单的一部分存在的东西,它们不需要 DbSet,它们只需通过导航属性通过各自的订单进行访问。
查询的样子:
使用订单类型获取订单:
var order = context.Orders.Include(x => x.OrderType).Where(x => x.OrderId == orderId).Single();
这是使用Include 的急切加载场景。它将检索单个订单,并填充它的 OrderType。从那里您可以使用 order.OrderType.Name 获取订单类型名称。急切加载是可选的。只要 OrderType 导航声明为虚拟且未在 DbContext 上禁用延迟加载,EF 就可以按需加载延迟加载属性。 OrderType 不会使用上述表达式中的订单获取,但访问订单上的.OrderType 属性将指示 EF 运行另一个查询以获取并填充该订单的订单类型。这仅在加载订单的 DbContext 尚未公开时才有效。如果严重依赖延迟加载,可能会导致性能问题。
为了获得更好的性能,您可以使用带有.Select 的投影来填充视图模型/DTO,而不是依赖于整个实体。如果我们想使用订单类型名称执行搜索列出订单,我们可以这样声明 OrderSummaryViewModel:
// Something like your OrderJoinTable?
[Serializable]
public class OrderSummaryViewModel
{
public int OrderId { get; set; }
public string OrderNumber { get; set; }
public string OrderType { get; set; }
}
然后在拉取这些记录时:
var orders = context.Orders.Select( x => new OrderSummaryViewModel
{
OrderId = x.OrderId,
OrderNumber = x.OrderNumber,
OrderType = x.OrderType.Name
}).ToList();
请注意,这不需要使用预先加载,并且会填充视图模型,因此我们也无需担心延迟加载。我们可以访问 Linq 表达式中的导航属性以根据需要进行过滤并填充我们的显示模型,而无需担心显式连接。
显式连接将保留用于我们需要将完全不相关的实体相互链接的情况,例如实体与几个可能实体之一有关系的情况。例如,如果我有一个 Address 表,它的 EntityId 键可以分别包含来自 Customer 或 Business 表的 CustomerId 或 BusinessId。然而,这种表结构效率非常低,通常只有在人们确信它“节省空间”或其他一些优化时才会存在。它将遭受没有 FK 关联的性能问题,并且在没有适当约束的情况下容易“损坏”。尽管如此,在处理具有此类内容的遗留系统时,您仍然可以利用实体之间的显式连接。
EF 可以为多对一引用映射关系,(如上)一对多子集合,(如 Order 到 OrderLines)1 对一关系(Order 到 OrderDeliveryDetails)和多对-许多关系(例如客户到地址,其中 1 个客户可以有多个地址,并且 1 个地址可能链接到多个客户,所有这些都使用 CustomerAddress 表 (CustomerId+AddressId))您可以找到有关如何映射每个场景的示例与EF一起出局。