IEnumerable 與 IQueryable 以 Entity Framework 為例的差異

  • 3810
  • 0

首先定義 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>,才能執行 IQueryable<T> 的 Expression 與 Provider。

 所以像下面這樣只要 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()),作為快取的查詢結果,可重複只在記憶體中快速的查詢。

 

參考文章:

LINQ to Entities
Enumerable
Queryable
EntityFramework