【EntityFramework 6.1.3】个人理解与问题记录

前言

又是一个炎热夏日的晚上,开着空调听着音乐又开始了我们今天的博文。此文并不是ROM工具哪家强之类的引战贴,只是本文自己的一点看法和见解,望前辈看官有望斧正


声明

本文欢迎转载原文地址:http://www.cnblogs.com/DjlNet/p/7220720.html


开始正文

话说回顾历史的话,在linq to sql的年代到后面linq to entity也就是ef4.1以至于现在的ef6.1.3历经了好些岁月的打磨,且也用ef6在真实项目中使用体会到了在开发速度和维护成本体现出来的优势,当然并不代表它就是没有弱点或者是说要通过一些方式去规避不当使用造成的问题哈,所以一个东西不可能做到尽善尽美,毕竟在我看来它也只是基于C#3开始带来linq基础的上面的一层数据库访问的抽象,当然不光是查询啦,也包含写操作啦或者事务之类的,反正啦上层抽象只能做到它自己功能的抽取也就是包含关系型数据库大家都有的一些特性和操作,这样的抽象也是合情合理所以就在linq的基础上面就这样孕育而生,以及具体的话就需要交给数据库对应的提供器去解析表达式去适配数据库自个儿的一些特点和异同,这也是复杂和小心的过程因为这基本都关系着到达数据库执行的sql语句到底如何的问题,实际反应的就是产生的执行计划消耗等等,这其中也是其他ORM工具(暂且这样称呼吧,例如一些Dapper+Extension之类说实话有点勉强,注意这里并没有故意贬低牛逼的Dapper的意思,毕竟我大Stackoverflow就是用的它,具体可以去github查看wiki)同样需要去做的一件事儿,当然这里不排除微软老爹对自家数据库sql server亲儿子有些小操作。

这里要额外提一下:感谢@Pomelo大大对MySql实现ef core驱动的奉献,也得到了微软的支持,所以应该可以放心食用
,微软官方目前的数据库驱动列表:https://docs.microsoft.com/zh-cn/ef/core/providers/,其中ef core对于sql server有个批量操作的优化,偶尔看看issues得知,同时发现同事园子一老哥也发文说明了情况,具体传送门:https://github.com/aspnet/EntityFramework/issues/9270,这个貌似是一直被业界所诟病的问题,当然也可以自定义扩展或者特殊情况配合sql+transaction来处理批量写问题我觉得也是可以的,以至于一些第三方ef batch框架(例如:entity framework plus)这里暂不评价了,个人觉得毕竟带来方便的同时引入了复杂度


