大量のデータを効率的にページングする (C#)

作成者: Scott Mitchell

PDF のダウンロード

データ プレゼンテーション コントロールの既定のページング オプションは、大量のデータを操作する場合には適していません。これは、データのサブセットのみが表示される場合でも、基になるデータ ソース コントロールがすべてのレコードを取得するためです。 このようなケースでは、カスタム ページングを利用する必要があります。

はじめに

前のチュートリアルで説明したように、ページングは次の 2 つの方法のいずれかで実装できます。

  • デフォルトのページングは、データ Web コントロールのスマート タグで [ページングを有効にする] オプションをオンにするだけで実装できます。ただし、データのページを表示するたびに、データの一部のみがページに表示される場合でも、ObjectDataSource は "すべて" のレコードを取得します。
  • カスタム ページングでは、ユーザーが要求したデータの特定のページに対して表示する必要があるレコードのみをデータベースから取得することで、デフォルトのページングよりもパフォーマンスが向上します。ただし、カスタム ページングでは、既定のページングよりも実装の作業が少し多くなります。

チェックボックスをオンにするだけで完了する実装の簡単さから、 デフォルトのページングは魅力的なオプションです。 ただし、すべてのレコードを取得するという単純なアプローチは、十分に大量のデータをページングする場合や、多数の同時ユーザーを持つサイトでは、不適切な選択になります。 このような状況では、応答性の高いシステムを提供するために、カスタム ページングを利用する必要があります。

カスタム ページングの課題は、特定のデータ ページに必要なレコードの正確なセットを返すクエリを記述できるようにすることです。 幸いなことに、Microsoft SQL Server 2005 には、結果をランク付けするための新しいキーワードが用意されています。これにより、レコードの適切なサブセットを効率的に取得できるクエリを作成できます。 このチュートリアルでは、この新しい SQL Server 2005 キーワードを使用して、GridView コントロールにカスタム ページングを実装する方法について説明します。 カスタム ページングのユーザー インターフェイスはデフォルトのページングの場合と同じですが、カスタム ページングを使用して 1 ページから次のページにステップ実行すると、デフォルトのページングよりも数桁高速になる場合があります。

Note

カスタム ページングによって実現するパフォーマンスの正確な向上は、ページングされるレコードの合計数と、データベース サーバーにかかる負荷によって異なります。 このチュートリアルの最後では、カスタム ページングによって得られるパフォーマンスの利点を示すメトリックの概算をいくつか見ていきます。

ステップ 1: カスタム ページングのプロセスについて

データをページングする場合に、ページに表示される正確なレコード数は、要求されているデータのページと、ページごとに表示されるレコードの数によって異なります。 たとえば、81 個の製品をページングし、1 ページあたり 10 個の製品を表示するとします。 最初のページを表示するときは、製品 1 から 10 が必要です。2 番目のページを表示するときは、製品 11 から 20 に関心があります。

取得する必要があるレコードとページング インターフェイスのレンダリング方法を指定する 3 つの変数があります。

  • 開始行インデックスは、表示するデータ ページ内の最初の行のインデックスです。このインデックスは、ページ インデックスにページごとに表示するレコードを乗算し、1 を追加することによって計算できます。 たとえば、一度に 10 個のレコードをページングする場合、最初のページ (ページ インデックスは 0) の開始行インデックスは 0 * 10 + 1、つまり 1 になります。2 番目のページ (ページ インデックスは 1) の場合、開始行インデックスは 1 * 10 + 1、つまり 11 です。
  • 最大行数は、ページごとに表示するレコードの最大数です。 この変数を最大行数と言うのは、最後のページで返されるレコードがページ サイズよりも少なくなる可能性があるためです。 たとえば、ページあたり 10 個のレコードで 81 個の製品をページングする場合、9 番目の最終ページにはレコードが 1 つだけ含まれます。 ただし、最大行数の値よりも多くのレコードが表示されるページはありません。
  • 合計レコード数は、ページングされるレコードの合計数です。 この変数は、特定のページに対して取得するレコードを決定するために必要ではありませんが、ページング インターフェイスを決定します。 たとえば、ページング対象の製品が 81 個ある場合、ページング インターフェイスはページング UI に 9 つのページ番号を表示することを認識します。

デフォルトのページングでは、開始行インデックスは、ページ インデックスとページ サイズの積に 1 を加算して計算されますが、最大行数は単にページ サイズです。 デフォルトのページングでは、データのページをレンダリングするときにデータベースからすべてのレコードが取得されるため、各行のインデックスがわかっており、開始行インデックスの行への移動は簡単なタスクになります。 さらに、合計レコード数は、単に DataTable (またはデータベースの結果を保持するために使用されているオブジェクト) 内のレコード数であるため、簡単に入手できます。

カスタム ページングの実装では、開始行インデックスと最大行数の変数が指定されたら、開始行インデックスから、その後の最大行数までの数のレコードの正確なサブセットのみを返す必要があります。 カスタム ページングには、次の 2 つの課題があります。

  • 指定された開始行インデックスでレコードを返すことを開始できるように、ページング対象のデータ全体の各行に行インデックスを効率的に関連付けることができる必要があります
  • ページング対象のレコードの合計数を指定する必要があります

次の 2 つのステップでは、これら 2 つの課題に対応するために必要な SQL スクリプトについ見ていきます。 SQL スクリプトに加えて、DAL と BLL でメソッドを実装する必要もあります。

ステップ 2: ページング対象のレコードの合計数を返す

表示するページのレコードの正確なサブセットを取得する方法を調べる前に、まずはページング対象のレコードの合計数を返す方法を見てみましょう。 この情報は、ページング ユーザー インターフェイスを適切に構成するために必要です。 特定の SQL クエリによって返されるレコードの合計数は、COUNT 集計関数を使用して取得できます。 たとえば、Products テーブル内のレコードの合計数を調べるために、次のクエリを使用できます。

SELECT COUNT(*)
FROM Products

この情報を返すメソッドを DAL に追加しましょう。 具体的には、上記の SELECT ステートメントを実行する TotalNumberOfProducts() という DAL メソッドを作成します。

まず、App_Code/DAL フォルダー内の Northwind.xsd Typed DataSet ファイルを開きます。 次に、デザイナーで ProductsTableAdapter を右クリックし、[クエリの追加] を選択します。 前のチュートリアルで説明したように、これにより、呼び出されたときに特定の SQL ステートメントまたはストアド プロシージャを実行する新しいメソッドを DAL に追加できます。 前のチュートリアルの TableAdapter メソッドと同様に、アドホック SQL ステートメントを使用することを選択します。

アドホック SQL ステートメントを使用する

図 1: アドホック SQL ステートメントを使用する

次の画面で、作成するクエリの種類を指定できます。 このクエリは Products テーブル内の合計レコード数の単一のスカラー値を返すので、[SELECT which returns a singe value] (単一値を返す SELECT) オプションを選択します。

単一の値を返す SELECT ステートメントを使用するようにクエリを構成する

図 2: 単一の値を返す SELECT ステートメントを使用するようにクエリを構成する

使用するクエリの種類を指定したら、次にクエリを指定する必要があります。

SELECT COUNT(*) FROM Products クエリを使用する

図 3: SELECT COUNT(*) FROM Products クエリを使用する

最後に、メソッドの名前を指定します。 前述のように、TotalNumberOfProducts を使用してみましょう。

DAL メソッドに TotalNumberOfProducts という名前を付けます

図 4: DAL メソッドに TotalNumberOfProducts と名前を付ける

[完了] をクリックすると、ウィザードによって TotalNumberOfProducts メソッドが DAL に追加されます。 DAL のスカラーを戻すメソッドは、SQL クエリ の結果が NULL の場合は null 許容型を返します。 ただし、COUNT クエリは常に非 NULL 値を返します。いずれにしろ、DAL メソッドは Null 許容整数を返します。

DAL メソッドに加えて、BLL のメソッドも必要です。 ProductsBLL クラス ファイルを開き、DAL の TotalNumberOfProducts メソッドを呼び出す TotalNumberOfProducts メソッドを追加します。

public int TotalNumberOfProducts()
{
    return Adapter.TotalNumberOfProducts().GetValueOrDefault();
}

DAL の TotalNumberOfProducts メソッドは Null 許容整数を返しますが、標準整数を返すように ProductsBLL クラスの TotalNumberOfProducts メソッドを作成しました。 したがって、DAL の TotalNumberOfProducts メソッドによって返される Null 許容整数の値部分を ProductsBLL クラスの TotalNumberOfProducts メソッドで返す必要があります。 Null 許容整数が存在する場合は、GetValueOrDefault() を呼び出すと Null 許容整数の値が返されます。ただし、Null 許容整数が null の場合は、既定値の整数値である 0 が返されます。

ステップ 3: レコードの正確なサブセットを返す

次のタスクは、前に説明した開始行インデックスと最大行数の変数を受け入れ、適切なレコードを返すメソッドを DAL および BLL に作成することです。 その前に、まず必要な SQL スクリプトを見てみましょう。 ここでの課題は、先頭行インデックスで始まるレコード (および最大レコード数の数のレコード) のみを返すことができるように、ページング対象の結果全体の各行にインデックスを効率的に割り当てることができる必要があるということです。

行インデックスとして機能する列がデータベース テーブルに既に存在する場合、これは難しいことではありません。 一見すると、Products テーブルの ProductID フィールドで十分であるように思われます。最初の製品の ProductID は 1 であり、2 番目は 2 であるためです。 ただし、製品を削除するとシーケンスにギャップが残るため、このアプローチは無効になります。

行インデックスをページイング対象のデータに効率的に関連付けるために使用される一般的な手法が 2 つあります。これにより、レコードの正確なサブセットを取得できます。

  • SQL Server 2005 の ROW_NUMBER() キーワードを使用する。SQL Server 2005 の新しい ROW_NUMBER() キーワードを使用すると、何らかの順序に基づいて、返される各レコードにランク付けが関連付けられます。 このランク付けを、各行の行インデックスとして使用できます。

  • テーブル変数と 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 値には、開始行インデックスの値と最大行数が割り当てられているためです。 データの最初の数ページなど、番号の小さいページをページングする場合、このアプローチは非常に効率的です。 ただし、最後近くのページを取得する場合は、デフォルトのページングのようなパフォーマンスを示します。

このチュートリアルでは、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: 返されるレコードごとに価格ランクが含まれる

Note

ROW_NUMBER() は、SQL Server 2005 で使用できる多くの新しいランク付け関数の 1 つに過ぎません。 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

この概念をさらに拡張すると、このアプローチを利用して、目的の開始行インデックスと最大行数の値を指定して、データの特定のページを取得できます。

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>)

