对大数据量数据的高效分页25

简介

正如我们在前一篇教程中讨论的那样,分页可以通过两种方式实现 :

  • 默认分页 – 只需选中 Web 数据控件的智能标记中的 Enable Paging 选项就可以实现;然而,每次查看页面数据,即使只需在页面中显示部分数据 ,ObjectDataSource 也会读取所有记录。
  • 自定义分页 – 仅从数据库中获取用户要求浏览的数据页面需要显示的记录,从而提升了分页的性能;然而与默认分页相比,实现自定义分页需要更多操作。

由于实现方式简单(只需选中一个复选框就可以了!),因此默认分页是一个吸引人的可选方案。但它每次都获取所有记录,当需要对大量数据进行分页时或对于拥有众多并发用户的网站,该方法并不适用。在这样的情况下,我们必须采用自定义分页来提供响应程度更高的系统。

自定义分页的难点是编写一个查询语句,以便精确地返回某一特定数据页面所需的记录。幸运的是, Microsoft SQL Server 2005 为结果排序提供了新的关键字,它可以帮助我们写出有效读取所需记录的查询。在本教程中,我们将了解怎样使用新的 SQL Server 2005 关键字在 GridView 控件中实现自定义分页。自定义分页与默认分页的用户界面相同,但在从一个页面转到另一个页面时,使用自定义分页的速度可能比使用默认分页快上几个数量级。

注意: 自定义分页带来的确切的性能提升取决于需要分页的记录总量以及数据库服务器的负载。到本教程结束时,我们会看到一些粗略的统计数据,它们展示了通过使用自定义分页所带来的性能提升。

步骤1:了解自定义分页的过程

对数据进行分页时,在一个页面显示的确切记录取决于所请求的数据页面以及每个页面显示的记录数量。例如,假设我们要对 81 件产品分页,每页显示 10 件产品。浏览首页时,我们希望查看第 1 到第 10 件产品;浏览第二个页面时,我们对第 11 到第 20 件产品感兴趣,依此类推。

对于需要获取哪些记录以及分页界面如何呈现,有三个相关的变量:

  • Start Row Index – 要显示的 数据页面的第一行索引,这个索引值可以通过页面索引乘以每页显示的记录数加1 得到。例如,如果一页显示 10 条记录,那么对第一页(页索引为 0 )来说, Start Row Index 为 0 * 10 + 1 ,即 1 ;对第二页(页索引为 1 )来说, Start Row Index 为 1 * 10 + 1 ,即 11 。
  • Maximum Rows – 每页最多显示的记录数。该变量被称为“最大”行数是因为,最后一页返回的记录数可能小于页面大小。例如,对 81 件产品分页时,每页显示 10 条记录,第九页(即最后一页)只含有一条记录。页面显示的记录数不会大于 Maximum Rows 的值。
  • Total Record Count – 需要分页的记录总数。确定需要为给定页面获取哪些记录时,不需要该变量,但它会影响到分页界面。例如,当对 81 件产品的数据进行分页时,分页界面知道要在分页 UI 上显示 9 个页码。

对于默认分页来说,Start Row Index 由页面索引和页面大小的乘积然后再加1 计算而来,而 Maximum Rows 即为页面大小。无论要显示哪个数据页面,默认分页都会从数据库中获取所有记录,每行的索引都已知,因此向 Start Row Index 行跳转变得没有价值了。此外, Total Record Count 也可以轻松获取,它是 DataTable (或任何用于保存数据库结果的对象)中记录的数量。

给定 Start Row Index 和 Maximum Rows 两个变量后,自定义分页必须仅返回所需的记录,这些记录开始于 Start Row Index ,记录的数量不超过 Maximum Rows 。自定义分页有两个难点:

  • 我们必须有效地将全部待分页数据中的每一行与一个行索引关联起来,这样才能从指定的 Start Row Index 开始返回需要的记录。
  • 需要提供待分页的记录总数。

在后面两个步骤中,我们将探讨应对上述两个难题所需的 SQL 脚本。除了 SQL 脚本之外,我们还需要在 DAL 和 BLL 中完成相应的方法。

步骤2 :返回待分页的记录总数

在探讨如何准确获取显示页面所需的记录之前,让我们先看看如何返回待分页记录的总数。我们需要该信息来正确的配置分页用户界面。特定SQL 查询语句返回的记录总数可通过使用 COUNT 合计函数 获得。例如,我们可以使用以下查询语句来确定 Products 表中的记录总数:

SELECT COUNT(*) FROM Products

我们在 DAL 中添加一个方法来返回这些信息。这个DAL 方法的名称是TotalNumberOfProducts() ,用于 执行上述 SELECT 语句。

首先,打开 App_Code/DAL 文件夹 中的 Northwind.xsd Typed DataSet 文件。然后,在设计器中右键单击 ProductsTableAdapter ,并选择 Add Query 。正如我们在前面的教程中所了解到的,我们可以向 DAL 添加一个方法,调用该方法时,它会执行特定的 SQL 语句或存储过程。与前面教程中使用的 TableAdapter 方法一样,这一次我们使用 ad-hoc SQL 语句。

图1 : 使用一个 Ad-Hoc SQL 语句

在下一个屏幕中,我们可以指定创建哪种类型的查询。由于此查询会返回一个数量值(Products 表中记录的总数),因此选择 “SELECT which returns a singe value” 选项。

图2 : 配置查询使用返回一个数值的 SELECT 语句

指定完使用的查询类型后,下一步是编写查询语句。

图3 :使用查询语句:SELECT COUNT(*) FROM Products

最后,指定方法的名称。如前所述,我们使用 TotalNumberOfProducts 。

图4 : 将 DAL 方法命名为 TotalNumberOfProducts

单击 Finish 后,向导将向 DAL 添加 TotalNumberOfProducts 方法。如果 SQL 查询结果为空,DAL 中的数量返回方法则返回空值。但是,不管 DAL 方法是否返回一个空的整数值,COUNT 查询都会返回一个非空值。

除了 DAL 方法之外,我们还需要在 BLL 中增加一个方法。打开 ProductsBLL 类文件,添加一个 TotalNumberOfProducts 方法,该方法要做的只是调用 DAL 的 TotalNumberOfProducts 方法:

public int TotalNumberOfProducts() 

    return Adapter.TotalNumberOfProducts().GetValueOrDefault(); 
}

DAL 的TotalNumberOfProducts 方法返回一个可为空的整型值,但是,我们已经在 ProductsBLL 类中创建了 TotalNumberOfProducts 方法,所以该方法返回一个标准的整型值。因此,我们需要让 ProductsBLL 类的 TotalNumberOfProducts 方法返回由 DAL 的TotalNumberOfProducts 方法所返回的可为空的整型值部分。调用GetValueOrDefault() 方法会返回可空整型值(如果该值存在的话);如果可空整型值为空,则返回默认整型值 0 。

步骤3 :返回所需的记录

下一个任务是在 DAL 和 BLL 中创建接受前面讨论过的 Start Row Index 和 Maximum Rows 变量的方法,并返回相应的记录。开始之前,我们先看一下所需的 SQL 脚本。我们面临的挑战是必须能够有效地为全部分页结果中的每一行分配一个索引,以便仅返回那些从 Start Row Index 开始的记录(最多 Maximum Rows 条记录)。

如果数据库表中已经有一列可用作行索引,上述工作就简单多了。乍看起来,我们可能会认为 Products 表中的 ProductID 字段能够满足要求,第一个产品的 ProductID 为 1 ,第二个为 2 ,依此类推。但是,删除一个产品将会给该序列留下一个间断,所以这个方法不行。

有两种通用的技术可以有效地将行索引与分页数据关联起来,从而能够精确地获取所需记录 :

  • 使用SQL Server 2005的ROW_NUMBER()关键字– ROW_NUMBER() 关键字是 SQL Server 2005 的新特性,它将排序顺序与基于某些顺序返回的每条记录关联起来。该排序顺序可作为每一行的行索引。
     
  • 使用表变量和SET ROWCOUNT – SQL Server 的 SET ROWCOUNT 语句可以用于确定一个查询在结束之前需要处理多少条记录; 表变量是可以含有表格数据的 T-SQL 局部变量,与临时表类似。该方法在 Microsoft SQL Server 2005 和 SQL Server 2000 下均运行良好(而 ROW_NUMBER() 方法只能在 SQL Server 2005 下运行) 。

    此方法的思路是创建一个表变量,该表变量具有待分页数据表的 IDENTITY 列和主键列。然后,待分页数据表的内容被转储至表变量中,从而(通过 IDENTITY 列)将有顺序的行索引与表中的每条记录关联起来。只要填充了这个表变量,就可以执行与基础表结合的表变量上的 SELECT 语句,从而获取特定的记录。SET ROWCOUNT 语句用于智能地限制转储至表变量的记录的数量。

    由于 SET ROWCOUNT 的值是 Start Row Index 与 Maximum Row 的和,因此该方案的效率取决于被请求的页面编码。当对较小编码的页面进行分页时(例如前几页),该方案的效率非常高。但是,在获取后几页的数据时,其效率与默认分页相当。

本教程使用 ROW_NUMBER() 关键字来实现自定义分页。关于使用表变量和SET ROWCOUNT 技术 的更多信息,参见 对大规模结果集进行分页的更有效的方法 。

ROW_NUMBER() 关键字将排序与使用如下语句返回的具有特定顺序的每条记录关联起来:

SELECT columnList, 
       ROW_NUMBER() OVER(orderByClause) 
FROM TableName

根据指定的排序方式,ROW_NUMBER() 返回指定每行记录顺序的数值。例如,要查看每个产品按照从最昂贵到最便宜的顺序排序,我们可以使用以下查询:

SELECT ProductName, UnitPrice, 
       ROW_NUMBER() OVER(ORDER BY UnitPrice DESC) AS PriceRank 
FROM Products

图 5 显示在 Visual Studio 中 运行该查询的结果。注意:产品根据价格进行排序,每行都有一个价格的顺序。

图5 :每条返回的记录都包含价格排序顺序

注意 :ROW_NUMBER() 只 是 SQL Server 2005 中众多新排序函数之一。关于 ROW_NUMBER() 的更深入的讨论以及其它排序函数的信息,参见 使用 Microsoft SQL Server 2005 返回排序的结果 。

当通过 OVER 子句中指定的ORDER BY 列(在 上述示例中是 UnitPrice )对结果排序时,SQL Server 必须对结果排序。如果为结果排序的列添加聚集索引或覆盖索引的话,可以提高排序操作的速度,但是这种方法成本较高。为了有效提升大数据量查询的效率,我们考虑为对结果排序的列添加非聚集索引。关于性能因素的更多详细信息,参见 SQL Server 2005 中的排序函数及性能 。

ROW_NUMBER() 返回的排序信息不能直接用于 WHERE 子句。但是,可使用派生表来返回 ROW_NUMBER() 结果,此结果可在 WHERE 子句中 使用 。例如,下面的查询使用了派生表来返回 ProductName 和 UnitPrice 列以及 ROW_NUMBER() 结果,然后使用 WHERE 子句仅返回价格排序在 11 到 20 之间的产品:

SELECT PriceRank, ProductName, UnitPrice 
FROM 
   (SELECT ProductName, UnitPrice, 
       ROW_NUMBER() OVER(ORDER BY UnitPrice DESC) AS PriceRank 
    FROM Products 
   ) AS ProductsWithRowNumber 
WHERE PriceRank BETWEEN 11 AND 20

对此概念稍作引申,我们可以利用这个机制来获取Start Row Index 和 Maximum Rows 已知的特定数据页面。

SELECT PriceRank, ProductName, UnitPrice 
FROM 
   (SELECT ProductName, UnitPrice, 
       ROW_NUMBER() OVER(ORDER BY UnitPrice DESC) AS PriceRank 
    FROM Products 
   ) AS ProductsWithRowNumber 
WHERE PriceRank > <i>StartRowIndex</i> AND 
    PriceRank <= (<i>StartRowIndex</i> + <i>MaximumRows</i>)

注意:在本教程稍后部分,我们会看到,ObjectDataSource 提供的 StartRowIndex 由零开始索引,而 SQL Server 2005 返回的 ROW_NUMBER() 值从 1 开始索引。因此,WHERE 子句返回满足以下条件的记录:PriceRank 大于 StartRowIndex 并且小于或等于 StartRowIndex + MaximumRows 。

我们已经讨论了怎样使用 ROW_NUMBER() 获取特定数据页面( 已知 Start Row Index 和 Maximum Rows 的值),现在我们需要在 DAL 和 BLL 中以方法的形式实现这些逻辑。

创建查询时,我们必须确定按照哪种顺序对结果排序;让我们按照产品名称的字母顺序进行排序。这意味着使用本教程中的自定义分页实现方法时,我们不能创建可进行排序的自定义分页报表。我们将在下一篇教程中讨论如何实现这种功能。

在前一部分中,我们创建了一个 ad-hoc SQL 语句形式的 DAL 方法。遗憾的是,TableAdapter 向导使用的Visual Studio 中的 T-SQL 解析器不能识别 ROW_NUMBER() 函数使用的 OVER 语法。因此,我们必须将此 DAL 方法创建为一个存储过程。从视图菜单中选择 Server Explorer (或同时按下 Ctrl+Alt+S ),并展开 NORTHWND.MDF 节点。要添加新的存储过程,右键单击 Stored Procedures 节点,选择 Add a New Stored Procedure( 参见图 6 )。

图6 : 添加一个新存储过程来对产品进行分页

该存储过程应接受两个整型输入参数 - @startRowIndex 和 @maximumRows – 并使用根据 ProductName 字段排序的 ROW_NUMBER() 函数,只返回那些大于指定 @startRowIndex 并且小于或等于 @startRowIndex + @maximumRows 的行。在新的存储过程中输入以下脚本,然后点击 Save 图标将此存储过程保存至数据库。

CREATE PROCEDURE dbo.GetProductsPaged 

    @startRowIndex int, 
    @maximumRows int 

AS 
    SELECT     ProductID, ProductName, SupplierID, CategoryID, QuantityPerUnit, 
               UnitPrice, UnitsInStock, UnitsOnOrder, ReorderLevel, Discontinued, 
               CategoryName, SupplierName 
FROM 
   ( 
       SELECT ProductID, ProductName, SupplierID, CategoryID, QuantityPerUnit, 
              UnitPrice, UnitsInStock, UnitsOnOrder, ReorderLevel, Discontinued, 
              (SELECT CategoryName 
               FROM Categories 
               WHERE Categories.CategoryID = Products.CategoryID) AS CategoryName, 
              (SELECT CompanyName 
               FROM Suppliers 
               WHERE Suppliers.SupplierID = Products.SupplierID) AS SupplierName, 
              ROW_NUMBER() OVER (ORDER BY ProductName) AS RowRank 
        FROM Products 
    ) AS ProductsWithRowNumbers 
WHERE RowRank > @startRowIndex AND RowRank <= (@startRowIndex + @maximumRows)

创建完存储过程后,花些时间来对其进行测试。在 Server Explorer 中右键单击 GetProductsPaged 存储过程名称,并选择 Execute 选项。Visual Studio 会提示键入输入参数 @startRowIndex 和 @maximumRows(参见图 7 )。试用不同的数值并查看结果。

图7 : 输入@startRowIndex 和 @maximumRows 参数的值

选择完这些输入参数的值后,输出窗口将显示结果。图 8 显示了 @startRowIndex 和 @maximumRows 参数值均为 10 时的结果。

图8 : 返回在第二个数据页面显示的记录 ( 单击此处查看实际大小的图像 )

此存储过程创建完成后,我们就可以创建 ProductsTableAdapter 方法了。打开 Northwind.xsd Typed DataSet ,右键单击 ProductsTableAdapter 并选择 Add Query 选项。我们使用已有的存储过程来创建查询,而不是使用 ad-hoc SQL 语句进行创建。

图9 :使用已有的存储过程创建DAL 方法

然后,系统会提示我们选择调用的存储过程。从下拉列表中选择GetProductsPaged 存储过程。

图10 :从下拉列表中选择GetProductsPaged 存储过程

下一个屏幕会询问存储过程返回的数据类型:表格数据、单一值或无值。由于GetProductsPaged 存储过程能够返回多行记录,所以选择返回表格数据。

图11 :指定存储过程返回表格数据

最后,指出所创建方法的名称。与前面的教程一样,使用 Fill a DataTable 和 Return a DataTable 创建方法。第一个方法命名为 FillPaged ,第二个方法命名为 GetProductsPaged 。

图12 :将方法命名为FillPaged 和 GetProductsPaged

除了创建一个 DAL 方法来返回特定的产品页面,我们还需在 BLL 中 提供相同功能。与 DAL 方法相同,BLL 的 GetProductsPaged 方法必须接受两个整型输入参数来指定Start Row Index 和 Maximum Rows ,并且必须仅返回在指定范围内的记录。在ProductsBLL 类中创建这个 BLL 方法,该方法只调用 DAL 的 GetProductsPaged 方法,如下所示:

[System.ComponentModel.DataObjectMethodAttribute( 
    System.ComponentModel.DataObjectMethodType.Select, false)] 
public Northwind.ProductsDataTable GetProductsPaged(int startRowIndex, int maximumRows) 

    return Adapter.GetProductsPaged(startRowIndex, maximumRows); 
}

BLL 方法的输入参数可使用任何名称,但是,正如我们将在稍后见到的:选择使用 startRowIndex 和 maximumRows 会让我们在配置 ObjectDataSource 使用此方法时轻松一些。

步骤4 :配置ObjectDataSource 使用自定义分页

创建完可以访问特定记录的 BLL 和 DAL 方法后,我们就可以创建 GridView 控件了,该控件将使用自定义分页机制来对基础记录进行分页。首先打开PagingAndSorting 文件夹中的 EfficientPaging.aspx 页面,向该页面 添加一个 GridView ,并配置它使用新的ObjectDataSource 控件。在前面的教程中 ,我们经常配置 ObjectDataSource 使用 ProductsBLL 类的 GetProducts 方法。但是,这一次我们改为使用 GetProductsPaged 方法,因为 GetProducts 方法会返回数据库中的所有产品,而GetProductsPaged 只会返回特定的所需记录。

图13 :配置ObjectDataSource 使用ProductsBLL 类的 GetProductsPaged 方法

由于我们创建的是只读的 GridView ,花些时间将INSERT 、UPDATE 和 DELETE 选项卡中的方法下拉列表设置为 (None) 。

然后,ObjectDataSource 向导会提示我们选择 GetProductsPaged 方法的 startRowIndex 和 maximumRows 输入参数值的来源。由于这两个输入参数将由GridView 自动设置,因此将来源设置为 None ,然后单击 Finish 。

图14 :将输入参数来源指定为 None

完成 ObjectDataSource 向导之后,GridView 将为每个产品数据字段包含一个 BoundField 或 CheckBoxField 。任意修改 GridView 的外观,直到你满意为止。我选择显示 ProductName 、CategoryName 、SupplierName 、QuantityPerUnit 和 UnitPrice BoundField 。同样,在 GridView 的智能标记中选中 Enable Paging 复选框,将其配置为支持分页功能。完成这些修改后,GridView 和 ObjectDataSource 的声明性标记应如下所示:

<asp:GridView ID="GridView1" runat="server" AutoGenerateColumns="False" 
    DataKeyNames="ProductID" DataSourceID="ObjectDataSource1" AllowPaging="True"> 
    <Columns> 
        <asp:BoundField DataField="ProductName" HeaderText="Product" 
            SortExpression="ProductName" /> 
        <asp:BoundField DataField="CategoryName" HeaderText="Category" 
            ReadOnly="True" SortExpression="CategoryName" /> 
        <asp:BoundField DataField="SupplierName" HeaderText="Supplier" 
            SortExpression="SupplierName" /> 
        <asp:BoundField DataField="QuantityPerUnit" HeaderText="Qty/Unit" 
            SortExpression="QuantityPerUnit" /> 
        <asp:BoundField DataField="UnitPrice" DataFormatString="{0:c}" 
            HeaderText="Price" HtmlEncode="False" SortExpression="UnitPrice" /> 
    </Columns> 
</asp:GridView> 
 
<asp:ObjectDataSource ID="ObjectDataSource1" runat="server" 
    OldValuesParameterFormatString="original_{0}" SelectMethod="GetProductsPaged" 
    TypeName="ProductsBLL"> 
    <SelectParameters> 
        <asp:Parameter Name="startRowIndex" Type="Int32" /> 
        <asp:Parameter Name="maximumRows" Type="Int32" /> 
    </SelectParameters> 
</asp:ObjectDataSource>

但是,通过浏览器访问该页则无法找到 GridView 。

图15 :没有显示GridView

由于 ObjectDataSource 当前使用 0 作为GetProductsPaged 方法的startRowIndex 和 maximumRows 输入参数的值,SQL 查询结果不返回任何记录,所以 GridView 不显示。