好了屁话说了一大推,说到这里你们肯定会以为又是一个EF长篇大论文入门文,例如什么是ORM,什么是OTO,什么是Code First,这些介绍的太多太多了,建议还是才看微软ef官方文档即可(https://msdn.microsoft.com/en-us/library/ee712907(v=vs.113).aspx)以及微软新上线的文档地址:https://docs.microsoft.com/en-us/ef/,也许能把官方文档大致浏览一边基本做到心中有数遇到问题再去翻翻即可,比我在这里BB可能或许有用,所以这里并不会对EF有一个详细的解读(当然做到详细解析,我也做到不到呀),这方面的博文已经数不胜数,在这里可能针对性的看某些问题发表一些个人的看法或者见解。其中回忆起来的问题以及经常园子讨论的问题包含如下:【可能想的不全后面可以更新

1、EF的数据库上下文实例的生命周期管理的标准实践 ?

2、EF的数据库连接打开和关闭的时间点和管理是怎么回事 ?

3、EF为何第一次启动这么慢和怎么解决 ?

4、EF在批量操作时对象跟踪时性能问题该如何解决 ?

5、EF批量数据时怎么提高速度和保证事务 ?

6、EF对复杂的查询表达式解析能力如何?

7、开发者怎么审查EF翻译的SQL语句?

8、开发者怎么监控EF在网站运行情况?

9、......

等等,可能问题还有很多这里暂时没有收录。接下来就是需要我们逐一对问题思考和解析,注意:可能这里的理解有主观意识,当然某些问题我也会用实验例子来证实大致的论述。

1、EF的数据库上下文实例的生命周期管理的标准实践 ?


分析:由于ef的读写操作都是基于DbContext数据库上下文来操作的,所以当进行这些操作的时候,就需要一个对象实例才可以,那么问题就在于我可以在同一个对象操作多次吗,什么时候该创建这个对象以及什么时候结束它,这个对象存在多个有什么影响吗,对象实现了IDisposable接口我必须要在using中使用吗,好,这里我假设我们是处于web应用程序中(通常情况也是如此),基于http请求来讨论这个问题

解答

(a)一个(同一个)对象本身就是可以多次连接访问数据库的包含了读写可能会多次打开和关闭数据库连接,当然这里是基于ado.net数据库连接池的无须过于担心,所以多次操作本身就是合情合理的,也是必须的

(b) 创建对象的时刻问题,上下文得知,基于http请求的话,每次请求可能会有数据库访问的需求(大致都有这个可能),那么每次请求什么时候需要创建一个DbContext对象呢,其实在你的程序结构中,无非就是在Controller或者Service或者Repository中包含(或者说成注入更恰当)DbContext对象的本身被实例化的时候创建DbContext,结束无非也就是1、在包裹类释放时跟着释放(多实例模式)2、在请求结束的时候释放(请求期间共享实例模式)

这里说点题外话:通常意义上来说,把这些这些对象通常由Ioc容器统一(例如: Unity Autofac Ninject等等)来创建和管理生命周期这样来得更合适些,拿unity举例说明(当然这里也可以作为一个私有字段存在于你的Repositotry或者Service中都看你自己选择):这里我们可以自定义个IDbContext接口让DbContext实现它这样这是为了方便注入**
container.RegisterType<IDbContext, DJLNETDBContext>(new PerRequestLifetimeManager());,然后设置为 PerRepuestLifetimeManager每次请求生命周期(基于HttpModule来实现的)Microsoft.Web.Infrastructure.DynamicModuleHelper.DynamicModuleUtility.RegisterModule(typeof(UnityPerRequestHttpModule));**,来实现了请求单例,其他框架同样具备类似功能,类似asp.net core中AddScoped的功能。这样做的好处在于在一个请求期间就可以共享一个DbContext对象啦,既可以让局面变得清晰和简单,又可以减少堆资源的消耗

(c)多个DbContext对象对于asp.net本身就是基于并发应用程序而言这种就是合理,且多个对象在不同的请求线程中也是相互不受干扰和影响的,可能也就是对于同一时刻并发请求数过多对于内存占用可能稍有提升,这不是你需要担心的问题,因为会释放的。

(d)IDisposable在我理解看来是提供给需要释放非托管资源的对象实现的,所以想当然以为DbContext在实现中有这种操作,通过翻源码和多方作证得知,并非如此(这里提前说明一下 1、使用using是因为一来为了准许一种契约或者模式二来为了保证安全性 2、至于connection的管理是DbContext自己的事情会自动处理好),详情请看问题2中有相应解析,所以不需要using已然可以使用它完成你应有的操作。

总结: 如果是EF,建议将其数据库实例上下文DbContext设置为请求期间共享一个实例对象保证创建和销毁操作保证按照您的预期执行,至于实现看自己喜好拉,不过交给一些现成的ioc框架去实现,不乏是一个不错的选择!


2、EF的数据库连接打开和关闭的时间点和管理是怎么回事 ?


思考:如果是你怎么设计关于数据库连接对象的管理(这里的管理是连接对象的打开和关闭,并不是创建和销毁,毕竟closedispose还是有区别的)更加合适呢?

解析: 首先上面1的的(d)也提到了关于using的问题同样也涉及connection,先让我来看一篇老外的博文:http://blog.jongallant.com/2012/10/do-i-have-to-call-dispose-on-dbcontext/可能博文有点老了,但是在 Diego Vega回信中我们发现关键信息(加粗标记关键信息):

The default behavior of DbContext is that the underlying connection is automatically opened any time is needed and closed when it is no longer needed. E.g. when you execute a query and iterate over query results using “foreach”, the call to IEnumerable.GetEnumerator() will cause the connection to be opened, and when later there are no more results available, “foreach” will take care of calling Dispose on the enumerator, which will close the connection. In a similar way, a call to DbContext.SaveChanges() will open the connection before sending changes to the database and will close it before returning.

Given this default behavior, in many real-world cases it is harmless[无害的] to leave the context without disposing it and just rely on garbage collection.

That said, there are two main reason our sample code tends to always use “using” or dispose the context in some other way:

  1. The default automatic open/close behavior is relatively easy to override: you can assume control of when the connection is opened and closed by manually opening the connection. Once you start doing this in some part of your code, then forgetting to dipose the context becomes harmful, because you might be leaking open connections.
  2. DbContext implements IDiposable following the recommended pattern, which includes exposing a virtual protected Dispose method that derived types can override if for example the need to aggregate other unmanaged resources into the lifetime of the context.
    By the way, with DbContext the pattern to open the connection manually and override the automatic open/close behavior is a bit awkward:

    ((IObjectContextAdapter)dbContext).ObjectContext.Connection.Open()
    But we have a bug to make this easier as it used to be with ObjectContext before, e.g.:
    dbContext.Database.Connection.Open()

总结: EF自身已经自动的去控制连接对象的开关了,也就是当你去迭代查询对象或者保存修改时会在恰当的时候(具体看上文的时间点黑体部分)帮你打开或者关闭连接,这样就算不明显去调用dispose,让GC去管理DbContext的剩余托管资源也是无害,至于后半段只是解释下为何还是要实现IDispose接口以及存在的必要和安全性,其实作为编码的我们是可以做到避免手动去控制连接对象,using只是作为最后的一道屏障而已。鉴于不能仅凭一片博文定论,可以参考一下源码中dispose中的实现:https://github.com/aspnet/EntityFramework6/blob/master/src/EntityFramework/DbContext.cs


3、EF为何第一次启动这么慢和怎么解决 ?

解析: 来先看看关于这个问题老外反馈的issues(包含老外的做的实验):https://github.com/aspnet/EntityFramework/issues/4372
以及dudu园长解释的原因所在以及解决方案:http://www.cnblogs.com/dudu/p/entity-framework-warm-up.html 在备注一下从IIS入手的如何优化 EF的博文地址:http://www.cnblogs.com/lkd3063601/p/4713637.html和从GAC入手优化的 https://www.fusonic.net/en/blog/3-steps-for-fast-entityframework-6.1-code-first-startup-performance/

总结 总的来说这里都是搬运工拉,记录备注一下,当然EF这样设计也是合乎情理,在第一次访问之后缓存下来映射视图和一些元数据相关的东西,以后直接复用


4、EF在批量操作时对象跟踪时性能问题该如何解决 ?

分析: 这里先说下对象跟踪,默认EF是启动对象跟踪的,也就是context.Configuration.AutoDetectChangesEnabled = true; 当发现使用查询出来的对象属性值变更之后,在DbContext.SaveChanges的时候这里会主动触发去检查对象的CurrentValues与OriginalValues的差异然后标记为Modified状态,当我们有成百上千的对象需要去修改或者添加,对应 AddRang 或者 RemoveRang 的时候就会触发很多次对象跟踪,所以这是一个坏操作,参考https://msdn.microsoft.com/en-us/library/jj556205(v=vs.113).aspx官方示例做法如下:

using (var context = new BloggingContext()) 
{ 
    try 
    { 
        context.Configuration.AutoDetectChangesEnabled = false; 
 
        // Make many calls in a loop 
        foreach (var blog in aLotOfBlogs) 
        { 
            context.Blogs.Add(blog); 
        } 
        context.SaveChanges();
    } 
    finally 
    { 
        context.Configuration.AutoDetectChangesEnabled = true; 
    } 
}

注意这里有个官方提示:Don’t forget to re-enable detection of changes after the loop — We've used a try/finally to ensure it is always re-enabled even if code in the loop throws an exception.


5、EF批量数据时怎么提高速度和保证事务 ?


分析解答: 一直以来这就是一个焦点问题,问题也是在于为何插入很多数据和修改很多数据的时候很慢,怎么解决这个问题以及还需要保持和原先逻辑(写逻辑)在一个事务里面等,当然你依然可使用context.Configuration.AutoDetectChangesEnabled = false;context.Configuration.ValidateOnSaveEnabled = false;这样的配置去提高代码层面的优化,这里的优化针对批量添加删除修改都是有效的,下面的System.Data.SqlClient.SqlBulkCopy针对添加当然也指定了使用环境sql server毕竟是数据库本身支持才可以,不过这里的基本和EF无关了只是提供批量插入的一个途径而已。

那么加上事务是否可以快点呢?答案是肯定的,使用Database.BeginTransaction()或者System.Transactions.TransactionScope将ef操作裹起来在SaveChanges之后,如若没发生异常打包提交在速度上面会有大幅度提升,相对于不显示使用事务机制,而使用EF默认事务机制的情况上面比较得出上面的结论,这个好处也得益于数据库本身的支持

除了批量添加对象可以忽略,但是我批量删除对象和批量修改对象则需要那拿到那些需要做此操作的源数据集合才可以,这里就需要先查询出来这些对象,然后遍历删除或者修改他们的属性,且还需要保持这些对象是被跟踪的对象,那么我可以不查询出来这些对象也想删除或者修改它们?

答案:可以的,其中基于目前的情况有两种做法,首先第一种也是我比较推荐的一种做法或者说是官方做法:https://msdn.microsoft.com/en-us/library/dn456843(v=vs.113).aspx

这里搬运一点官方代码(详情请参考官方代码):

using (var context = new BloggingContext())
{
	using (var dbContextTransaction = context.Database.BeginTransaction())
	{
		try
		{
			context.Database.ExecuteSqlCommand(
				@"UPDATE Blogs SET Rating = 5" +
					" WHERE Name LIKE '%Entity Framework%'"
				);

			var query = context.Posts.Where(p => p.Blog.Rating >= 5);
			foreach (var post in query)
			{
				post.Title += "[Cool Blog]";
			}

			context.SaveChanges();

			dbContextTransaction.Commit();
		}
		catch (Exception)
		{
			dbContextTransaction.Rollback();
		}
	}
}

在不引入第三方框架的情况下,能够自己清晰的掌握代码以及处理方法,这是比较好的,并且能够和你想的处理结果一致,第二种做法: 即使用一些基于EF的第三方扩展,这里就展示了,因为本身对其不还不是很了解,其实我个人觉得处理20%的需求但是又不想引入新的nuget包的情况,这样特殊处理也是可以的,再加上一点自己的封装例如:实现AOP层面的事务封装,这样在你ExecuteNonQuery(); SaveChanges就不会明显的感觉到事务的存在从而把问题简化,抽到公共逻辑当中去


总结与后续

一 一,为何在问题5之后就没了呀,关于后面

6、EF对复杂的查询表达式解析能力如何?

7、开发者怎么审查EF翻译的SQL语句?

8、开发者怎么监控EF在网站运行情况?

这几个问题将会在下一篇博文给出分析和解读,并不是故意卖关子是博主本身还没有准备好下一篇文的内容哈,望各位原谅,关于以上的问题和解析及其回答属于个人意见,如有不对的地方欢迎讨论哈

【2017年8月3日21:50:50】

划重点拉,顺便再补充一下示例代码

后记

在下先干为敬

不哭长夜者,不足以与人生。不曾为梦想奋斗,拿什么去燃烧青春。有梦之人亦终将老去,但少年心气如昨。

posted @ 2017-07-30 18:02  DJLNET  阅读(2148)  评论(15编辑  收藏  举报