天天看點

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沒有主鍵的表資料時: 

Entity Framework初探
 所幸,微軟似乎意識到這個問題,于是默默地寫了一篇 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. 簡單起見,設有一張庫存表,表結構:
    Entity Framework初探
    ,木有主鍵,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>      
    這一步驟也可以直接在關系圖中設定
    Entity Framework初探
  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()做了什麼事,您的蛋基本上就碎了。 

Entity Framework初探

沒錯,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>會報異常,如下面示例代碼: 

Entity Framework初探

那麼我們不using行不行?using的作用是保證上下文呢能Dispose掉,上下文Dispose的作用是取消各實體對象由于儲存狀态指向上下文自身的引用,以及上下文指向它們的引用,這樣不論是實體對象還是上下文占用記憶體都能被GC回收(Dispose并不是我們下意識認為是關閉資料庫連接配接,資料庫連接配接在任意生成的SQL執行完就自動關閉)。也許我可以嘗試使用

文中提到的AsNoTracking特性,單獨列幾個Context作為全局上下文,不用using,因為本身不跟蹤實體狀态,是以不會導緻記憶體溢出,可以一直存在。注意AsNoTracking并不表示傳回的IQueryable能獨立于上下文存在,畢竟還需要上下文去構造SQL語句之類的工作。 

 ps:截圖例子中,若将兩個SearchXXX方法内的using去掉,會出現什麼情況呢?

Entity Framework初探

其餘代碼相同。看到,即使是同樣類型的兩個不同上下文執行個體,也不能放一起關聯查詢。 

5、其它

  • IQToolkit執行無誤,EF報錯: 
    Entity Framework初探
  • 引用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之深入分析 http://msdn.microsoft.com/en-us/magazine/hh781018.aspx 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