1. 程式人生 > 其它 >IQueryable 和 IEnumerable 的區別

IQueryable 和 IEnumerable 的區別

 

來源:https://blog.guoqianfan.com/2019/11/17/distinguish-between-IQueryable-and-IEnumerable-in-CSharp/

前言

不管是Linq to object,還是Linq to sql或Linq to Entity,IQueryableIEnumerable都是延遲執行的,它們之間的區別僅僅在於擴充套件方法的引數型別不同。(迭代/列舉方式不同?作用物件不同?)

IQueryable 和 IEnumerable 的區別

  • IQueryable:擴充套件方法接受的是Expression

    對於Linq to sql或Linq to Entity,Expression

    必須要能轉成sql,否則會報錯。

  • IEnumerable:擴充套件方法接受的是Func(Func是C#語法)。

    IEnumerable跑的是Linq to Object,會強制從資料庫中讀取所有資料到記憶體裡,所以可以使用C#語法。

    對於Linq to sql或Linq to Entity,Func是無限制的,因為它是使用C#語法操作資料的。這也從側面說明:資料已經讀取到記憶體中了。

    IEnumerable的擴充套件方法是Func,不會轉換為sql。轉換為sql的是內部的IQueryable。所以要注意條件最好限制在IQueryable裡,否則IQueryable可能會讀取大量資料,增加耗時和記憶體。

AsEnumerable() 和 ToList() 的區別

  • ToList():立即執行。會立即執行sql,取出資料到記憶體中。

  • AsEnumerable():延遲執行,真正使用時才執行sql讀取資料。此處有坑,一定要往下看

IQueryable物件使用AsEnumerable()後,仍然是延遲執行,不過此時物件本質已經變了。

前面已經說了 IEnumerable的擴充套件方法接受的是Func(C#語法),當ie物件(iq轉變) 真正使用時,會有2個步驟:

  1. 它會把iq物件(轉變之前的) 的擴充套件方法翻譯成sql語句,查詢出資料載入到記憶體中,變為ie物件;
  2. 此時再把ie物件(轉變之後的) 的擴充套件方法,使用C#求解,得到最終結果。

例如:

iq物件的Skip、Take方法,會被翻譯成sql,在資料庫裡執行取出最終結果。

而ie物件的Skip、Take方法,則會取出全部資料到記憶體中,在記憶體中執行Skip、Take,會耗費大量資源。

使用場景

  • IQueryable:使用EFCore動態拼接多個where條件時使用。(延遲查詢,每次真正使用時都會重新讀取資料。)

  • IEnumerable:當擴充套件方法無法轉換為sql時,可以使用AsEnumerable()轉換為IEnumerable。因為IEnumerable的擴充套件方法都是使用C#語法處理資料的。(延遲查詢,每次真正使用時都會重新讀取資料。)

  • ToList():當 where條件已經確定了,就可以使用ToList()從資料庫中立即取出資料,後面重複使用這些資料就行。

不過為了省事,我一般都是使用IQueryable拼接好條件後,直接ToList()來使用了。。。

備註

異常:System.InvalidOperationException: 無法列舉查詢結果多次

異常出現條件

在Linq to sql或EF(非EFCore)中,直接執行sql語句來查詢資料後,對資料集(IEnumerable)進行多次列舉操作就會引發這個異常。

經測試,多次Count()會引發此異常。其他的Sum()foreach等等應該也是,有待驗證。。。

我的理解是:資料集(IEnumerable)是使用列舉器來處理每項資料的,而列舉器只能走一次。 搞不懂列舉器和迭代器了,需要研究下。。。

注意,EFCore中不會出現這個異常,原因請搜尋efcore執行sql

解決方法

  • 方法1:把查詢結果ToList(),後續使用List來操作資料。
  • 方法2【推薦】: 拋棄內建的,使用Dapper,因為Dapper的查詢結果本質是List。(多結果集不是,更多資訊搜尋Dapper。)

異常重現程式碼

下面的程式碼是Linq to sql的,網上說EF也會出現該異常,程式碼應該類似。

另外網上搜索該異常大部分都是執行儲存過程時出現的,其實也是直接執行sql來查詢資料,本質一樣。

string sql = @"
select top 100 *
from [dbo].[BaseSupplier_OTAOnline]";

//return db.ExecuteQuery<T>(sql, parameters);
IEnumerable<BaseSupplier_OTAOnline> ieBs02 = bdb.QueryBySql<BaseSupplier_OTAOnline>(sql);//"exec Pro_BaseSpOtaOnline_Test01"

int count = ieBs02.Count();

ieBs02 = ieBs02.Skip(1).Take(2);

//****此處會引發異常****
int count02 = ieBs02.Count();

誤區:對 iq物件 和 ie物件 使用foreach時,對於迴圈的每項都要查詢資料庫

錯誤!

foreach針對的是資料集整體物件(迭代器?)。當使用foreach時,不管是iq物件還是ie物件,它們都是查詢資料庫一次,然後開始迴圈,直至迴圈結束。不過,當後續再次使用iq物件或ie物件的具體資料時,它們仍然會再次查詢資料庫。

注意:iq物件的結果是資料集。它只能把當前儲存的表示式樹轉換為sql。它無法對其進行處理來做到一次一條的取出資料,因為根本就不可能!怎麼能無中生有呢?

反向驗證

也可以這樣想:如果是一條一條取資料的話,程式怎麼知道每次應該取哪條資料?

  • 使用DataReader

    不行,效率太低下。因為取出每條資料後,還需要對資料進行一系列的操作(程式碼邏輯),這需要耗費時間。而DataReader是需要線上保持資料庫連線的,耗時太長會導致同一時間有很多資料庫連線,很快就會達到資料庫連線池上限。這種方法很不可取。

  • 對生成的sql進行top 1處理?

    那要怎麼知道每次取出哪條資料呢?使用上一條資料的資訊作為where條件?不行,這麼做太傻逼,網路資料傳輸增加;查詢效率也低下;佔用資料庫連線池資源。種種缺點,簡單問題複雜化。

由上面的反例可以看出,一條一條查資料可以實現,但是太二逼。完全不如一次性全部讀取資料的好。

其他

IQueryable和IEnumerable生成sql的測試程式碼

先說下結論:

  • 只會把IQueryable的條件(Expression)翻譯成sql,IEnumerable的條件(Func)不會被翻譯成sql。程式碼中生成的sql可以驗證。
  • 二者都是延遲執行的,真正使用過的時候才會查詢資料庫。

NetFramework

測試環境:

  • .NET Framework 4.5
  • LINQ to SQL類(不是EntityFramework)
            BaseSpDB bdb = new BaseSpDB();
            //不查詢資料庫
            IQueryable<BaseSupplier_OTAOnline> iqBs = bdb.baseSpByAll().Where(p => p.ID < 10);
            //不查詢資料庫
            IEnumerable<BaseSupplier_OTAOnline> ieBs = iqBs.AsEnumerable();
            //不查詢資料庫
            ieBs = ieBs.Where(p => p.ID > 5);

            //執行sql
            //只執行iq的條件
            //查詢資料庫
//exec sp_executesql N'SELECT [t0].[ID], [t0].[OTAName], [t0].[OnlineSupplier], [t0].[PushUrl], [t0].[Note], [t0].[EditCode], [t0].[AddUser], [t0].[PushDate], [t0].[GetDate], [t0].[EditDate], [t0].[AddDate]
//FROM [dbo].[BaseSupplier_OTAOnline] AS [t0]
//WHERE [t0].[ID] < @p0',N'@p0 int',@p0=10
            List<BaseSupplier_OTAOnline> bsList = ieBs.ToList();

            //再次查詢資料庫
//exec sp_executesql N'SELECT [t0].[ID], [t0].[OTAName], [t0].[OnlineSupplier], [t0].[PushUrl], [t0].[Note], [t0].[EditCode], [t0].[AddUser], [t0].[PushDate], [t0].[GetDate], [t0].[EditDate], [t0].[AddDate]
//FROM [dbo].[BaseSupplier_OTAOnline] AS [t0]
//WHERE [t0].[ID] < @p0',N'@p0 int',@p0=10
            foreach (var item in ieBs)
            {
                
            }

NetCore

測試環境:

  • AspNetCore 2.1
  • EFCore
            //不查詢資料庫
            IQueryable<BaseSupplier_OTAOnline> iqOta = ctx.BaseSupplier_OTAOnline.Where(p => p.ID < 10);

            //不查詢資料庫
            IEnumerable<BaseSupplier_OTAOnline> ieOta = iqOta.AsEnumerable();

            //不查詢資料庫
            ieOta = ieOta.Where(p => p.ID > 5);

            //執行sql
            //只執行iq的條件
            //查詢資料庫
//SELECT [p].[ID], [p].[AddDate], [p].[AddUser], [p].[EditCode], [p].[EditDate], [p].[GetDate], [p].[Note], [p].[OTAName], [p].[OnlineSupplier], [p].[PushDate], [p].[PushUrl]
//FROM [BaseSupplier_OTAOnline] AS [p]
//WHERE [p].[ID] < 10
            List<BaseSupplier_OTAOnline> bsList = ieOta.ToList();

            //再次查詢資料庫
//SELECT [p].[ID], [p].[AddDate], [p].[AddUser], [p].[EditCode], [p].[EditDate], [p].[GetDate], [p].[Note], [p].[OTAName], [p].[OnlineSupplier], [p].[PushDate], [p].[PushUrl]
//FROM [BaseSupplier_OTAOnline] AS [p]
//WHERE [p].[ID] < 10
            foreach (var item in ieOta)
            {

            }

參考

  1. LINQ查詢中的IEnumerable<T>和IQueryable<T>
  2. LINQ使用細節之.AsEnumerable()和.ToList()的區別
  3. 建議29:區別LINQ查詢中的IEnumerabl<T>和IQueryable<T> - 陸敏技《編寫高質量程式碼改善C#程式的157個建議》
    2人點贊   日記本

 

 轉 https://www.jianshu.com/p/daeae7ceec91?from=singlemessage&isappinstalled=0