EF Core 探析IQueryable 的延迟执行与动态查询构建


前言

在我们刚接触Entity Framework这个ORM工具的时候,经常会困惑查询出来的IQueryable类型的对象是什么,为什么执行toList()方法之后才会从数据库取到数据。本文将带带大家深入探析IQueryable,探讨这个实现了为什么要使用IQueryable,以及和IEnumerable的不同


一、什么是 IQueryable

IQueryable< T> 是 EF Core 实现数据库查询翻译的核心接口,它同时实现了 IEnumerable< T> 和 IQueryable 接口。EF Core 通过解析IQueryable 表达式树表示的查询逻辑,将其翻译为数据库可识别的 SQL 语句。

表达式树(Expression Tree)是一种以树形数据结构表示代码逻辑的编程概念,它将代码的 “逻辑结构” 显式地拆解为树状节点,每个节点对应一个具体的操作(如变量、常量、运算符、方法调用等)。这种结构让代码逻辑可以在运行时被分析、修改或转换,是动态编程和框架(如 EF Core)的核心技术之一

换句话说对于IQueryable 对象而言,查询不会立即执行。直到调用 ToList()、First() 等终结方法时,才会将完整的表达式树翻译为 SQL 并发送到数据库执行。这也称之为服务器端评估

二、IQueryable与IEnumerable 差异

IEnumerable 是 C# 中最基础的集合接口,表示可枚举(可迭代)的对象集合

IQueryable是将表达式树翻译为 SQL并发送到数据库执行,而对于IEnumerable 对象而言,调用一些列linq表达式,实际上是在内存中过滤
以下分别以IQueryable和IEnumerable 的Select方法举例
IQueryable对象的Select方法

[DynamicDependency("Select`2", typeof(Enumerable))]
public static IQueryable<TResult> Select<TSource, TResult>(this IQueryable<TSource> source, Expression<Func<TSource, TResult>> selector)
{
    ArgumentNullException.ThrowIfNull(source);
    ArgumentNullException.ThrowIfNull(selector);

    return source.Provider.CreateQuery<TResult>(
        Expression.Call(
            null,
            new Func<IQueryable<TSource>, Expression<Func<TSource, TResult>>, IQueryable<TResult>>(Select).Method,
            source.Expression, Expression.Quote(selector)));
}

IEnumerable 对象的Select方法

public static IEnumerable<TResult> Select<TSource, TResult>(
    this IEnumerable<TSource> source, Func<TSource, TResult> selector)
{
    if (source is null)
    {
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source);
    }
    if (selector is null)
    {
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.selector);
    }
    if (source is Iterator<TSource> iterator)
    {
        return iterator.Select(selector);
    }
    if (source is IList<TSource> ilist)
    {
        if (source is TSource[] array)
        {
            if (array.Length == 0)
            {
                return [];
            }
            return new ArraySelectIterator<TSource, TResult>(array, selector);
        }
        if (source is List<TSource> list)
        {
            return new ListSelectIterator<TSource, TResult>(list, selector);
        }
        return new IListSelectIterator<TSource, TResult>(ilist, selector);
    }
    return new IEnumerableSelectIterator<TSource, TResult>(source, selector);
}

IQueryable的Select是IQueryable的静态扩展方法,接受一个查询表达式树,并返回IQueryable对象。而IEnumerable的Select方法是IEnumerable的静态扩展方法,接受一个泛型委托,并返回IEnumerable对象。虽然是同名的方法,但底层的实现不一样。

特性IQueryableIEnumerable
执行位置数据库服务器内存中
延迟执行是(表达式树翻译为 SQL)是(迭代时计算)
适用场景动态组合查询、大数据集内存集合操作、小数据集

三、 IQueryable对象的延迟执行

上文我们提到IQueryable 的查询操作实际上是构建表达式树,而非直接执行查询。这些方法返回的是新的 IQueryable 对象,通过内部的表达式树表示查询逻辑。当IQueryable 对对象执行终结方法时,才会触发表达式树翻译成SQL,将SQL发送到服务器执行。
以下内容通过大学课程数据这个实例来一一论述。

3.1 实例背景

设想一下一个关于大学课程数据库,分别有Class (班级),Student (学生),Course (课程),Teacher (教师)四张表,和两者StudentCourse (学生-课程中间表),TeacherCourse (教师-课程中间表)中间表。班级有多个学生,一个学生只有一个班级。一个班级只有一个班主任(教师),一个教师可以是多个班的班主任。学生和课程之间是多对多,教师和课程是多对多。
模型