Note

このチュートリアルの後半で説明するように、ObjectDataSource によって提供される StartRowIndex のインデックスは 0 から始まりますが、SQL Server 2005 によって返される ROW_NUMBER() 値のインデックスは 1 から始まります。 したがって、WHERE 句は、PriceRank が厳密に StartRowIndex より大きく、かつ StartRowIndex + MaximumRows 以下のレコードを返します。

ROW_NUMBER() を使用して、開始行インデックスと最大行数の値を指定してデータの特定のページを取得する方法について説明したので、次はこのロジックを DAL および BLL のメソッドとして実装する必要があります。

このクエリを作成するときは、結果をランク付けする順序を決定する必要があります。ここでは、製品を名前のアルファベット順で並べ替えます。 つまり、このチュートリアルのカスタム ページング実装では、並べ替えも可能なカスタム ページ レポートを作成することはできません。 ただし、次のチュートリアルでは、このような機能を提供する方法について説明します。

前のセクションでは、アドホック SQL ステートメントとして DAL メソッドを作成しました。 残念ながら、TableAdapter ウィザードで使用される Visual Studio の T-SQL パーサーは、ROW_NUMBER() 関数で使用される OVER 構文を気に入りません。 したがって、この DAL メソッドをストアド プロシージャとして作成する必要があります。 [表示] メニューからサーバー エクスプローラーを選択し (または Ctrl + Alt + S キーを押します)、NORTHWND.MDF ノードを展開します。 新しいストアド プロシージャを追加するには、[ストアド プロシージャ] ノードを右クリックし、[新しいストアド プロシージャの追加] を選択します (図 6 を参照)。

製品をページングするための新しいストアド プロシージャを追加する

図 6: 製品をページングするための新しいストアド プロシージャを追加する

このストアド プロシージャは、2 つの整数入力パラメーター (@startRowIndex@maximumRows) を受け取り、ProductName フィールドによって並べ替えられた ROW_NUMBER() 関数を使用して、指定された @startRowIndex より大きく、@startRowIndex + @maximumRow 以下の行のみを返す必要があります。 新しいストアド プロシージャに次のスクリプトを入力し、[保存] アイコンをクリックして、ストアド プロシージャをデータベースに追加します。

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)

ストアド プロシージャを作成した後、少し時間を置いてからテストします。サーバー エクスプローラーで、GetProductsPaged ストアド プロシージャ名を右クリックし、[実行] オプションを選択します。 その後、Visual Studio によって入力パラメーター (@startRowIndex@maximumRow) の入力が求められます (図 7 を参照)。 さまざまな値を試し、結果を調べます。

<span クラス= の値を入力します。@startRowIndex と @maximumRows パラメーター

図 7: @startRowIndex および @maximumRows パラメーターの値を入力する

これらの入力パラメーター値を選択すると、[出力] ウィンドウに結果が表示されます。 図 8 は、@startRowIndex@maximumRows パラメーターの両方に対して 10 を渡したときの結果を示しています。

データの 2 ページ目に表示されるレコードが返されます。

図 8: データの 2 ページ目に表示されるレコードが返されます (フルサイズの画像を表示するにはクリックします)

このストアド プロシージャを作成したので、ProductsTableAdapter メソッドを作成する準備ができました。 Northwind.xsd Typed DataSet を開いて ProductsTableAdapter を右クリックし、[クエリの追加] オプションを選択します。 アドホック SQL ステートメントを使用してクエリを作成する代わりに、既存のストアド プロシージャを使用してクエリを作成します。

既存のストアド プロシージャを使用して DAL メソッドを作成する

図 9: 既存のストアド プロシージャを使用して DAL メソッドを作成する

次に、呼び出すストアド プロシージャを選択するように求められます。 ドロップダウン リストから GetProductsPaged ストアド プロシージャを選択します。

ドロップダウン リストから GetProductsPaged ストアド プロシージャを選択する

図 10: ドロップダウン リストから GetProductsPaged ストアド プロシージャを選択する

次の画面では、ストアド プロシージャによって返されるデータの種類 (表形式データ、1 つの値、値なし) の選択を求められます。 GetProductsPaged ストアド プロシージャは複数のレコードを返すことができるため、表形式データが返されることを示します。

ストアド プロシージャから表形式データが返されることを指定する

図 11: ストアド プロシージャが表形式データを返すことを示す

最後に、作成するメソッドの名前を指定します。 前のチュートリアルと同様に、[Fill a DataTable] (DataTable にデータを格納する) と [Return a DataTable] (DataTable を返す) の両方を使用してメソッドを作成します。 最初のメソッドに FillPaged と名前を付け、2 番目のメソッドに GetProductsPaged と名前を付けます。

メソッドに FillPaged と GetProductsPaged という名前を付ける

図 12: メソッドに FillPaged と GetProductsPaged の名前を付ける

特定の製品ページを返す DAL メソッドを作成するだけでなく、BLL でこのような機能を提供する必要もあります。 DAL メソッドと同様に、BLL の GetProductsPaged メソッドは、開始行インデックスと最大行数を指定するための 2 つの整数入力を受け入れる必要があり、指定された範囲内にあるレコードのみを返す必要があります。 次のように、DAL の GetProductsPaged メソッドを呼び出すだけの BLL メソッドを ProductsBLL クラスに作成します。

[System.ComponentModel.DataObjectMethodAttribute(
    System.ComponentModel.DataObjectMethodType.Select, false)]
public Northwind.ProductsDataTable GetProductsPaged(int startRowIndex, int maximumRows)
{
    return Adapter.GetProductsPaged(startRowIndex, maximumRows);
}

BLL メソッドの入力パラメーターには任意の名前を使用できますが、この後説明するように、startRowIndexmaximumRows を選択すると、このメソッドを使用するように ObjectDataSource を構成するときに余分な作業をしないで済みます。

ステップ 4: カスタム ページングを使用するように ObjectDataSource を構成する

