1. 概念理解
IQueryable:
可以想象成一个“聪明”的查询接口,它能够理解你想要查询什么,并且能够直接在数据库服务器上执行这些查询逻辑。
当你编写LINQ查询表达式并使用IQueryable时,这些查询不会立即执行,而是等到你实际需要结果的时候才会被执行。这种延迟执行的设计有助于优化性能。
-
IQueryable 只是代表一个“可以放到数据库服务器去执行的查询” 定义: IQueryable 是一个接口,它表示一个可查询的对象,可以理解为一个尚未执行的查询计划。当你创建一个基于 IQueryable的查询时,你实际上是在构建一个查询树,这个树描述了你想要执行的操作。 特点:
查询不会立即执行。相反,它会等待某个特定的触发点(例如,调用终结方法)才会真正运行。这意味着 IQueryable
对象在创建时只包含查询逻辑,而不是查询结果。 -
对于IQueryable接口调用非终结方法的时候不会执行查询,而调用终结方法的时候则会立即执行查询 非终结方法: 这些方法允许你构建查询逻辑,但不会立即执行查询。例如,你可以连续调用多个非终结方法来构建复杂的查询逻辑。 终结方法: 当你调用一个终结方法时,查询计划会被编译成 SQL 语句并发送到数据库服务器执行,然后将结果返回给客户端。这是触发实际数据检索的点。
-
终结方法 示例: 一些常见的终结方法包括但不限于: 遍历循环(如 .ForEach()) ToArray(), ToList()
Min(), Max(), Count() FirstOrDefault(), LastOrDefault()
SingleOrDefault(), ElementAt() Sum(), Average() 作用:
终结方法通常用于获取数据的聚合信息,或是将查询结果转换为某种集合类型,从而可以直接访问结果。 -
非终结方法 示例: 非终结方法包括但不限于: GroupBy() OrderBy(), OrderByDescending()
Include() Skip(), Take() Where() 作用:
这些方法允许你对数据进行排序、过滤、分组等操作,但不会立即执行查询。你可以将它们看作是构建查询计划的一部分。 -
简单判断: 一个方法的返回值类型如果是IQueryable类型,那么这个方法一般就是非终结方法。否则就是终结方法 规则:
如果一个方法返回 IQueryable 或其派生类型,那么这个方法通常是一个非终结方法。这是因为返回的 IQueryable
表示一个待执行的查询计划。 例外: 有一些特殊情况,例如 AsQueryable() 方法返回
IQueryable,但它本身就是一个终结方法,因为它将一个集合转换为 IQueryable 形式。 -
每次一个终结方法,就会生成一条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、可以复用查询条件 比如分页查询
- 在实际执行之前,分布构建 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() 调用终结方法时,查询才会被发送到数据库并执行。
- 复用查询条件 (比如分页查询)
/// 假设我们需要实现一个分页功能,我们可以通过复用之前构建的查询来实现这一点,这样就不必重新构建整个查询链
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 的延迟执行特性是如何帮助我们构建更高效和灵活的查询逻辑的。