产品说,我只需要一个有亿点复杂的查询界面

2023-11-05 21:38

本文主要是介绍产品说,我只需要一个有亿点复杂的查询界面,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

有的时候,你需要动态构建一个比较复杂的查询条件,传入数据库中进行查询。而条件本身可能来自前端请求或者配置文件。那么这个时候,表达式树,就可以帮助到你。本文我们将通过几个简短的示例来了解如何完成这些操作。

你也可能接到过这些需求

c43054f5cb2773e2ee0d552b705f3c56.png
从模型进行查询
c9702c6ba27ee6dc325b23b8e30a18dc.png
基于配置查询

今天我们看看表达式树如何实现这些需求。

一切都还要从盘古开天开始说起

以下是一个简单的单元测试用例。接下来,我们将这个测试用例改的面目全非。

[Test]
public void Normal()
{var re = Enumerable.Range(0, 10).AsQueryable() // 0-9.Where(x => x >= 1 && x < 5).ToList(); // 1 2 3 4var expectation = Enumerable.Range(1, 4); // 1 2 3 4re.Should().BeEquivalentTo(expectation);
}

很久很久以前天和地还没有分开

由于是 Queryable 的关系,所以Where当中的其实是一个表达式,那么我们把它单独定义出来,顺便水一下文章的长度。

[Test]
public void Expression00()
{Expression<Func<int, bool>> filter = x => x >= 1 && x < 5;var re = Enumerable.Range(0, 10).AsQueryable().Where(filter).ToList();var expectation = Enumerable.Range(1, 4);re.Should().BeEquivalentTo(expectation);
}

有个叫盘古的巨人在这混沌之中

Expression 右侧是一个 Lambda ,所以可以捕获上下文中的变量。

这样你便可以把 minValue 和 maxValue 单独定义出来。

于是乎你可以从其他地方来获取 minValue 和 maxValue 来改变 filter。

[Test]
public void Expression01()
{var minValue = 1;var maxValue = 5;Expression<Func<int, bool>> filter = x => x >= minValue && x < maxValue;var re = Enumerable.Range(0, 10).AsQueryable().Where(filter).ToList();var expectation = Enumerable.Range(1, 4);re.Should().BeEquivalentTo(expectation);
}

他睡了一万八千年也都不用上班

那既然这样,我们也可以使用一个方法来创建 Expression。

这个方法,实际上就可以认为是这个 Expression 的工厂方法。

[Test]
public void Expression02()
{var filter = CreateFilter(1, 5);var re = Enumerable.Range(0, 10).AsQueryable().Where(filter).ToList();var expectation = Enumerable.Range(1, 4);re.Should().BeEquivalentTo(expectation);Expression<Func<int, bool>> CreateFilter(int minValue, int maxValue){return x => x >= minValue && x < maxValue;}
}

有一天盘古突然醒了但天还没亮

那可以使用 minValue 和 maxValue 作为参数来制作工厂方法,那么用委托当然也可以。

于是,我们可以把左边和右边分别定义成两个 Func,从而由外部来决定左右具体的比较方式。

[Test]
public void Expression03()
{var filter = CreateFilter(x => x >= 1, x => x < 5);var re = Enumerable.Range(0, 10).AsQueryable().Where(filter).ToList();var expectation = Enumerable.Range(1, 4);re.Should().BeEquivalentTo(expectation);Expression<Func<int, bool>> CreateFilter(Func<int, bool> leftFunc, Func<int, bool> rightFunc){return x => leftFunc.Invoke(x) && rightFunc.Invoke(x);}
}

他就抡起大斧头朝前方猛劈过去

实际上,左右两个不仅仅是两个Func,其实也可以直接是两个表达式。

不过稍微有点不同的是,表达式的合并需要用 Expression 类型中的相关方法创建。

我们可以发现,调用的地方这次其实没有任何改变,因为 Lambda 既可以隐式转换为 Func 也可以隐式转换为 Expression。

每个方法的意思可以从注释中看出。

