责编 | 胡雪蕊

出品 | CSDN (ID: CSDNnews)

Entity Framework 是 .NET 中快速天生/操作数据库ORM框架。
无论你是刚入行 .NET ,还是已经是从事开拓 .NET 开拓多年的 “老人儿”,都或多或少听到过这么一句话:Entity Framework 性能很差,操作大量数据会很慢乃至超时。
那么,事实真的是这样吗?答案是否定的,如果真的这样的话,我们可想而知微软这么大个公司,推出这样的产品,岂不是在啪啪打脸。
好了,废话不多说,下面来讲解一下 Entity Framework 的优化方案,方案偏多请各位耐心阅读。

phpcollectionmapping若何解决 Entity Framework 机能差的难题 HTML

零、减少初始化与数据库交互

我们可以利用 Entity Framework Code First 内置的自动功能帮我们初始化/运行数据库,并且在可能导致失落败时提醒我们。
这个逻辑只是针对每个高下文类,并且只会发生一次开销很小,因此关闭以下功能没有很大的浸染。
1. 关闭数据库初始化该方法只需在高下文类中注册一个空数据库初始化程序即可,直策应用基于代码的配置进行设置,代码如下:

csharppublic class EfConfiguration : DbConfiguration{public EfConfiguration{SetDatabaseInitialiser<EfContext>;}}

同样,利用如下代码也可以实现:

csharppublic class EfConfiguration : DbConfiguration{public Poliey polley;public EfContext{SetDatabaseInitialiser<EfContext>(new DatabaseInitializer<EfContext>);}}

2. 避免数据库版本查询我们进行查询时,Entity Framework 无法从数据库连接字符串确定数据库版本,因此 Entity Framework 将会天生事宜来查询数据库版本。
如果你能确定生产环境/开拓环境的数据库版本,这时你就可以将数据库版本硬编码进代码中,这时 Entity Framework 将不会查询数据库版本。
我们只须要创建继续自IManifestTokenResolver的类,利用ResolverManifestToken方法返回我们设定的数据库版本,然后创建继续自DbConfiguration的类,并且在布局函数中调用我们的ManifestTokenResolver类,代码如下:

csharppublic 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);}}

通过上面的代码设定,我们见告了 Entity Framework SQL Server 的版本,因此将不会在天生查询数据库版本的事宜。

3. 单链接多要求Entity Framework 支持一个链接进行多次数据库要求,并返回多个数据集,这个特点一样平常会针对网络高延迟的情形下。
我们只需在链接字符串中添加MultipleActiveResultSets=True 即可。

预编译视图

我们在利用 Entity Framework 的时候会创造,当第一次利用 Entity Framework 查询数据的时候会非常的慢,有时乃至涌现超时问题。
这是由于每次程序重启或者初始化时会重新天生用于查询的视图导致的。
既然须要重新天生用于查询的视图,那么我们为什么不手动将用于查询的视图创建出来呢,这样就不会每次重启或者初始化重新天生视图了。

手动天生视图只须要利用Entity Framework 6 Power Tools Community Edition工具即可,我们来看一下这个工具该怎么用:

1. 在VS菜单栏工具选项中选择扩展与更新,然后在搜索栏中输入Entity Framework 6 Power Tools Community Edition,点击安装即可;

2.在高下文文件上右键选择Entity Framework>Generate Views将会天生视图文件,文件的名称为高下文类名称.Views.cs。

上述步骤天生的文件便是用于查询的视图,当运用程序启动时会直策应用这个视图,而不是重新天生视图。
但是这里须要把稳的是当我们的模型发生了改变就必须重新天生这个视图,如若不然程序将会报错。

除了利用Entity Framework 6 Power Tools Community Edition来天生视图外,还可以利用代码的办法天生视图,这里我们可以利用2种办法:

1. Entity Framework API

这种办法开放度比较高,我们可以为所欲为的序列化视图。
天生视图的 API 位System.Data.Entity.Core.Mapping.StorageMappingItemCollection类中,我们可以利用ObjectContext中的MetadataWorkspace来检索高下文的 StorageMappingItemCollection。
我们只需在项目全局文件的Application_Start方法中放入如下代码,担保每次启动运用程序时都预先编译天生映射视图:

csharpusing(var ef = new EfContext){var objectContext =((IObjectContextAdapter)ef).ObjectContext;var mappCollection = (StorageMappingItemCollection)objectContext.MetadataWorkspace.GetItemCollection(DataSpace.CSSpace);}

2. EFInteractiveViews

这种办法和前一种办法比较比较繁琐,我们须要通过NuGet下载 EFInteractiveViews 然后通过如下两种办法中的任意一种来手动实现视图:

