Entity Framework中IQueryable, IEnumerable, IList的区别

本文介绍如何使用TracingandCachingProviderWrappersforEntityFramework工具追踪Entity Framework生成的SQL,并通过实例比较了IQueryable、IEnumerable和IList在性能与数据一致性上的区别。

使用工具追踪EF生成的SQL

使用Entity Framework等ORM框架的时候,SQL对于使用者来说是透明的,往往很多人也不关心ORM所生成的SQL,然而系统出现性能问题的时候就必须关注生成的SQL以发现问题所在。

使用过Toplink的朋友知道很只要设置日志打印级别=FINE就可以配置使之生成的SQL在服务器中打印出来,Entiry Framework没有那么幸运,在以前要检测生成SQL的唯一方法是SQL Server Profiler,但使用起来并不方便,结果也不能自动保存到文件中。

Tracing and Caching Provider Wrappers for Entity Framework是Entity Framework Team新推出的开源SQL追踪和二级缓存的解决方案。原理是在负责执行具体SQL语句的data provider(SqlClient或者其他Client)之上插入了一层WrappingProvider,用于监控DbCommand.ExecuteReader(), ExecuteScalar() and ExecuteNonQuery(),将Sql命令输出到指定介质或者将查询结果缓存起来以重用。

使用方法很简单,下载源代码编译后将dll添加到项目中,新加一个类WrappedNorthWindEntities继承原有的Entities即可,详见源代码中的示例。

测试IQueryable, IEnumerable, IList的区别

下面我们使用EF Wrapper来监测Entify Framework中IQueryable, IEnumerable和IList所生成的SQL。

复制代码
private static void TestIQueryable()
{
    using (var ctx = new WrappedNorthWindEntities())
    {
        IQueryable<Product> expression = ctx.Products.Take(5);
        IQueryable<Product> products = expression.Take(2);   // A  不执行SQL
        Console.WriteLine(products.Count());          // B SELECT COUNT(1) FROM ( SELECT TOP (2) * FROM ( SELECT TOP (5) * FROM [dbo].[Products] ))
        Console.WriteLine(products.Count());          // C  SELECT COUNT(1) FROM ( SELECT TOP (2) * FROM ( SELECT TOP (5) * FROM [dbo].[Products] ))
        foreach (Product p in products)             // D  SELECT TOP (2) * FROM ( SELECT TOP (5) * FROM [dbo].[Products] 
        {
            Console.WriteLine(p.ProductName);
        }
        foreach (Product p in products)              // E  SELECT TOP (2) * FROM ( SELECT TOP (5) * FROM [dbo].[Products] )
        {
            Console.WriteLine(p.ProductName);
        }
    }
}
复制代码

 

复制代码
private static void TestIEnumerable()
{
    using (var ctx = new WrappedNorthWindEntities())
    {
        IEnumerable<Product> expression = ctx.Products.Take(5).AsEnumerable();
        IEnumerable<Product> products = expression.Take(2);  // A  不执行SQL
        Console.WriteLine(products.Count());          // B SELECT TOP (5) * FROM [dbo].[Products]
        Console.WriteLine(products.Count());          // C  SELECT TOP (5) * FROM [dbo].[Products]
        foreach (Product p in products)             // D  SELECT TOP (5) * FROM [dbo].[Products] 
        {
            Console.WriteLine(p.ProductName);
        }
        foreach (Product p in products)              // E  SELECT TOP (5) * FROM [dbo].[Products]
        {
            Console.WriteLine(p.ProductName);
        }
    }
}
复制代码

 

复制代码
private static void TestIList()
{
    using (var ctx = new WrappedNorthWindEntities())
    {
        var expression = ctx.Products.Take(5);
        IList<Product> products = expression.Take(2).ToList(); // A  SELECT TOP (2) * FROM ( SELECT TOP (5) * FROM [dbo].[Products]

        Console.WriteLine(products.Count());            // B 不执行SQL
        Console.WriteLine(products.Count());            // C  不执行SQL
        foreach (Product p in products)               // D  不执行SQL
        {
            Console.WriteLine(p.ProductName);
        }
        foreach (Product p in products)                // E  不执行SQL
        {
            Console.WriteLine(p.ProductName);
        }
    }
}
复制代码

测试结果

  1. IQueryable和IEnumerable都是延时执行(Deferred Execution)的,而IList是即时执行(Eager Execution)
  2. IQueryable和IEnumerable在每次执行时都必须连接数据库读取,而IList读取一次后,以后各次都不需连接数据库。前两者很容易造成重复读取,性能低下,并且可能引发数据不一致性
  3. IQueryable和IEnumerable的区别:IEnumberalb使用的是LINQ to Object方式,它会将AsEnumerable()时对应的所有记录都先加载到内存,然后在此基础上再执行后来的Query。所以上述TestIEnumerable例子中执行的SQL是"select top(5) ...",然后在内存中选择前两条记录返回。

