应用程序框架实战二十三:基础查询扩展
上面两篇已经作好准备,本文将进行基础查询扩展。当使用了Entity Framework这样的ORM框架以后,我们查询的核心被集中在IQueryable的Where方法上。
如果UI需要通过姓名查询一个客户,会在UI上放置一个输入框作为客户姓名的查询条件。服务端接收以后通过Where方法进行过滤,如下所示,entities表示DbContext的子类。
var queryable = entities.Customers.Where( t => t.Name == name );
当然,也可以使用Linq语句来完成。
var queryable = from c in entities.Customerswhere c.Name == nameselect c;
这些代码看上去很不错,但不论是上面的扩展方法还是Linq语句,其结果都是错的。如果操作人员正好在查询条件的框中输入了一个“张三”,确实会把名称为“张三”的客户全部找出来,但是如果操作人员什么也不输入,直接点击查询按钮,结果会怎样?
上面的代码会强制引入查询条件,哪怕输入值是空的,这与我们的预期不符,所以大家的办法是添加一个判断,像下面这样。
IQueryable<Customer> queryable = entities.Customers; if( name != "" )queryable = queryable.Where( t => t.Name == name );
将输入值与""进行比较并不健壮,如果操作人员在某个查询条件输入框中不小心打了个空格,依然会引入错误查询条件,所以你把代码改造为下面这样。
IQueryable<Customer> queryable = entities.Customers; if(!string.IsNullOrWhiteSpace( name ) )queryable = queryable.Where( t => t.Name == name );
但是string.IsNullOrWhiteSpace只能针对字符串,对于其它类型需要先调用ToString,代码继续修改。
IQueryable<Customer> queryable = entities.Customers; if( value != null && !string.IsNullOrWhiteSpace(value.ToString() ) )queryable = queryable.Where( t => t.XXX == value );
对于非字符串类型的查询条件,为了保障ToString的安全,需要在之前判断是否为null,否则可能抛出null异常。上面的代码比较健壮了,但是非常丑陋,如果只有一个查询条件,这不是大问题,但有10个条件呢?
IQueryable<Customer> queryable = entities.Customers; if( value1 != null && !string.IsNullOrWhiteSpace(value1.ToString() ) )queryable = queryable.Where( t => t.F1 == value1 ); if( value2 != null && !string.IsNullOrWhiteSpace(value2.ToString() ) )queryable = queryable.Where( t => t.F2 == value2 ); if( value3 != null && !string.IsNullOrWhiteSpace(value3.ToString() ) )queryable = queryable.Where( t => t.F3 == value3 );......
打开你自己的项目来检查一下,应该和上面代码类似,这些杂乱无章的判断把查询的主题冲淡了。
我上面讨论的是相等(==)运算符,对于像Contains这样的Like查询,它不害怕空字符串“”,但是如果字符串中带了空格“ ”,查询结果也是错的。可见,Where这个核心查询方法,并不适合直接在应用程序中使用,除非你的查询条件是必填项。对于从界面传过来的查询条件基本都是可选的,所以我们有必要进行查询扩展。
以上介绍了扩展Where方法的动机,下面开始进行扩展。
通过上面的示例代码可以看出,每当需要调用where时,都需要进行一个判断,我们的目标就是把这个判断隐藏到框架背后。
首先考虑过滤方法的名称,我命名为Filter,表示这是一个过滤器方法。
再考虑Filter的方法签名,很显然返回类型是泛型的IQueryable<>,那么参数呢?
我最初的做法是提供两个参数,第一个参数是Lambda表达式,第二个参数是查询条件的输入值。之所以需要第二个参数,是因为我当时不清楚怎么从Lambda表达式中把输入值提取出来,方法如下所示。
/// <summary>/// 过滤/// </summary>/// <typeparam name="TEntity">实体类型</typeparam>/// <typeparam name="TMember">实体属性类型</typeparam>/// <param name=" queryable">查询对象</param>/// <param name="predicate">过滤条件</param>/// <param name="value">属性值</param>public static IQueryable<TEntity> Filter<TEntity, TMember>( this IQueryable<TEntity> queryable, Expression<Func<TEntity, bool>> predicate, TMember value ){if (value == null)return queryable;if (string.IsNullOrWhiteSpace(value.ToString()))return queryable;return queryable.Where( predicate ); }
调用代码如下。
IQueryable<Customer> queryable = entities.Customers; queryable = queryable.Filter( t => t.F1 == value1, value1 ).Filter( t => t.F2 == value2, value2 ).Filter( t => t.F3 == value3, value3 );
可以看到,调用代码比直接使用Where已经清爽多了,不过这个Filter不是完美的,对于值类型的输入条件,结果是错的。比如value1是一个int类型,它的默认值为0,它将逃过string.IsNullOrWhiteSpace的检测。那么我们添加一个条件来检测默认值好不好呢,比如if(value == default(TMember)) return; 。这是不行的,如果你要搜索某字段为0的记录就会失效。
导致这个问题的原因是值类型无法为空,对引用类型没有影响,我的解决方案是强制使用可空值类型。对于查询来讲,一般不会直接传递一个条件参数,因为大部分UI都要求分页,传递多个参数是不方便的。我通过创建一个查询实体来强制实施上面的原则,查询实体拥有一些查询属性,且每个属性都是可空的,并且会帮我过滤掉字符串参数中的空格,待我介绍到应用层的时候再详细说明。
无独有偶,我在园子里看到一篇文章和我上面的查询扩展非常类似,只是他的第二个参数用了bool类型。使用bool类型的好处是更加灵活,当然代价是需要写更多代码。调用代码如下所示。
IQueryable<Customer> queryable = entities.Customers; queryable = queryable.Filter( t => t.F1 == value1, !string.IsNullOrWhiteSpace(value1)).Filter( t => t.F2 == value2, value2 != 0 );
在长时间使用了两个参数的方案后,我感觉非常别扭,我为什么要传入第二个值?直接从Lambda参数中提取出输入值不是更好?下面我们说干就干。
public static IQueryable<T> Filter<T>( this IQueryable<T> queryable, Expression<Func<T, bool>> predicate ) {if ( predicate.Value() == null )return queryable;if ( string.IsNullOrWhiteSpace( predicate.Value().ToString() ) )return queryable;return queryable.Where( predicate );}
这里的关键方法是Value,这个自定义方法是上一篇扩展的,它能够从Lambda谓词表达式中把输入值提取出来。
这个方案与我之前使用的方案类似,只是省下一个参数,它同样需要使用可空值类型。
目前的代码还有一个问题,如果程序员一次传入多个条件,会导致什么结果?
IQueryable<Customer> queryable = entities.Customers; queryable = queryable.Filter( t => t.F1 == value1 && t.F2 == value2 && t.F3 == value3 )
如果value1=”a”,value2和value3是空值,我得把t.F1 == value1拆出来,再传到where中去。当然是可以做到,但太费力,所以我想了个偷懒的方法,一次只允许传递一个条件,一次传入多个条件将抛出异常。
public static IQueryable<T> Filter<T>( this IQueryable<T> queryable, Expression<Func<T, bool>> predicate ) {if ( Lambda.GetCriteriaCount( predicate ) > 1 )throw new InvalidOperationException( String.Format( "仅允许添加一个条件,条件:{0}", predicate ) );if ( predicate.Value() == null )return queryable;if ( string.IsNullOrWhiteSpace( predicate.Value().ToString() ) )return queryable;return queryable.Where( predicate ); }
GetCriteriaCount是我在上一篇创建的第二个方法,用来获取Lambda谓词表达式中的条件个数,只要大于1个,就会抛出InvalidOperationException异常。
为了保证程序员不会把null传进来,添加一个null检测。
public static IQueryable<T> Filter<T>( this IQueryable<T> queryable, Expression<Func<T, bool>> predicate ) {predicate.CheckNull( "predicate" );if ( Lambda.GetCriteriaCount( predicate ) > 1 )throw new InvalidOperationException( String.Format( "仅允许添加一个条件,条件:{0}", predicate ) );if ( predicate.Value() == null )return queryable;if ( string.IsNullOrWhiteSpace( predicate.Value().ToString() ) )return queryable;return queryable.Where( predicate ); }
CheckNull用于检测对象是否空值,如果为null将抛出异常。
上面介绍了Filter方法的封装过程,现在开始扩展Util应用程序框架。
创建一个名为Util.Datas的类库,并添加相关依赖,这个项目用于放置数据相关公共操作。创建Extensions.Query.cs文件,它用来对查询进行扩展,代码如下。
using System; using System.Linq; using System.Linq.Expressions; using Util.Datas.Queries;namespace Util.Datas {/// <summary>/// 查询扩展/// </summary>public static class Extensions {/// <summary>/// 过滤/// </summary>/// <typeparam name="T">实体类型</typeparam>/// <param name="queryable">查询对象</param>/// <param name="predicate">谓词</param>public static IQueryable<T> Filter<T>( this IQueryable<T> queryable, Expression<Func<T, bool>> predicate ) {predicate = QueryHelper.ValidatePredicate( predicate );if ( predicate == null )return queryable;return queryable.Where( predicate );}} }
检测代码移到一个名为QueryHelper的internal类中,因为我后面还需要用到这段逻辑,代码如下。
using System; using System.Linq.Expressions;namespace Util.Datas.Queries {/// <summary>/// 查询操作/// </summary>internal class QueryHelper {/// <summary>/// 验证谓词,无效返回null/// </summary>/// <typeparam name="T">实体类型</typeparam>/// <param name="predicate">谓词</param>public static Expression<Func<T, bool>> ValidatePredicate<T>( Expression<Func<T, bool>> predicate ) {predicate.CheckNull( "predicate" );if ( Lambda.GetCriteriaCount( predicate ) > 1 )throw new InvalidOperationException( String.Format( "仅允许添加一个条件,条件:{0}", predicate ) );if ( predicate.Value() == null )return null;if ( string.IsNullOrWhiteSpace( predicate.Value().ToString() ) )return null;return predicate;}} }
为了让大家可以把Demo运行起来,我还创建了Util.Datas.Ef.Tests测试项目,SqlScripts目录中的Test.sql用来建库,数据库名为UnitTest,之所以不使用Test,是害怕把你本地的Test数据库给删掉了,这个数据库安装在你的D:\Data目录中,如果不合适请自行修改。
Samples目录中的Employee类是测试的实体,它非常简单,只有一个Name属性。
Repositories目录中的EmployeeRepository是测试仓储,为了简单,没有创建仓储的接口,因为这里没什么用。
本文的集成测试FilterTest位于QueryTests目录,代码如下。
using System.Linq; using Microsoft.VisualStudio.TestTools.UnitTesting; using Util.Datas.Ef.Tests.Repositories; using Util.Datas.Ef.Tests.Samples;namespace Util.Datas.Ef.Tests.QueryTests {/// <summary>/// 过滤测试/// </summary> [TestClass]public class FilterTest {/// <summary>/// 测试初始化/// </summary> [TestInitialize]public void TestInit() {EmployeeRepository repository = GetEmployeeRepository();repository.Clear();repository.Add( Employee.GetEmployee() );repository.Add( Employee.GetEmployee2() );}/// <summary>/// 获取员工仓储/// </summary>private EmployeeRepository GetEmployeeRepository() {return new EmployeeRepository( new TestUnitOfWork() );}/// <summary>/// 测试Filter过滤/// </summary> [TestMethod]public void TestFilter() {EmployeeRepository repository = GetEmployeeRepository();//用where查询var result = repository.Find().Where( t => t.Name == "" );Assert.AreEqual( 0, result.Count() );//用Fileter查询result = repository.Find().Filter( t => t.Name == "" );Assert.AreEqual( 2, result.Count() );Assert.AreEqual( Employee.GetEmployee().Name, result.ToList()[0].Name );Assert.AreEqual( Employee.GetEmployee2().Name, result.ToList()[1].Name );}} }
我在测试中比较了Where与Filter的不同,你可以自己运行一下,如果还不知道如何运行测试,请参考Util应用程序框架公共操作类(二):数据类型转换公共操作类(源码篇)。
当然使用Where查询比较死板,你需要在编译时期固定查询字段和操作符,这对于某些需要更灵活的场景并不合适,不过一般的系统对查询灵活性要求都不高。
本文虽然是针对IQueryable进行扩展,但思路上对于更原始的Ado.Net直接操作Sql同样适用。可以看出,.Net Framework给你提供的API比较原始,如果需要满足自己的需求,就需要扩展你的应用程序框架。另外不要轻视这个小小的扩展和封装,因为你的大多业务都需要查询,如果你有100个模块,每个模块有5个查询条件,能帮你省下500个判断。判断语句不仅枯燥而且容易喧宾夺主,扰乱你的查询主题。
.Net应用程序框架交流QQ群: 386092459,欢迎有兴趣的朋友加入讨论。
谢谢大家的持续关注,我的博客地址:http://www.cnblogs.com/xiadao521/
如果需要下载代码,请参考Util应用程序框架公共操作类(六):验证扩展
应用程序框架实战二十三:基础查询扩展相关推荐
- 应用程序框架实战二十六:查询对象
信息系统的查询需求千变万化,在仓储中为每个查询需求创建一个特殊方法,将导致大量乏味而臃肿的接口. 一种更加可行的办法是,在应用层服务中描述查询需求,并通过仓储执行查询. 为了能够更好的描述查询需求,可 ...
- 应用程序框架实战二十二 : DDD分层架构之仓储(层超类型基础篇)
前一篇介绍了仓储的基本概念,并谈了我对仓储的一些认识,本文将实现仓储的基本功能. 仓储代表聚合在内存中的集合,所以仓储的接口需要模拟得像一个集合.仓储中有很多操作都是可以通用的,可以把这部分操作抽取到 ...
- 应用程序框架实战二十一:DDD分层架构之仓储(介绍篇)
前面已经介绍过Entity Framework的工作单元和映射层超类型的封装,从本文开始,将逐步介绍仓储以及对查询的扩展支持. 什么是仓储 仓储表示聚合的集合. 仓储所表现出来的集合外观,仅仅是一种模 ...
- MySQL入门 (二) : SELECT 基础查询
1 查询资料前的基本概念 1.1 表格.纪录与栏位 表格是资料库储存资料的基本元件,它是由一些栏位组合而成的,储存在表格中的每一笔纪录就拥有这些栏位的资料. 以储存城市资料的表格「city」来说,设计 ...
- es match 查询时间段_elasticsearch 笔记二 之基础查询
这一篇笔记介绍几种 es 的基础查询,非聚合查询. 目录如下: 数据导入 排序查询 es 中的 limit 和offset 匹配字符串 匹配词组 数字精确查找 es 中的或与非 es 中的大小于过滤 ...
- JSTL实战二之基础
JSTL实战二之基础 一.JSTL的灵感 JSTL的设计灵感来自JavaScript和XPath WEB编程基于http,而http是简单的协议,所有的数据以字符形式提交,而java是一种强类型的语言 ...
- 应用程序框架实战三十六:CRUD实战演练介绍
从本篇开始,本系列将进入实战演练阶段. 前面主要介绍了一些应用程序框架的概念和基类,本来想把所有概念介绍完,再把框架内部实现都讲完了,再进入实战,这样可以让初学者基础牢靠.不过我的精力很有限,文章进度 ...
- 应用程序框架实战十八:DDD分层架构之聚合
前面已经介绍了DDD分层架构的实体和值对象,本文将介绍聚合以及与其高度相关的并发主题. 我在之前已经说过,初学者第一步需要将业务逻辑尽量放到实体或值对象中,给实体"充血",这样可以 ...
- (后续更新)【微信小程序】毕业设计 租房小程序开发实战,零基础开发房屋租赁系统小程序
文章目录 说在前面 小程序展示 一.各功能模块介绍 1.房产服务模块 2.个人中心模块 3.动态发布模块 4.订单管理模块 5.房屋评价模块 二.开发环境准备 1.注册微信小程序账号 2.下载微信开发 ...
最新文章
- Flex/Silverlight的技术比较转
- UA MATH ECE636 信息论10 Non-adaptive Group Testing
- 【Java注解】自定义注解、与数据库结合使用
- 微软上线Try .NET,支持在浏览器运行C#代码
- 计算机网络(二十五)-IP数据报格式
- 基于TableStore/MaxCompute的数据采集分析系统介绍
- 西门子和阿里云要搞啥事情?| 极客头条
- 利用FSMT进行文件服务器迁移及整合
- python双划线_Python中单下划线(_)和双下划线(__)的特殊用法
- java实验类与对象_【实验课件】上机实践2 类与对象
- spring整合shiro
- android中的weight
- 深度学习的研究方向: 你会为AI转型么?
- 汇新云为何给出严格的入驻审核标准?
- java 什么时候使用内部类
- Go 企业级框架 Revel 版全新发布
- 量化经济学:手把手教你如何使用EXCEL分析股票历史数据
- java开发网易电话面试 一面总结
- vMotion迁移报错’目标主机不支持虚拟机的当前硬件要求’
- Communications link failure的解决办法
热门文章
- 利用Kafka发送/消费消息-Java示例
- Oracle修改实例名SID
- 2019最新k8s集群搭建教程 (centos k8s 搭建)
- Javascript基础之-强制类型转换(三)
- Could not retrieve transaction read-only status from server
- linux vsftpd 550 create directory operation failed解决方法
- Spring源代码解析
- 安装更新Lenovo Solution Center更新失败!具体问题看内容!要是等官方技术人员解决,估计要等上好一段时间!...
- 快速向表中插入大量数据Oracle中append与Nologgin的作用
- oracle中least()和greastest()函数的使用,其中还包含一些if...then..elseif的使用