浅谈EFCore中IQueryable和IEnumerable的区别

1. 概念理解
IQueryable:
可以想象成一个“聪明”的查询接口,它能够理解你想要查询什么,并且能够直接在数据库服务器上执行这些查询逻辑。
当你编写LINQ查询表达式并使用IQueryable时,这些查询不会立即执行,而是等到你实际需要结果的时候才会被执行。这种延迟执行的设计有助于优化性能。

  1. IQueryable 只是代表一个“可以放到数据库服务器去执行的查询” 定义: IQueryable 是一个接口,它表示一个可查询的对象,可以理解为一个尚未执行的查询计划。当你创建一个基于 IQueryable的查询时,你实际上是在构建一个查询树,这个树描述了你想要执行的操作。 特点:
    查询不会立即执行。相反,它会等待某个特定的触发点(例如,调用终结方法)才会真正运行。这意味着 IQueryable
    对象在创建时只包含查询逻辑,而不是查询结果。

  2. 对于IQueryable接口调用非终结方法的时候不会执行查询,而调用终结方法的时候则会立即执行查询 非终结方法: 这些方法允许你构建查询逻辑,但不会立即执行查询。例如,你可以连续调用多个非终结方法来构建复杂的查询逻辑。 终结方法: 当你调用一个终结方法时,查询计划会被编译成 SQL 语句并发送到数据库服务器执行,然后将结果返回给客户端。这是触发实际数据检索的点。

  3. 终结方法 示例: 一些常见的终结方法包括但不限于: 遍历循环(如 .ForEach()) ToArray(), ToList()
    Min(), Max(), Count() FirstOrDefault(), LastOrDefault()
    SingleOrDefault(), ElementAt() Sum(), Average() 作用:
    终结方法通常用于获取数据的聚合信息,或是将查询结果转换为某种集合类型,从而可以直接访问结果。

  4. 非终结方法 示例: 非终结方法包括但不限于: GroupBy() OrderBy(), OrderByDescending()
    Include() Skip(), Take() Where() 作用:
    这些方法允许你对数据进行排序、过滤、分组等操作,但不会立即执行查询。你可以将它们看作是构建查询计划的一部分。

  5. 简单判断: 一个方法的返回值类型如果是IQueryable类型,那么这个方法一般就是非终结方法。否则就是终结方法 规则:
    如果一个方法返回 IQueryable 或其派生类型,那么这个方法通常是一个非终结方法。这是因为返回的 IQueryable
    表示一个待执行的查询计划。 例外: 有一些特殊情况,例如 AsQueryable() 方法返回
    IQueryable,但它本身就是一个终结方法,因为它将一个集合转换为 IQueryable 形式。

  6. 每次一个终结方法,就会生成一条SQL语句 行为: 每次你调用一个终结方法,EF Core
    会根据之前累积的所有非终结方法构建出完整的查询逻辑,并将其转换成 SQL 语句发送到数据库执行。 注意事项:
    如果你在同一个查询中调用了多个终结方法,EF Core 通常会尝试将它们合并成一个 SQL 语句执行,而不是为每个终结方法生成单独的
    SQL 语句。然而,这取决于具体的 EF Core 版本及其优化机制。 总结 IQueryable
    提供了一种灵活的方式来构建查询计划,并且只有在需要实际结果时才执行查询。
    通过区分终结方法和非终结方法,你可以更高效地管理数据加载和处理过程。 使用 IQueryable
    可以帮助你避免一次性加载大量数据到内存中,从而提高应用的性能和响应速度。

IEnumerable:
相比之下,可以将其视为一个“简单”的集合接口,它只是负责获取数据,而不管这些数据是如何被获取的。
使用IEnumerable时,查询会先加载所有数据到内存中,然后再进行处理。这意味着如果你的数据集很大,可能会消耗大量的内存资源。
2. 工作原理
IQueryable (服务器端评估):
当你在EF Core中使用IQueryable时,EF Core会分析你的LINQ查询表达式,并将其转换为SQL语句发送给数据库服务器。
数据库服务器执行SQL语句,返回结果集的一部分(通常是根据需要按需加载),这种方式称为“分页”或“流式处理”,可以有效地减少内存使用。
因为数据是在服务器端处理的,所以数据库连接会被持续占用直到查询完全完成
IEnumerable (客户端评估):
如果使用IEnumerable,EF Core会尝试获取所有匹配的数据,并将这些数据一次性加载到客户端应用程序的内存中。
这种方式适合于数据量较小的情况,或者当你的查询非常复杂,以至于不能完全在服务器端执行时。
由于数据已经全部加载到了客户端,所以数据库连接占用的时间较短。
3. 区别总结
性能影响:
IQueryable: 适用于大数据量的场景,因为它减少了网络传输和内存使用。
IEnumerable: 适用于小数据量或特定情况下的查询,比如需要复杂的客户端过滤。
资源占用:
IQueryable: 内存占用相对较少,但数据库连接占用时间较长。
IEnumerable: 内存占用可能较大,但数据库连接占用时间较短。
4. 验证方法
可以通过以下方法来验证这两种方式的行为差异:

实现两个不同的查询,一个使用IQueryable,另一个使用IEnumerable。
在查询结果的循环中加入延时(如Thread.Sleep()),这可以帮助观察数据库连接的状态。
尝试在查询过程中关闭数据库服务,如果查询还在执行并且依赖于数据库连接,则会报错。

static void IQueryableAndIEnumerableDiffer()
{
    using (MyDbContext dbContext = new MyDbContext())
    {
        IEnumerable<Material> materials = dbContext.Materials;
        var mater = materials.Where(p => p.MaterialId > 1).First();
        // sql= SELECT [m].[MaterialId], [m].[MaterialName] FROM[Materials] AS[m] 
        // 上述的sql语句是这样,说明同一样的方法,上面的时获取所有的数据,然后在内存中在进行筛选
        IQueryable<Material> materials1 = dbContext.Materials;
        var materials2 = materials1.Where(p => p.MaterialId > 1).First();
        // sql=SELECT TOP(1) [m].[MaterialId], [m].[MaterialName] FROM[Materials] AS[m] WHERE[m].[MaterialId] > 1
        // 上述的sql说明,翻译成了sql语句在服务器中执行
    }
}

为什么延迟执行,用代码示例讲解以下两点:
1、可以在实际执行之前,分布构建IQueryable (动态构建查询条件)
2、可以复用查询条件 比如分页查询

  1. 在实际执行之前,分布构建 IQueryable (动态构建查询条件)
/// 假设我们有一个 Employee 类,我们想要构建一个动态的查询,该查询可以根据不同的条件筛选员工列表。
public class Employee
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }
    public decimal Salary { get; set; }
}
/// 接下来我们可以定义一个方法,该方法接受不同的参数来构建查询
public static IQueryable<Employee> GetEmployeesQuery(EmployeeContext context, string name, int? age, decimal? salary)
{
    var query = context.Employees;

    if (!string.IsNullOrEmpty(name))
    {
        query = query.Where(e => e.Name.Contains(name));
    }

    if (age.HasValue)
    {
        query = query.Where(e => e.Age == age);
    }

    if (salary.HasValue)
    {
        query = query.Where(e => e.Salary >= salary);
    }

    return query;
}

/// 现在我们可以使用这个方法来构建查询,并且只有当我们需要结果时才会执行查询:
public static void QueryEmployee(){
		using (var context = new EmployeeContext())
		{
		    // 构建查询
		    var employeesQuery = GetEmployeesQuery(context, "John", null, 50000);
		
		    // 查询未执行
		    Console.WriteLine("Query has not been executed yet.");
		
		    // 执行查询
		    var results = employeesQuery.ToList();
		
		    // 查询已执行
		    Console.WriteLine("Query has now been executed.");
		}
}

/// 在这个例子中,GetEmployeesQuery 方法接收不同的过滤参数,构建了一个 IQueryable 对象。只有当我们在 employeesQuery.ToList() 调用终结方法时,查询才会被发送到数据库并执行。
  1. 复用查询条件 (比如分页查询)
/// 假设我们需要实现一个分页功能,我们可以通过复用之前构建的查询来实现这一点,这样就不必重新构建整个查询链
public static IEnumerable<Employee> GetPagedEmployees(EmployeeContext context, int page, int pageSize, string name, int? age, decimal? salary)
{
    var query = GetEmployeesQuery(context, name, age, salary);

    // 应用分页
    var pagedQuery = query.Skip((page - 1) * pageSize).Take(pageSize);

    // 执行查询
    return pagedQuery.ToList();
}
/// 我们可以使用这个方法来实现分页功能:
public static void PageEmployeeList(){
		using (var context = new EmployeeContext())
		{
		    // 获取第一页的数据
		    var page1 = GetPagedEmployees(context, 1, 10, "John", null, 50000);
		
		    // 获取第二页的数据
		    var page2 = GetPagedEmployees(context, 2, 10, "John", null, 50000);
		
		    // 分别输出结果
		    Console.WriteLine("Page 1 Results:");
		    foreach (var employee in page1)
		    {
		        Console.WriteLine($"Name: {employee.Name}, Age: {employee.Age}, Salary: {employee.Salary}");
		    }
		
		    Console.WriteLine("Page 2 Results:");
		    foreach (var employee in page2)
		    {
		        Console.WriteLine($"Name: {employee.Name}, Age: {employee.Age}, Salary: {employee.Salary}");
		    }
		}
}

在这个例子中,GetPagedEmployees 方法利用了之前定义的 GetEmployeesQuery 方法来复用查询条件。通过这种方式,我们只需要构建一次查询,然后在需要分页时重复使用它,从而避免了不必要的重复工作。

通过这两个示例,我们可以看到 IQueryable 的延迟执行特性是如何帮助我们构建更高效和灵活的查询逻辑的。

EFCore是一个用于.NET平台的开源对象关系映射(ORM)框架,它提供了一种简化数据库访问的方式。与原生的ADO.Net相比,EFCore在内存消耗上较大,因为它会在内存生成实体和数据库表的映射视图。然而,EFCore提供了各种扩展方法来支持增删改查,提高了开发效率。对于小型项目,使用原生的ADO.Net可能更加灵活,因为它允许编写灵活的SQL语句。但对于大型项目,使用EFCore可以减少编写SQL语句的工作量,并且在表结构变更时更加方便。EFCore还提供了事务支持,可以使用DbContext.Database.BeginTransaction方法开启事务,并使用Commit方法提交事务,Rollback方法回滚事务,Dispose方法销毁事务。使用Lambda表达式时需要引入System.Linq命名空间。在EFCore进行数据查询可以使用AsEnumerable方法或AsQueryable方法将集合类型转换为IEnumerable类型或IQueryable类型。删除操作可以使用EFCore提供的删除方法进行操作。 #### 引用[.reference_title] - *1* *2* [C# EFCore学习总结](https://blog.csdn.net/qq_38192821/article/details/130604484)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [C# EF Core可视化工具的使用以及EF Core入门语句](https://blog.csdn.net/qq_39935495/article/details/121599467)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值