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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM