• 基础查询


    基础查询扩展 - 分页与排序

     

      上一篇介绍了IQueryable的Where方法存在的问题,并扩展了一个名为Filter的过滤方法,它是Where方法的增强版。本篇将介绍查询的另一个重要主题——分页与排序。

      对于任何一个信息系统,查询都需要分页,因为不可能直接返回表中的所有数据。

      如果直接使用原始的Ado.Net,我们可以编写一个通用分页存储过程来进行分页查询,然后通过一个DataTable返回给业务层。不过进入Entity Framework时代,分页变得异常简单,通过Skip和Take两个方法配合就可以完成任务。

      为了让分页查询变得更加简单,我们需要进一步扩展和封装。

      先考虑输入参数,表现层需要将一些分页参数传递到应用层,为此我们可以定义一个分页对象来承载和计算分页相关的数据。

      在Util.Domains项目的Repositories目录中,创建IPager接口和它的实现类Pager。

      IPager接口代码如下。

    复制代码
    namespace Util.Domains.Repositories {
        /// <summary>
        /// 分页
        /// </summary>
        public interface IPager {
            /// <summary>
            /// 页数,即第几页,从1开始
            /// </summary>
            int Page { get; set; }
            /// <summary>
            /// 每页显示行数
            /// </summary>
            int PageSize { get; set; }
            /// <summary>
            /// 总行数
            /// </summary>
            int TotalCount { get; set; }
            /// <summary>
            /// 总页数
            /// </summary>
            int PageCount { get; }
            /// <summary>
            /// 跳过的行数
            /// </summary>
            int SkipCount { get; }
            /// <summary>
            /// 排序条件
            /// </summary>
            string Order { get; set; }
        }
    }
    复制代码

      Pager类代码如下。

    复制代码
    namespace Util.Domains.Repositories {
        /// <summary>
        /// 分页
        /// </summary>
        public class Pager : IPager {
            /// <summary>
            /// 初始化分页
            /// </summary>
            public Pager()
                : this( 1 ) {
            }
    
            /// <summary>
            /// 初始化分页
            /// </summary>
            /// <param name="page">页索引</param>
            /// <param name="pageSize">每页显示行数,默认20</param> 
            /// <param name="totalCount">总行数</param>
            public Pager( int page, int pageSize = 20, int totalCount = 0 ) {
                Page = page;
                PageSize = pageSize;
                TotalCount = totalCount;
            }
    
            private int _pageIndex;
            /// <summary>
            /// 页索引,即第几页,从1开始
            /// </summary>
            public int Page {
                get {
                    if ( _pageIndex <= 0 )
                        _pageIndex = 1;
                    return _pageIndex;
                }
                set { _pageIndex = value; }
            }
    
            /// <summary>
            /// 每页显示行数
            /// </summary>
            public int PageSize { get; set; }
    
            /// <summary>
            /// 总行数
            /// </summary>
            public int TotalCount { get; set; }
    
            /// <summary>
            /// 总页数
            /// </summary>
            public int PageCount {
                get {
                    if ( TotalCount == 0 )
                        return 0;
                    if ( ( TotalCount % PageSize ) == 0 )
                        return TotalCount / PageSize;
                    return ( TotalCount / PageSize ) + 1;
                }
            }
    
            /// <summary>
            /// 跳过的行数
            /// </summary>
            public int SkipCount {
                get {
                    if ( Page > PageCount )
                        Page = PageCount;
                    return PageSize * ( Page - 1 );
                }
            }
    
            /// <summary>
            /// 排序条件
            /// </summary>
            public string Order { get; set; }
        }
    }
    复制代码

      我将排序条件Order也打包到IPager接口中,这是因为排序与分页密切相关,甚至在调用Skip方法之前,.Net强制要求设置排序条件。

      在调用Skip方法时需要计算出跳过的行数,SkipCount提供了这个功能。

      由于客户端可能传递错误的分页参数,所以需要在Pager中进行修正。

      PagerTest单元测试代码如下。

    复制代码
    using Microsoft.VisualStudio.TestTools.UnitTesting;
    using Util.Domains.Repositories;
    
    namespace Util.Domains.Tests.Repositories {
        /// <summary>
        /// 分页测试
        /// </summary>
        [TestClass]
        public class PagerTest {
    
            #region 测试初始化
    
            /// <summary>
            /// 分页
            /// </summary>
            private Pager _pager;
    
            /// <summary>
            /// 测试初始化
            /// </summary>
            [TestInitialize]
            public void TestInit() {
                _pager = new Pager();
            }
    
            #endregion
    
            #region 默认值
    
            /// <summary>
            /// 分页默认值
            /// </summary>
            [TestMethod]
            public void Test_Default() {
                Assert.AreEqual( 1, _pager.Page );
                Assert.AreEqual( 20, _pager.PageSize );
                Assert.AreEqual( 0, _pager.TotalCount );
                Assert.AreEqual( 0, _pager.PageCount );
            }
    
            #endregion
    
            #region PageCount(总页数)
    
            /// <summary>
            /// 总行数为0,每页20行,页数为0
            /// </summary>
            [TestMethod]
            public void TestPageCount_TotalCountIs0() {
                _pager.TotalCount = 0;
                Assert.AreEqual( 0, _pager.PageCount );
            }
    
            /// <summary>
            /// 总行数为100,每页20行,页数为5
            /// </summary>
            [TestMethod]
            public void TestPageCount_TotalCountIs100() {
                _pager.TotalCount = 100;
                Assert.AreEqual( 5, _pager.PageCount );
            }
    
            /// <summary>
            /// 总行数为1,每页20行,页数为1
            /// </summary>
            [TestMethod]
            public void TestPageCount_TotalCountIs1() {
                _pager.TotalCount = 1;
                Assert.AreEqual( 1, _pager.PageCount );
            }
    
            /// <summary>
            /// 总行数为100,每页10行,页数为10
            /// </summary>
            [TestMethod]
            public void TestPageCount_PageSizeIs10_TotalCountIs100() {
                _pager.PageSize = 10;
                _pager.TotalCount = 100;
                Assert.AreEqual( 10, _pager.PageCount );
            }
    
            #endregion
    
            #region Page(页索引)
    
            /// <summary>
            /// 页索引小于1,则修正为1
            /// </summary>
            [TestMethod]
            public void TestPage_Less1() {
                _pager.Page = 0;
                Assert.AreEqual( 1, _pager.Page );
    
                _pager.Page = -1;
                Assert.AreEqual( 1, _pager.Page );
            }
    
            #endregion
    
            #region SkipCount(跳过的行数)
    
            /// <summary>
            /// 跳过的行数
            /// </summary>
            [TestMethod]
            public void TestSkipCount() {
                _pager.TotalCount = 100;
    
                _pager.Page = 0;
                Assert.AreEqual( 0, _pager.SkipCount );
    
                _pager.Page = 1;
                Assert.AreEqual( 0, _pager.SkipCount );
    
                _pager.Page = 2;
                Assert.AreEqual( 20, _pager.SkipCount );
    
                _pager.Page = 3;
                Assert.AreEqual( 40, _pager.SkipCount );
    
                _pager.Page = 4;
                Assert.AreEqual( 60, _pager.SkipCount );
    
                _pager.Page = 5;
                Assert.AreEqual( 80, _pager.SkipCount );
    
                _pager.Page = 6;
                Assert.AreEqual( 80, _pager.SkipCount );
            }
    
            /// <summary>
            /// 跳过的行数
            /// </summary>
            [TestMethod]
            public void TestSkipCount_2() {
                _pager.TotalCount = 99;
    
                _pager.Page = 0;
                Assert.AreEqual( 0, _pager.SkipCount );
    
                _pager.Page = 1;
                Assert.AreEqual( 0, _pager.SkipCount );
    
                _pager.Page = 2;
                Assert.AreEqual( 20, _pager.SkipCount );
    
                _pager.Page = 3;
                Assert.AreEqual( 40, _pager.SkipCount );
    
                _pager.Page = 4;
                Assert.AreEqual( 60, _pager.SkipCount );
    
                _pager.Page = 5;
                Assert.AreEqual( 80, _pager.SkipCount );
    
                _pager.Page = 6;
                Assert.AreEqual( 80, _pager.SkipCount );
            }
    
            /// <summary>
            /// 跳过的行数
            /// </summary>
            [TestMethod]
            public void TestSkipCount_3() {
                _pager.TotalCount = 0;
                _pager.Page = 1;
                Assert.AreEqual( 0, _pager.SkipCount );
            }
    
            #endregion
        }
    }
    复制代码

      现在有了Pager来传递分页参数,但分页结果采用什么类型返回呢?一种办法是通过List<T>返回对象集合,再定义几个out参数来返回分页参数,但这种做法比较丑陋,out只应该在必要时才使用。

      一个更好的办法是创建派生自List<T>的自定义集合,只需要添加几个分页属性即可。

      在Util.Domains项目的Repositories目录中,创建PagerList分页列表,代码如下。

    复制代码
    using System;
    using System.Collections.Generic;
    using System.Linq;
    
    namespace Util.Domains.Repositories {
        /// <summary>
        /// 分页集合
        /// </summary>
        /// <typeparam name="T">元素类型</typeparam>
        public class PagerList<T> : List<T> {
            /// <summary>
            /// 分页集合
            /// </summary>
            /// <param name="pager">查询对象</param>
            public PagerList( IPager pager )
                : this( pager.Page, pager.PageSize, pager.TotalCount, pager.Order ) {
            }
    
            /// <summary>
            /// 分页集合
            /// </summary>
            /// <param name="totalCount">总行数</param>
            public PagerList( int totalCount )
                : this( 1, 20, totalCount ) {
            }
    
            /// <summary>
            /// 分页集合
            /// </summary>
            /// <param name="page">页索引</param>
            /// <param name="pageSize">每页显示行数</param>
            /// <param name="totalCount">总行数</param>
            public PagerList( int page, int pageSize, int totalCount )
                : this( page, pageSize, totalCount, "" ) {
            }
    
            /// <summary>
            /// 分页集合
            /// </summary>
            /// <param name="page">页索引</param>
            /// <param name="pageSize">每页显示行数</param>
            /// <param name="totalCount">总行数</param>
            /// <param name="order">排序条件</param>
            public PagerList( int page, int pageSize, int totalCount, string order ) {
                var pager = new Pager( page, pageSize, totalCount );
                TotalCount = pager.TotalCount;
                PageCount = pager.PageCount;
                Page = pager.Page;
                PageSize = pager.PageSize;
                Order = order;
            }
    
            /// <summary>
            /// 页索引,即第几页,从1开始
            /// </summary>
            public int Page { get; private set; }
    
            /// <summary>
            /// 每页显示行数
            /// </summary>
            public int PageSize { get; private set; }
    
            /// <summary>
            /// 总行数
            /// </summary>
            public int TotalCount { get; private set; }
    
            /// <summary>
            /// 总页数
            /// </summary>
            public int PageCount { get; private set; }
    
            /// <summary>
            /// 排序条件
            /// </summary>
            public string Order { get; private set; }
    
            /// <summary>
            /// 转换分页集合的元素类型
            /// </summary>
            /// <typeparam name="TResult">目标元素类型</typeparam>
            /// <param name="converter">转换方法</param>
            public PagerList<TResult> Convert<TResult>( Func<T, TResult> converter ) {
                var result = new PagerList<TResult>( Page, PageSize, TotalCount, Order );
                result.AddRange( this.Select( converter ) );
                return result;
            }
        }
    }
    复制代码

      PagerList可以接收一个IPager的参数,这样可以快速设置分页参数。

      当你从仓储中获取到PagerList<T>,T类型参数是一个领域层的聚合,如果你的应用层操作的是Dto,这个PagerList就无法使用,将一个PagerList<TEntity>完整转换为PagerList<TDto>需要好几行乏味的赋值代码。为了解决这个问题,提供了一个Convert方法,该方法接收一个Func<T, TResult>参数,Func是.Net内置的一个标准委托,我们可以传递一个方法完成Entity到Dto的转换,其它分页参数的赋值操作会在Convert中完成。

      PagerListTest单元测试代码如下。

    复制代码
    using Microsoft.VisualStudio.TestTools.UnitTesting;
    using Util.Domains.Repositories;
    using Util.Domains.Tests.Samples;
    
    namespace Util.Domains.Tests.Repositories {
        /// <summary>
        /// 分页集合测试
        /// </summary>
        [TestClass]
        public class PagerListTest {
            /// <summary>
            /// 分页集合
            /// </summary>
            private PagerList<Employee> _list;
    
            /// <summary>
            /// 测试初始化
            /// </summary>
            [TestInitialize]
            public void TestInit() {
                _list = new PagerList<Employee>( 1, 2, 3 );
                _list.Add( new Employee() );
                _list.Add( new Employee(){Name = "B"} );
            }
    
            /// <summary>
            /// 元素个数
            /// </summary>
            [TestMethod]
            public void TestCount() {
                Assert.AreEqual( 2, _list.Count );
            }
    
            /// <summary>
            /// 用索引获取元素
            /// </summary>
            [TestMethod]
            public void TestIndex() {
                Assert.AreEqual( "B", _list[1].Name );
            }
    
            /// <summary>
            /// 转换类型
            /// </summary>
            [TestMethod]
            public void TestConvert() {
                var result = _list.Convert( t => new EmployeeDto() );
                Assert.AreEqual( 2, result.Count );
                Assert.AreEqual( 1, result.Page );
                Assert.AreEqual( 2, result.PageSize );
                Assert.AreEqual( 3, result.TotalCount );
                Assert.AreEqual( 2, result.PageCount );
            }
        }
    }
    复制代码

      准备工作已经就绪,现在开始扩展IQueryable的分页和排序功能。

      注意观察IPager接口中的排序条件Order,它是一个字符串类型,使用弱类型的字符串是有原因的。要在IQueryable上进行排序,第一次升序调用OrderBy,降序调用OrderByDescending,如果要继续添加第二个排序条件,升序调用ThenBy,降序调用ThenByDescending。可以看到,排序API并不易用,如果要设置多个排序条件相当麻烦。更重要的一点是这些方法的参数是强类型的Func或Expression,而表现层传过来的参数一般都是字符串,这些字符串无法直接传递给上述方法,更不要谈排序方向和多个排序字段。

      从上面可以看出,弱类型也不是一无是处,它可以提供强大的灵活性。为了弥补Linq强类型查询的不足,微软提供了一组动态查询帮助类,其中DynamicQueryable为IQueryable扩展了几个常用方法,它可以接收字符串参数,并解析为相应的Expression。

      由于这一组帮助类内容很少,所以我不想为此引用一个额外的程序集。我将这些帮助类放到了Util项目的Lambdas目录的Dynamics子目录中,并修改它们的命名空间为Util.Lambdas.Dynamics,这样Resharper就不会显示警告了。

      这几个动态查询帮助类的代码就不贴了,有兴趣可下载本文的示例代码文件。

      在Util.Datas项目中找到Extensions.Query.cs文件,添加下面的扩展代码。

    复制代码
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Linq.Expressions;
    using Util.Datas.Queries;
    using Util.Domains.Repositories;
    using Util.Lambdas.Dynamics;
    
    namespace Util.Datas {
        /// <summary>
        /// 查询扩展
        /// </summary>
        public static class Extensions {
            /// <summary>
            /// 过滤
            /// </summary>
            /// <typeparam name="T">实体类型</typeparam>
            /// <param name="source">数据源</param>
            /// <param name="predicate">谓词</param>
            public static IQueryable<T> Filter<T>( this IQueryable<T> source, Expression<Func<T, bool>> predicate ) {
                predicate = QueryHelper.ValidatePredicate( predicate );
                if ( predicate == null )
                    return source;
                return source.Where( predicate );
            }
    
            /// <summary>
            /// 排序
            /// </summary>
            /// <typeparam name="T">实体类型</typeparam>
            /// <param name="source">数据源</param>
            /// <param name="propertyName">排序属性名,多个属性用逗号分隔,降序用desc字符串,范例:Name,Age desc</param>
            public static IQueryable<T> OrderBy<T>( this IQueryable<T> source, string propertyName ) {
                return source.OrderByDynamic( propertyName );
            }
    
            /// <summary>
            /// 创建分页列表
            /// </summary>
            /// <typeparam name="T">实体类型</typeparam>
            /// <param name="source">数据源</param>
            /// <param name="page">页索引,表示第几页,从1开始</param>
            /// <param name="pageSize">每页显示行数,默认20</param>
            public static PagerList<T> PagerResult<T>( this IQueryable<T> source, int page, int pageSize = 20 ) {
                return PagerResult( source, new Pager( page, pageSize ) );
            }
    
            /// <summary>
            /// 创建分页列表
            /// </summary>
            /// <typeparam name="T">实体类型</typeparam>
            /// <param name="source">数据源</param>
            /// <param name="pager">分页对象</param>
            public static PagerList<T> PagerResult<T>( this IQueryable<T> source, IPager pager ) {
                source = OrderBy( source, pager );
                source = Pager( source, pager );
                return CreatePageList( source, pager );
            }
    
            /// <summary>
            /// 排序
            /// </summary>
            private static IQueryable<T> OrderBy<T>( IQueryable<T> source, IPager pager ) {
                if ( pager.Order.IsEmpty() )
                    return source;
                return source.OrderBy( pager.Order );
            }
    
            /// <summary>
            /// 分页
            /// </summary>
            private static IQueryable<T> Pager<T>( IQueryable<T> source, IPager pager ) {
                if ( pager.TotalCount <= 0 )
                    pager.TotalCount = source.Count();
                return source.Skip( pager.SkipCount ).Take( pager.PageSize );
            }
    
            /// <summary>
            /// 创建分页列表
            /// </summary>
            private static PagerList<T> CreatePageList<T>( IEnumerable<T> source, IPager pager ) {
                var result = new PagerList<T>( pager );
                result.AddRange( source.ToList() );
                return result;
            }
        }
    }
    复制代码

      这里扩展了OrderBy方法,在方法内部委托给OrderByDynamic执行,OrderByDynamic方法由DynamicQueryable提供。

      PagerResult方法用来获取分页结果,有两个重载,第一个重载方法 PagerList<T> PagerResult<T>( this IQueryable<T> source, int page, int pageSize = 20 ) 接收两个分页参数,在使用这个重载之前假定排序已经完成。另一个重载方法 PagerList<T> PagerResult<T>( this IQueryable<T> source, IPager pager ) 接收一个分页对象,它会同时完成分页和排序操作。

      我在实际应用中,几乎总是使用第二个重载,因为我在应用层使用了查询实体,查询实体是从Pager派生的查询参数对象,待介绍到应用层再详述。

      还有一点需要注意,Pager对象的TotalCount是允许设置的,我在获取总行数的时候作了一个判断,如果TotalCount已经被设置,就不会调用Count方法。这样设计的原因是调用Count方法的开销很高,可能导致表扫描或索引扫描,如果在执行 PagerResult之前已经执行过Count,就不需要再重复执行。

      本篇介绍的方法,应用层可以这样调用。

    var dtos = Repository.Find().Filter( t => t.Name.Contains( "a" ) ).OrderBy( t => t.CreateTime ).PagerResult( 1 ).Convert( t => t.ToDto() );

    var dtos = Repository.Find().Filter( t => t.Name.Contains( testQuery.Name ) ).PagerResult( testQuery ).Convert( t => t.ToDto() );

      上面的代码已经比较简单,不过我将查询功能单独提取出来,使用查询对象模式进行封装,进一步简化操作。

      下一篇将介绍查询条件,它是规约模式的一种实现。

      

      .Net应用程序框架交流QQ群: 386092459,欢迎有兴趣的朋友加入讨论。

      谢谢大家的持续关注,我的博客地址:http://www.cnblogs.com/xiadao521/

      下载地址:http://files.cnblogs.com/xiadao521/Util.2015.1.3.1.rar

    版权所有,转载请注明出处 何镇汐的技术博客
     
  • 相关阅读:
    Ocelot(一)- .Net Core开源网关
    Extensions for Vue
    Vue Study [2]: Vue Router
    Vue Study [1]: Vue Setup
    获取当月的第一天和最后一天示例
    常规正则验证表达式
    当需要向数据库插入空值时,sql语句的判断
    让 IE支持圆角的方法
    服务器上传图片案例
    validatebox相关验证
  • 原文地址:https://www.cnblogs.com/Leo_wl/p/4200053.html
Copyright © 2020-2023  润新知