/// <summary>
/// 班级
/// </summary>
public class Class
{
    public int Id { get; set; }
    public string ClassName { get; set; } = null!;

    // 一对多:班级的班主任(一个教师可以管多个班)
    public int? HeadTeacherId { get; set; }
    public Teacher? HeadTeacher { get; set; }

    // 一对多:班级里的学生
    public ICollection<Student>? Students { get; set; }
}

/// <summary>
/// 学生
/// </summary>
public class Student
{
    public int Id { get; set; }
    public string Name { get; set; } = null!;
    public int Age { get; set; }
    // 一对多:学生所属班级
    public int ClassId { get; set; }
    public Class Class { get; set; } 

    // 多对多:学生选修课程(通过中间表)
    public ICollection<StudentCourse>? StudentCourses { get; set; }
}

/// <summary>
/// 课程
/// </summary>
public class Course
{
    public int Id { get; set; }
    public string CourseName { get; set; } = null!;

    public int Credit { get; set; }  // 课程学分

    // 多对多:选修该课程的学生(通过StudentCourse中间表)
    public ICollection<StudentCourse>? StudentCourses { get; set; }

    // 多对多:教授该课程的教师(通过TeacherCourse中间表)
    public ICollection<TeacherCourse>? TeacherCourses { get; set; }
}

/// <summary>
/// 教师
/// </summary>
public class Teacher
{
    public int Id { get; set; }
    public string Name { get; set; } = null!;
    public string Subject { get; set; } = null!;  // 所授学科
    // 一对多:教师作为班主任管理的班级
    public ICollection<Class>? Classes { get; set; }
    // 多对多:教师教授的课程(通过中间表)
    public ICollection<TeacherCourse>? TeacherCourses { get; set; }
}

/// <summary>
/// 学生课程关联表
/// </summary>
public class StudentCourse
{
    // 复合主键(学生ID+课程ID)
    public int StudentId { get; set; }
    public int CourseId { get; set; }
    // 附加属性:课程成绩
    public decimal? Score { get; set; }
    // 导航属性
    public Student? Student { get; set; }
    public Course? Course { get; set; }
}

/// <summary>
/// 教师课程关系
/// </summary>
public class TeacherCourse
{
    // 复合主键(教师ID+课程ID)
    public int TeacherId { get; set; }
    public int CourseId { get; set; }
    // 附加属性:授课年份(例如2024年教授该课程)
    public int TeachingYear { get; set; }
    // 导航属性
    public Teacher? Teacher { get; set; }
    public Course? Course { get; set; }
}

关系配置

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    base.OnModelCreating(modelBuilder);
    modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly);
    // 学生-班级 一对多配置
    modelBuilder.Entity<Student>()
        .HasOne(s => s.Class)
        .WithMany(c => c.Students)
        .HasForeignKey(s => s.ClassId)
        .OnDelete(DeleteBehavior.Cascade);  // 班级删除时级联删除学生

    // 班级-班主任 一对多(通过中间表)
    modelBuilder.Entity<Class>()
        .HasOne(c => c.HeadTeacher)
        .WithMany(t => t.Classes)
        .HasForeignKey(c => c.HeadTeacherId)
        .IsRequired(false)  // 允许班级暂时没有班主任
        .OnDelete(DeleteBehavior.SetNull);  // 班主任删除时设为null

    // 教师-课程 多对多配置
    modelBuilder.Entity<TeacherCourse>()
        .HasKey(tc => new { tc.TeacherId, tc.CourseId });  // 复合主键

    modelBuilder.Entity<TeacherCourse>()
        .HasOne(tc => tc.Teacher)
        .WithMany(t => t.TeacherCourses)
        .HasForeignKey(tc => tc.TeacherId)
        .OnDelete(DeleteBehavior.Cascade);  // 教师删除时级联删除授课记录

    modelBuilder.Entity<TeacherCourse>()
        .HasOne(tc => tc.Course)
        .WithMany(c => c.TeacherCourses)
        .HasForeignKey(tc => tc.CourseId)
        .OnDelete(DeleteBehavior.Cascade);  // 课程删除时级联删除授课记录

    // 学生-课程 多对多(通过中间表)
    modelBuilder.Entity<StudentCourse>()
        .HasKey(sc => new { sc.StudentId, sc.CourseId });  // 复合主键

    modelBuilder.Entity<StudentCourse>()
        .HasOne(sc => sc.Student)
        .WithMany(s => s.StudentCourses)
        .HasForeignKey(sc => sc.StudentId)
        .OnDelete(DeleteBehavior.Cascade);  // 学生删除时级联删除选课记录

    modelBuilder.Entity<StudentCourse>()
        .HasOne(sc => sc.Course)
        .WithMany(c => c.StudentCourses)
        .HasForeignKey(sc => sc.CourseId)
        .OnDelete(DeleteBehavior.Cascade);  // 课程删除时级联删除选课记录

    // 索引配置(优化查询)
    modelBuilder.Entity<Student>()
        .HasIndex(s => s.Name);  // 学生姓名索引
}

