Entity Framework规范
Entity Framework版本规范
Entity Framework版本:6.1.0及以上。地址:http://99.48.236.128:8090/packages/EntityFramework/6.1.0
.NET Framework 4.5.1及以上
如有EF 6以下的情况,私聊相关调优问题。以后不建议使用EF 6以下版本。
Entity Framework 6.0 Native Image调优
一般情况下在.NET环境下生成的程序集都是托管代码,在运行环境,MSIL托管代码到本地Native代码之间有一个本地代码生成阶段,这是由JIT编译器在每次程序启动时触发。由于.NET Framework自带的基础类库,都做了Native Image,所以不会有启动性能损耗的感觉,但是自有的Assembly就必须经历这个一个Native Code Generation的过程。
在Entity Framework 5.0之前,EF组件都是随System.Data.Entity发布,这是一个.NET Framework原生组件,已经注册了Native Image。但是到了Entity Framework 6,EF小组为了提高包更新自由度和发布频度,将EF的核心功能提取到了EntityFramework.dll和EntityFramework.SqlServer.dll这2个程序集中,与.NET Framework完全独立,所以在程序每次启动之时,会触发Native Code Generation,直观感觉就是程序启动速度很慢。下面我们通过命令的形式,手动将Managed Code注册成Native Code。
NGen.exe(Native Image Generator)使用JIT编译器生成Native Code Image,并将生成的Image存储到本地硬盘(确切的说是GAC目录下一个特定子目录)。当程序需要某个程序集时,CLR加载Native Image,而不是原始程序集。
下面我们通过一个程序演示一下Native Image模式下EF程序的加载速度区别。
首先创建一个简单EF项目。如下:
代码很简单,通过SQLQuery使用原生T-SQL获取一条数据。
默认,我的本地测试环境未注册EF 6 Native Image。执行情况如下:
第一次加载EF,花了463ms。
通过NGen对EF 6的2个程序集EntityFramework.dll和EntityFramework.SqlServer.dll(注意:32位和64位是不同的程序集,需要分别注册)进行注册后,再次运行程序。执行情况如下:
第一次加载明显加快,花了172ms。由于我使用了SqlQuery,比较原始的SQL原语,如果使用ObjectContext和DBContext的话,启动性能将会有10倍左右的提升。
既然NGen这么神奇,那么我们一起来学一下NGen是怎么使用的。
首先,是将程序集Native Image化,并将Native Image注册到缓存。
很简单,通过NGen install <程序集路径>即可将相关程序集的Native Image进行注册(注意,同一个程序集,不管路径是否相同,重复注册,没有任何卵用,当然也不会报错),注意管理员权限运行Command哦。
接着,验证Native Image是否注册成功。
通过NGen display <程序集路径>即可验证。
全部工作就这么多。提个问题?我们注册的Native Image存在硬盘的哪里呢?给个提示GAC目录下的某个目录。有兴趣的童鞋自已研究分享。
Entity Framework T4 Template调优
在DataBase First模式下,通过数据库生成EDMX,再通过T4 Template生成Context类和Entity类。由于Code First模式下,功能有限,且在处理过程中也是先生成EDMX再进行处理,性能较DataBase First有所下降,所以项目中禁绝用Code First模式进行编程,任何理由都不可以!
在Entity Framework中有2个T4 Template。如图:
一个是Context模板,生成DBContext派生类,提供数据库操作、监控和管理接口。一个是Entity模板,基于EDMX生成Entity类,提供强类型实体。
在开始T4 Template优化之前,先说说Entity Framework中的性能关注点:
1、Lazy Loading和Eager Loading:EF作为一个强大的ORM框架,提供了实体关系查询的功能。一个是Lazy Loading,在需要使用关联实体时由EF自动加载关联实体,这种模式有一个问题,一是Loading过程不可控,另一个是按需加载,会导致多次数据库查询;另一个是Eager Loading通过Include的方式构建类似Join语句进行查询,这种模式问题在于如果表过多,导致低性能。说了这么多,其实我要说的是,项目中必须杜绝Lazy Loading和Eager Loading,两者都不是好东西。从数据库的设计面来说,支持范式是为了保证数据完整性和杜绝数据冗余,但是在生成数据库时引入外键,会导致项目的维护性急剧下降,在高并发的情况下,性能更成问题。所以外键到数据设计文档为止,为后人留下设计依据即可,项目中的数据完整性通过代码保证,可提供最佳性能。所以,既然没有Relation,Lazy Loading和EagerLoading都是浮云。
2、Change Tracking:EF中Entity有如下几种状态:
Added
Deleted
Modifiedϒ
Unchangedϒ
Detached
通过DBContext或者ObjectContext提供的操作方法,可以实现Entity状态变迁,最终体现在持久化过程。对于Entity的实现,EF中支持2中模式:一种是POCO Entity,另一种是Change Tracking Proxy(也有另一种叫法:POCO Entity wrapped by a proxy)。
POCO Entity全称叫Plain-Old CLR Object Entity,是一种纯自然态.NET对象,没有任何附加,对于这种对象EF在做Change Tracking时,需要遍历当前对象的所有属性和原始副本对象的所有属性进行判断,最终确认5中状态中的一种,这个耗时的过程在调用DbContext.SaveChanges时都会触发(假设AutoDetectChangesEnabled没有关闭)。可以想象Entity越复杂,Tracking的耗费越大。
另一种是Change Tracking Proxy,这种对象会在内存中生成一个继承于POCO Entity类的Proxy类,该类会修改每个Property,附加状态变更事件处理,任何针对Property的修改都会实时通知到Object State Manager,到时候把AutoDetectChangesEnabled一关,就可以实现实时状态变更通知。那么如何实现Change Tracking Proxy?很简单,就是在原来POCO Entity上针对每个Property,将之修改为Virtual即可。这种模式也有一个缺陷,就是增加事件通知的开销。
总体来说,如果你的Entity很多,每个Entity字段又比较复杂,可以使用Change Tracking Proxy,否则POCO Entity也不会慢多少。
其实,EF中的性能关注点有很多,这里指出的这2点,可以通过修改T4 Template(Lazy Loading可以通过VS进行配置)进行变更。
首先,是禁用Lazy Loading。双击Test.edmx(不同项目,名称不一样,自行识别) ,打开设计器,右键,选择“Properties”,打开“Properties”设计框,将“Lazy Loading Enabled”置为“False”即可。如图:
设置完后,再看Context模板生成的代码,如图:
多个禁用Lazy Loading的配置项。
接着,是将POCO Entity模式切换成Change Tracking Proxy模式。
第一步是将AutoDetectChangesEnabled关闭,这个需要修改Context.tt模板,打开Context.tt文件,增加如下代码,如图:
保存Context.tt后,生成的Context.cs会增加如下代码,如图:
第二步是将生成Change Tracking Proxy Entity,打开Test.tt(不同项目不一样,自行识别),找到如下代码,在字符串中添加“virtual”,如图:
保存Test..tt后,生成的Test1.cs会增加如下代码,如图:
到此,EF会实时跟踪Entity属性变更,不会在SaveChanges时对DetectChanges检查,但是Entity状态不会丢失。
Entity Framework查询过程分析
EF进行第一次查询时需要做很多事情,比如模型加载,验证,视图生成等。所以第一次查询也被称为“冷”查询。相应的,后续的查询被称为“热”查询,速度更快。“冷”查和“热”查的执行过程分析如下。
First Query Execution – cold query
Code User Writes | Action | EF4 Performance Impact | EF5 Performance Impact | EF6 Performance Impact |
---|---|---|---|---|
using(var db = new MyContext()) | Context creation | Medium | Medium | Low |
{ | ||||
var q1 = | Query expression creation | Low | Low | Low |
from c in db.Customers | ||||
where c.Id == id1 | ||||
select c; | ||||
var c1 = q1.First(); | LINQ query execution | |||
| High but cached | High but cached | High but cached | |
| Potentially very high but cached | Potentially very high but cached | Medium but cached | |
| Medium | Low | Low | |
| Medium | Medium but cached | Medium but cached | |
| Medium but cached | Medium but cached | Medium but cached | |
| Potentially high | Potentially high (Better queries in some situations) | Potentially high (Better queries in some situations) | |
| ||||
| ||||
| ||||
| Medium | Medium | Medium (Faster than EF5) | |
| Medium | Medium | Medium | |
} | Connection.Close | Low | Low | Low |
Second Query Execution – warm query
Code User Writes | Action | EF4 Performance Impact | EF5 Performance Impact | EF6 Performance Impact |
---|---|---|---|---|
using(var db = new MyContext()) | Context creation | Medium | Medium | Low |
{ | ||||
var q1 = | Query expression creation | Low | Low | Low |
from c in db.Customers | ||||
where c.Id == id1 | ||||
select c; | ||||
var c1 = q1.First(); | LINQ query execution | |||
| ~~High but cached~~Low | ~~High but cached~~Low | ~~High but cached~~Low | |
| ~~Potentially very high but cached~~Low | ~~Potentially very high but cached~~Low | ~~Medium but cached~~Low | |
| Medium | Low | Low | |
| Medium | ~~Medium but cached~~Low | ~~Medium but cached~~Low | |
| ~~Medium but cached~~Low | ~~Medium but cached~~Low | ~~Medium but cached~~Low | |
| Potentially high | Potentially high (Better queries in some situations) | Potentially high (Better queries in some situations) | |
| ||||
| ||||
| ||||
| Medium | Medium | Medium (Faster than EF5) | |
| Medium | Medium | Medium | |
} | Connection.Close | Low | Low | Low |
有很多方法可提高“冷”查和“热”查的性能,比如,我们可以通过View预生成来提高模型加载速度。
Entity Framework View生成调优
何为View?在EF的执行过程中,任何一个操作都会经历如下过程:
1、将相关Query View(或Update View)合并成一个完成View。
2、将合并View通过计划编译器后,产生存储可以识别的表现形式(比如T-SQL)
在该过程中,View的合并和编译在每个操作过程中都会执行,视整个模型的复杂程度,性能会各有不同。说白了,最终生成一个SQL,如果可以预先生成这个SQL,就可以避免不必要的性能损耗,这个过程叫“Pre-Generation View”。
预生成View,可以通过Entity Framework Power Tools工具(下载地址:https://visualstudiogallery.msdn.microsoft.com/72a60b14-1581-4b9b-89f2-846072eff19d/,可惜官方不支持2015,我自己打了个包,可以支持2015,地址:EFPowerTools15.vsix)比较方便的实现。
下载后是一个VSIX扩展,按向导安装。安装完后在VS中打开项目,找到EDMX文件,右键,按图所示,完成操作:
完成后会在当前项目下,生成一个View.cs文件,该文件就是预编译的View脚本,打开就是各种脚本。如果你看不惯自动生成的脚本,改之,优化到你想要的方式为止,呵呵。
Entity Framework只读查询调优
刚刚讲完Change Tracking,对EF Entity状态跟踪有个大概了解,就是通过Object State Manager进行状态跟踪管理, 不管是POCO Entity还是Change Tracking Proxy,都逃不出Object State Manager监管。但是,有时候我们只需要以“只读”形式获取数据,不做任何操作,这种查询称为“No Tracking Query”。有2中性能较优的方式实现查询:一种是通过DbContext.Database.SqlQuery<T>(“sql”),另一种是通过DbSet.AsNoTracking()。
DbContext.Database.SqlQuery方式:
DbContext.Database.SqlQuery
using (var context = new TestEntities())
{
var test1Entity =
context.Database.SqlQuery<Test1>(“select top 1 * from Test1 with(nolock)”)
.FirstOrDefault();
Console.WriteLine(“{0}-{1}-{2}”, test1Entity.KeyID, test1Entity.Name, test1Entity.Age);
}
DbSet.AsNoTracking方式:
DbSet.AsNoTracking
using (var context = new TestEntities())
{
var test1Entity = context.Test1.AsNoTracking().Take(1).FirstOrDefault();
Console.WriteLine(“{0}-{1}-{2}”, test1Entity.KeyID, test1Entity.Name, test1Entity.Age);
}
在这2中模式下,任何对Entity的修改都不会触发状态变更。从性能上看,SqlQuery较优,但是要手动写SQL;
Entity Framework方法性能分析
在开始EF方法性能分析之前,先普及一下EF支持哪些形式的查询,各种查询的优缺点。
LINQ to Entities(建议使用)
No Tracking LINQ to Entities(在“只读”查询时建议使用)
Entity SQL over an ObjectQuery(不建议使用,需要配合ObjectContext使用)
Entity SQL over an EntityCommand(不建议使用,太原始,几乎就是ADO.NET原生方法的简单封装)
SqlQuery and ExecuteStoreQuery(在性能要求特别高的情况建议使用)
CompiledQuery(不建议使用,编程难度大)
LINQ to Entities
LINQ to Entities
var test1Entity = context.Test1.Where(testEntity => testEntity.Name == “EF”).FirstOrDefault();
优点:
适用于C(reate)U(pdate)D(elete)操作,有状态跟踪。
支持语法糖。
性能良好。
缺点:
- 不支持复杂查询语法(其实也不建议用复杂语法,害己害人)
No Tracking LINQ to Entities
No Tracking LINQ to Entities
var test1Entity = context.Test1.AsNoTracking().Where(testEntity => testEntity.Name == “EF”).FirstOrDefault();
优点:
比LINQ to Entities性能高好多。
支持语法糖。
缺点:
不适用于C(reate)U(pdate)D(elete)操作,无状态跟踪。
不支持复杂查询语法。
SqlQuery and ExecuteStoreQuery
有3中方式:
- SqlQuery on Database:
SqlQuery on Database
var test1Entity = context.Database.SqlQuery<Test1>(“select top 1 * from Test1 with(nolock)”).FirstOrDefault();
- SqlQuery on DbSet:
SqlQuery on DbSet
var test1Entity = context.Test1.SqlQuery(“select top 1 * from Test1 with(nolock)”).FirstOrDefault();
ExecuteStoreQuery:
ObjectContxt模式下方式,不建议采用,以后建议在DbContext模式下编程。当然性能吗,是所有方式中最好的。
优点:
通常来说,在DbContext系列中是性能最好的方法。
DbSet.SqlQuery适用于C(reate)U(pdate)D(elete)操作,有状态跟踪。
缺点:
文本SQL,不支持多数据类型(SQL Server,Oracle,MySql),而且无法进行编译时验证。
Database.SqlQuery不适用于C(reate)U(pdate)D(elete)操作,无状态跟踪。
ObjectContext.ExecuteStoreQuery不适用于C(reate)U(pdate)D(elete)操作,无状态跟踪。
下面是网上有人做的基线测试,比对各种查询之间的性能。
EF | Test | Time (ms) | Memory |
---|---|---|---|
EF5 | ObjectContext Entity Command | 621 | 39350272 |
EF5 | DbContext Sql Query on Database | 825 | 37519360 |
EF5 | ObjectContext Store Query | 878 | 39460864 |
EF5 | ObjectContext Linq Query No Tracking | 969 | 38293504 |
EF5 | ObjectContext Entity Sql using Object Query | 1089 | 38981632 |
EF5 | ObjectContext Compiled Query | 1099 | 38682624 |
EF5 | ObjectContext Linq Query | 1152 | 38178816 |
EF5 | DbContext Linq Query No Tracking | 1208 | 41803776 |
EF5 | DbContext Sql Query on DbSet | 1414 | 37982208 |
EF5 | DbContext Linq Query | 1574 | 41738240 |
EF | Test | Time (ms) | Memory |
EF6 | ObjectContext Entity Command | 480 | 47247360 |
EF6 | ObjectContext Store Query | 493 | 46739456 |
EF6 | DbContext Sql Query on Database | 614 | 41607168 |
EF6 | ObjectContext Linq Query No Tracking | 684 | 46333952 |
EF6 | ObjectContext Entity Sql using Object Query | 767 | 48865280 |
EF6 | ObjectContext Compiled Query | 788 | 48467968 |
EF6 | DbContext Linq Query No Tracking | 878 | 47554560 |
EF6 | ObjectContext Linq Query | 953 | 47632384 |
EF6 | DbContext Sql Query on DbSet | 1023 | 41992192 |
EF6 | DbContext Linq Query | 1290 | 47529984 |
从基线测试来看,ObjectContext整体性能要优于DbContext,有能力的童鞋可以使用DbContext的ObjectContext包装进行编程,建议还是DbContext,比较简单。从EF 5到EF6,性能上提高了,相对的内存使用量也多了。
Entity Framework Async编程
在开始EF Async编程前,先简单说一下异步编程模式,有APM、EAP、async/await模式3种,具体原理各自翻MSDN(async/await:https://msdn.microsoft.com/zh-cn/library/hh191443.aspx)。有几点需要说明一下,异步模式不会提高程序的性能,绝对不会,它提高的是程序的响应能力,对Web来说还有一个更专业的说法叫,提高Web的吞吐量;其次,异步模式不是多线程,它也不会开启多个线程去处理任务(好吧,绝大多数时候不会开启其他线程去处理),它只会在当前线程上下文中记录异步信息,待到异步操作(一般都是IO)完成后,通知当前线程(或者到线程池中找一个未使用的线程)完成后续工作。千万不要将异步和多线程搞混了。异步编程的目的旨在将程序中线程等待的空余时间利用起来,让线程不至于傻等,它不是戏法,如果你的线程从头到尾都是满满当当的计算密集型工作,异步编程对你毫无意义,请注意这一点,额,也不算毫无意义,鉴于现在框架太牛逼,人家还是会帮你执行下,但是会重新开一个线程,重新开一个线程哦,然后呢,然后,你的CPU就爆表了,^O^ 。像Redis、NodeJs这种单线程的程序可以高并发,秘诀就在这里,当然Linux下人家叫NIO,Windows也有一个不错的名字叫非阻塞IO。
下面开始正题,先通过一段代码演示一下EF Async效果。
Demo
class Program
{
static private void Log(string message)
{
Console.WriteLine($”{DateTime.Now.ToString(“yyyy-MM-dd HH:mm:ss.fff”)} ThreadID:{Thread.CurrentThread.ManagedThreadId} : ” + message);
}
private static void Main(string[] args)
{
if (args == null) throw new ArgumentNullException(nameof(args));
Log(“A : 我要开始干活了…”);
Log(“A : ……”);
Log(“A : 报表中要放一个测试数据…”);
Log(“A : 喂,B你去给我拿下测试数据…”);
#region Async Operation
var retriveDataTask = RetriveDataAsync();
#endregion
Log(“A : 接着苦逼干活…”);
Log(“A : ……”);
retriveDataTask.Wait();
Log(“A : 将测试数据插入报表…测试数据:” + retriveDataTask.Result.Name);
}
static async Task<Test1> RetriveDataAsync()
{
Log(“B : B去拿测试数据…”);
using (var context = new TestEntities())
{
var test1EntityTask = await context.Test1.SqlQuery(“select top 1 * from Test1 with(nolock)”).FirstOrDefaultAsync();
Log(“B : A,已经拿到数据了…”);
return test1EntityTask;
}
}
}
执行效果如下:
从执行结果可以看出在B开始处理到B处理结束期间,A一直在处理数据,有一点要注意,就是B处理的线程是6,A是1,但这是随机的,有可能A和B都是同一线程,看主线程的空闲情况,也就是说EF Async操作是可以在同一线程上执行的。在代码中我使用了FirstOrDefaultAsync来启动EF的异步模式,EF还有其他异步方法:SaveChangesAsync、ToListAsync、FindAsync等等。本质上,就是对async/await Pattern的简单应用,前提还是要明白async/await Pattern的工作原理和编程模式。其他更详细的应用,请自行Googling。
Entity Framework其他优化
Context存活周期尽量短。DBContext(或者ObjectContext)在EF被设计为轻量级对象,说白了,就是反复回收重建,并不会占用太多资源,所以不要将Context的生命周期延长,用完即丢。
GC服务端配置。 以前的CLR中GC模式是有一个线程专门负责回收垃圾,这个线程在对垃圾对象遍历做标记时,会锁定所有应用的线程。后来,为了提高性能,增加了Server GC模式。在Server GC模式下,会针对每个CPU(其实是核(Core)),专门有一个线程负责回收当前CPU上线程的资源,锁也只锁当前CPU上的线程,提高垃圾回收效率,减少线程锁定时长,但只对服务端程序起作用(何为服务端程序?能让Web.config起作用的都是)。这个配置也不只是针对EF啦,原则上所有的服务端配置都应遵循。配置如下,直接在Web.config中添加即可:
Server Garbage Collection
<configuration>
<runtime>
<gcServer enabled=”true” />
</runtime>
</configuration>