(1)普通办法

csharpusing(var ef = EfContext){InteractiveViews.SetViewCacheFactory(ef,new FileViewCacheFactory(@\"大众D:\MyProject\Ef\EFMappingViews.xml\"大众))var customer = ef.Customers.AsNoTracking.ToList;}

上述代码将会在D:\MyProject\Ef\文件夹下天生EFMappingViews.xml文件,这个文件便是视图文件。

(2)数据工厂办法

csharpusing(var ef = new EfContext){InteractiveViews.SetViewCacheFactory(ef,SqlServerViewCacheFactory(ef.Database.Connection.ConnectionString));}

数据工厂办法只适宜SQL Server,如果须要其他数据库的支持,则须要继续数据工厂接口。

把稳:以上方法都须要放在Application_Start方法中。
我比较推举Entity Framework API的办法,首先这种办法开放程度高,另一方面利用这种方法代码比较清晰,我最不推举的是Entity Framework 6 Power Tools Community Edition这种办法,应为这种办法每次在模型改变的情形下都须要手动重新天生映射。

如果你目前正在利用的是Entity Framework 6.2.x,那么在该版本中存在基本代码配置 API, 只需进行如下设置即可,不须要向上面那样利用:

csharppublic class EfConfiguration:DbConfiguration{public EfConfiguration{SetModelStore(new DefaultDbModelStore(Directory.GetCurrentDireCtory));}}

设置完上述代码后,Entity Framework将在第一次加载完 Code First模型后永久从缓存中获取,这样就坚守了启动韶光。
当实行完 Enable-Migrations 命令后,项目所在目录将会天生视图文件,文件名称格式为: 项目名.高下文派生类名称.edmx。

ngen 安装 Entity Framework

在C:\Windows\Microsoft.NET\Framework\v4.0.30319文件夹下存在大量的dll文件,这些文件是.NET为托管运用程序和库天生的本机映像,通过这些映像程序可以快速启动,并且占用内存很小。
那么我们可以在程序运行前,将托管代码翻译本钱机映像,来减轻编译器在运用程序运行时天生本机指令的本钱。

我们须要利用NGen.exe命令行工具天生本机映像。
微软官方说 Entity Framework 运行时程序集的本级映像可以缩短运用程序启动事宜1到3秒。
详细操作方法如下:

我们以管理员身份运行命令行,将目录切换到项目办理方案中Entity Framework.dll所在文件夹,运行如下命令:

shell%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

利用 AsNoTracking

Entity Framework 通过快照式变更追踪来讲数据持久化到数据库中。
将数据持久化数据库中这一过程会不可避免的花费性能。
但是我们在进行数据查询时并不须要快照式变更追踪,这时我们可以利用AsNoTracking方法见告 Entity Framework ,代码如下:

csharp

using(var ef = new EfContext)

{

var user = ef.Users.AsNoTracking.FirstOrDefault(p=>p.Id=1);

}

这时如果我们修正数据后调用 SaveChanges 方法保存数据将会报错,只有手动改变工具状态后再调用 SaveChanges 方法才能成功。

利用缓存

缓存可以加快我们查询数据的速率,提高运用程序的性能。
在 Entity Framework 中已经实现了实体缓存和查询翻译缓存。
下面我们来详细看一下:1. 实体缓存默认情形下从数据库查询出来的数据会被快照式跟踪并缓存,例如我们第一次从数据库中查询出数据后,接下来我们再次利用相同的条件查询数据,这时 Entity Framework 创造找到的条件与缓存的数据相同,那么 Entity Framework 将不会再去数据库读取数据,而是读取缓存中的数据。
不管我们是利用 LINQ 查询还是利用 DbSet<T> 在数据库中直接实行 SQL 查询, Entity Framework 都会利用缓存数据。
在这里我推举利用DbSet.Find方法进行查询数据,该方法吸收一个主键作为查询条件,并返回符合条件的实体。
由于这个方法在第一次查询数据后,会将数据缓存起来,这样我们后面的查询就会直策应用缓存的数据。
我们通过一个大略的例子来理解上面所讲的:

csharpusing(var ef = new EfContext){var user1 = ef.Users.Find(123);var user2 = ef.Users.Find(123);}

上面的代码中吗,user1 的数据是从数据库中查询出来的,查询出来后 Entity Framework 将数据缓存起来,当第二次进行查询时 Entity Framework 从缓存中读取数并赋值给 user2 。

2. 查询翻译缓存

我们如果将 Entity Framework 查询转换成 SQL 语句须要进行如下两个步骤:

(1)首先将 LINQ 表达式转换为数据库表达式树;

(2)然后将数据库表达式树转换为SQL语句。

这个过程是十分耗时的,因此 Entity Framework 将查询缓存在了MemoryCache中。
在处理 LINQ 查询前,Entity Framework 会通过打算缓存密钥,来查找翻译缓存,如果被找到就会重复翻译,如果未找到就会将查询进行翻译并缓存。
这里有一点须要把稳,在开拓过程中我们必须禁止将查询参数值放入 LINQ 查询中。
如果这么做了那么会涌现一个问题,当我们利用别的查询参数值再次进行查询时, Entity Framework 将不会利用前面的翻译缓存,而是将本次的查询进行翻译,并缓存。
办理这个问题的方法实在很大略,只须要将查询参数值赋值给一个变量,然后将变量放到 LINQ 查询中即可。
同样,我们来看一下例子:

csharpusing(var ef = new EfContext){var user1 = ef.Users.Where(p=>p.Id=1);var user2 = ef.Users.Where(p=>p.Id=2);}

在上述代码中,第一个查询所天生翻译缓存无法和第二个查询共用,我们只需改进一下代码就可以事变翻译缓存的共用:

csharpusing(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 表达式即可,代码如下:

csharpusing(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派生类中的布局函数种添加拦截器即可。

规避 N+1

默认情形下,Entity Framework 的加载策略是延迟加载。
延迟加载在大部分情形下是一个不错的方法,但是当根据查询条件查询出多个符合条件的数据时,必须对多个数据中的每个数据来单独查询导航属性的数据。

我们该当担保延迟加载用到须要的地方:

只有在我们确定须要关联的数据时才会用到上述的情形(例如我们须要得到与班级关联的学生数信息)。

那么我们如何规避 N+1 这种情形呢?我们可以利用Include实行饥饿加载策略,这时将在单个查询中获取导航属性中的数据。
如果运用与数据库之间存在高延迟,而且数据量很大这时饥饿加载就派上了用场。
我们通过例子来看一下该当怎么利用Include避免 N+1:

csharpusing(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 并且查询多次,这时我们查询几次就将会查询数据库几次,那么我们可以建立复合索引,我们先按照一样平常情形下建立索引的办法编写:

csharpProperty(p=>p.Name).HasColumnAnnotation(\"大众Index\公众,new IndexAnnotation(new {new IndexAttribute(\公众Phone\"大众)}));

看到上面代码你一定以为很大略对吧,和前面所说的轻微麻烦点不一样,那么我这能说你想大略了,这段代码末了天生的索引并不是我们所想的那样,而是在 Name 字段上建立了一个名字叫 Phone 的索引。
下面我们就利用轻微麻烦点的方法来办理这个问题。

Entity Framework 可以在迁移文件中利用 SQL 语句,因此我们就利用这种办法来办理创建复合索引:

第一步,通过Add-Migration AddUserIndex搭建基架

第二步,在天生的AddUserIndex文件中编写创建索引的代码

csharppublic 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);}}

末了将修正迁移到数据库中。

关闭 DetectChanges

批量插入是运用中常见的场景(比如导入 EXCEL 数据),ADO.NET 中我们可以利用SqlBulkCopy来进行批量插入,并且性能非常好,但是 Entity Framework 中并没有这样的方法,那么如果按照新增/修正单条数据的办法进行批量新增/修正会造成 CPU 利用率靠近乃至到达 100%,进而造成系统性能低下。
造成这种情形的缘故原由便是将工具添加到高下文中耗时过长。
针对这个问题我们分两步来办理:第一步,将数据新增到凑集,将凑集传入AddRange方法,让后将工具添加到高下文中,末了包存入数据库,经由这一步的处理,批量新增/修正数据的耗时将减少 60% 旁边。
性能提高这么多缘故原由是,Entity Framework 针对AddRange方法做了优化,大大提高了数据存储的性能。
详细代码如下:

csharpList<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;}

第二步,关闭 DetectChanges

经由了第一步,我们还可以进一步提高性能。
当我们调用SaveChanges方法时内部回调DetectChanges方法,我们批量插入多少条数据就会回调用多少次该方法,那么我们可想而是,当批量插上万条数据时,性能可想而知是多么的差。
这个时候我们就须要关闭DetectChanges方法,该方法关闭后,性能又比上步提高了 20% 旁边。

csharpList<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客服系统的开拓,以及互联网招聘网站的架构设计,目前就职于北京恒创融慧科技发展有限公司,从事企业级安全监控系统的开拓。

【END】