レコードの特定のサブセットにアクセスするための BLL メソッドと DAL メソッドが完成したので、カスタム ページングを使用して基になるレコードをページングする GridView コントロールを作成する準備ができました。 まず、PagingAndSorting フォルダー内の EfficientPaging.aspx ページを開き、ページに GridView を追加し、新しい ObjectDataSource コントロールを使用するように構成します。 以前のチュートリアルでは、多くの場合、ProductsBLL クラスの GetProducts メソッドを使用するように ObjectDataSource を構成していました。 ただし、ここでは代わりに GetProductsPaged メソッドを使用します。GetProducts メソッドはデータベース内の "すべて" の製品を返すのに対し、GetProductsPaged はレコードの特定のサブセットのみを返すためです。

ProductsBLL クラスの GetProductsPaged メソッドを使用するように ObjectDataSource を構成する

図 13: ProductsBLL クラスの GetProductsPaged メソッドを使用するように ObjectDataSource を構成する

読み取り専用の GridView を作成するため、INSERT、UPDATE、DELETE の各タブのメソッド ドロップダウン リストを [なし] に設定します。

次に、ObjectDataSource ウィザードによって、GetProductsPaged メソッドの startRowIndexmaximumRows の入力パラメーターの値のソースの入力が求められます。 これらの入力パラメーターは、実際には GridView によって自動的に設定されるため、ソースを [なし] に設定したまま、[完了] をクリックします。

入力パラメーターのソースを [なし] のままにします

図 14: 入力パラメーターのソースを [なし] のままにする

ObjectDataSource ウィザードが完了すると、GridView には各製品データ フィールドの BoundField または CheckBoxField が含まれます。 必要に応じて、GridView の外観を自由に調整できます。 ここでは、ProductNameCategoryNameSupplierNameQuantityPerUnit、および UnitPrice の BoundField のみを表示することを選択しました。 また、スマート タグの [ページングを有効にする] チェック ボックスをオンにして、ページングをサポートするように GridView を構成します。 これらの変更後、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 はどこにも見つかりません。

GridView が表示されない

図 15: GridView が表示されない

ObjectDataSource が現在、 GetProductsPaged startRowIndexmaximumRows の両方の入力パラメーターの値として 0 を使用しているため、GridView がありません。 そのため、結果の SQL クエリはレコードを返さず、GridView は表示されません。

これを解決するには、カスタム ページングを使用するように ObjectDataSource を構成する必要があります。 これは、次の手順で実行できます。

  1. ObjectDataSource の EnablePaging プロパティを true に設定します。これにより、ObjectDataSource に対して、2 つの追加パラメーターを SelectMethod に渡す必要があることを示します。1 つは開始行インデックス (StartRowIndexParameterName) を指定し、1 つは最大行数 (MaximumRowsParameterName) を指定します。
  2. ObjectDataSource の StartRowIndexParameterNameMaximumRowsParameterName プロパティを適切に設定します。StartRowIndexParameterName および MaximumRowsParameterName プロパティは、カスタム ページングのために SelectMethod に渡される入力パラメーターの名前を示します。 既定では、これらのパラメーター名は startIndexRowmaximumRows です。このため、BLL で GetProductsPaged メソッドを作成するときに、入力パラメーターにこれらの値を使用しました。 BLL の GetProductsPaged メソッドに startIndexmaxRows などの別のパラメーター名を使用することを選択した場合は、ObjectDataSource の StartRowIndexParameterNameMaximumRowsParameterName を適切に設定する必要があります (StartRowIndexParameterName に startIndex、MaximumRowsParameterName に maxRows など)。
  3. ObjectDataSource の SelectCountMethod プロパティを、ページングされるレコードの合計数 (TotalNumberOfProducts) を返すメソッドの名前に設定します。ProductsBLL クラスの TotalNumberOfProducts メソッドは、SELECT COUNT(*) FROM Products クエリを実行する DAL メソッドを使用してページングされるレコードの合計数を返します。 この情報は、ページング インターフェイスを正しくレンダリングするために ObjectDataSource によって必要です。
  4. ObjectDataSource の宣言型マークアップから startRowIndexmaximumRows<asp:Parameter> 要素を削除する。ウィザードを使用して ObjectDataSource を構成するときに、Visual Studio によって GetProductsPaged メソッドの入力パラメーターに対して 2 つの <asp:Parameter> 要素が自動的に追加されます。 EnablePagingtrue に設定すると、これらのパラメーターは自動的に渡されます。これらが宣言構文にも含まれる場合、ObjectDataSource は 4 つのパラメーターを GetProductsPaged メソッドに渡し、2 つのパラメーターを TotalNumberOfProducts メソッドに渡そうとします。 これらの <asp:Parameter> 要素を削除し忘れた場合、ブラウザーからページにアクセスすると、次のようなエラー メッセージが表示されます。"ObjectDataSource 'ObjectDataSource1' で、次のパラメーターが含まれる非ジェネリック メソッド 'TotalNumberOfProducts' が見つかりませんでした: 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 は、これらの変更が行われた後の [プロパティ] ウィンドウのスクリーン ショットを示しています。

カスタム ページングを使用するには、ObjectDataSource コントロールを構成します

図 16: カスタム ページングを使用するために、ObjectDataSource コントロールを構成する

これらの変更を行った後、ブラウザー経由でこのページにアクセスします。 アルファベット順に並べられた 10 個の製品が表示されます。 少し時間を取って、一度に 1 ページずつデータを見ていきます。 デフォルトのページングとカスタム ページングの間でエンド ユーザーの観点からは視覚的な違いはありませんが、カスタム ページングは特定のページに対して表示する必要があるレコードのみを取得するため、大量のデータをより効率的にページングできます。

製品名で並べ替えられたデータは、カスタム ページングを使用してページングされます

図 17: 製品名で並べ替えられたデータが、カスタム ページングを使用してページングされる (フルサイズの画像を表示するにはクリックします)。

Note

カスタム ページングでは、ObjectDataSource の SelectCountMethod によって返されるページ数の値は GridView のビュー状態に格納されます。 その他の GridView 変数、PageIndexEditIndexSelectedIndexDataKeys コレクションなどは、GridView の EnableViewState プロパティの値に関係なく永続化される "コントロール ステート" に保存されます。 PageCount 値はビュー状態を使用してポストバック間で保持されるため、最後のページに移動するリンクを含むページング インターフェイスを使用する場合は、GridView のビュー状態を有効にする必要があります。 (ページング インターフェイスに最後のページへの直接リンクが含まれていない場合は、ビュー状態を無効にすることができます)。

最後のページのリンクをクリックするとポストバックが発生し、GridView にその PageIndex プロパティを更新するよう指示します。 最後のページのリンクがクリックされた場合、GridView はその PageIndex プロパティに PageCount プロパティより 1 少ない値を割り当てます。 ビュー状態を無効にすると、ポストバック間で PageCount 値が失われ、代わりに PageIndex に最大整数値が割り当てられます。 次に、GridView は、PageSizePageCount プロパティを乗算して、開始行インデックスの決定を試みます。 この積は許容される最大整数サイズを超えるため、この結果は OverflowException になります。

カスタム ページングと並べ替えを実装する

現在のカスタム ページング実装では、GetProductsPaged ストアド プロシージャの作成時にデータのページング順序を静的に指定する必要があります。 ただし、GridView のスマート タグには、[ページングを有効にする] オプションに加えて、[並べ替えを有効にする] チェック ボックスが含まれていることに気が付いたかもしれません。 残念ながら、現在のカスタム ページング実装を使用した GridView に並べ替えのサポートを追加しても、現在表示されているデータ ページのレコードのみが並べ替えられます。 たとえば、ページングもサポートするように GridView を構成した場合、データの最初のページを表示するときに、製品名で降順に並べ替えると、1 ページ目の製品の順序が逆になります。 図 18 に示すように、逆アルファベット順に並べ替えると、Carnarvon Tigers が最初の製品として表示され、アルファベット順で Carnarvon Tigers の後に続く 71 のその他の製品は無視されます。並べ替えでは、最初のページのレコードのみが考慮されます。

現在のページに表示されているデータのみが並べ替えられます

図 18: 現在のページに表示されているデータのみが並べ替えられる (フルサイズの画像を表示するにはクリックします)

BLL の GetProductsPaged メソッドからデータが取得された後に並べ替えが行われるため、このメソッドは特定のページのレコードのみを返すため、並べ替えは現在のデータ ページにのみ適用されます。 並べ替えを正しく実装するには、データの特定のページを返す前にデータを適切にランク付けできるように、並べ替え式を GetProductsPaged メソッドに渡す必要があります。 これを実現する方法については、次のチュートリアルで説明します。

カスタム ページングと削除の実装

カスタム ページング手法を使用してデータがページングされる GridView で削除機能を有効にすると、最後のページから最後のレコードを削除すると、GridView の PageIndex が適切に減らされる代わりに、GridView が消えてしまいます。 このバグを再現するには、先ほど作成したチュートリアルで削除を有効にします。 最後のページ (9 ページ) に移動します。81 個の製品を一度に 10 個の製品でページングしているため、1 つの製品が表示されます。 この製品を削除します。

最後の製品を削除すると、GridView は 自動的に 8 ページ目に移動 "しなければならず"、このような機能は既定のページングでは実装されています。 ただし、カスタム ページングでは、最後のページでその最後の製品を削除すると、GridView は画面から完全に消えてしまいます。 これが発生する正確な "理由" は、このチュートリアルの範囲を少し超えています。この問題の原因に関する低レベルの詳細については、「カスタム ページングを使用する GridView から最後のページの最後のレコードを削除する」を参照してください。 要約すると、削除ボタンがクリックされたときに GridView によって実行される次の一連の手順が原因です。

  1. レコードを削除する
  2. 指定した PageIndexPageSize に対して表示する適切なレコードを取得する
  3. PageIndex がデータ ソース内のデータのページ数を超えていないことを確認する。超えている場合は、GridView の PageIndex プロパティを自動的に減らす
  4. 手順 2 で取得したレコードを使用して、データの適切なページを GridView にバインドする

この問題は、手順 2 で、表示するレコードを取得するときに使用された PageIndex が、単一のレコードが削除されたばかりの最後のページの PageIndex のままであることに起因します。 そのため、手順 2 では、データの最後のページにレコードが含まれていないため、レコードは返され "ません"。 その後、手順 3 で GridView は、その PageIndex プロパティがデータ ソース内のページの合計数より大きい (最後のページの最後のレコードを削除したため) ことを認識し、その PageIndex プロパティを減らします。 手順 4 では、GridView は、手順 2 で取得したデータに自身をバインドしようとします。ただし、手順 2 でレコードが返されていないため、空の GridView になります。 デフォルトのページングでは、手順 2 で "すべて" のレコードがデータ ソースから取得されるため、この問題は発生しません。

これを修正するには、2 つのオプションがあります。 1 つ目は、削除されたページに表示されていたレコードの数を特定する GridView の RowDeleted イベント ハンドラー用のイベント ハンドラーを作成することです。 レコードが 1 つしかなかった場合は、削除したレコードが最後のレコードであるはずであり、GridView の PageIndex を減らす必要があります。 もちろん、削除操作が実際に成功した場合にのみ PageIndex を更新します。これは、e.Exception プロパティが null であることを確認して特定できます。

この方法が機能するのは、手順 1 の後、手順 2 より前に PageIndex を更新するためです。 そのため、手順 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 イベントのイベント ハンドラーを作成し、AffectedRows プロパティの値を 1 に設定します。 手順 1 でレコードを削除した後 (ただし、手順 2 でデータを再取得する前)、GridView は、1 つ以上の行が操作の影響を受けた場合にその PageIndex プロパティを更新します。 ただし、AffectedRows プロパティは ObjectDataSource によって設定されないため、この手順は省略されます。 この手順を実行する 1 つの方法は、削除操作が正常に完了した場合に 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 の例の分離コード クラスにあります。

デフォルトのページングとカスタム ページングのパフォーマンスの比較

カスタム ページングでは必要なレコードのみが取得されますが、デフォルトのページングでは表示されるページごとに "すべて" のレコードが返されるため、カスタム ページングの方がデフォルトのページングよりも効率的であることは明らかです。 ただし、カスタム ページングはどのくらい効率的なのでしょうか。 デフォルトのページングからカスタム ページングに移行すると、どのようなパフォーマンス向上が得られるのでしょうか。

残念ながら、ここにすべてに当てはまる回答はありません。 パフォーマンスの向上は、さまざまな要因によって異なります。最も顕著な 2 つの要因は、ページング対象のレコード数と、データベース サーバーおよび Web サーバーとデータベース サーバー間の通信チャネルにかかる負荷です。 わずか数十件のレコードを含む小さなテーブルの場合、パフォーマンスの違いはごくわずかの場合があります。 ただし、数千から数十万行の大きなテーブルの場合、パフォーマンスの違いは重大です。

「SQL Server 2005 を使用した ASP.NET 2.0 でのカスタム ページング」の記事には、50,000 レコードのデータベース テーブルをページングしたときの、これら 2 つのページング手法のパフォーマンスの違いを示すために実行したパフォーマンス テストがいくつか含まれています。 これらのテストでは、クエリを実行する時間を、SQL Server レベル (SQL Profiler を使用) と、ASP.NET ページ (ASP.NET のトレース機能を使用) の両方で調べました。 これらのテストは、アクティブなユーザーが 1 人の開発ボックスで実行されたため、非科学的であり、一般的な Web サイトの読み込みパターンを模倣しないことに注意してください。 とはいえ、十分に大量のデータを操作する場合のデフォルトのページングとカスタム ページングの実行時間の相対的な違いが結果に示されています。

平均期間 (秒) Reads
既定のページング SQL Profiler 1.411 383
カスタム ページング SQL Profiler 0.002 29
既定のページング ASP.NET トレース 2.379 N/A
カスタム ページング ASP.NET トレース 0.029 N/A

ご覧のように、データの特定のページを取得する際に必要な読み取りは平均で 354 回少なく、一瞬で完了しています。 ASP.NET ページでは、デフォルトのページングを使用するときに要した時間の 1/100 に近い時間でカスタム ページをレンダリングできています。

まとめ

デフォルトのページングは、データ Web コントロールのスマート タグの [ページングを有効にする] チェックボックスをオンにするだけで実装できますが、このシンプルさはパフォーマンスを犠牲にしています。 デフォルトのページングでは、ユーザーがデータの任意のページを要求すると、そのごく一部しか表示できない場合でも、"すべて" のレコードが返されます。 このパフォーマンスのオーバーヘッドに対処するために、ObjectDataSource には代替ページング オプションであるカスタム ページングが用意されています。

カスタム ページングは、表示する必要があるレコードのみを取得することでデフォルトのページングのパフォーマンスの問題を改善しますが、カスタム ページングの実装はより複雑になります。 最初に、要求された特定のレコード サブセットに正しく (かつ効率的に) アクセスするクエリを記述する必要があります。 これは、さまざまな方法で実現できます。このチュートリアルで説明したのは、SQL Server 2005 の新しい ROW_NUMBER() 関数を使用して結果をランク付けし、そのランクが指定された範囲内にある結果のみを返す方法でした。 さらに、ページングするレコードの合計数を決定する手段を追加する必要があります。 これらの DAL メソッドと BLL メソッドを作成した後、ObjectDataSource を構成して、ページングされるレコードの合計数を決定し、開始行インデックスと最大行数の値を BLL に正しく渡すことができるようにする必要があります。

カスタム ページングの実装には多くの手順が必要であり、デフォルトのページングほど単純ではありませんが、十分に大量のデータをページングする場合は、カスタム ページングが必要です。 調査結果が示すように、カスタム ページングは、ASP.NET ページのレンダリング時間を数秒削減することができ、データベース サーバーの負荷を 1 桁以上軽くすることができます。

プログラミングに満足!

著者について

7 冊の ASP/ASP.NET 書籍の著者であり、4GuysFromRolla.com の創設者である Scott Mitchell は、1998 年から Microsoft Web テクノロジに取り組んでいます。 Scott は、独立したコンサルタント、トレーナー、ライターとして働いています。 彼の最新の本は サムズは24時間で2.0 ASP.NET 自分自身を教えています。 にアクセスするか、ブログを使用して にアクセスmitchell@4GuysFromRolla.comできます。これは でhttp://ScottOnWriting.NET見つけることができます。