[Test]
public void Expression04()
{var filter = CreateFilter(x => x >= 1, x => x < 5);var re = Enumerable.Range(0, 10).AsQueryable().Where(filter).ToList();var expectation = Enumerable.Range(1, 4);re.Should().BeEquivalentTo(expectation);Expression<Func<int, bool>> CreateFilter(Expression<Func<int, bool>> leftFunc,Expression<Func<int, bool>> rightFunc){// xvar pExp = Expression.Parameter(typeof(int), "x");// (a => leftFunc(a))(x)var leftExp = Expression.Invoke(leftFunc, pExp);// (a => rightFunc(a))(x)var rightExp = Expression.Invoke(rightFunc, pExp);// (a => leftFunc(a))(x) && (a => rightFunc(a))(x)var bodyExp = Expression.AndAlso(leftExp, rightExp);// x => (a => leftFunc(a))(x) && (a => rightFunc(a))(x)var resultExp = Expression.Lambda<Func<int, bool>>(bodyExp, pExp);return resultExp;}
}

只听枯叉一声黑暗渐渐地就分开

但是,上面的方法,其实可以在优化一下。避免对左右表达式的直接调用。

使用一个叫做 Unwrap 的方法,可以将 Lambda Expression 解构成只包含 Body 部分的表达式。

这是一个自定义的扩展方法,你可以通过 ObjectVisitor[1] 来引入这个方法。

限于篇幅,我们此处不能展开谈 Unwrap 的实现。我们只需要关注和前一个示例中注释的不同即可。

[Test]
public void Expression05()
{var filter = CreateFilter(x => x >= 1, x => x < 5);var re = Enumerable.Range(0, 10).AsQueryable().Where(filter).ToList();var expectation = Enumerable.Range(1, 4);re.Should().BeEquivalentTo(expectation);Expression<Func<int, bool>> CreateFilter(Expression<Func<int, bool>> leftFunc,Expression<Func<int, bool>> rightFunc){// xvar pExp = Expression.Parameter(typeof(int), "x");// leftFunc(x)var leftExp = leftFunc.Unwrap(pExp);// rightFunc(x)var rightExp = rightFunc.Unwrap(pExp);// leftFunc(x) && rightFunc(x)var bodyExp = Expression.AndAlso(leftExp, rightExp);// x => leftFunc(x) && rightFunc(x)var resultExp = Expression.Lambda<Func<int, bool>>(bodyExp, pExp);return resultExp;}
}

天和地分开后盘古怕它们还合并

我们可以再优化以下,把 CreateFilter 方法扩展为支持多个子表达式和可自定义子表达式的连接方式。

于是,我们就可以得到一个 JoinSubFilters 方法。

[Test]
public void Expression06()
{var filter = JoinSubFilters(Expression.AndAlso, x => x >= 1, x => x < 5);var re = Enumerable.Range(0, 10).AsQueryable().Where(filter).ToList();var expectation = Enumerable.Range(1, 4);re.Should().BeEquivalentTo(expectation);Expression<Func<int, bool>> JoinSubFilters(Func<Expression, Expression, Expression> expJoiner,params Expression<Func<int, bool>>[] subFilters){// xvar pExp = Expression.Parameter(typeof(int), "x");var result = subFilters[0];foreach (var sub in subFilters[1..]){var leftExp = result.Unwrap(pExp);var rightExp = sub.Unwrap(pExp);var bodyExp = expJoiner(leftExp, rightExp);result = Expression.Lambda<Func<int, bool>>(bodyExp, pExp);}return result;}
}

他就头顶着天脚蹬着地不知多久

有了前面的经验,我们知道。其实x => x >= 1这个表达式可以通过一个工厂方法来创建。

所以,我们使用一个 CreateMinValueFilter 来创建这个表达式。

[Test]
public void Expression07()
{var filter = JoinSubFilters(Expression.AndAlso,CreateMinValueFilter(1),x => x < 5);var re = Enumerable.Range(0, 10).AsQueryable().Where(filter).ToList();var expectation = Enumerable.Range(1, 4);re.Should().BeEquivalentTo(expectation);Expression<Func<int, bool>> CreateMinValueFilter(int minValue){return x => x >= minValue;}Expression<Func<int, bool>> JoinSubFilters(Func<Expression, Expression, Expression> expJoiner,params Expression<Func<int, bool>>[] subFilters){// xvar pExp = Expression.Parameter(typeof(int), "x");var result = subFilters[0];foreach (var sub in subFilters[1..]){var leftExp = result.Unwrap(pExp);var rightExp = sub.Unwrap(pExp);var bodyExp = expJoiner(leftExp, rightExp);result = Expression.Lambda<Func<int, bool>>(bodyExp, pExp);}return result;}
}

盘古也累得倒下来变成山石河流

当然,可以只使用 Expression 相关的方法来创建x => x >= 1

[Test]
public void Expression08()
{var filter = JoinSubFilters(Expression.AndAlso,CreateMinValueFilter(1),x => x < 5);var re = Enumerable.Range(0, 10).AsQueryable().Where(filter).ToList();var expectation = Enumerable.Range(1, 4);re.Should().BeEquivalentTo(expectation);Expression<Func<int, bool>> CreateMinValueFilter(int minValue){// xvar pExp = Expression.Parameter(typeof(int), "x");// minValuevar rightExp = Expression.Constant(minValue);// x >= minValuevar bodyExp = Expression.GreaterThanOrEqual(pExp, rightExp);var result = Expression.Lambda<Func<int, bool>>(bodyExp, pExp);return result;}Expression<Func<int, bool>> JoinSubFilters(Func<Expression, Expression, Expression> expJoiner,params Expression<Func<int, bool>>[] subFilters){// xvar pExp = Expression.Parameter(typeof(int), "x");var result = subFilters[0];foreach (var sub in subFilters[1..]){var leftExp = result.Unwrap(pExp);var rightExp = sub.Unwrap(pExp);var bodyExp = expJoiner(leftExp, rightExp);result = Expression.Lambda<Func<int, bool>>(bodyExp, pExp);}return result;}
}

那么看来盘古也吃不了上班的苦

那既然都用了 Expression 来创建子表达式了,那就干脆再做一点点改进,把x => x < 5也做成从工厂方法获取。

[Test]
public void Expression09()
{var filter = JoinSubFilters(Expression.AndAlso,CreateValueCompareFilter(Expression.GreaterThanOrEqual, 1),CreateValueCompareFilter(Expression.LessThan, 5));var re = Enumerable.Range(0, 10).AsQueryable().Where(filter).ToList();var expectation = Enumerable.Range(1, 4);re.Should().BeEquivalentTo(expectation);Expression<Func<int, bool>> CreateValueCompareFilter(Func<Expression, Expression, Expression> comparerFunc,int rightValue){var pExp = Expression.Parameter(typeof(int), "x");var rightExp = Expression.Constant(rightValue);var bodyExp = comparerFunc(pExp, rightExp);var result = Expression.Lambda<Func<int, bool>>(bodyExp, pExp);return result;}Expression<Func<int, bool>> JoinSubFilters(Func<Expression, Expression, Expression> expJoiner,params Expression<Func<int, bool>>[] subFilters){// xvar pExp = Expression.Parameter(typeof(int), "x");var result = subFilters[0];foreach (var sub in subFilters[1..]){var leftExp = result.Unwrap(pExp);var rightExp = sub.Unwrap(pExp);var bodyExp = expJoiner(leftExp, rightExp);result = Expression.Lambda<Func<int, bool>>(bodyExp, pExp);}return result;}
}

所以如果可以不做这需求就别搞

最后,我们在把子表达式的创建通过一点点小技巧。通过外部参数来决定。就基本完成了一个多 And 的值比较查询条件的动态构建。

[Test]
public void Expression10()
{var config = new Dictionary<string, int>{{ ">=", 1 },{ "<", 5 }};var subFilters = config.Select(x => CreateValueCompareFilter(MapConfig(x.Key), x.Value)).ToArray();var filter = JoinSubFilters(Expression.AndAlso, subFilters);var re = Enumerable.Range(0, 10).AsQueryable().Where(filter).ToList();var expectation = Enumerable.Range(1, 4);re.Should().BeEquivalentTo(expectation);Func<Expression, Expression, Expression> MapConfig(string op){return op switch{">=" => Expression.GreaterThanOrEqual,"<" => Expression.LessThan,_ => throw new ArgumentOutOfRangeException(nameof(op))};}Expression<Func<int, bool>> CreateValueCompareFilter(Func<Expression, Expression, Expression> comparerFunc,int rightValue){var pExp = Expression.Parameter(typeof(int), "x");var rightExp = Expression.Constant(rightValue);var bodyExp = comparerFunc(pExp, rightExp);var result = Expression.Lambda<Func<int, bool>>(bodyExp, pExp);return result;}Expression<Func<int, bool>> JoinSubFilters(Func<Expression, Expression, Expression> expJoiner,params Expression<Func<int, bool>>[] subFilters){// xvar pExp = Expression.Parameter(typeof(int), "x");var result = subFilters[0];foreach (var sub in subFilters[1..]){var leftExp = result.Unwrap(pExp);var rightExp = sub.Unwrap(pExp);var bodyExp = expJoiner(leftExp, rightExp);result = Expression.Lambda<Func<int, bool>>(bodyExp, pExp);}return result;}
}

还要更多

如果逻辑关系更复杂,有多层嵌套像树形一样,比较方法也很多花样,甚至包含方法,怎么办?

可以参考以下示例:

https://github.com/newbe36524/Newbe.Demo/tree/main/src/BlogDemos/Newbe.ExpressionsTests/Newbe.ExpressionsTests/FilterFactory

如果你对此内容感兴趣,还可以浏览我之前录制的视频进行进一步了解:

  • 戏精分享 C#表达式树,第一季[2]

  • 戏精分享 C#表达式树,第二季[3]

你也可以参阅之前一篇入门:

《只要十步,你就可以应用表达式树来优化动态调用》[4]

或者看MSDN文档,我觉得你也可以有所收获:

https://docs.microsoft.com/dotnet/csharp/programming-guide/concepts/expression-trees/?WT.mc_id=DX-MVP-5003606

这篇相关的代码,可以通过以下地址得到:

https://github.com/newbe36524/Newbe.Demo/blob/main/src/BlogDemos/Newbe.ExpressionsTests/Newbe.ExpressionsTests/Examples/Z01SingleWhereTest.cs

如果你觉得本文不错,记得收藏、点赞、评论、转发。告诉我还想知道点什么哟。

参考资料

[1]

Newbe.ObjectVisitor: https://github.com/newbe36524/Newbe.ObjectVisitor

[2]

戏精分享 C#表达式树,第一季: https://www.bilibili.com/video/BV15y4y1r7pK

[3]

戏精分享 C#表达式树,第二季: https://www.bilibili.com/video/BV1Mi4y1L7oR

[4]

只要十步,你就可以应用表达式树来优化动态调用: https://www.newbe.pro/Newbe.Claptrap/Using-Expression-Tree-To-Build-Delegate/index.html

这篇关于产品说,我只需要一个有亿点复杂的查询界面的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/352665

相关文章

SQL表间关联查询实例详解

《SQL表间关联查询实例详解》本文主要讲解SQL语句中常用的表间关联查询方式,包括:左连接(leftjoin)、右连接(rightjoin)、全连接(fulljoin)、内连接(innerjoin)、... 目录简介样例准备左外连接右外连接全外连接内连接交叉连接自然连接简介本文主要讲解SQL语句中常用的表

MySQL高级查询之JOIN、子查询、窗口函数实际案例

《MySQL高级查询之JOIN、子查询、窗口函数实际案例》:本文主要介绍MySQL高级查询之JOIN、子查询、窗口函数实际案例的相关资料,JOIN用于多表关联查询,子查询用于数据筛选和过滤,窗口函... 目录前言1. JOIN(连接查询)1.1 内连接(INNER JOIN)1.2 左连接(LEFT JOI

MySQL 中查询 VARCHAR 类型 JSON 数据的问题记录

《MySQL中查询VARCHAR类型JSON数据的问题记录》在数据库设计中,有时我们会将JSON数据存储在VARCHAR或TEXT类型字段中,本文将详细介绍如何在MySQL中有效查询存储为V... 目录一、问题背景二、mysql jsON 函数2.1 常用 JSON 函数三、查询示例3.1 基本查询3.2

MySQL中的交叉连接、自然连接和内连接查询详解

《MySQL中的交叉连接、自然连接和内连接查询详解》:本文主要介绍MySQL中的交叉连接、自然连接和内连接查询,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、引入二、交php叉连接(cross join)三、自然连接(naturalandroid join)四

mysql的基础语句和外键查询及其语句详解(推荐)

《mysql的基础语句和外键查询及其语句详解(推荐)》:本文主要介绍mysql的基础语句和外键查询及其语句详解(推荐),本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋... 目录一、mysql 基础语句1. 数据库操作 创建数据库2. 表操作 创建表3. CRUD 操作二、外键

resultMap如何处理复杂映射问题

《resultMap如何处理复杂映射问题》:本文主要介绍resultMap如何处理复杂映射问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录resultMap复杂映射问题Ⅰ 多对一查询:学生——老师Ⅱ 一对多查询:老师——学生总结resultMap复杂映射问题

Mybatis 传参与排序模糊查询功能实现

《Mybatis传参与排序模糊查询功能实现》:本文主要介绍Mybatis传参与排序模糊查询功能实现,本文通过实例代码给大家介绍的非常详细,感兴趣的朋友跟随小编一起看看吧... 目录一、#{ }和${ }传参的区别二、排序三、like查询四、数据库连接池五、mysql 开发企业规范一、#{ }和${ }传参的

浅谈mysql的sql_mode可能会限制你的查询

《浅谈mysql的sql_mode可能会限制你的查询》本文主要介绍了浅谈mysql的sql_mode可能会限制你的查询,这个问题主要说明的是,我们写的sql查询语句违背了聚合函数groupby的规则... 目录场景:问题描述原因分析:解决方案:第一种:修改后,只有当前生效,若是mysql服务重启,就会失效;

MySQL多列IN查询的实现

《MySQL多列IN查询的实现》多列IN查询是一种强大的筛选工具,它允许通过多字段组合快速过滤数据,本文主要介绍了MySQL多列IN查询的实现,具有一定的参考价值,感兴趣的可以了解一下... 目录一、基础语法:多列 IN 的两种写法1. 直接值列表2. 子查询二、对比传统 OR 的写法三、性能分析与优化1.

mybatis-plus 实现查询表名动态修改的示例代码

《mybatis-plus实现查询表名动态修改的示例代码》通过MyBatis-Plus实现表名的动态替换,根据配置或入参选择不同的表,本文主要介绍了mybatis-plus实现查询表名动态修改的示... 目录实现数据库初始化依赖包配置读取类设置 myBATis-plus 插件测试通过 mybatis-plu