C# Web开发教程(四)

EF Core SQL性能优化底层原理之表达式树(Expression Trees)

好的,我们来详细探讨一下表达式树(Expression Trees)在 Entity Framework Core 中的核心作用。

简单来说,表达式树是 EF Core 能够将你的 C# 代码(如 LINQ 查询)翻译成高效 SQL 语句的基石。没有表达式树,EF Core 就无法实现其最核心、最有价值的功能。


核心作用:提供可翻译(Translatable)的查询结构

表达式树的核心作用在于,它不是一个可执行的方法,而是一个数据结构。这个数据结构可以被 EF Core 的查询提供程序(Query Provider,例如 SqlServerQueryProvider) 分析、解读,并最终转换为目标数据库的 SQL 语言。

1. 与委托(Delegate)的关键区别

要理解表达式树,最好先把它和普通的委托(如 Func<T, bool>)对比一下。

  • 使用 Func<T, bool>(委托,即内存中的方法):

    Func<Post, bool> myDelegate = p => p.Title.Contains("EF Core");
    

    这是一个可立即执行的代码。如果你在 DbSet<Post> 上使用 .Where(myDelegate),EF Core 无法看到 p.Title.Contains("EF Core") 这个逻辑。它只能调用这个方法,并得到返回的 truefalse。结果就是,EF Core 必须从数据库拉取所有 Post 数据到内存中,然后在客户端进行过滤。这在大多数情况下性能极差。

  • 使用 Expression<Func<T, bool>>(表达式树):

    Expression<Func<Post, bool>> myExpression = p => p.Title.Contains("EF Core");
    

    这是一个数据的描述。它不是一个可执行的方法,而是一个可以被遍历和检查的树形结构。EF Core 可以解析这棵树,发现它由以下节点组成:

    • 一个参数 p(类型为 Post
    • 访问 pTitle 属性
    • 调用 String.Contains 方法,参数是 "EF Core"
    • 等等...

    正因为 EF Core 能读懂你的意图,它才能生成对应的 SQL 语句:

    SELECT [p].[Id], [p].[Title], [p].[Content], ...
    FROM [Posts] AS [p]
    WHERE [p].[Title] LIKE N'%EF Core%'
    

    查询在数据库服务器上执行,只返回匹配的结果,效率极高。

2. 实现跨数据库平台的查询翻译

EF Core 支持多种数据库(SQL Server, SQLite, PostgreSQL, MySQL等)。其底层为每个数据库提供了不同的查询提供程序。

  • 表达式树提供了一个公共的、抽象的查询中间语言
  • 当你编写 LINQ 查询时,EF Core 会将其构建为表达式树。
  • 然后,SQL Server 提供程序会将其翻译成 T-SQL,而 SQLite 提供程序则会将其翻译成 SQLite 的 SQL 方言。

这个过程使得你可以用同一套 C# LINQ 代码与不同的数据库进行交互。

3. 实现高效的延迟执行(Deferred Execution)

著名的 IQueryable<T> 接口(提供 LINQ 查询功能的接口)的核心就是一个表达式树和一个查询提供程序。

IQueryable<Post> query = _context.Posts
                       .Where(p => p.Likes > 10)
                       .OrderBy(p => p.CreatedDate);
  • 这行代码并没有立即执行查询。
  • 它只是构建了一个表达式树,将 .Where.OrderBy 操作依次组合起来。
  • 只有当你真正需要数据时(例如调用 .ToList().FirstOrDefault()foreach 循环),EF Core 才会将整个表达式树翻译成 SQL 并执行。

这种“组合性”是构建复杂动态查询的基础。

4. 支持动态查询构建

这是表达式树非常强大的一个高级用法。因为表达式树是可以在运行时动态构建和修改的,所以你可以在程序运行时根据用户输入或其他条件来动态创建查询。

示例:根据用户选择动态过滤

// 假设这是用户从前端传递过来的过滤条件
string searchTitle = "Hello";
DateTime? minDate = new DateTime(2023, 1, 1);

// 从基础查询开始
IQueryable<Post> query = _context.Posts;

// 动态添加 Where 条件
if (!string.IsNullOrEmpty(searchTitle))
{
    // 动态构建表达式: p => p.Title.Contains(searchTitle)
    query = query.Where(p => p.Title.Contains(searchTitle));
}

if (minDate.HasValue)
{
    // 动态构建表达式: p => p.CreatedDate >= minDate
    query = query.Where(p => p.CreatedDate >= minDate.Value);
}

// 最终执行的 SQL 会组合所有条件
var results = query.ToList();

EF Core 会将所有动态添加的条件组合到最终的表达式树中,并生成一个包含所有过滤条件的单一、高效的 SQL 语句。


总结

表达式树在 EF Core 中的作用可以概括为以下几点:

作用 说明
查询翻译 最核心的作用。将 C# LINQ 代码转换为等价的 SQL 语句,确保查询在数据库端执行,而不是在客户端内存中。
提供抽象层 作为公共中间语言,使同一套 LINQ 代码可以跨不同数据库工作。
实现延迟执行 使得 IQueryable 可以组合多次查询操作,最后再统一翻译和执行。
支持动态查询 允许在运行时程序化地构建复杂的查询逻辑,极大提升了灵活性。

简而言之,表达式树是 EF Core 的“翻译官”,它让 C# 语言能够与 SQL 数据库进行高效、无缝的对话。 没有表达式树,LINQ to SQL 之类的 ORM 技术就无法实现其核心价值。

表达式树测试

  • 安装第三方库 Install-Package ExpressionTreeToString实现打印表达式树结构
......
using ExpressionTreeToString;
......
Expression<Func<Article, bool>> res = a => a.Price > 0; // 创建表达式树
Console.WriteLine(res.ToString("Object notation","C#")); // 使用 ExpressionTreeToString 输出表达式树

- 结果:

var a = new ParameterExpression {
    Type = typeof(Article),
    IsByRef = false,
    Name = "a"
};

new Expression<Func<Article, bool>> {
    NodeType = ExpressionType.Lambda,
    Type = typeof(Func<Article, bool>),
    Parameters = new ReadOnlyCollection<ParameterExpression> {
        a
    },
    Body = new BinaryExpression {
        NodeType = ExpressionType.GreaterThan,
        Type = typeof(bool),
        Left = new MemberExpression {
            Type = typeof(int),
            Expression = a,
            Member = typeof(Article).GetProperty("Price")
        },
        Right = new ConstantExpression {
            Type = typeof(int),
            Value = 0
        }
    },
    ReturnType = typeof(bool)
}

总结

它:

  1. 展示了表达式树的内部结构层次
  2. 揭示了每个表达式节点的各种属性(如 NodeType, Type 等)
  3. 说明了表达式树是如何由多个嵌套表达式组成的
  4. 提供了如何手动构建相同表达式树的"配方"

虽然这个输出不能直接编译执行,但它对于理解表达式树的内部工作原理非常有帮助,特别是在调试复杂表达式或学习表达式树API时

  • 使用Factory methods构建表达式树可执行代码
......
 Expression<Func<Article, bool>> res = a => a.Price > 0;
 //Console.WriteLine(res.ToString("Object notation","C#"));
 Console.WriteLine(res.ToString("Factory methods", "C#"));
 ......
 - 输出结果(以下代码可以放到主程序中直接执行):
 
 // using static System.Linq.Expressions.Expression

var a = Parameter(
    typeof(Article),
    "a"
);

Lambda(
    GreaterThan(
        MakeMemberAccess(a,
            typeof(Article).GetProperty("Price")
        ),
        Constant(0)
    ),
    a
)
 
- 实例演示:

			// 使用静态导入后,可以直接调用工厂方法
            var a = Parameter(
                typeof(Article),
                "a"
            );

            // 假设 Price 是 int 类型,那么常量也应该是 int 类型
            var expr = Lambda<Func<Article, bool>>(
                GreaterThan(
                    Property(a, typeof(Article).GetProperty("Price")), // 使用 Expression.Property 访问属性
                    Constant(5) // 常量类型与成员类型匹配,例如这里是 int 类型
                ),
                a
            );

            using (MyDbContext contextObj = new MyDbContext())
            {
                var result = contextObj.Articles.Where(expr).ToArray();
            }
            
  - 运行结果:
  ......
  Executing DbCommand [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT [t].[Id], [t].[Content], [t].[IsDelete], [t].[Price], [t].[Title]
      FROM [T_Articles] AS [t]
      WHERE ([t].[IsDelete] <> CAST(1 AS bit)) AND ([t].[Price] > 5)
  • 使用Expression构建动态查询,提供了更大的灵活性和更好的性能,基础示例如下
static IEnumerable<Article> QueryArticle(string propertyName, object value)
{
    // 参数验证
    if (string.IsNullOrEmpty(propertyName))
        throw new ArgumentException("属性名不能为空", nameof(propertyName));
    
    if (value == null)
        throw new ArgumentNullException(nameof(value), "值不能为null");

    using (MyDbContext contextObj = new MyDbContext())
    {
        // 1. 创建参数表达式
        var parameter = Expression.Parameter(typeof(Article), "e");
        
        // 2. 创建属性访问表达式
        var property = Expression.Property(parameter, propertyName);
        
        // 3. 创建常量表达式,确保类型匹配
        var constant = Expression.Constant(value, value.GetType());
        
        // 4. 创建比较表达式(这里使用相等比较,您可能需要根据需求调整)
        var body = Expression.Equal(property, constant);
        
        // 5. 创建Lambda表达式
        var lambda = Expression.Lambda<Func<Article, bool>>(body, parameter);
        
        // 6. 执行查询并返回结果(不立即物化)
        return contextObj.Articles.Where(lambda).ToList();
    }
}

// 主程序调用
var queryRes = QueryArticle("Title", "阿尔卑斯山"); 
  • 高级示例: 构建动态查询,根据传入不同字段,查询对应的结果
static IEnumerable<Article> QueryArticle(string propertyName, object value, string operatorType = "equals")
        {
            // 参数验证...
            if (string.IsNullOrEmpty(propertyName))
                throw new ArgumentException("属性名不能为空", nameof(propertyName));

            if (value == null)
                throw new ArgumentNullException(nameof(value), "值不能为null");

            using (MyDbContext contextObj = new MyDbContext())
            {
                var parameter = Expression.Parameter(typeof(Article), "e");
                var property = Expression.Property(parameter, propertyName);
                var constant = Expression.Constant(value, value.GetType());

                Expression body;

                // 根据操作符类型创建不同的表达式
                switch (operatorType.ToLower())
                {
                    case "contains":
                        // 确保属性是字符串类型
                        if (property.Type != typeof(string))
                            throw new ArgumentException("Contains操作只能用于字符串属性");

                        // 调用Contains方法
                        var containsMethod = typeof(string).GetMethod("Contains", new[] { typeof(string) });
                        body = Expression.Call(property, containsMethod, constant);
                        break;

                    case "greaterthan":
                        body = Expression.GreaterThan(property, constant);
                        break;

                    case "lessthan":
                        body = Expression.LessThan(property, constant);
                        break;

                    case "equals":
                    default:
                        body = Expression.Equal(property, constant);
                        break;
                }

                var lambda = Expression.Lambda<Func<Article, bool>>(body, parameter);
                return contextObj.Articles.Where(lambda).ToList();
            }
        }
        
- 主程序调用: 支持Title和Id查询...
......
// var queryRes = QueryArticle("Title", "自然", "contains");
var queryRes = QueryArticle("Id", 5, "lessthan");
foreach (var item in queryRes)
{
	Console.WriteLine(item.Title);
}
  • 如果不使用表达式树来构建动态查询,可以这么写
static IEnumerable<Article> QueryArticle(string propertyName, object value)
{
    using (var contextObj = new MyDbContext())
    {
        IQueryable<Article> query = contextObj.Articles;

        switch (propertyName)
        {
            case "Title":
                query = query.Where(a => a.Title.Contains(value.ToString()));
                break;
            case "Id":
                if (int.TryParse(value.ToString(), out int id))
                    query = query.Where(a => a.Id == id);
                break;
            // 其他属性...
            default:
                throw new ArgumentException("不支持的属性名");
        }

        return query.ToList();
    }
}
posted @ 2025-09-11 10:37  清安宁  阅读(13)  评论(0)    收藏  举报