以下是一个IQueryable引发数据不一致性的例子:记录总数和记录详情两者本应一致,但由于IQueryable前后两次读取数据库,结果是现实有10条记录,却输出11条详情。

复制代码
    IQueryable<Product> products = ctx.Products.All();
    //开始的时候数据库product表中有10条记录, count = 10
    int count = products.Count();
    Console.WriteLine("Count of products:"+count);  
        
    //此时另一进程添加一个产品进数据库
    //会重新读取数据库并输出11个产品名称
    foreach (Product p in products)       
    {
    Console.WriteLine(p.ProductName);    
    }
复制代码

结论

基于性能和数据一致性这两点,我们使用IQueryable时必须谨慎,而在大多数情况下我们应使用IList

  • 当你打算马上使用查询后的结果(比如循环作逻辑处理或者填充到一个table/grid中),并且你不介意该查询会即时执行,使用ToList()
  • 当你希望查询后的结果可以供调用者(Consummer)作后续查询(比如这是一个"GetAll"的方法),或者你希望该查询延时执行,使用AsQueryable()
using System.Net; using System.Text.RegularExpressions; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; namespace TestProject; internal class Answer { #region >试题1: 简单的依赖注入< /// <summary> /// 请使用DI注入IUserService服务和MainDbContext实例,以便获取到用户登陆服务 /// </summary> /// <param name="services"></param> private readonly IUserService _userService; private readonly DbContext _db; public Answer(IUserService userService, DbContext db) { _userService = userService; _db = db; } internal static void DependencyInjection(IServiceCollection services) { //数据库连接为sqlite,connectstring为:Data Source=test.db; DbContext db = new DbContext(()); //注入IUserService服务 throw new NotImplementedException(); } /// <summary> /// 使用ServiceProvider获取上面注入的ILoginService的实例 /// </summary> /// <returns></returns> internal static IUserService GetUserService(IServiceProvider serviceProvider) { throw new NotImplementedException(); } #endregion #region >试题2:实现简单的Linq操作< /// <summary> ///使用Linq 查询 IT 部门中年龄在 28 以上的用户社保缴纳月数和缴纳总和 /// </summary> /// <param name="users"></param> /// <param name="departments"></param> /// <returns></returns> /// <exception cref="NotImplementedException"></exception> public static IEnumerable<InsuranceReport> GetUp28Insurance(IList<User> users, IList<Department> departments) { // 查询 IT 部门中 年龄在 28 以上的用户及其社保缴纳情况 var Users = users.Where(a => a.Age > 28 && a.DepartmentId == 1).Select(a => a.Id).ToList(); var UserInfo = User. throw new NotImplementedException(); } #endregion #region >试题3:ORM的简单操作< /// <summary> /// 根据userService任意新增一个用户,譬如new DmUser { user_name = "{名字}", password = "{密码}", create_at = DateTime.Now } /// </summary> /// <param name="userService"></param> /// <returns></returns> public static User AddUser(IUserService userService) { DmUser newUser = new DmUser(); newUser.id = 999; newUser.user_name = "张三"; newUser.password = "1234567"; var User = userService.AddUserAsync(newUser); throw new NotImplementedException(); } //通过用户名,密码查询到自定义添加用户 public static User GetUser(IUserService userService) { //读取上面方法新增的用户 👆 throw new NotImplementedException(); } //用你新添加的新用户发布篇新闻 public static DmNews? AddNews(IUserService userService, MainDbContext dbContext) { //添加新增用户关联的新闻 //读取新增用户关联的新闻(注意,需重新读取,不能返回当前插入值) throw new NotImplementedException(); } /// <summary> /// 按要求获取第二页的10条最新新闻,按create_at字段排序 /// </summary> /// <param name="ctx">数据库连接上下文</param> /// <param name="pageIndex">页码</param> /// <param name="pageSize">条数</param> /// <returns></returns> public static IQueryable<DmNews> GetNewsByPage(MainDbContext ctx, Int32 pageIndex, Int32 pageSize = 10) { throw new NotImplementedException(); } /// <summary> /// 按条件查询名字字母J开头的用户发布的新闻列表,注意startChar有可能为空,并打印查询的SQL语句(请观察语句合理性) /// </summary> /// <param name="ctx">数据库连接上下文</param> /// <param name="startChar">用户的开头字母</param> /// <param name="pageIndex">页码</param> /// <param name="pageSize">条数</param> /// <returns></returns> public static IQueryable<DmNews> GetNews(MainDbContext ctx, Char startChar, Int32 pageIndex, Int32 pageSize = 10) { var sql = string.Empty; //作答区域(注意startChar可能为空) if (string.IsNullOrEmpty(sql)) { throw new Exception("请打印相关的查询语句"); } Console.WriteLine(sql); throw new NotImplementedException(); } #endregion }
最新发布
05-21
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值