zoukankan      html  css  js  c++  java
  • 基础查询扩展

    基础查询扩展

    上面两篇已经作好准备,本文将进行基础查询扩展。当使用了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应用程序框架公共操作类(六):验证扩展

  • 相关阅读:
    Quicksum -SilverN
    uva 140 bandwidth (好题) ——yhx
    uva 129 krypton factors ——yhx
    uva 524 prime ring problem——yhx
    uva 10976 fractions again(水题)——yhx
    uva 11059 maximum product(水题)——yhx
    uva 725 division(水题)——yhx
    uva 11853 paintball(好题)——yhx
    uva 1599 ideal path(好题)——yhx
    uva 1572 self-assembly ——yhx
  • 原文地址:https://www.cnblogs.com/Leo_wl/p/4185754.html
Copyright © 2011-2022 走看看