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")这个逻辑。它只能调用这个方法,并得到返回的true或false。结果就是,EF Core 必须从数据库拉取所有Post数据到内存中,然后在客户端进行过滤。这在大多数情况下性能极差。 -
使用
Expression<Func<T, bool>>(表达式树):Expression<Func<Post, bool>> myExpression = p => p.Title.Contains("EF Core");这是一个数据的描述。它不是一个可执行的方法,而是一个可以被遍历和检查的树形结构。EF Core 可以解析这棵树,发现它由以下节点组成:
- 一个参数
p(类型为Post) - 访问
p的Title属性 - 调用
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)
}
总结
它:
- 展示了表达式树的内部结构层次
- 揭示了每个表达式节点的各种属性(如
NodeType,Type等) - 说明了表达式树是如何由多个嵌套表达式组成的
- 提供了如何手动构建相同表达式树的"配方"
虽然这个输出不能直接编译执行,但它对于理解表达式树的内部工作原理非常有帮助,特别是在调试复杂表达式或学习表达式树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();
}
}

浙公网安备 33010602011771号