要解决上述问题,我们需要配置 ObjectDataSource 使用自定义分页。配置步骤如下所示 :

  1. 将ObjectDataSource的EnablePaging属性设置为true – 该设置指示 ObjectDataSource 必须向 SelectMethod 传递两个额外的参数:一个用于指定 Start Row Index ( StartRowIndexParameterName) ,另一个用于指定 Maximum Rows (MaximumRowsParameterName) 。
  2. 相应地设置ObjectDataSource的StartRowIndexParameterName和MaximumRowsParameterName的属性– StartRowIndexParameterName 和 MaximumRowsParameterName 属性指定两个输入参数的名称,这两个输入参数会传递给 SelectMethod ,用于自定义分页。默认情况下,这些参数的名称是 startIndexRow 和 maximumRows ,这就是在创建 BLL 中的 GetProductsPaged 方法时使用它们作为输入参数名称的原因。如果你为BLL 的 GetProductsPaged 方法选择其它的参数名称( 如 startIndex 和 maxRows ),则需要据此相应地设置 ObjectDataSource 的 StartRowIndexParameterName 和 MaximumRowsParameterName 属性(例如,设置 StartRowIndexParameterName 为 startIndex ,设置 MaximumRowsParameterName 为 maxRows ) 。
  3. 将ObjectDataSource的 SelectCountMethod 属性设置为返回待分页记录总数的方法的名称(TotalNumberOfProducts) –ProductsBLL 类的 TotalNumberOfProducts 方法通过使用 DAL 方法(该方法执行 SELECT COUNT(*) FROM Products 查询语句)返回待分页记录总数。ObjectDataSource 需要该信息来正确地呈现分页界面。
  4. 从ObjectDataSource的声明性标记中删除startRowIndex和maximumRows <asp:Parameter>元素 – 通过向导配置 ObjectDataSource 时,Visual Studio 自动为 GetProductsPaged 方法的输入参数添加两个 <asp:Parameter> 元素。通过将 EnablePaging 设为 true ,这些参数会被自动传递,如果它们还出现在声明性语法中,ObjectDataSource 会试图向 GetProductsPaged 方法传递四个参数,并向 TotalNumberOfProducts 方法传递两个参数。如果忘记删除这些<asp:Parameter> 元素,通过浏览器访问该页面时会得到如下错误信息:“ObjectDataSource 'ObjectDataSource1' could not find a non-generic method 'TotalNumberOfProducts' that has parameters:startRowIndex, maximumRows.”

完成这些修改后,ObjectDataSource 的声明性语法应如下所示 :

<asp:ObjectDataSource ID="ObjectDataSource1" runat="server" 
    OldValuesParameterFormatString="original_{0}" TypeName="ProductsBLL" 
    SelectMethod="GetProductsPaged" EnablePaging="True" 
    SelectCountMethod="TotalNumberOfProducts"> 
</asp:ObjectDataSource>

注意:已经完成 EnablePaging 和 SelectCountMethod 属性的设置,并且已经删除 <asp:Parameter> 元素。图 16 显示了进行这些修改后的 Properties 窗口的屏幕截图。

图16 :要使用自定义分页,需配置 ObjectDataSource 控件

完成这些修改后,通过浏览器访问该页面。应该看到按照字母顺序列出的 10 件产品。花些时间查看其中一页的数据。尽管对于最终用户来说,默认分页与自定义分页在外观上没有差别,但是由于自定义分页只获取给定页面需要显示的记录,因此自定义分页方法在对大量数据分页时效率更高。

17 :使用自定义分页对根据产品名称排序的数据分页

注意使用自定义分页时,ObjectDataSource 的SelectCountMethod 返回的页数值存储在GridView 的视图状态中。其它GridView 变量(PageIndex 、EditIndex 、SelectedIndex 、DataKeys 集合等)存储在具有持续性的控件状态中,该状态 与GridView 的EnableViewState 属性值无关。由于PageCount 值在 回传期间存在于视图状态中,因此当使用的分页界面上有转至最后一页的链接时,必须启用GridView 的视图状态。(如果分页界面中不包含转至最后一页的链接,则可以禁用视图状态。)

单击最后一页链接会引发一次回传并指示GridView 更新其 PageIndex 属性。如果单击最后一页链接,GridView 指定其PageIndex 属性为小于 PageCount 属性的值。禁用视图状态时,PageCount 值在回传 时丢失,而 PageIndex 被赋以最大整型数值。然后,GridView 尝试通过PageSize 和 PageCount 属性相乘来确定起始行索引。由于乘积会超出允许的最大整数范围,因此会引发 OverflowException 。

执行自定义分页和排序

