EF6学习笔记十四:上下文管理
要专业系统地学习EF前往《你必须掌握的Entity Framework 6.x与Core 2.0》这本书的作者(汪鹏,Jeffcky)的博客:https://www.cnblogs.com/CreateMyself/
前面学的东西只算入门,为了理解EF,书中的EF进阶非学不可,我看的很吃力。在百度上看关于EF上下文的文章,很少,让人很无助。
我反复看了几遍,还是有很多东西不理解,没办法了,那些东西真的没有什么接触,想象不出来。现在只能努力用自己的话复述一遍,并记录一些自己的问题,主要是想记录自己的思考的过程。
使用EF我们要自己写一个派生自DbContext的上下文类,这是我们与数据库交互的桥梁。
我对这个类了解多吗?其实很少。在EF的最初版本是利用ObjectContext来进行数据访问的,现在的DbContext底层还是基于ObjectContext,经过改造后的DbContext,性能更好,官方推荐使用DbContext。如果我们想用ObjectContext也没问题,因为他们之间可以互相转换。
DbContext 转 ObjectContext
using (EFDbContext ctx = new EFDbContext()) { var objCtx = ((System.Data.Entity.Infrastructure.IObjectContextAdapter)ctx).ObjectContext; }
View Code
ObjectContext 转 DbContext
System.Data.Entity.Core.Objects.ObjectContext objCtx = new System.Data.Entity.Core.Objects.ObjectContext("connectionStr"); var ctx = new System.Data.Entity.DbContext(objCtx,true);
View Code
现在来说我理解的一些上下文管理的东西。
上下文生命周期管理
上下文我知道,不过也是后来才知道。当一个东西,不给出上下文你是不能很好的理解他的,因为没有约束,范围太大,对于不同的环境有不同的理解。和说话一样,对于一句话你还要给出这句话的语境,别人才能真正知道。
当我说出一句“真香”,你知道我说的是真的香还是真香定律?这和泛型约束一样,我们对类型添加的那些约束也可以看做是这个类型的上下文。
我最开始弄EF,同样的是派生一个上下文,但是那时我给上下文取的变量名是“db”,因为我就认为它是一个虚拟的数据库。但是觉得不准确,而且直到我无意中看到上下文中有个属性叫Database……
如果上下文就是数据库,那么它怎么还要有一个Database属性呢?肯定不是这样的,Database应该看做是EF这个语境中的一句话,这个大语境中还有配置、追踪……这些内容,所以要理解整个语境才能理解一句话。弄懂上下文和他里面database的关系。
生命周期,什么意思,按照字面意思来说,一个人从出生到死亡的过程就是这个人的生命周期,一个手机生产到使用报废的过程,再怎么玄之又玄的解释肯定不能怀疑不能动摇,它就是一个出生到死亡的过程。
我这说了好像等于没说啊,其实,我想要对自己强调一遍。根本性的东西不能动摇。既然它叫了这个名字,那么如果实际情况不是这样,只能说明是它的问题不是我的问题。
因为概念性的东西老是听别人说来说去,一个这样说,另一个那样说,如果你不去继续研究,他对你就是一个模糊的、说不出来的东西,而且会怀疑他的命名。
上下文生命周期管理,说的就是……上下文生命周期管理
上下文的生命周期开始于初始化,结束于被垃圾回收器回收并释放。释放?你释放就是了,你不是内存托管吗?
原来,要是你不使用using来包括上下文,就要自己手动释放资源。
可是你不是内存托管吗?我自己创建一个Studen类,也从来不用去手动释放啊
在以前写ado的时候我记得要创建数据库连接,用try/catch包裹起来,最后在finally中调用close方法关闭连接。
后来我知道了托管代码和非托管代码这一个东西。我的理解就是只要你不写指针不自己操作内存,那么就是写的托管代码。
所以我认为是不是有些东西不能托管,百度了一下果然:
“系统的资源包括托管资源和非托管资源。例如文件流,数据库的连接,系统的窗口句柄,打印机资源等等这些是非托管资源。” https://www.cnblogs.com/enamorbreeze/p/4711468.html
那就一定是上下文中使用了非托管资源了,难道是上下文对象不销毁,那么由它创建的数据库连接就不会关闭?应该不是,因为我在前面弄EF时候看过EF生成的SQL和SQL的执行情况
比如我根据导航属性订单来查询产品,订单和产品一对多的关系
var res = ctx.Products.Where(x => x.Order != null).ToList(); Console.WriteLine(JsonConvert.SerializeObject(res, set));
SQL的执行情况是这样
可以看到EF每执行一次SQL,就会打开/关闭一次数据库连接
那就说明他自己有关闭数据库连接的能力啊,并不是我的上下文对象销毁了,数据库连接才关闭。
但是总的来说上下文你不使用using来包裹,就需要手动释放资源,肯定是用到了哪些非托管资源。
如何更干净地使用上下文
我一开始使用EF,实例化上下文和普通对象是一样的写法,因为我不知道除非using包裹就要手动释放资源。
using几年前我就见到了,一直没有去弄过,不知道怎么回事,在写ado的时候我用的try/catch也看到有人写using
原来这个using可以看做就是try/catch的语法糖,using经过反编译后的本质是创建try/finally块,并最终在finally中调用Dispose方法释放。
书中这样说:”在使用上下文时,始终要谨记一个准则:一个请求对应一个上下文。在项目中上下文全局唯一,这种方式显然不可取,在多线程web应用程序中性能极低,尤其对并发操作。“
我们来看三种上下文使用方式
Using Pattern 使用using,这种方式的缺点,控制器中的方法可能用到上下文的情况非常多,这样就会到处充斥这些using
using(EFDbContext ctx = new EFDbContext()) { }
View Code
DisPose Pattern 上下文和控制器一同创建和销毁
public class TestController : Controller { private EFDbContext _ctx; public TestController() { _ctx = new EFDbContext(); } protected override void Dispose(bool disposing) { if (disposing) { _ctx.Dispose(); } base.Dispose(disposing); } }
View Code
Dependency Injection 依赖注入的方式是最被开发者推荐的模式
public class TestController : Controller { private EFDbContext _ctx; public TestController(EFDbContext ctx) { _ctx = ctx; } }
View Code
现在上下文就不是由控制器来创建和销毁了,而且被注入进来的,控制器只管使用就行了。我对依赖注入没有什么认识,一直没有去弄。注入明白,依赖呢?难道是这个被注入的对象和控制器的构造函数绑定在一起了,二者缺一不可,形成了依赖关系?
行吧,书中讲到了生命周期追溯,这里我就实在看不明白了,因为缺少经验,完全想象不出那种多数据库,分布式系统多线程和并行操作那种对我来说很陌生的环境,所以,这里我就不说了。
作者说上下文派生实例并非类线程安全,因此我们不能再多线程中访问上下文实例,这会造成多次查询通过相同的数据库连接同时并发返回,也会破坏通过上下文维护实体的变更追踪和工作单元的一级缓存,所以在多线程应用程序中必须为每个线程使用独立的上下文派生实例。
因此EF6.x引入了异步查询功能,异步查询只支持在同一时刻查询一次,超过一次就会抛出异常
来看一下
var res1 = ctx.Products.ToListAsync(); var res2 = ctx.Products.ToListAsync();
View Code
这样直接报错,并且VS崩溃
需要使用await来等待上一次查询完成,再接着执行下一步的查询
public async Task<string> Test() { JsonSerializerSettings set = new JsonSerializerSettings(); set.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; string result = string.Empty; using (EFDbContext ctx = new EFDbContext()) { var res1 = await ctx.Products.ToListAsync(); var res2 = await ctx.Products.ToListAsync(); var res3 = res1.Concat(res2); result = JsonConvert.SerializeObject(res3,set); } return result; } Task<string> res = new Program2().Test(); Console.WriteLine(res.Result);
View Code
行了,就这吧。弄明白了一些,也有很多没弄明白,路漫修远兮。