.NET Core 之EFCore 查询性能优化及要注意的坑
微软官方在EFCore2.x开始,推荐使用DbContextPool以提高应用的性能。
Azure上使用的是SQL Server Basic Edition
Azure SQL的使用限制文档:
一句话:付费级别和计算资源大小决定了Azure SQL最大会话数/请求数。
若要缓解,要么升级硬件资源,要么优化查询利用率。
本次使用EFCore操作SQL Server的方式, 是官方默认用法:
依赖注入框架注册一个自定义的 DbContext类型
在Controller构造函数中获取 DbContext实例
这意味着每次请求都会创建一个 DbContext实例, 可以想象到
① 在高并发请求下,连接数不断累积,最终某时刻会超过 Azure 的连接限制数量。
② 频繁创建和销毁 DbContext 实例,影响App Service自身性能。
EFCore2.0 为DbContext引入新的注册方式:透明地注册了 DbContext实例池:
services.AddDbContextPool<CarModelContext>(options => options.UseSqlServer(Configuration.GetConnectionString("SQL")));
- 一如既往支持lambda方式注册连接字符串
- 默认的连接池数量为 128
- 每次使用完DbContext不会释放对象,而是重置并回收到DBContextPool
Web程序中通过重用池中DbContext实例可提高高并发场景下的吞吐量, 这在概念上类似于ADO.NET Provider原生的连接池操作方式,具有节省DbContext实例化成本的优点, 这也是EFCore2.0 其中一个性能亮点。
高性能-DbContext 池
在 ASP.NET Core 应用程序中使用 EF Core 的基本模式通常包括将自定义 DbContext 类型注册到依赖注入系统中,然后通过控制器中的构造函数参数获取该类型的实例。这意味着为每个请求创建一个新的 DbContext 实例。
在 2.0 版本中,我们引入了一种在依赖注入中注册自定义 DbContext 类型的新方法,它透明地引入了可重用的 DbContext 实例池。要使用 DbContext 池,请在服务注册期间使用AddDbContextPool而不是:AddDbContext
services.AddDbContextPool<BloggingContext>(options => options.UseSqlServer(connectionString));
如果使用此方法,则在控制器请求 DbContext 实例时,我们将首先检查池中是否有可用的实例。一旦请求处理完成,实例上的任何状态都会被重置,并且实例本身会返回到池中。
这在概念上类似于连接池在 ADO.NET 提供程序中的操作方式,并且具有节省一些 DbContext 实例初始化成本的优势。
高性能-限制
OnConfiguring()新方法在 DbContext的方法中引入了一些限制。
如果您在派生的 DbContext 类中维护自己的状态(例如,私有字段),不应在请求之间共享,请避免使用 DbContext 池。EF Core 只会在将 DbContext 实例添加到池之前重置它所知道的状态。
显式编译的查询
这是第二个选择加入的性能功能,旨在为大规模场景提供优势。
手动或显式编译的查询 API 在早期版本的 EF 和 LINQ to SQL 中都可用,以允许应用程序缓存查询的转换,以便它们可以只计算一次并执行多次。
虽然通常 EF Core 可以根据查询表达式的散列表示自动编译和缓存查询,但这种机制可以通过绕过散列计算和缓存查找来获得小的性能提升,允许应用程序使用已经通过调用委托编译查询。
// Create an explicitly compiled query
private static Func<CustomerContext, int, Customer> _customerById =
EF.CompileQuery((CustomerContext db, int id) =>
db.Customers
.Include(c => c.Address)
.Single(c => c.Id == id));
// Use the compiled query by invoking it
using (var db = new CustomerContext())
{
var customer = _customerById(db, 147);
}
更改跟踪
tach 可以跟踪新实体和现有实体的图表
EF Core 支持通过多种机制自动生成键值。使用此功能时,如果键属性是 CLR 默认值(通常为零或空),则会生成一个值。这意味着可以将实体图传递给DbContext.AttachorDbSet.Attach
并且 EF Core
会将那些已设置键的实体标记为,Unchanged而那些没有键集的实体将标记为Added。这使得在使用生成的密钥时附加混合的新实体和现有实体的图表变得容易。DbContext.Update
并DbSet.Update
以相同的方式工作,除了带有键集的实体被标记为Modified
而不是Unchanged
.
改进的 LINQ 翻译
使更多查询能够成功执行,在数据库中评估更多逻辑(而不是在内存中),并且从数据库中不必要地检索更少的数据。
GroupJoin 改进
这项工作改进了为组连接生成的 SQL。组连接通常是对可选导航属性的子查询的结果
FromSql 和 ExecuteSqlCommand 中的字符串插值
C# 6 引入了字符串插值,该功能允许将 C# 表达式直接嵌入到字符串文字中,从而提供了一种在运行时构建字符串的好方法。在 EF Core 2.0 中,我们为接受原始 SQL 字符串的两个主要 API 添加了对插值字符串的特殊支持:FromSql和ExecuteSqlCommand. 这种新的支持允许以“安全”的方式使用 C# 字符串插值。也就是说,以一种防止在运行时动态构造 SQL 时可能发生的常见 SQL 注入错误的方式。
var city = "London";
var contactTitle = "Sales Representative";
using (var context = CreateContext())
{
context.Set<Customer>()
.FromSql($@"
SELECT *
FROM ""Customers""
WHERE ""City"" = {city} AND
""ContactTitle"" = {contactTitle}")
.ToArray();
}
在此示例中,SQL 格式字符串中嵌入了两个变量。EF Core 将生成以下 SQL:
@p0='London' (Size = 4000)
@p1='Sales Representative' (Size = 4000)
SELECT *
FROM ""Customers""
WHERE ""City"" = @p0
AND ""ContactTitle"" = @p1
EF.Functions.Like()
我们添加了 EF.Functions 属性,EF Core 或提供程序可以使用它来定义映射到数据库函数或运算符的方法,以便可以在 LINQ 查询中调用这些方法。这种方法的第一个例子是 Like():
var aCustomers =
from c in context.Customers
where EF.Functions.Like(c.Name, "a%")
select c;
Like() 带有一个内存实现,这在处理内存数据库或需要在客户端评估谓词时非常方便。
参考:
https://stackoverflow.com/questions/48443567/adddbcontext-or-adddbcontextpool
https://docs.microsoft.com/en-us/ef/core/what-is-new/ef-core-2.0#dbcontext-pooling