如何解决 Entity Framework 性能差的难题?
作者 | 羽生结弦
责编 | 胡雪蕊
出品 | CSDN (ID: CSDNnews)
Entity Framework 是 .NET 中快速生成/操作数据库ORM框架。无论你是刚入行 .NET ,还是已经是从事开发 .NET 开发多年的 “老人儿”,都或多或少听到过这么一句话:Entity Framework 性能很差,操作大量数据会很慢甚至超时。那么,事实真的是这样吗?答案是否定的,如果真的这样的话,我们可想而知微软这么大个公司,推出这样的产品,岂不是在啪啪打脸。好了,废话不多说,下面来讲解一下 Entity Framework 的优化方案,方案偏多请各位耐心阅读。
我们可以利用 Entity Framework Code First 内置的自动功能帮我们初始化/运行数据库,并且在可能导致失败时提醒我们。这个逻辑只是针对每个上下文类,并且只会发生一次开销很小,因此关闭以下功能没有很大的作用。
public class EfConfiguration : DbConfiguration
{
public EfConfiguration()
{
SetDatabaseInitialiser<EfContext>(null);
}
}
public class EfConfiguration : DbConfiguration
{
public Poliey polley;
public EfContext()
{
SetDatabaseInitialiser<EfContext>(new NullDatabaseInitializer<EfContext>());
}
}
public class ManifestTokenResolver : IManifestTokenResolver
{
private readonly IManifestTokenResolver defaultResolver = new DefaultManifestTokenResolver();
public string ResolverManifestToken(DbConnection con)
{
if (con is SqlConnectipon sqlCon)
{
return "2008";
}
else
{
return defaultResolver.ResolverManifestToken(con);
}
}
}
public class EfConfiguration : DbConfiguration
{
public Poliey polley;
public EfContext()
{
SetManifestTokenResolver(new ManifestTokenResolver());
}
}
这种方式开放度比较高,我们可以随心所欲的序列化视图。生成视图的 API 位System.Data.Entity.Core.Mapping.StorageMappingItemCollection类中,我们可以使用ObjectContext中的MetadataWorkspace来检索上下文的 StorageMappingItemCollection。我们只需在项目全局文件的Application_Start方法中放入如下代码,保证每次启动应用程序时都预先编译生成映射视图:
using(var ef = new EfContext())
{
var objectContext =((IObjectContextAdapter)ef).ObjectContext;
var mappCollection = (StorageMappingItemCollection)objectContext.MetadataWorkspace.GetItemCollection(DataSpace.CSSpace);
}
using(var ef = EfContext)
{
InteractiveViews.SetViewCacheFactory(ef,new FileViewCacheFactory(@"D:\MyProject\Ef\EFMappingViews.xml"))
var customer = ef.Customers.AsNoTracking().ToList();
}
using(var ef = new EfContext)
{
InteractiveViews.SetViewCacheFactory(ef,SqlServerViewCacheFactory(ef.Database.Connection.ConnectionString));
}
public class EfConfiguration:DbConfiguration
{
public EfConfiguration()
{
SetModelStore(new DefaultDbModelStore(Directory.GetCurrentDireCtory()));
}
}
ngen 安装 Entity Framework
在C:\Windows\Microsoft.NET\Framework\v4.0.30319文件夹下存在大量的dll文件,这些文件是.NET为托管应用程序和库生成的本机映像,通过这些映像程序可以快速启动,并且占用内存很小。那么我们可以在程序运行前,将托管代码翻译成本机映像,来减轻编译器在应用程序运行时生成本机指令的成本。
%WINDIR%\Microsoft.NET\Framework\v4.0.30319\ngen install EntityFramework.dll
%WINDIR%\Microsoft.NET\Framework64\v4.0.30319\ngen install EntityFramework.dll
%WINDIR%\Microsoft.NET\Framework\v4.0.30319\ngen install EntityFramework.SqlServer.dll
%WINDIR%\Microsoft.NET\Framework64\v4.0.30319\ngen install EntityFramework.SqlServer.dll
Entity Framework 通过快照式变更追踪来讲数据持久化到数据库中。将数据持久化数据库中这一过程会不可避免的消耗性能。但是我们在进行数据查询时并不需要快照式变更追踪,这时我们可以利用AsNoTracking方法告诉 Entity Framework ,代码如下:
csharp
using(var ef = new EfContext())
{
var user = ef.Users.AsNoTracking().FirstOrDefault(p=>p.Id=1);
}
缓存可以加快我们查询数据的速度,提高应用程序的性能。在 Entity Framework 中已经实现了实体缓存和查询翻译缓存。下面我们来具体看一下:
using(var ef = new EfContext())
{
var user1 = ef.Users.Find(123);
var user2 = ef.Users.Find(123);
}
2. 查询翻译缓存
我们如果将 Entity Framework 查询转换成 SQL 语句需要进行如下两个步骤:
(1)首先将 LINQ 表达式转换为数据库表达式树;
(2)然后将数据库表达式树转换为SQL语句。
这个过程是十分耗时的,因此 Entity Framework 将查询缓存在了MemoryCache中。在处理 LINQ 查询前,Entity Framework 会通过计算缓存密钥,来查找翻译缓存,如果被找到就会重复翻译,如果未找到就会将查询进行翻译并缓存。这里有一点需要注意,在开发过程中我们必须禁止将查询参数值放入 LINQ 查询中。如果这么做了那么会出现一个问题,当我们利用别的查询参数值再次进行查询时, Entity Framework 将不会利用前面的翻译缓存,而是将本次的查询进行翻译,并缓存。解决这个问题的方法其实很简单,只需要将查询参数值赋值给一个变量,然后将变量放到 LINQ 查询中即可。同样,我们来看一下例子:
using(var ef = new EfContext())
{
var user1 = ef.Users.Where(p=>p.Id=1);
var user2 = ef.Users.Where(p=>p.Id=2);
}
在上述代码中,第一个查询所生成翻译缓存无法和第二个查询共用,我们只需改进一下代码就可以事项翻译缓存的共用:
using(var ef =new EfContext())
{
var userId = 1;
var user1 = ef.Users.Where(p=>p.Id=userId);
userId = 2;
var user2 = ef.Users.Where(p=>p.Id=userId);
}
当我们利用Skip和Take 进行分页查询时这个方法就不管用了。当我们把变量传递给 Skip 和 Take 方法时 Entity Framework 无法识别到底传递的是变量还是查询参数值,因此 Entity Framework 会生在每次查询时生成不同的翻译缓存。如果要解决这个问题很简单,我们只需要使用 lambda 表达式即可,代码如下:
using(var ef =new EfContext())
{
var user = ef.Users.OrderBy(p=>p.Id).Skip(()=>model.Offset).Take(()=>model.Limit).ToList();
}
查询重新编译
当我们通过多种不通的逻辑进行查询时, Entity Framework 生成的 SQL 语句会很复杂,这样会造成查询成本变高,性能降低。每次使用不同的参数值查询数据 Entity Framework 都将会将 SQL 语句缓存起来,如果是很频繁的查询将会增加处理器负担。这时我们可以通过自定义的方式实现数据库命令拦截器就,这样我们就可以在运行之前来修改 SQL 。自定义数据库命令拦截器,只需要新建一个继承自DbCommandInterceptor的类,然后在DbConfiguration派生类中的构造函数种添加拦截器即可。
默认情况下,Entity Framework 的加载策略是延迟加载。延迟加载在大部分情况下是一个不错的方法,但是当根据查询条件查询出多个符合条件的数据时,必须对多个数据中的每个数据来单独查询导航属性的数据。
我们应该保证延迟加载用到需要的地方:
只有在我们确定需要关联的数据时才会用到上述的情况(例如我们需要获得与班级关联的学生数信息)。
那么我们如何规避 N+1 这种情况呢?我们可以利用Include执行饥饿加载策略,这时将在单个查询中获取导航属性中的数据。如果应用与数据库之间存在高延迟,而且数据量很大这时饥饿加载就派上了用场。我们通过例子来看一下应该怎么使用Include避免 N+1:
using(var ef = new EfContext())
{
var users = ef.Users.AsNoTracking().Where(p=>p.Age==12).Include(p=>p.Addresses).ToList();
foreach(var user in users)
{
Console.WriteLine(u.Addresses.Count);
}
}
索引在数据库中经常使用,利用索引可以大大提高数据的查询速度。在 Entity Framework 中使用索引稍微麻烦点,虽然可以使用代码的形式设置索引,但往往会出现问题,例如我们需要通过 Name 字段查询出所有的 Phone 并且查询多次,这时我们查询几次就将会查询数据库几次,那么我们可以建立复合索引,我们先按照一般情况下建立索引的方式编写:
Property(p=>p.Name).HasColumnAnnotation("Index",new IndexAnnotation(new []
{
new IndexAttribute("Phone")
}));
看到上面代码你一定觉得很简单对吧,和前面所说的稍微麻烦点不一样,那么我这能说你想简单了,这段代码最后生成的索引并不是我们所想的那样,而是在 Name 字段上建立了一个名字叫 Phone 的索引。下面我们就利用稍微麻烦点的方法来解决这个问题。
Entity Framework 可以在迁移文件中使用 SQL 语句,因此我们就利用这种方式来解决创建复合索引:
第一步,通过Add-Migration AddUserIndex搭建基架
第二步,在生成的AddUserIndex文件中编写创建索引的代码
public partial class AddUserIndex : DbMigration
{
private const string IndexName = "idxName";
public override void Up()
{
Sql("create nonclustered index [{IndexName}] on [dbo].[Users]([Name]) include ([Phone])");
}
public override void Down()
{
DropIndex("dbo.Users",IndexName);
}
}
批量插入是应用中常见的场景(比如导入 EXCEL 数据),ADO.NET 中我们可以使用SqlBulkCopy来进行批量插入,并且性能非常好,但是 Entity Framework 中并没有这样的方法,那么如果按照新增/修改单条数据的方式进行批量新增/修改会造成 CPU 使用率接近甚至到达 100%,进而造成系统性能低下。造成这种情况的原因就是将对象添加到上下文中耗时过长。针对这个问题我们分两步来解决:
List<User> users = new List<User>();
using(var ef = new EfContext())
{
for (int i=0; i<5000;i++)
{
var user = new User
{
Id = i;
Name = "张三"+i;
}
users.Add(user);
}
ef.Users.AddRange(users);
ef.SaveChanges();
}
List<User> users = new List<User>();
using(var ef = new EfContext())
{
bool acd = ef.Configuration.AutoDetectChangesEnabled;
try
{
ef.Configuration.AutoDetectChangesEnabled = false;
for (int i=0; i<5000;i++)
{
var user = new User
{
Id = i;
Name = "张三"+i;
}
users.Add(user);
}
ef.Users.AddRange(users);
ef.SaveChanges();
}
finally
{
ef.Configuration.AutoDetectChangesEnabled = acd;
}
}
异步查询是在 Entity Framework 6+ 的 c#5.0 中出现的。当我们每次就处理一个请求时异步查询并不能体现出优势,但是当我们需要处理大量数据并发加载的时候,如果不利用异步查询就会造成查询阻塞,这样就造成了请求超时、页面等待甚至数据丢失的问题,这时就可以利用异步查询的ToListAsync 、CountAsync、FisrtAsync、SaveChangesAsync方法来处理。
作者简介:朱钢,笔名羽生结弦,CSDN博客专家,.NET高级开发工程师,7年一线开发经验,参与过电子政务系统和AI客服系统的开发,以及互联网招聘网站的架构设计,目前就职于北京恒创融慧科技发展有限公司,从事企业级安全监控系统的开发。
热 文 推 荐
☞马云谈 5G 危机;腾讯推出车载版微信;Ant Design 3.22.1 发布 | 极客头条
☞与旷视、商汤等上百家企业同台竞技?AI Top 30+案例评选等你来秀!
☞他是叶问制片人也是红色通缉犯, 他让泰森卷入ICO, 却最终演变成了一场狗血的罗生门……