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,IQueryable
和IEnumerable
都是延遲執行的,它們之間的區別僅僅在於擴充套件方法的引數型別不同。(迭代/列舉方式不同?作用物件不同?)
IQueryable 和 IEnumerable 的區別
-
IQueryable
:擴充套件方法接受的是Expression
對於Linq to sql或Linq to Entity,
Expression
-
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個步驟:
- 它會把iq物件(轉變之前的) 的擴充套件方法翻譯成sql語句,查詢出資料載入到記憶體中,變為ie物件;
- 此時再把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)
{
}
參考
- LINQ查詢中的IEnumerable<T>和IQueryable<T>
- LINQ使用細節之.AsEnumerable()和.ToList()的區別
- 建議29:區別LINQ查詢中的IEnumerabl<T>和IQueryable<T> - 陸敏技《編寫高質量程式碼改善C#程式的157個建議》
轉 https://www.jianshu.com/p/daeae7ceec91?from=singlemessage&isappinstalled=0