Entity Framework初探

近期公司打算使用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返回的是上下文跟踪实体。
  • 在使用DbContext.Set<XXX>()时发生错误:实体类型不是当前上下文的模型的一部分——解决方法:在DbContext中增加针对该实体类型的属性 public DbSet<XXX> XXXs{ get; set; } 或 ToTable("TableName")。推测EF在初始化上下文会用到它们进行数据库映射?然而导航属性对应的实体类又不需要如此,如下:
    public class BillOrder
    {
        public int ID { get; set; }
        public string Title { get; set; }
        [ForeignKey("OrderID")]
        public ICollection<BillOrderSub> Items { get; set; }
    }
    
    public class BillOrderSub
    {
        public int ID { get; set; }        
        public int OrderID { get; set; }
    }

    此时只要在DbContext中写一行 public virtual DbSet<BillOrder> BillOrders { get; set; } 即可,使用context.Set<BillOrderSub>()也不会有错。。。

  • The entity or complex type 'Categories' cannot be constructed in a LINQ to Entities query——解决方法:This is by design, EF doesn't allow you to project the results of a query onto a mapped entity. You can either do what you've done and use a DTO which doesn't inherit from the mapped entity, or you could instantiate the TypeWrapper in memory by first projecting to an anonymous type, then using LINQ to Objects to project to a TypeWrapper——EF Core貌似没这个问题
  • 若有类继承了数据表对应的实体类,那么在SqlServer里,EF会给那个表加上一个名为Discriminator的列,存储数据来源(类名),然后生成的Sql语句会使用in去查询,基本上是in了所有类名(父类和所有子类),蛋疼;Postgresql里这种情况倒没发现。 总之如果不想让EF自作多情地额外加列,在子类定义上加上[NotMapped]特性即可。
  • public JsonResult GetOrder(int id)
    {
        var order = new BillOrder();
        using (var context = new Entities())
        {
            order = context.BillOrders.Find(id);
            var result = Json(order, JsonRequestBehavior.AllowGet);
            return result;
        }
                
    }

    此 ObjectContext 实例已释放,不可再用于需要连接的操作——当return JsonResult,同时序列化的对象拥有导航属性(且该属性未指定ScriptIgnore之类的特性),由于导航属性默认为延迟加载,就会抛出这个异常,即使如上述代码在using内返回也没用。可以认为真正的序列化过程是在后续步骤。

 

更多参考:

在Entity Framework中重用现有的数据库连接字符串

Entity Framework之深入分析 

Add/Attach and Entity States 

EF中使用SQL语句或存储过程

Entity Framework Code-Based Configuration (EF6 onwards)

 

转载请注明本文出处:http://www.cnblogs.com/newton/archive/2013/05/27/3100927.html

posted @ 2013-05-27 08:38  莱布尼茨  阅读(8017)  评论(17编辑  收藏  举报