IQueryable VS IEnumerable
在 EFCore 和 Linq 中,一个重要的接口就是 IQueryable,例如 EFCore 的 Where 方法就会返回一个 IQueryable,DbSet 类也实现了这个接口。这个接口有什么用呢?
首先,IQueryable 接口继承了 IEnumerable 接口,所以后者有的功能前者也有,比如它们都表示该对象可以迭代。
那 IQueryable 和 IEnumerable 有什么区别呢?最重要的区别就是查询过滤的时机不同。IEnumerable 通常被用于位于内存的数组或者集合,这时查询是在本机进行的,.NET 在本机内存中对该数组或集合进行数据过滤,返回符合条件的部分。但如果数据并不在本机中,就可能将过滤操作在数据库系统中完成,也可能将过滤操作在本机中完成。
在一般情况下,IQueryable 中的查询条件会被转换为 SQL 语句,然后交给数据库系统执行,这个 SQL 语句就已经包含了数据过滤。在 EFCore 中,我们可以用 Linq 语法来表示查询条件,但这个 Linq 语句其实并没有在本机中运行过,它只是被转换为 SQL 语句交给数据库,然后 EFCore 从数据库拿取过滤后的数据而已。这个是 IQueryable 和 IEnumerable 的最重要的区别。
using (MyDbContext ctx = new MyDbContext()) { var books = ctx.Books.Where(e => e.Price > 10); foreach (var book in books) { Console.WriteLine(book.Title); } }
上面这段代码,Where 方法里的查询条件会被翻译为 SQL 语句的一部分,完整的 SQL 语句如下:
SELECT [t].[Id], [t].[Price], [t].[Time], [t].[Title] FROM [T_Books] AS [t] WHERE [t].[Price] > 10.0E0
这里使用了 var 类型推断,其实这里 books 的类型是 IQueryable<T>(在这里,T 是 Book)。
如果使用 IEnumerable<T>,那么事情就不一样了。.NET 会先通过 SQL 语句从数据库拿取所有数据,然后在本机中进行过滤。即此时表示查询条件的 Linq 语句会在本机中执行,而不会翻译为 SQL 语句的一部分。通过 AsEnumerable 方法可以将 IQueryable<T> 接口转换为 IEnumerable<T> 接口,由于前者继承了后者,这个转换是安全的。
using (MyDbContext ctx = new MyDbContext()) { var books = ctx.Books.AsEnumerable().Where(e => e.Price > 10); foreach (var book in books) { Console.WriteLine(book.Title); } }
EFCore 生成的 SQL 语句如下:
SELECT [t].[Id], [t].[Price], [t].[Time], [t].[Title] FROM [T_Books] AS [t]
可以看出这只是一个简单的 Select 语句,将整个表都获取了,过滤是在本机进行的。
从这个区别可以看出 IQueryable 的优势。一般情况下,IQueryable 的行为显然更符合预期,因为它可以提高性能、减少网络负担并最大化利用 SQL 语句。而使用 IEnumerable,可能会造成极高的内存占用。除非你希望在本机内存中处理数据,或者过滤条件会给数据库服务器造成极大负担,否则应该使用 IQueryable。
IQueryable 这种在服务器执行数据查询的机制称为 Server Evaluation,而 IEnumerable 这种在客户端执行数据查询的机制称为 Client Evaluation。
在 EFCore 中,绝大多数时候的默认行为就是 Server Evaluation,例如 Where 方法的查询。但在顶层投影的时候,部分行为会采用 Client Evaluation,所谓顶层投影,一般是指查询的最后一次 Select 操作。例如下面这段代码:
static async Task Main(string[] args) { using (MyDbContext ctx = new MyDbContext()) { var books = ctx.Books.Where(e => e.Price > 10).Select(e => new { Title = StrTransform(e.Title) }); foreach (var book in books) { Console.WriteLine(book.Title); } } } static string StrTransform(string str) { return new string(str.Reverse().ToArray()); }
这段代码实现查询价格大于 10 的书,并通过 Select 操作将书的标题返回,但在投影时调用了一个静态方法将标题反转。显然 EFCore 并不能将这个静态方法翻译为 SQL 语句,将这个静态方法放进 Where 里执行是会报错的。但如果放在最后一次 Select 操作里不会报错,EFCore 会将这个顶层投影拿到本机执行,但前面的 Where 查询依旧会在数据库服务器执行。这段代码生成的 SQL 语句和上面那段是一样的。