首页 > 代码库 > 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:非也。然后后续的步骤更加不知所云。下面说说我是怎么处理的: 

  1. 简单起见,设有一张库存表,表结构:技术分享,木有主键,now,从数据库生成Model;
  2. 用记事本打开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>

    这一步骤也可以直接在关系图中设置技术分享

  3. 继续在记事本中查找<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就是表明对应实体为只读。
  4. 在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初探