首先定義 IEnumerable 與 IQueryable 這兩個 Interface 主要是兩種應用,
IEnumerable:用於列舉記憶體中的資料,IEnumerable<T> 只是變成泛型,並有很多 Enumerable 擴充方法可用。
IQueryable:用於列舉自訂資料來源的資料,IQueryable<T> 只是變成泛型,並有很多 Queryable 擴充方法可用。
而這兩個 Interface 所擁有的方法跟屬性其實不多,可查看 msdn IQueryable, IEnumerable,
實際在使用的時候,都是使用 Enumerable 或 Queryable 的擴充方法來建立新的 IEnumerable<T> 或 IQueryable<T> 物件,
等到使用像是使用 Enumerable.ToList() 擴充方法時,
IEnumerable<T> 就會執行 IEnumerable.GetEnumerator() 來列舉資料,
而 IQueryable 繼承了 IEnumerable,
所以 IQueryable.GetEnumerator() 就是真正執行資料查詢的,並列舉資料的觸發點。
Entity Framework 查詢資料主要就是查 Database 的資料,所以預設 DbSet<T> 就實作 IQueryable<T>,
這樣就會以自訂資料來源 Provider 這方式來解析 Expression 執行想要的資料,
現在先舉例一段簡單的 Linq To Entities 代碼:
var list = db.Orders.Where(x => x.OrderId == 5).ToList();
Orders 就是 DbSet<Order>,因為 DbSet<T> 實作 IQueryable<T>,
所以優先會用衍生繼承的實作類別 Where 擴充方法,實際長這樣:
public static IQueryable<TSource> Where<TSource>(this IQueryable<TSource> source, Expression<Func<TSource, bool>> predicate) {
if (source == null)
throw Error.ArgumentNull("source");
if (predicate == null)
throw Error.ArgumentNull("predicate");
return source.Provider.CreateQuery<TSource>(
Expression.Call(
null,
GetMethodInfo(Queryable.Where, source, predicate),
new Expression[] { source.Expression, Expression.Quote(predicate) }
));
}
你會看到這裡的 Where 擴充方法不是 Enumerable.Where,而是 Queryable.Where,
Queryable.Where 主要使用 Provider.CreateQuery<T> 來建立新的 IQueryable<T>,
主要就是為了保留 Expression 與 Provider ,
在呼叫 ToList() 的時候,上面提到會執行 GetEnumerator(),
而現在實際的物件是 InternalDbSet (實作了抽象類別 DbSet),
InternalDbSet 會實作 GetEnumerator 執行的方式,
裡面就會用 IQueryable<T> 的 Expression 與 Provider 來轉譯成 SQL 執行查詢取得資料,
所以像下面這樣只要 IQueryable<T> 尚未被執行過列舉,先轉換 IEnumerable<T> 在轉換 IQueryable<T> 是沒差的:
var query1 = db.Orders.Where(x => x.OrderId != 5);
var query2 = query1.AsEnumerable();
var query3 = query2.AsQueryable();
var list = query3.Where(x => x.CreateDate < DateTime.Now).ToList();
轉譯的 SQL:
SELECT [x].[OrderId], [x].[CreateDate]
FROM [Orders] AS [x]
WHERE ([x].[OrderId] <> 5) AND ([x].[CreateDate] < GETDATE())
但是如果中間已經先被列舉了,例如像這樣轉換為 IEnumerable 就先 Where 日期條件:
var query1 = db.Orders.Where(x => x.OrderId != 5);
var query2 = query1.AsEnumerable();
var query3 = query2.Where(x=>x.CreateDate < DateTime.Now).AsQueryable();
var list = query3.ToList();
轉譯的 SQL:
SELECT [x].[OrderId], [x].[CreateDate]
FROM [Orders] AS [x]
WHERE [x].[OrderId] <> 5
ToList() 在執行中間的 Enumerable 的
public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource, int, bool> predicate)
擴充方法時候,就是在呼叫 IEnumerable.GetEnumerator(),這個動作就會列舉資料,
所以就會依照前面有 Queryable.Where(x => x.OrderId != 5) 的 IQueryable<T>,
先查詢了DB,查詢回來的資料已經是記憶體,
然後在記憶體中過濾 x.CreateDate < DateTime.Now 資料,
此時當下的物件型態已經不在是實作 IQueryable<T> 的物件,
頓時像是失去了魔法,後續只能在記憶體中過濾資料。
所以實際應用重點是
要執行 IQueryable<T>.GetEnumerator() 相關方法的時機,應該是確定要進行DB查詢了,在那之前,你可以任意組合查詢條件,
而 IEnumerable<T>.GetEnumerator() 則是用於所需的結果資料已在記憶體中(例如ToList()),作為快取的查詢結果,可重複只在記憶體中快速的查詢。
參考文章: