应用程序框架实战二十三:基础查询扩展

  上面两篇已经作好准备,本文将进行基础查询扩展。当使用了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.Customers
                where c.Name == name
                select c;    

  这些代码看上去很不错,但不论是上面的扩展方法还是Linq语句,其结果都是错的。如果操作人员正好在查询条件的框中输入了一个“张三”,确实会把名称为“张三”的客户全部找出来,但是如果操作人员什么也不输入,直接点击查询按钮,结果会怎样?

  上面的代码会强制引入查询条件,哪怕输入值是空的,这与我们的预期不符,所以大家的办法是添加一个判断,像下面这样。

IQueryable<Customer> queryable = entities.Customers;
if( name != null )
    queryable = queryable.Where( t => t.Name == name );

  将输入值与null进行比较并不健壮,如果操作人员在某个查询条件输入框中不小心打了个空格,依然会引入错误查询条件,所以你把代码改造为下面这样。

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应用程序框架公共操作类(六):验证扩展

时间: 2024-11-03 01:33:54

应用程序框架实战二十三:基础查询扩展的相关文章

应用程序框架实战二十五:查询条件(规约模式应用)

前面已经做了一些准备工作,本篇将介绍查询条件的封装,它是规约模式的一个应用. 规约使用一个对象来封装谓词,我之前已经介绍过它在验证方面的应用,本篇是规约模式在查询方面的应用. 规约的强大之处在于,能够将一堆杂乱无章的条件判断或查询条件封装起来,以一个清晰的概念来表达,并使得这些谓词具备了可复用的能力. 首先在Util.Domains项目的Repositories目录中创建ICriteria接口,这个接口表示一个查询条件,代码如下. using System; using System.Linq.

应用程序框架实战二十六:查询对象

信息系统的查询需求千变万化,在仓储中为每个查询需求创建一个特殊方法,将导致大量乏味而臃肿的接口. 一种更加可行的办法是,在应用层服务中描述查询需求,并通过仓储执行查询. 为了能够更好的描述查询需求,可以将查询功能从仓储中抽取出来,专门创建一个查询对象. 查询最复杂的部分是条件过滤,这也是查询对象的主要职责.查询对象可以认为是规约模式的一个变种,允许查询对象动态创建查询条件. 在Util.Domains项目Repositories目录中,创建查询对象基接口IQueryBase,代码如下. usin

应用程序框架实战二十四:基础查询扩展 - 分页与排序

上一篇介绍了IQueryable的Where方法存在的问题,并扩展了一个名为Filter的过滤方法,它是Where方法的增强版.本篇将介绍查询的另一个重要主题——分页与排序. 对于任何一个信息系统,查询都需要分页,因为不可能直接返回表中的所有数据. 如果直接使用原始的Ado.Net,我们可以编写一个通用分页存储过程来进行分页查询,然后通过一个DataTable返回给业务层.不过进入Entity Framework时代,分页变得异常简单,通过Skip和Take两个方法配合就可以完成任务. 为了让分

应用程序框架实战二十一:DDD分层架构之仓储(介绍篇)

前面已经介绍过Entity Framework的工作单元和映射层超类型的封装,从本文开始,将逐步介绍仓储以及对查询的扩展支持. 什么是仓储 仓储表示聚合的集合. 仓储所表现出来的集合外观,仅仅是一种模拟,除了测试以外,没有理由使用内存中真正的集合来创建仓储. 不应该为所有实体建立仓储,只有聚合才拥有仓储. 仓储用来重建已持久化的聚合,而工厂用于新建聚合. 使用仓储的优点 直接使用Entity Framework的DbContext不是很好吗,为什么还要在DbContext的上方封装一层仓储呢,这

应用程序框架实战二十七: 基于Mvc+EasyUi+EF+Autofac的CRUD DEMO免费发放,纯干货,附截图

不知不觉,这个系列已经写了好几十篇了.我本来打算把基础介绍完再发放Demo进行整体说明,不过大部分人更喜欢看得见摸得着的表现层,对后端不是太感兴趣,所以我决定先发一个简单的CRUD Demo出来,让大家先感受一下,被应用程序框架封装之后的代码大体是什么样子. 采用EasyUi作为前端框架,主要是它比Dwz强大,另外也是基于Html扩展,比更强大的Ext要简单得多,更重要的是它越来越流行了,对于更详细的决择或前端架构设计,我会在后续文章说明. 虽然是一个简单的单表CRUD操作,但是分层架构和各方面

应用程序框架实战二十:映射层超类型

上一篇介绍了工作单元层超类型的封装演化过程,本文将介绍对Entity Framework映射层超类型的封装. 使用Entity Framework一般需要映射三种类型的对象,即实体.聚合.值对象. 聚合与实体映射的主要区别是:聚合映射单属性标识Id,并需要映射乐观离线锁Version,而实体的标识往往需要映射成复合属性,这样方便物理删除聚合中的实体.Entity Framework通过EntityTypeConfiguration进行实体映射. 值对象以嵌入值模式映射,这需要使用ComplexT

应用程序框架实战二:十年前的回忆

大约10年前,我刚刚步入.Net开发,那时候还很流行单层架构,直接在界面上拖控件,然后绑定数据.数据库操作使用原生的Ado.Net,每次都要创建数据库连接,打开连接,发送Sql,获取结果.关闭连接.每当我需要进行数据库操作的时候,就把这一段复制粘贴过去,就这样干了几个月. 一日,一位师兄给我介绍了名为SqlHelper的数据库辅助类,使用了这玩意以后,我发现开发效率和质量倍增.由于不需要来回复制粘贴,冗余代码变少,代码简洁很多.另外不需要手工关闭数据库连接,也让BUG变得更少.虽然SqlHelp

【WePY小程序框架实战四】-使用async&amp;await异步请求数据

[WePY小程序框架实战一]-创建项目 [WePY小程序框架实战二]-页面结构 [WePY小程序框架实战三]-组件传值 async await 是对promise的近一步优化,既解决了promise链式then的这种写法壁垒,又让异步请求更像同步,若对async await不太了解的同学可以直接参考阮一峰老师的文章async 函数的含义和用法,这里我们只关注怎么在小程序wepy架构中如何使用. 依赖库 import 'wepy-async-function' app.wpy中启用 export

应用程序框架实战十五:DDD分层架构之领域实体(验证篇)

在应用程序框架实战十四:DDD分层架构之领域实体(基础篇)一文中,我介绍了领域实体的基础,包括标识.相等性比较.输出实体状态等.本文将介绍领域实体的一个核心内容——验证,它是应用程序健壮性的基石.为了完成领域实体的验证,我们在前面已经准备好了验证公共操作类和异常公共操作类. .Net提供的DataAnnotations验证方法非常强大,Mvc会自动将DataAnnotations特性转换为客户端Js验证,从而提升了用户体验.但是客户端验证是靠不住的,因为很容易绕开界面向服务端提交数据,所以服务端