当前的自定义分页实现方法要求在创建GetProductsPaged 存储过程时静态指定分页时依赖的顺序。然而,你可能已经注意到 GridView 的智能标记中除包含 Enable Paging 选项之外,还包含一个 Enable Sorting 复选框。遗憾的是,使用当前的自定义分页方法向 GridView 添加排序支持时,我们只能够对当前查看的数据页面上的记录排序。例如,如果配置 GridView 也支持分页,那么在查看根据产品名称以降序排列的首页数据时,它将倒转第一页上的产品顺序。如图 18 所示,Carnarvon Tigers 为按照字母逆序排序后的第一个产品,这忽略了按照字母顺序排在 Carnarvon Tigers 之后的其余 71 个产品,在排序时只考虑了首页面的记录。

图18 :仅对在当前页面显示的数据排序

因为排序过程发生在从 BLL 的GetProductsPaged 方法获取数据之后,并且 GetProductsPaged 方法只返回特定页面的记录,因此排序只对当前数据页面起作用。为了正确地实现排序,我们需要向 GetProductsPaged 方法传递排序表达式,这样就可以在返回特定数据页面之前对数据进行相应的排序。我们将在下一篇教程中探讨如何实现这种方法。

执行自定义分页和删除

如果启用了 GridView 的删除功能,且该 GridView 使用了自定义分页技术对数据进行分页,当删除最后一页的最后一行纪录时,你会发现 GridView 消失了,而不是 GridView 的 PageIndex 相应地减小。要展示这个 bug ,我们在刚创建的教程中启用删除功能。由于我们对 81 个产品进行分页,每页 10 个产品,因此转至最后一页(第 9 页)时,你只会看到一个产品。删除该产品。

删除最后一个产品时,GridView 应当 自动转至第八页,这是默认分页方法具有的功能。但是,使用自定义分页时,删除最后一页的最后一个产品后, GridView 从屏幕中完全消失。发生这种情况的确切原因超出了本教程的讨论范围,参见 从应用了自定义分页的 GridView 中删除最后一页的最后一行记录 ,来了解产生该问题的原因。总之,这是由于在单击 Delete 按钮之后, GridView 执行了以下处理步骤:

  1. 删除记录。
  2. 根据指定的 PageIndex 和 PageSize 显示 相应记录。
  3. 检查以确保 PageIndex 没有超过数据源中数据页面的数量;如果超过,则自动减小 GridView 的 PageIndex 属性值。
  4. 使用在步骤 2 中得到的记录将相应的数据页面绑定到GridView 。

问题的根源是,在步骤 2 中获取记录时使用的PageIndex 仍然是最后一页的 PageIndex ,该页仅有的一条记录刚刚被删除了。因此,在步骤 2 中没有 记录返回,因为最后一页已经不包含任何记录了。然后,在步骤 3 中,GridView 意识到 PageIndex 属性值大于数据源中页面总数(因为我们已经删除了最后一页中的最后一条记录),因此它减小了 PageIndex 的属性值。在步骤 4 中,GridView 试图将自身绑定到步骤 2 中返回的数据,然而步骤 2 没有返回任何记录,所以生成了一个空的 GridView 。采用默认分页时,该问题不会出现,因为在步骤 2 中,GridView 获取数据源中的所有 记录。

解决该问题有两个方案:第一个方案是为 GridView 的 RowDeleted 事件创建一个Event Handler ,它会确定刚刚删除的页面显示多少条记录。如果只有一条记录,那么刚刚删除的记录一定是最后一条记录,因此我们要减小 GridView 的 PageIndex 。当然,我们只希望在删除操作真正成功(可通过确保 e.Exception 属性为空来确定)时才更新 PageIndex 。

该方案可行是因为 PageIndex 在第 1 步和第 2 步之间更新。 因此,在第 2 步中,正确的记录被返回。要实现该方案,我们需使用如下代码:

protected void GridView1_RowDeleted(object sender, GridViewDeletedEventArgs e) 

    // If we just deleted the last row in the GridView, decrement the PageIndex 
    if (e.Exception == null && GridView1.Rows.Count == 1) 
        // we just deleted the last row 
        GridView1.PageIndex = Math.Max(0, GridView1.PageIndex - 1); 
}