ER 图
在这里插入图片描述

3.2 延迟执行的触发

这里我们查询每个老师的所教的课程里考试分的平均分。

using (ApplicationDbContext db = new ApplicationDbContext())
{
    //获取每个老师的所教的课程里考试分的平均分
    Console.WriteLine("==prepare search==");
    var teachers = db.Teachers.Select(t => new
    {
        TeacherId = t.Id,
        TeacherName = t.Name,
        Course = t.TeacherCourses.Select(tc => new
        {
            CourseId = tc.CourseId,
            CourseName = tc.Course!.CourseName,
            AvgScore = tc.Course.StudentCourses.Where(sc => sc.CourseId == tc.CourseId).Average(sc => sc.Score),
        })
    });
    Console.WriteLine("==completed search==");
    Console.WriteLine("==prepare foreach result==");
    foreach (var teacher in teachers)
    {
        foreach (var item in teacher.Course)
        {
            Console.WriteLine($"TeacherId:{teacher.TeacherId} TeacherName:{teacher.TeacherName} CourseName:{item.CourseName}-AvgScore:{item.AvgScore}");
        }
    }
    Console.WriteLine("==completed foreach result==");
}

在这里插入图片描述

这里我们发现执行sql语句的部分在遍历查询IQueryable对象的时候,而不是执行EF Core linq查询的时候。由此可见

  • IQueryable只是代表一个“可以放到数据库服务器去执行的查询”,它没有立即执行,只是“可以被执行”而已。
  • 对于IQueryable接口调用非终结方法的时候不会执行查询,而调用终结方法的时候则会立即执行查询
  • 终结方法:遍历、ToArray()、ToList()、Min()、Max()、Count()等。
  • 非终结方法:GroupBy0)、OrderBy()、Include()、skip()、Take(等。
  • 一个方法的返回值类型如果是IQueryable类型,那么这个方法一般就是非终结方法,否则就是终结方法
  • 终结方法包括返回非 IQueryable 结果的操作(如 ToList()、First())或强制求值的聚合函数(如 Count()、Average())。

3.3 为什么要延迟执行(动态查询构建)

在工作中,我们经常能碰到根据多种类型的筛选条件查询数据的情况,每次查询中都有不同的查询组合方式。传统方式一般采用拼接SQL字符串,非常的不方便。
这里我们还是以上文的大学课程数据库为例,这次我们要在查询每个老师的所教的课程里考试成绩的平均分的基础上添加一个条件,查询每个班主任所教的课程里考试成绩的平均分。
代码如下

var headTeachers = db.Teachers
.Where(t => t.Classes.Any()) // 确保是班主任
.Select(t => new
{
    TeacherId = t.Id,
    TeacherName = t.Name,
    Course = t.TeacherCourses.Select(tc => new
    {
        CourseId = tc.CourseId,
        CourseName = tc.Course!.CourseName,
        AvgScore = tc.Course.StudentCourses.Where(sc => sc.CourseId == tc.CourseId).Average(sc => sc.Score),
    })
}).ToList();

比起普通教师的查询,对于班主任的查询我们多一个Where(t => t.Classes.Count > 0) 的判断。在面对各种各样的业务查询时,通过延迟执行的特性,能够方便灵活的去组合查询表达式。

IQueryable代表一个对数据库中数据进行查询的一个逻辑,这个查询是一个延迟查询。我们可以调用非终结方法向IOueryable中添加查询逻辑,当执行终结方法的时候才真正生成SOL语句来执行查询


总结

IQueryable 通过延迟执行和表达式树机制,实现了高效的数据库查询翻译能力,特别适合动态筛选条件和大数据集场景。与 IEnumerable 的内存操作相比,它能减少不必要的数据传输。合理使用终结方法和非终结方法,是优化 EF Core 性能的关键。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值