首页 > 代码库 > Entity Framework初探
Entity Framework初探
参考页面:
http://www.yuanjiaocheng.net/entity/change-tracking.html
http://www.yuanjiaocheng.net/entity/Persistence-in-EF.html
http://www.yuanjiaocheng.net/entity/crud-in-connected.html
http://www.yuanjiaocheng.net/entity/crud-in-Disconnected.html
http://www.yuanjiaocheng.net/entity/add-entity-in-disconnected.html
近期公司打算使用EF,于是这两天我特地研究了它的一些特性。本文记录的是我的一些研究成果。。。哎哟,说成果是不是大了点?
ps:对于EF,每次它有新版发布,我都一笑而过,为啥?因为我一直非常安逸于使用一个叫IQToolkit的开源组件,该组件作者有专门写了一系列博文记录IQToolkit从无到有的诞生历程,我估计市面上很多基于Linq的ORM或多或少都借鉴过他的经验[和代码]。我从中也受益良多,虽然偶有不足,但大部分略作改造即可弥补。它和EF相比,恰如穷屌丝和高富帅,下面有几个地方我会拿它们作一对比。
1、原有项目引入EF
EF有个DB First模式,可以根据数据库自动生成POCO实体和映射关系,生成的实体是与数据表一一对应,各自独立的。若原有项目已存在实体类库,由于一些原因想保留之,比如各实体可能共享一些基类或接口,以便在业务层针对基类抽取共同逻辑,这些继承关系不想抛弃。我们可以这么做,当新建edmx文件后,删除所有自动生成的POCO,包括xxx.tt模板文件一同删除,否则当修改edmx时,系统会根据模板重新生成POCO。删完之后将xxx.Context.cs文件中的实体引用改为原项目实体。我们还可以修改xxx.Context.tt模板文件,使之生成的相应DbContext类(在xxx.Context.cs中)符合我们的要求。
2、EF是主键控
EF的上下文特性需要能唯一标识实体的方法,这无可厚非,然而EF非常固执地只认主键,当数据表没有主键时,对应的实体就不能Update和Delete,这是一个非常严重的“Bug”。很多人会问:“难道表不应该有主键吗?”不幸的是,这种情况很普遍。主键存在的意义是标示数据表中的某一条记录,以便于我们能通过它去精确定位[、更新和删除]数据。但很多时候我们并不会独独去get某一条记录。比如发货单,分为主表和子表,对子表的都是整单查询操作,或者数据汇总,或者根据业务字段作为索引去查,因此并不会为子表的记录新增一个毫无意义的主键。另一种考虑是,由于主键对Insert操作的效率影响,常用非聚集索引代替,以尽量减少全表排序。
当我们试图Delete没有主键的表数据时:
所幸,微软似乎意识到这个问题,于是默默地写了一篇How to: Create an Entity Key When No Key Is Inferred。不过这篇文章里的内容虽然号称是最新版本,但是跟我实际所得有很大出入,文中说没有主键的数据表是不会产生Model的(原话:If no entity key is inferred, the entity will not be added to the model.文中所述还是正确的,意思为如果数据库中没有主键且EF不能自动定义出主键(默认是所有字段为一个组合主键),如有字段为null的情况,而非我之前认为的单单数据库没有主键;另外EF自动定义的主键所在的表默认是只读的),I say:非也。然后后续的步骤更加不知所云。下面说说我是怎么处理的:
- 简单起见,设有一张库存表,表结构:,木有主键,now,从数据库生成Model;
- 用记事本打开edmx文件,我们会找到两处同样的片段:
1 <EntityType Name="Stock"> 2 <Key> 3 <PropertyRef Name="StorageID" /> 4 <PropertyRef Name="ProductID" /> 5 <PropertyRef Name="Quantity" /> 6 </Key> 7 <Property Name="StorageID" Type="int" Nullable="false" /> 8 <Property Name="ProductID" Type="int" Nullable="false" /> 9 <Property Name="Quantity" Type="int" Nullable="false" /> 10 </EntityType>
一个是在SSDL节点下,一个是CSDL节点(就刚才的文说在SSDL中是注释掉的,其实没有;说CSDL中没有,其实有的),由于没有主键,框架自作聪明地将所有字段都列为复合主键,而且该片段对应的实体是只读的……由于StorageID和ProductID已经组成了一个非聚集唯一索引(这么做的原因前已表述),对于UD操作来说等同于主键,因此删除<PropertyRef Name="Quantity" />片段变为:
1 <EntityType Name="Stock"> 2 <Key> 3 <PropertyRef Name="StorageID" /> 4 <PropertyRef Name="ProductID" /> 5 </Key> 6 <Property Name="StorageID" Type="int" Nullable="false" /> 7 <Property Name="ProductID" Type="int" Nullable="false" /> 8 <Property Name="Quantity" Type="int" Nullable="false" /> 9 </EntityType>
这一步骤也可以直接在关系图中设置
- 继续在记事本中查找<EntitySet Name="Stock" EntityType="DistributionModel.Store.Stock" store:Type="Tables" store:Schema="dbo" store:Name="Stock">......</EntitySet>这一段,改为<EntitySet Name="Stock" EntityType="DistributionModel.Store.Stock" store:Type="Tables" Schema="dbo" />,目测store:XXX就是表明对应实体为只读。
- 在Stock实体属性StorageID和ProductID加上特性[Key]。完毕。
ps:EF并不负责维护使用该方式设置的“主键”的唯一性,这仍然需要我们在业务层面控制。
3、什么!?EF的字典里没有“批量”的概念?
上述方法“完美地”解决了主键问题,我们来试试看:
1 [TestMethod] 2 public void TestMethod6() 3 { 4 using (var entities = new DistributionEntities()) 5 { 6 var test = entities.Stock.Where(o => o.Quantity == 0).ToList(); 7 foreach (var t in test) 8 entities.Stock.Remove(t); 9 entities.SaveChanges(); 10 } 11 }
不出所料,执行成功,不过我要说的并不是这个,而是这种删除模式——先从数据库里取出要删的数据,然后代码层跟上下文说我要将这些数据从表里删除,上下文再去执行最后的步骤——是不是很坑爹?我相信您肯定有蛋疼的感觉(这里假定你是男人),and,(人生最害怕的就是这个and!)如果您去到数据库里走一遍跟踪,想看看entities.SaveChanges()做了什么事,您的蛋基本上就碎了。
没错,EF的上下文特性的前提是所有对数据的更改都要通过主键定位完成,这也就是第2条描述的内容。so,它会针对每个已编辑或已删除实体单独生成一条语句。如果一次操作有上万个实体需要更新,效率会否有影响?
不管怎样,有人按捺不住,写了一个扩展组件EntityFramework.Extended,可以通过NuGet获取,可参看Entity Framework Batch Update and Future Queries。现在我们可以这样:
1 [TestMethod] 2 public void TestMethod4() 3 { 4 using (var entities = new DistributionEntities()) 5 { 6 entities.Stock.Delete(o => o.Quantity == 0); 7 } 8 }
避免了往返数据库两次的尴尬,同时只生成了一条语句:
DELETE [dbo].[Stock] FROM [dbo].[Stock] AS j0 INNER JOIN ( SELECT [Extent1].[StorageID] AS [StorageID], [Extent1].[ProductID] AS [ProductID], [Extent1].[Quantity] AS [Quantity] FROM (SELECT [Stock].[StorageID] AS [StorageID], [Stock].[ProductID] AS [ProductID], [Stock].[Quantity] AS [Quantity] FROM [dbo].[Stock] AS [Stock]) AS [Extent1] WHERE 0 = [Extent1].[Quantity] ) AS j1 ON (j0.[StorageID] = j1.[StorageID] AND j0.[ProductID] = j1.[ProductID] AND j0.[Quantity] = j1.[Quantity])
似乎跟预想的有点不太一样,印象中,偶觉得,可能,大概,或许,Maybe不应该是这么长一段吧……在代码的世界中,追求的是短小精悍!于是我招呼屌丝IQToolkit给观众展示一下:
1 [TestMethod] 2 public void TestMethod5() 3 { 4 QueryGlobal distrContext = new QueryGlobal("DistributionConstr"); 5 distrContext.LinqOP.Delete<Stock>(o => o.Quantity == 0); 6 }
这里的distrContext可以理解为上下文,关于这点后面说。LinqOP是我封装IQToolkit的通用操作,最终数据库跟踪到这么一条:
DELETE FROM [Stock] WHERE ([Quantity] = 0)
所以说,屌丝总有逆袭时!由于只对必要字段做比较,肯定比EntityFramework.Extended生成的语句执行效率高。如果真用上EF,我得改进这方面的SQL构造算法,要是哪位朋友已经做了相关工作,请务必提供出来造福猿类社会……
ps:关于通过主键定位数据然后删除 or 判断Quantity是否为0,若是则删除,两者效率对比情况如何我没做深入研究,估计具体情况具体分析,有经验的朋友可以说说看。
4、所谓上下文
EF的上下文有两个概念:DbContext和ObjectContext,它们有一定区别,能相互转换,具体可看Data Points,这里一般指DbContext。我认为,上下文的主要作用就是跟踪实体状态,这样注定了会生成如第3条那样的数量巨大的SQL语句,也就难怪没有批量更新的原生方法。由于上下文在SaveChanges时提交所有已更改的数据,所以我们也不能将之设为单例模式,只能在每次用到的时候,不厌其烦地using。优点是使得SaveChanges能让多个操作集中在一次数据库连接会话内完成。but,很多时候我们并不需要跟踪实体状态,也不需要更新数据,比如报表系统。我喜欢将一些通用操作抽取出来,比如我封装IQToolkit的几个方法:
1 /// <summary> 2 /// 查询符合条件的集合 3 /// </summary> 4 /// <typeparam name="T">类型参数</typeparam> 5 /// <param name="condition">查询条件</param> 6 /// <param name="order">排序规则,目前只支持单属性升序排序</param> 7 /// <param name="skip">从第几条数据开始</param> 8 /// <param name="take">取几条数据</param> 9 /// <returns>符合条件的对象集合</returns> 10 public IQueryable<T> Search<T>(Expression<Func<T, bool>> condition = null, Expression<Func<T, dynamic>> order = null, int skip = 0, int take = int.MaxValue) 11 { 12 return Search(t => t, condition, order, skip, take); 13 } 14 15 public IQueryable<R> Search<T, R>(Expression<Func<T, R>> selector, Expression<Func<T, bool>> condition = null, Expression<Func<T, dynamic>> order = null, int skip = 0, int take = int.MaxValue) 16 { 17 var entities = this._provider.GetTable<T>(typeof(T).Name); 18 if (selector == null) 19 throw new ArgumentNullException("selector", "涉及类型转换的构造委托不能为空"); 20 if (condition == null) 21 condition = t => true; 22 IQueryable<T> query = entities.Where(condition); 23 if (order != null) 24 query = query.OrderBy(order).Skip(skip).Take(take); 25 return query.Select(selector); 26 }
注意它返回的是IQueryable<T>,因此能在外部多次调用,并任意组装,一定程度上更灵活。this._provider.GetTable<T>(typeof(T).Name),要去哪个表里取数,它并没有上下文的概念。用EF则不能如此封装,IQueryable<T>只在上下文中才有效,你想在上下文using块返回后再去使用IQueryable<T>会报异常,如下面示例代码:
那么我们不using行不行?using的作用是保证上下文呢能Dispose掉,上下文Dispose的作用是取消各实体对象由于保存状态指向上下文自身的引用,以及上下文指向它们的引用,这样不论是实体对象还是上下文占用内存都能被GC回收(Dispose并不是我们下意识认为是关闭数据库连接,数据库连接在任意生成的SQL执行完就自动关闭)。也许我可以尝试使用Data Points文中提到的AsNoTracking特性,单独列几个Context作为全局上下文,不用using,因为本身不跟踪实体状态,所以不会导致内存溢出,可以一直存在。注意AsNoTracking并不表示返回的IQueryable能独立于上下文存在,毕竟还需要上下文去构造SQL语句之类的工作。
ps:截图例子中,若将两个SearchXXX方法内的using去掉,会出现什么情况呢?
其余代码相同。看到,即使是同样类型的两个不同上下文实例,也不能放一起关联查询。
5、其它
- IQToolkit执行无误,EF报错:
- 引用EF后,需要using System.Data.Entity;否则木有智能提示!
- 当已存在实体类库和数据库,要引入EF,需要注意实体类要显式定义与数据表的列名对应的所有属性(计算列未知是否一定要定义相应属性);而IQToolkit的实体类可以缺省某些类型的列(如该列自动填充默认值)。当数据表中的列没有在类型中找到对应属性,会报“the entity type is not part of the model for the current context”(中文为:实体类型不是当前上下文的模型的一部分)的异常,让人摸不着头脑。我曾为此折腾了足足两天,最后才发现是因为少了一个字段!ps:不过EF中的实体可以定义数据表中不存在的额外字段,而不会报错。
- 在查询条件中设置如o.CreateTime <= time.AddDays(1).Date条件,EF会报“Linq to Entities不识别方法DateTime.AddDays(double),该方法无法转为存储过程”的错误,IQToolkit表示无压力。这是因为EF默认在Query内部不支持正常方式调用CLR方法,而是提供了EntityFunctions,其中内置了部分常用方法,还提供了自定义方法的方式,在运行时这些方法会转换为对应的sql语句(估计自定义方法的方法体可以不用实现,因为它起到的是映射作用)。
- dbContext.Database.SqlQuery返回结果上下文不跟踪,默认情况下,dbContext.DbSet.SqlQuery返回的是上下文跟踪实体。
更多参考:
在Entity Framework中重用现有的数据库连接字符串
Entity Framework之深入分析
Add/Attach and Entity States
EF中使用SQL语句或存储过程
转载请注明本文出处:http://www.cnblogs.com/newton/archive/2013/05/27/3100927.html
Entity Framework初探