作者 | 喵叔
责编 | 胡巍巍
出品 | CSDN(ID:CSDNnews)
这篇文章我们来讲解一下 Entity Framework Core 的优化方案。Entity Framework Core 是微软针对跨平台开发推出的 ORM 框架,继承了 Entity Framework 的众多优点,也对 Entity Framework 中的不足进行了优化和补充。
虽然 Entity Framework Core 进行了性能上的优化,但是这些在进行大量数据库操作的时候依然存在性能问题。
针对Entity Framework Core 的性能优化方案,不仅可以使用 Entity Framework 大部分的优化方案,还有一套专门针对 Entity Framework Core 的优化方案。 现在我们就来具体讲解一下针对 Entity Framework Core 的优化方案。
零、禁用实体追踪
当我们从数据库中查询出数据时,上下文就会创建实体快照,从而追踪实体。在调用 SaveChanges 时,实体有任何更改都会保存到数据库中。
但是当我们只需要查询出实体而不需要修改时(只读),实体追踪就没有任何用途了。这时我们就可以调用 AsNoTracking 获取非追踪的数据,这样可以提高查询性能。具体代码如下:
using (var db = new EFCDbContext)
{
var users = db.Users.AsNoTracking.ToList;
}
Entity Framework Core 默认使用的是快照式便跟追踪,因此我们可以通过 ChangeTracker 来关闭 DetectChanges 来提高性能。我们来看一下具体的例子:
public override int SaveChanges(bool acceptAllChangeOnSuccess)
{
ChangeTracker.DetectChanges;
foreach (var entry in ChangeTracker.Entries.Where(p=>p.State==EntityState.Added))
{
this.AddRange(entry.Entity);
}
ChangeTracker.AutoDetectChangesEnabled = false;
var result = base.SaveChanges(acceptAllChangeOnSuccess);
ChangeTracker.AutoDetectChangesEnabled = true;
return result;
}
上述代码,我们重写了 SaveChanges 方法,通过
ChangeTracker.AutoDetectChangesEnabled = false;代码关闭了变更追踪,然后调用 Entity Framework Core 的 SaveChanges 方法保存数据,最后再次调用
ChangeTracker.AutoDetectChangesEnabled = true; 来开启变更追踪。这样一来 Entity Framework Core 的最终性能得到了优化。
下面我们来思考一个问题,当需要多表关联查询的时候,我们应该怎么优化查询性能?
这时你一定会想到使用前面所说的 AsNoTracking 方法,那么我们就把你想到的这个方法以代码的形式展示出来:
using (var db = new EFCDbContext)
{
var user = from u in db.Users.AsNoTracking
join o in db.Orders.AsNoTracking
on u.Id equals o.UserId
select u;
}
看到上述代码,你第一感受是什么?每个表都要写一个 AsNoTracking 方法,很麻烦吧?代码很长吧?可读性很低吧?
那么怎么来解决呢?Entity Framework Core 给我们提供了一个很好的就觉方案,就是通过上下问设置跟踪行为为 AsNoTracking 。
using (var db = new EFCDbContext)
{
db.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
var users = db.Users;
var orders = db.Orders;
var user = from u in Users
join o in orders
on u.Id equals o.UserId
select u;
}
优化模糊查询
模糊查询在开发过程中经常用到,比如查询姓名、电话号码、邮箱等等都会用到模糊查询。
我们也知道模糊查询会造成全表扫描的问题,因此 Entity Framework Core 专门针对模糊查询进行了一番优化,出现了 EF.Functions.Like 方法。我们可以利用这个特性来自定义模糊查询。Like 方法有两个重载:
1. 自定义匹配模式
举个例子来讲解这个重载的使用方法,例如我们需要查询出姓名中包含 燕 的人员,我们可以这么写:
using (var db = new EFCDbContext)
{
var users =db.Users;
var user = users.Where(p=>EF.Functions.Like(p.Name,"%燕%")).ToList;
}
2. 将转义字符当作普通字符
当我们传递的查询参数包含转义字符时,我们可以使用这个重载来将转义字符转换为普通字符来处理:
`using (var db = new EFCDbContext)
{
var users =db.Users;
var user = users.Where(p=>EF.Functions.Like(p.Name,@"%\燕%",@"\")).ToList;
}
自定义标量函数
Entity Framework Core 有一个重要特性就是自定义标量函数。
自定义标量函数可以将数据库中的标量函数映射到类中的方法,并且在使用 LINQ 查询时会用到。
自定义标量函数为我们提供了一个快捷创建方法,并在方法上应用 DbFunctionAttribute 属性。
DbFunctionAttribute 属性可以将静态方法映射到数据库函数。
默认情况下数据库函数中的静态方法名必须相同,我们也可以通过 DbFunctionAttribute 属性来指定不同的名称。在创建自定义标量函数的时候我们必须遵循如下两个要求:
1. 函数必须是静态方法,而且在上下文中声明;
2. 只能作为参数标量值返回。
下面我们来看一下如何在上下文中定义标量函数:
[DbFunction(FunctionName="DbFunction",Schema="dbo")]
public static string MyFunction(string name)
{
//more code
}
上述代码中我们定义了 MyFunction 方法映射进数据库中的标量函数名称 DbFunction,并且也定义了使用数据库的架构名称 dbo。
如果自定义标量函数方法没有在上下文中定义,我们还可以在 OnCinfiguring 方法中利用 HasDbFunction 方法通过反射获取自定义标量函数。我们将上面的代码改造一下来看看:
modelBuilder.HasDbFunction(this.GetType.GetMeth("MyFunction"),options=>
{
options.HasName("DbFunction");
options.HasSchema("dbo");
})
注意:函数使用数据库架构的名称是必须存在的,否则将会抛出异常。
讲了这么多我们来看一下自定义标量函数到底该怎么优化性能。我们都知道 Entity Framework Core 不支持将 LINQ 中的 Min、Max、Average 等函数翻译成SQL查询,只会在本地执行查询,我们可以通过自定义标量函数来使 Min、Max、Average 等函数在远端执行查询。下面我们通过例子来看一下:
首先定义自定义标量函数:
public static class FunctionDemoClass
{
public static void DemoAverage(this EFCDbContext db)
{
using(var transaction = db.Database.BeginTransaction)
{
try
{
db.Database.ExecuteSqlCommand("IF OBJECT ID ('dbo.DemoAverage',N'FN') IS NOT DROP FUNCTION db0.DemoAverage");
db.Database.ExecuteSqlCommand("CREATE FUNCTION DemoAverage (@UserId int) RETURNS FLOAT "+
@"AS BEGIN
DECLARE @result AS FLOAT
SELECT @result AVG(CAST(ScoreAvg AS FLOAT)) FROM db.Score as s WHERE s.UserId=@UserId
RETURN @result END");
transaction.Commit;
}
catch(Exception ex)
{
throw ex;
}
}
}
}
接着我们在上下文中进行映射:
`public static float? DemoAverage(int userId)
{
// more code
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasDbFunction(=>DemoAverage(default(int).HasSchema(dbo)));
base.OnModelCreating(modelBuilder);
}
通过这种方法我们将计算平均值的工作交给了数据库,这样就可以提高 Entity Framework Core 的查询速度。
显式编译查询
显式编译查询也是 Entity Framework Core 的重要特性,主要用在提供高可用的场景下。
默认情况下 Entity Framework Core 使用查询表达式的散列来表示自动编译和缓存查询,如果代码需要重用 Entity Framework Core 将会使用用哈希查找从缓存中返回已编译的查询。
但是散列计算和高速缓存查找也会带来性能问题,这时我们就需要抛弃散列计算和高速缓存查找。
Entity Framework Core 已经为我们想到了这一点,我们只需要调用 Entity Framework Core 静态类中使用以下方法即可:
- EF.CompileQuery
- EF.CompileAsyncQuery
上述方法的第一个参数必须是上下文,第二个参数类型不限。小提示:上述两个方法的第二个参数数量一共有8个。
我们依然通过例子来讲解一下:
using (var _db = new EFCDbContext)
{
var query = EF.CompileQuery(
(EFCDbContext db ,int id)=>db.Users.FirstOrDefault(p=>p.Id==id)
);
User user1 = query(_db,123);
User user2 = query(_db,123);
}
上述代码知识展示了显示编译查询的使用方法,这段代码并不能真实的反映出显示编译查询的优点。
当我们同时进行上万次查询的时候就会发现 Entity Framework Core 的性能有显著提升。因此当查询频繁访问时,我们就可以使用显示编译查询来提高查询性能。小提示:显示编译查询的执行速度是常规模式的2倍。
上下文实例池
Entity Framework Core 增加了上下文连接池的概念,可以在依赖注入中注册 DbContext 来创建一个可重用的 DbContext 实例池。
也就是说每次请求都会从实例池中提取一个实例,而不是重新创建一个实例,这样有利于程序性能的提高。默认情况下上下文实例池有128个实例,但是我们可以通过配置来改变默认值。
我们可以使用 AddDbContextPool 方法在依赖注入中注册 DbContext 类。方法接收 DbContextOptionBuilder 用于定义链接字符串,第二个参数时实例池最大的实例数值。我们需要在 Startup 类的 ConfigureServices 方法中定义。
代码如下:
public void ConfigureServices(IServiceCollection services)
{
var conStr="连接字符串";
services.AddDbContextPool<EFCDbContext>(options=>{
options.UseSqlServer(conStr,p=>p.MigrationAssembly(this.GetType.GetTypeInfo.Assembly.FullName));
},1000);
}
上述代码中我们定义的上下文实力数量最大时1000,当请求数量超过1000时,Entity Framework Core 将会为后续的请求创建新的上下文实例,而不是从实例池中获取。
因此设置最大实力数量只是限制了 Entity Framework Core 实例池中实例的数量,而不是限制上下文实例的总数。
总结
上述几方面就是针对 Entity Framework Core 的性能优化,我不建议使用上下文实例池的方式,就如同我结尾所说的那样最大实力数量只不过是限制了实力池中实例的数量,一旦请求超过实力池中最大的实力数量,将会创建新的实例,而不是排队等候。
作者简介:朱钢,笔名喵叔,CSDN博客专家,.NET高级开发工程师,7年一线开发经验,参与过电子政务系统和AI客服系统的开发,以及互联网招聘网站的架构设计,目前就职于北京恒创融慧科技发展有限公司,从事企业级安全监控系统的开发。
【END】