另一种方案是为 ObjectDataSource 的 RowDeleted 事件创建一个 Event Handler ,并且将 AffectedRows 属性值设置为 1 。在步骤 1 中删除记录之后(在步骤 2 中重新获取数据之前),如果该操作影响了一行或多行记录,则 GridView 更新其 PageIndex 属性值。但是,AffectedRows 属性不是通过 ObjectDataSource 设置的,因此这个步骤被忽略了。执行这步操作的一个方式是在删除操作成功完成后,手动地设置 AffectedRows 属性。使用如下代码完成该操作:

protected void ObjectDataSource1_Deleted( 
    object sender, ObjectDataSourceStatusEventArgs e) 

    // If we get back a Boolean value from the DeleteProduct method and it's true, 
    // then we successfully deleted the product. Set AffectedRows to 1 
    if (e.ReturnValue is bool && ((bool)e.ReturnValue) == true) 
        e.AffectedRows = 1; 
}

可以在EfficientPaging.aspx 示例的代码文件类中找到这两个 Event Handler 的代码。

默认分页与自定义分页的性能比较

由于自定义分页只获取所需的记录,而默认分页为每个浏览的页面返回所有 记录,因此自定义分页比默认分页更有效率是显而易见的。但是自定义分页能提升多少效率呢?从默认分页到自定义分页,我们能得到哪种程度的性能提升呢?

很遗憾,这里没有“ 放之四海皆准” 的答案。性能提升取决于多个因素,最主要的两个因素是待分页记录的数量和带给数据库服务器以及 web 服务器与数据库服务器之间的信道的负载。对于只有几十条记录的小数据表,性能的差别可以忽略不计。然而,对于拥有成千上万条记录的大型数据表,性能的差别则非常显著。

我的一篇文章 在 ASP.NET 2.0 中使用 SQL Server 2005 进行自定义分页 中有一些我运行过的性能测试,它们展示了这两种分页技术对一个有 50,000 条记录的数据库表作分页处理时的性能差异。在这些测试中,我分别测试了在 SQL Server 上使用 SQL Profiler执行查询的时间,以及在 ASP.NET 页上使用 ASP.NET 跟踪特性 的执行时间。注意:这些测试是在我的开发环境下只有一个活动用户的情况下进行的,并且没有模仿典型网站的负载模式,因此并不科学。无论如何,这些结果还是展示了默认分页和自定义分页机制在处理大量数据时在执行时间上的相对差异。

  平均时间(秒) 获取次数

默认分页– SQL Profiler

1.411

383

自定义分页– SQL Profiler

0.002

29

默认分页– ASP.NET Trace

2.379

自定义分页– ASP.NET Trace

0.029

由此可见,在自定义分页中,获取特定数据页面的平均次数减少了354 次,并且完成时间非常短。在 ASP.NET 页面中,自定义分页所花时间接近默认分页的 1/100 。关于这些结果的更多信息以及详细代码与数据库,请参阅 我的文章 。你可以下载代码和数据库,在自己的环境中重现这些测试。

小结

默认分页的实现方式简单:只需在Web 数据控件的智能标记中选中 Enable Paging 复选框即可,但是操作的简单须以性能为代价。采用默认分页时,用户请求任何一个数据页面都会返回所有 记录,即使可能只需显示很小一部分数据。为了提升性能, ObjectDataSource 提供了一个替代的分页方案:自定义分页。

自定义分页只获取需要显示的记录,从而改进了默认分页的性能问题。但实现自定义分页涉及的内容较多。首先,编写的查询语句必须能正确地(并且高效地)访问所请求的特定记录。这可以通过多种方式来实现:我们在本教程中探讨的一种方式是使用 SQL Server 2005 的新 ROW_NUMBER() 函数来对结果排序,然后只返回那些排序在指定范围内的记录。此外,我们还需要添加一种方法来确定待分页的记录总数。创建这些DAL 和 BLL 方法后 ,我们还需要配置 ObjectDataSource , 使它能够确定待分页的记录总数并且能够正确地将 Start Row Index 和 Maximum Rows 传递给BLL 。

虽然实现自定义分页需要多个步骤,而且远没有 默认分页那样简单,但当需要对大量数据进行有效地分页时,我们必须采用自定义分页。正如研究结果所示,自定义分页能够显著减少 ASP.NET 页面的呈现时间,并能将数据库服务器的负载降低一个或多个数量级。

快乐编程!

 

posted @ 2016-05-01 21:36  迅捷之风  阅读(805)  评论(0编辑  收藏  举报