.NET データ アクセス アーキテクチャ ガイド

.NET による分散アプリケーションの構築

Alex Mackman、Chris Brooks、Steve Busby、and Ed Jezierski
Microsoft Corporation

October 2001
日本語版最終更新日 2003 年 6 月 6 日

概要 : このドキュメントでは、複数層 .NET アプリケーションに ADO.NET ベースのデータ アクセス層を実装するためのガイドラインを提供します。ある範囲の共通のデータ アクセス タスクとシナリオに注目し、最適なアプローチや技法の選択に役立つガイドラインを提供します。

目次

ADO.NET の紹介
データベース接続の管理
エラー処理
パフォーマンス
ファイアウォール経由の接続
BLOB の処理
トランザクション
データのページ処理

はじめに

.NET アプリケーションのデータ アクセス層をデザインする場合、データ アクセス モデルとして Microsoft® ADO.NET を使用する必要があります。 ADO.NET は、機能が豊富で、疎結合の複数層 Web アプリケーションや Web サービスでのデータ アクセスの必要条件をサポートします。 ADO.NET は、豊富な機能を持つ多くのオブジェクト モデルを提供すると共に、特定の問題を解決するまざまな方法を提供します。

このドキュメントは、最適なデータ アクセス アプローチの選択に役立つガイダンスを提供します。広範な共通データ アクセス シナリオを詳しく説明し、パフォーマンスのヒントを提供し、推奨事例を示すことで、ガイダンスを提供していきます。また、このドキュメントでは、次のようなその他のよく寄せられる質問への回答も提供します。データベースの接続文字列を格納するのに最適な場所はどこですか? どのように接続プールを実装すればよいのでしょうか? トランザクションを使ってどのように作業すればよいのでしょうか? ユーザーが大量のレコードをスクロールできるように、ページ処理を実装するにはどうすればよいのでしょうか?

このドキュメントは、 ADO.NET に同梱されている 2 つのプロバイダのうちの 1 つ、SQL Server .NET データ プロバイダを使って、 Microsoft SQL Server™ 2000 へのアクセスに ADO.NET を使用する方法に注目していることに注意してください。このドキュメントでは必要に応じて、 OLE DB .NET データ プロバイダを使ってその他の OLE DB 対応データ ソースにアクセスするときに認識しておく必要のある違いにも注目しています。

このドキュメントで説明しているガイドラインと推奨事例を使用して開発したデータ アクセス コンポーネントを実際に実装する場合は、「Data Access Application Block」を参照してください。この実装用のソース コードを入手でき、それを任意の .NET アプリケーション内で直接使用できることに注意してください。

このドキュメントの対象読者

このドキュメントは、 .NET アプリケーションを構築するアプリケーションの設計者や企業の開発者向けのガイドラインを提供しています。複数層 .NET アプリケーションのデータ層のデザインおよび開発担当者が、このドキュメントをお読みください。

知っておく必要があること

このガイドを使用して .NET アプリケーションを構築するためには、 Active® Data Object (ADO) や OLE DB を使ってデータ アクセス コードを実施に開発した経験と、 SQL Server の使用経験が必要になります。また、.NET プラットフォーム用のマネージ コードを開発する方法を理解し、 ADO .NET データ アクセス モデルに行われた基本的な変更点を知っておく必要があります。 .NET の開発に関する詳細については、 https://www.microsoft.com/japan/msdn/net/default.asp を参照してください。

ADO.NET の紹介

ADO.NET は、.NET アプリケーション用のデータ アクセス モデルです。 SQL Server 2000 などのリレーショナル データベース システムや、 OLE DB プロバイダが存在する多くの新たなデータ ソースへのアクセスに使用できます。 ADO.NET は、ある程度は ADO テクノロジの最新の進化形態であると言えます。ただし、ADO.NET には、いくつか大きな変更と革新が行われています。特に疎結合で、本質的には非接続であるという Web アプリケーションの特性を活かした変更と革新が行われています。 ADO と ADO.NET の比較については、 MSDN の記事、「ADO プログラマのための ADO.NET」を参照してください。

ADO.NET で行われた主要な変更点の 1 つは、 ADO Recordset オブジェクトを DataTableDataSetDataAdapter、および DataReader オブジェクトの組み合わせに置き換えたことです。 DataTable は、1 つのテーブルからの行のコレクションを表していて、この点では Recordset に似ています。 DataSet は、さまざまなテーブルを関連付けるリレーションシップや制約と共に、 DataTable オブジェクトのコレクションを表します。実際には、DataSet は組み込みの XML (Extensible Markup Language) サポートを備えたインメモリ リレーショナル構造です。

DataSet の主な特性の 1 つは、データを設定するために使用した元になるデータ ソースの知識を必要としないことです。 DataSet は、データのコレクションを表すために使用する非接続のスタンドアロン エンティティで、複数層アプリケーションのさまざまな層を経由してコンポーネントからコンポーネントに渡すことができます。また、XML データ ストリームとしてシリアル化できるので、異種プラットフォーム間のデータ転送に最適です。 ADO.NET は DataAdapter オブジェクトを使用して、 DataSet と元になるデータ ソース間でデータを受け渡します。また、DataAdapter オブジェクトは、以前 Recordset に関連付けられていたバッチ更新機能を強化した機能も提供します。

図 1 は、DataSet オブジェクト モデル全体を示しています。

図 1. DataSet オブジェクト モデル

.NET データ プロバイダ

ADO.NET は、.NET データ プロバイダのサービスに依存しています。これらのサービスは、元になるデータ ソースへのアクセスを提供し、 4 つの主要なオブジェクト (ConnectionCommandDataReader、および DataAdapter) で構成されています。現在、ADO.NET には以下の 2 つのプロバイダが同梱されています。

  • SQL Server .NET データ プロバイダ。これは、Microsoft SQL Server 7.0 以降のデータベース用のプロバイダです。このプロバイダは SQL Server へのアクセスを最適化し、 SQL Server のネイティブなデータ転送プロトコルを使用して直接 SQL Server と通信します。
    通常、SQL Server 7.0 または SQL Server 2000 に接続するときにこのプロバイダを使用します。
  • OLE DB .NET データ プロバイダ。これは、OLE DB データ ソース用のマネージ プロバイダです。このプロバイダはデータベースと通信するときに OLE DB 層経由で呼び出すので、 SQL Server .NET データ プロバイダよりもやや機能的に劣ります。このプロバイダは ODBC (Open Database Connectivity) 用の OLE DB プロバイダ、 MSDASQL をサポートしていないことに注意してください。代わりに、ODBC データ ソースには、ODBC .NET データ プロバイダを使用してください (これについては、後半で説明します)。 ADO.NET と互換性がある OLE DB プロバイダの一覧については、 https://www.microsoft.com/japan/msdn/library/ja/cpguide/html/cpconadonetproviders.asp を参照してください。

現在ベータ テスト中のその他の .NET データ プロバイダには、以下のものがあります。

名前空間の構成

各 .NET データ プロバイダに関連付けられている型 (クラス、構造体、列挙など) は、それぞれ独自の名前空間に配置されています。

  • System.Data.SqlClient。 SQL Server .NET データ プロバイダ型を保持しています。
  • System.Data.OleDb。 OLE DB .NET データ プロバイダ型を保持しています。
  • System.Data.Odbc。 ODBC .NET データ プロバイダ型を保持しています。
  • System.DataDataSetDataTable など、プロバイダに依存しない型を保持しています。

各プロバイダは関連付けられた名前空間内で、 ConnectionCommandDataReader、および DataAdapter オブジェクトの実装を提供します。 SqlClient の実装には、プレフィックス "Sql" を、 OleDb の実装には、プレフィックス "OleDb" を付けます。たとえば、Connection オブジェクトの SqlClient の実装は SqlConnection で、 Connection オブジェクトの OleDb の実装は OleDbConnection になります。同様に、DataAdapter オブジェクトの実装はそれぞれ、 SqlDataAdapterOleDbDataAdapter になります。

汎用プログラミング

異なるデータ ソースを対象にして、あるデータ ソースから別のデータ ソースにコードを移動する場合、 System.Data 名前空間内に配置されている IDbConnection インターフェイス、IDbCommand インターフェイス、 IDataReader インターフェイス、および IDbDataAdapter インターフェイスに対してプログラムを記述することを検討してください。 Connection オブジェクト、Command オブジェクト、 DataReader オブジェクト、および DataAdapter オブジェクトのすべての実装は、これらのインターフェイスをサポートする必要があります。

.NET データ プロバイダの実装に関する詳細については、 https://www.microsoft.com/japan/msdn/library/ja/cpguide/html/cpconimplementingnetdataprovider.asp を参照してください。

図 2 は、データ アクセス スタックと、 ADO.NET が ADO や OLE DB などのその他のデータ アクセス テクノロジと関連する方法を示しています。また、この図は、ADO.NET モデル内の 2 つのマネージ プロバイダと主要なオブジェクトも示しています。

図 2. データ アクセス スタック

ADO から ADO.NET への進化に関する詳細については、 2000 年 11 月の MSDN Magazine に掲載された「ADO .NET入門 Microsoft .NET Framework のデータアクセスサービス」というタイトルのドキュメントを参照してください。

ストアド プロシージャと直接 SQL

このドキュメントの残りの部分で示している大部分のコードは、 SqlCommand オブジェクトを使用して、データベースの操作を実行するストアド プロシージャを呼び出します。場合によっては、ストアド プロシージャ名が SqlDataAdapter オブジェクトに直接渡されていて、 SqlCommand オブジェクトを確認できないものもあります。ただし、内部的には依然として SqlCommand オブジェクトを作成することになります。

埋め込みの SQL ステートメントの代わりにストアド プロシージャを使用するのは、以下のような多くの理由があります。

  • プロシージャが使用するデータ アクセス プランをデータベースが最適化し、それを後で再利用するためにキャッシュできるので、一般的に、ストアド プロシージャはパフォーマンスを向上します。
  • データベース内で個別にストアド プロシージャを保護できます。ストアド プロシージャを実行するためのアクセス許可をクライアントに与えることができます。その際、元になるテーブルのアクセス許可は必要ありません。
  • 配置されたコンポーネント内にハードコーディングされた SQL ステートメントを変更するよりも、ストアド プロシージャを変更する方が通常は簡単なので、ストアド プロシージャの方が簡単に管理できます。
  • ストアド プロシージャは、元になるデータベース スキーマから特別なレベルの抽象概念を追加します。ストアド プロシージャのクライアントは、ストアド プロシージャの実装の詳細や元になるスキーマから切り離されています。
  • クライアントから複数の要求を送信するのではなく、 SQL ステートメントを一括して実行できるので、ストアド プロシージャはネットワーク トラフィックを減少できます。

プロパティと コンストラクタの引数

ADO.NET オブジェクト固有のプロパティ値は、コンストラクタの引数を使用するか、プロパティを直接設定することによって設定できます。たとえば、以下のコードは機能的には同等です。

// コンストラクタの引数を使用して、コマンド オブジェクトを構成します
SqlCommand cmd = new SqlCommand( "SELECT * FROM PRODUCTS", conn );

// 上記の行と以下の 3 行は、機能的には同等です。
// 以下の 3 行は、プロパティを明示的に設定します。
sqlCommand cmd = new SqlCommand();
cmd.Connection = conn;
cmd.CommandText = "SELECT * FROM PRODUCTS";

.NET オブジェクトに対するプロパティの設定と取得は、 COM オブジェクトに対するプロパティの設定と取得よりも効率的なので、パフォーマンスの観点からは、 2 つのアプローチにやや違いがあります。

どちらを選択するかは、個人の好みやコーディング スタイルによります。ただし、プロパティを明示的に設定する方が、コードが理解しやすくなり (特に、ADO.NET オブジェクト モデルに不慣れな場合)、デバッグしやすくなります。

**注意 **過去に、Microsoft Visual Basic® の開発者は、 "Dim x As New…" コンストラクトを使ってオブジェクトを作成しないように忠告されたことがありました。 COM の世界では、このコードは COM オブジェクト作成プロセスの短絡的な方法ですが、やや捕らえにくいバグやそれほど捕らえにくくないバグの原因になっていました。ただし、.NET の世界では、この問題はもう発生しません。

データベース接続の管理

特に複数層 Web アプリケーションでは、データベース接続は、重要で、負荷が高く、かつ制限されたリソースです。接続を管理するアプローチはアプリケーションのスケーラビリティ全体に非常に影響するので、接続を正しく管理することが必須となります。また、接続文字列の格納場所についても慎重に検討してください。構成可能で保護された場所が必要になります。

データベース接続と接続文字列を管理するときは、以下のことに励む必要があります。

  • 複数のクライアント間のデータベース接続のプールを多重化することによって、アプリケーションのスケーラビリティの実現を支援します。
  • 構成可能でパフォーマンスの高い接続プールの方法を採用します。
  • SQL Server にアクセスするときに、 Microsoft Windows® オペレーティング システムの認証を使用します。
  • 中間層での偽装を回避します。
  • 接続文字列を安全に格納します。
  • データベース接続をなるべく遅く開き、早く閉じます。

このセクションでは、接続プールについて説明し、適切な接続プールの方法の選択を支援します。多くの代替手段を利用できます。このセクションでは、データベースの接続文字列を管理、格納する方法も考察しています。このセクションでは、最終的に 2 つのコーディング パターンを提供します。それらは、接続が安全に閉じられ、確実に接続プールに返されるのを支援するために使用できます。

接続プール

アプリケーションは、データベース接続プールを利用して、既存の接続をプールから再利用できます。データベースとの新たな接続を繰り返し確立する必要はありません。この技法は、限られたデータベース接続数で、より多くのクライアントにサービスを提供できるので、アプリケーションのスケーラビリティを大幅に向上できます。また、この技法は、新しい接続を確立するのに必要な非常に多くの時間を節約できるので、パフォーマンスも向上します。

ODBC や OLE DB などのデータ アクセス テクノロジは、接続プールの形式を提供します。接続プールは、さまざまな度合いで構成できます。両方のアプローチは、データベース クライアント アプリケーションに対して大きな透過性があります。 OLE DB 接続プールは、よくセッション プールまたはリソース プールと呼ばれます。

MDAC (Microsoft Data Access Components) 内でのプールの一般的な説明については、 https://www.microsoft.com/japan/msdn/dataaccess/mdac/pooling2.asp を参照してください。

ADO.NET データ プロバイダは、透過的な接続プールを提供します。正確なメカニズムはプロバイダごとに異なります。このセクションでは、以下に項目に関連する接続プールについて説明します。

  • SQL Server .NET データ プロバイダ
  • OLE DB .NET データ プロバイダ

SQL Server .NET データ プロバイダでのプール処理

SQL Server .NET データ プロバイダを使用する場合、プロバイダが提供する接続プール サポートを使用します。これは、プロバイダがマネージ コード内に内部的に実装しているトランザクション対応の効率的なメカニズムです。プールはプロセスごとに作成されるので、プロセスが終了するまで破棄されません。

この形式の接続プールは透過的に使用できますが、プールが管理される方法や、接続プールを細かくチューニングするために使用できるさまざまな構成オプションを知っておく必要があります。

SQL Server .NET データ プロバイダ接続プールを構成する方法

接続文字列を使って提供される名前と値のペアのセットを使って、接続プールを構成できます。たとえば、プールが有効かどうか (既定では有効です)、プールの最大および最小サイズ、キューに登録された要求が接続を開始するまでにブロックできる時間などを構成できます。以下は、プールの最大および最小サイズを構成する接続文字列の例です。

"Server=(local); Integrated Security=SSPI; Database=Northwind;
Max Pool Size=75; Min Pool Size=5"

接続が開始され、プールが作成されると、複数の接続がプールに追加され、接続数を構成済みの最小レベルにします。その後、構成済みの最大のプール数に達するまで、接続をプールに追加できます。最大数に達すると、接続を開始する新しい要求は構成可能な期間キューに登録されます。

プール サイズの選択

何千ものクライアントの同時実行要求を管理する大規模なシステムでは、最大のしきい値を設定できることが非常に重要になります。接続プールとアプリケーションのパフォーマンスを監視して、システムに最適なプール サイズを確認する必要があります。また、最適サイズは、SQL Server を実行しているハードウェアによっても異なります。

開発中に、既定の最大プール サイズ (現在は 100) を減らして、接続リークを見つけるのに役立てることをお勧めします。

最小プール サイズを設定すると、プールが最初に作成されるときに、プールのサイズをそのレベルにするためにわずかなパフォーマンス オーバーヘッドが生じるでしょう。しかし、最初に接続するわずかなクライアントは、利益を得ることになります。新しい接続は直列に作成されることに注意してください。これは、プールに最初に接続が設定されるときに、サーバーが同時要求であふれてしまうことがないことを意味します。

接続プールの監視に関する詳細については、このドキュメントの「接続プールの監視」セクションを参照してください。

接続プールの接続文字列キーワードの完全な一覧については、 https://www.microsoft.com/japan/msdn/library/ja/cpguide/html/cpconconnectionpoolingforsqlservernetdataprovider.asp を参照してください。

関連情報

SQL Server .NET データ プロバイダの接続プールを使用するときに、以下のことを知っておく必要があります。

  • 接続は、接続文字列で完全一致アルゴリズムを使ってプールされます。プール メカニズムは、名前と値のペアの間の空白さえも区別します。たとえば、以下の 2 つの接続文字列は、 2 番目の接続プールが余分な空白文字を含んでいるので、 2 つの個別のプールになります。

    SqlConnection conn = new SqlConnection(
             "Integrated Security=SSPI;Database=Northwind");
    conn.Open(); // プール A が作成されます
    
    SqlConmection conn = new SqlConnection(
             "Integrated Security=SSPI ; Database=Northwind");
    conn.Open(); // プール B が作成されます (文字列に余分な空白が含まれています)
    
  • 接続プールは、複数のトランザクション固有のプールと、トランザクションに現在参加していない接続用の 1 つのプールに分けられます。特定のトランザクション コンテキストに関連付けられたスレッドには、適切なプールからの接続 (そのトランザクションに参加した接続を含む) が返されます。これは、参加した接続を使った作業を透過的な処理にします。

OLE DB .NET データ プロバイダでのプール処理

OLE DB .NET データ プロバイダは、 OLE DB リソース プールの元になるサービスを使用して接続をプールします。以下のような、リソース プールを構成する多くのオプションがあります。

  • 接続文字列を使って、リソース プールを構成したり、有効または無効にしたりできます。
  • レジストリを使用できます。
  • リソース プールをプログラムから構成できます。

レジストリ関連の配置の問題点を回避するために、レジストリを使って OLE DB リソース プールを構成しないようにします。

OLE DB リソース プールの詳細については、 MSDN の「OLE DB Programmer's Reference」で「Chapter 19: OLE DB Services」の「Resource Pooling」を参照してください。

プールされたオブジェクトを使った接続プールの管理

Windows DNA 開発者には、 OLE DB リソース プールや ODBC 接続プールを無効にして、データベース接続をプールする技法として、 COM+ オブジェクト プールを使用することをお勧めします。これには、主に以下の 2 つの理由があります。

  • プール サイズとしきい値を (COM+ カタログ内で) 明示的に構成できます。
  • パフォーマンスが向上します。オブジェクトがプールされるアプローチは、ほんの少しネイティブなプールより高性能です。

ただし、SQL Server .NET データ プロバイダは内部的にプールを使用するので、 (このプロバイダを使用するときに) 独自のオブジェクト プール メカニズムを開発する必要はなくなります。したがって、手動トランザクションの参加に関連付けられる複雑性を回避できます。

高度な構成やパフォーマンスを向上するために OLE DB .NET データ プロバイダを使用する場合、 COM+ オブジェクト プールを検討することをお勧めします。この目的でプールされるオブジェクトを開発する場合、 OLE DB リソース プールと自動トランザクションの参加 (たとえば、接続文字列に "OLE DB Services=-4" を含めることによる) を無効にする必要があります。プールされるオブジェクトの実装内でトランザクションの参加を処理する必要があります。

接続プールの監視

アプリケーションでの接続プールの使用を監視するには、 SQL Server に同梱されているプロファイラ ツールを使用できます。または、Microsoft Windows 2000 に同梱されているパフォーマンス モニタ ツールも使用できます。

SQL Server プロファイラを使って接続プールを監視する方法

  1. [スタート] をクリックし、 [プログラム]、[Microsoft SQL Server] を順にポイントし、 [プロファイラ] をクリックしてプロファイラを起動します。
  2. [ファイル] メニューの [新規追加] をポイントして、 [トレース] をクリックします。
  3. 接続情報を指定して、[OK] をクリックします。
  4. [トレース プロパティ] ダイアログ ボックスで、 [イベント] タブをクリックします。
  5. [選択したイベント クラス] リストで、 [セキュリティ監査] に [Audit Login] イベントと [Audit Logout] イベントが表示されていることを確認します。トレースをより明確にするために、リストから他のイベントをすべて削除します。
  6. [実行] をクリックして、トレースを開始します。接続が確立されるときに Audit Login イベントが、接続が閉じられるときに Audit Logout が表示されます。

パフォーマンス モニタを使って接続プールを監視する方法

  1. [スタート] をクリックし、[プログラム]、 [管理ツール] を順にポイントし、 [パフォーマンス] をクリックしてパフォーマンス モニタを起動します。
  2. グラフの背景を右クリックして、[カウンタの追加] をクリックします。
  3. [パフォーマンス オブジェクト] ドロップダウン リストで、 [SQL Server: General Statistics] をクリックします。
  4. 表示されるリストから [User Connections] をクリックします。
  5. [追加] をクリックしてから [閉じる] をクリックします。

注意 .NET Framework の製品版には、 ADO.NET パフォーマンス カウンタの追加セットが含まれています。 SQL Server .NET データ プロバイダの接続プールの統計を監視および蓄積するために、これをパフォーマンス カウンタと組み合わせて使用できます。

セキュリティの管理

データベース接続プールは、アプリケーション全体のスケーラビリティを向上しますが、データベースでセキュリティが管理できなくなることを意味します。これは、接続プールをサポートするために、接続文字列が同一である必要があるからです。ユーザーごとのデータベース操作を監視する必要がある場合は、それぞれの操作にユーザー ID を渡すパラメータを追加することを検討し、ユーザーの操作を手動でデータベースに記録します。

Windows 認証の使用

SQL Server に接続するときは、以下のような多くの利点が提供されるので、 Windows 認証を使用する必要があります。

  • 個別の SQL Server セキュリティ モデルではなく、単一の (Windows) セキュリティ モデルを使って動作するので、セキュリティの管理が容易になります。
  • ユーザー名とパスワードを接続文字列に埋め込む必要がありません。
  • ユーザー名とパスワードが、ネットワークを経由でクリア テキストとして渡されません。
  • パスワードの期限、パスワードの最小の長さ、および複数の無効なログオン要求後のアカウントのロックアウトによってログオン セキュリティが向上します。

関連情報

Windows 認証を使用して SQL Server にアクセスする場合、以下のことを知っておく必要があります。

パフォーマンス

パフォーマンス テストでは、Windows 認証を使用すると、SQL Server 認証を使用するのと比べて、プールされたデータベース接続を開くのに時間がかかることが示されました。ただし、Windows 認証は負荷が高くなりますが、コマンドまたはストアド プロシージャを実行するのにかかる時間と比較すると、パフォーマンスの低下はわずかです。結果として、上記で示した Windows 認証の利点は、通常この微妙なパフォーマンスの低下に勝ります。

中間層での偽装の回避

Windows 認証には、データベース アクセス用の Windows アカウントが必要です。中間層で偽装を使用することが論理的に思える場合がありますが、それは行わないでください。中間層で偽装を行うと、接続プールが無効になり、アプリケーションのスケーラビリティに大きく影響します。

この問題を解決するには、 (認証済みのプリンシパルではなく) 制限された数の Windows アカウントを、特定のロールを表す各アカウントと共に偽装することを検討してください。

たとえば、以下のアプローチが考えられます。

  • 2 つの Windows アカウントを作成します。 1 つは読み取り操作用、1 つは書き込み操作用です。 (または、アプリケーション固有のロールを反映するようにアカウントを分離します。たとえば、インターネット ユーザー用に 1 つのアカウントを使用し、内部オペレータや管理者用にもう 1 つのアカウントを使用します。)
  • 各アカウントを SQL Server データベース ロールにマップし、ロールごとに必要なデータベースのアクセス許可を確立します。
  • データ アクセス層でアプリケーション ロジックを使用して、データベース操作を実行する前に、どの Windows アカウントを偽装するかを判断します。

**注意 **各アカウントは、同一のドメインまたは信頼関係ドメインで、 IIS (インターネット インフォメーション サービス) と SQL Server でのドメイン アカウントである必要があります。または、各コンピュータ上で一致するアカウント (同一の名前とパスワードを使用する) を作成できます。

ネットワーク ライブラリに TCP/IP を使用

SQL Server 7.0 以降は、すべてのネットワーク ライブラリで Windows 認証をサポートしています。 TCP/IP を使用することには、構成、パフォーマンス、およびスケーラビリティの利点があります。 TCP/IP の使用に関する詳細については、このドキュメントの「ファイアウォール経由の接続」セクションを参照してください。

接続文字列の格納

データベースの接続文字列を格納するには、さまざまな選択肢があり、選択肢ごとに異なるレベルの柔軟性とセキュリティがあります。ソース コード内に接続文字列をハード コーディングすることは最適なパフォーマンスを提供しますが、ファイル システムの外部に文字列を格納することに関するパフォーマンスの低下は、ファイル キャッシュにより無視できます。接続文字列を外部に保存することにより提供される柔軟性は、管理者による構成をサポートし、事実上すべての場合に選択されます。

接続文字列を格納するアプローチを選択している場合、 2 つの最も重要な考慮事項はセキュリティと構成の容易さで、これらはパフォーマンスと密接に結びついています。

データベースの接続文字列を格納する場所を以下の場所から選択できます。

  • アプリケーション構成ファイル : たとえば、ASP.NET Web アプリケーションの Web.config
  • UDL (ユニバーサル データ リンク) ファイル (OLE DB .NET データ プロバイダのみにサポートされています)
  • Windows レジストリ
  • カスタム ファイル
  • COM+ カタログ : 構成文字列を使用して格納します (サービスを受けるコンポーネントのみ)

Windows 認証を使用して SQL Server にアクセスすることによって、接続文字列にユーザー名とパスワードを格納することを回避できます。セキュリティの必要要件がより厳格な手段を要する場合、暗号化された形式で接続文字列を格納することを検討してください。

ASP.NET Web アプリケーションでは、 Web.config ファイル内に暗号化された形式で接続文字列を格納することが、セキュアな構成可能なソリューションを表します。

**注意 **接続文字列の Persist Security Info 値を False に設定し、パスワードなどのセキュリティに依存する詳細が SqlConnection オブジェクトまたは OleDbConnection オブジェクトの ConnectionString プロパティで返されることを回避します。

以下のセクションでは、さまざまなオプションを使用して接続文字列を格納する方法を説明し、各アプローチの利点と欠点を提供します。これにより、詳しい情報を知った上で、特定のアプリケーションのシナリオに基づいて選択できます。

XML アプリケーション構成ファイルの使用

<appSettings> 要素を使用して、アプリケーション構成ファイルのカスタム設定セクションにデータベースの接続文字列を格納できます。この要素は、以下に例示されているような任意のキーと値のペアをサポートします。

<configuration>
 <appSettings>
  <add key="DBConnStr"
     value="server=(local);Integrated Security=SSPI;database=northwind"/>
 </appSettings>
</configuration>

注意 <appSettings> 要素は、<system.web> の直下ではなく、<configuration> 要素の下に表示されます。

利点

  • 配置の容易さ。接続文字列は、通常の .NET xcopy 配置を使って構成ファイルと共に配置されます。
  • プログラムからのアクセスの容易さConfigurationSettings クラスの AppSettings プロパティは、構成済みのデータベース接続文字列の実行時の読み取りを簡単な作業にします。
  • 動的更新のサポート (ASP.NET のみ)。管理者が Web.config ファイルの接続文字列を更新すると、その変更は次回その文字列がアクセスされるときに適用されます。つまり、状態なしのコンポーネントで、クライアントが次回そのコンポーネントを使用してデータ アクセス要求を行うときに適用されることに似ています。

欠点

  • セキュリティ。 ASP.NET ISAPI (Internet Server Application Programming Interface) DLL は、クライアントが .config ファイル拡張子を持つファイルに直接アクセスすることを防ぎ、 NTFS ファイル システムのアクセス許可を使用してよりアクセスに制限できますが、フロントエンド Web サーバーにクリア テキストでこれらの詳細が格納されることを避けたい場合があります。追加のセキュリティとして、構成ファイルに暗号化された形式で接続文字列を格納します。

関連情報

  • System.Configuration.ConfigurationSettings クラスの静的 AppSettings プロパティを使用して、カスタム アプリケーションの設定を取得できます。以下のコードは、これを示しており、以前示した DBConnStr と呼ばれるカスタム キーを想定しています。

    using System.Configuration;
    private string GetDBaseConnectionString()
    {
      return ConfigurationSettings.AppSettings["DBConnStr"];
    }
    
  • .NET Framework アプリケーションに関する詳細については、https://www.microsoft.com/japan/msdn/library/ja/cpguide/html/cpconconfiguringnetframeworkapplications.asp を参照してください。

UDL ファイルの使用

OLE DB .NET データ プロバイダは、接続文字列に UDL (ユニバーサル データ リンク) ファイル名をサポートしています。コンストラクタの引数を使ってその接続文字列を OleDbConnection オブジェクトに渡せます。または、オブジェクトの ConnectionString プロパティを使って接続文字列を設定できます。

注意 SQL Server .NET データ プロバイダは、接続文字列に UDL ファイルをサポートしません。そのため、このアプローチは、OLE DB .NET データ プロバイダを使用している場合にのみ適用されます。

OLE DB プロバイダでは、接続文字列で UDL ファイルを参照するために、"File Name=name.udl" を使用します。

利点

  • 標準アプローチ。接続文字列の管理に UDL ファイルを既に使用している場合があります。

欠点

  • パフォーマンス。UDL を含む接続文字列は、接続が開かれるたびに読み取られ、解析されます。
  • セキュリティ。 UDL ファイルは、テキスト形式で格納されます。 NTFS ファイルのアクセス許可を使用してこれらのファイルを保護できますが、それを行うと、.config ファイルで生じたのと同じ問題が発生します。
  • SqlClient が UDL ファイルをサポートしない。このアプローチは、SQL Server 7.0 以降へのアクセスに使用する SQL Server .NET データ プロバイダではサポートされていません。

関連情報

  • 管理者が管理をサポートするためのファイルへの読み取り/書き込みアクセス許可を持っていること、およびアプリケーションの実行に使用される ID が読み取りアクセス許可を持っていることを確認する必要があります。 ASP.NET Web アプリケーションでは、アプリケーションのワーカー プロセスが、既定で SYSTEM アカウントを使って実行します。しかし、コンピュータ全体の構成ファイル (Machine.config) の <processModel> 要素を使ってこれをオーバーライドできます。また、オプションで指定されるアカウントを使い、 Web.config ファイルの <identity> 要素を使って、偽装することもできます。
  • Web アプリケーションでは、 Web 経由でファイルをダウンロードできるようにする仮想ディレクトリに UDL ファイルを配置していないことを確認してください。
  • これらのセキュリティや、その他のセキュリティに関連する ASP.NET の機能の詳細については、 https://www.microsoft.com/japan/msdn/net/bda/authaspdotnet.asp を参照してください。

Windows レジストリの使用

Windows レジストリでカスタム キーを使用して、接続文字列を格納できますが、これは配置の問題によりお勧めしません。

利点

  • セキュリティ。アクセス制御リスト (ACL) を使用して、選択したレジストリキーへのアクセスを管理できます。より高レベルのセキュリティには、データの暗号化を検討してください。
  • プログラムからのアクセスの容易さ。レジストリからの文字列の読み取りをサポートする .NET クラスを利用できます。

欠点

  • 配置。関連するレジストリ設定が、アプリケーションと共に配置される必要があります。xcopy 配置の利点をいくらか低下させます。

カスタム ファイルの使用

カスタム ファイルを使用して、接続文字列を格納できます。ただし、この技法には利点がないので、お勧めしません。

利点

  • なし。

欠点

  • 余分なコーディング。このアプローチは、余分なコーディングを必要とし、同時実行問題を明示的に処理することになります。
  • 配置。ファイルを他の ASP.NET アプリケーション ファイルと共にコピーする必要があります。 ASP.NET アプリケーション ディレクトリまたはサブディレクトリにファイルを配置することを避け、 Web 経由でダウンロードできないようにします。

コンストラクタ引数と COM+ カタログの使用

COM+ カタログにデータベースの接続文字列を格納し、オブジェクトの接続文字列を使用してそれを自動的にオブジェクトに渡すようにできます。 COM+ は、構成済みコンストラクタ文字列を指定し、オブジェクトのインスタンスを作成した直後に、オブジェクトの Construct メソッドを呼び出します。

**注意 **このアプローチは、サービスを受けるコンポーネントのみで機能します。マネージ コンポーネントが分散トランザクションのサポートやオブジェクトのプールなどの他のサービスを使用する場合にのみそれを考慮してください。

利点

  • 管理。管理者は、コンポーネント サービス MMC スナップインを使用して、接続文字列を簡単に構成できます。

欠点

  • セキュリティ。 COM+ カタログは、保護されていないストレージ エリアと見なされます (ただし、COM+ ロールを使ってアクセスを制限できます)。そのため、COM+ カタログをテキスト形式で接続文字列を管理するために使用しないでください。
  • 配置。 COM+ カタログのエントリは、.NET アプリケーションと共に配置される必要があります。分散トランザクションやオブジェクト プールなどの他のエンタープライズ サービスを使用している場合は、カタログにデータベースの接続文字列を格納することは余分な配置オーバーヘッドを提供することになります。これは、COM+ カタログがそのような他のサービスをサポートするように配置される必要があるからです。
  • コンポーネントがサービスを受けられる必要がある。サービスを受けるコンポーネントのみで接続文字列を使用できます。コンストラクタ文字列を単に有効にするために、 (コンポーネントがサービスを受けられるように) コンポーネントのクラスを ServicedComponent から派生すべきではありません。

関連情報

接続の使用方法

.NET データ プロバイダとは無関係に、以下のことを行う必要があります。

  • データベースの接続をできる限り遅く開きます。
  • 接続の使用をできる限り短期間にします。
  • 接続をできる限りすぐに閉じます。接続は Close メソッドまたは Dispose メソッドのいずれかを使って閉じられるまで、プールに返されません。また、接続が壊れた状態に陥っていることを検出した場合も、接続を閉じる必要があります。これにより、接続がプールに返され、無効に設定されます。オブジェクト プーラーがプールを定期的にスキャンします。これは、無効として設定されているオブジェクトを検索するためです。

メソッドから戻る前に接続が閉じられることを保証するために、以下の 2 つのコード サンプルで例示されているアプローチのいずれかを考慮してください。最初のコードは、finally ブロックを使用します。 2 番目のコードは、C# using ステートメントを使用します。これは、オブジェクトの Dispose メソッドが確実に呼び出されるようにします。

以下のコードは、finally ブロックが接続を閉じることを確実にします。このアプローチは、Visual Basic .NET は構造化例外処理をサポートするので、 C# 以外に Visual Basic .NET でも機能することに注意してください。

public void DoSomeWork()
{
  SqlConnection conn = new SqlConnection(connectionString);
  SqlCommand cmd = new SqlCommand("CommandProc", conn );
  cmd.CommandType = CommandType.StoredProcedure;

  try
  {
    conn.Open();
    cmd.ExecuteNonQuery();
  }
  catch (Exception e)
  {
    // エラーを処理し、ログに記録します
  }
  finally
  {
    conn.Close();
  }
}

以下のコードは、C# using ステートメントを採用した別のアプローチを示しています。 Visual Basic .NET は using ステートメントまたはそれと等価な機能を提供しないことに注意してください。

public void DoSomeWork()
{
  // Dispose が conn で呼び出されることを保証します。
  //これが、接続を閉じます。
  using (SqlConnection conn = new SqlConnection(connectionString))
  {
    SqlCommand cmd = new SqlCommand("CommandProc", conn);
    fcmd.CommandType = CommandType.StoredProcedure;
    conn.Open();
    cmd.ExecuteQuery();
  }
}

また、このアプローチを他のオブジェクト、たとえば SqlDataReaderOleDbDataReader に適用することもできます。これらのオブジェクトでは、現在の接続を使って何か他の処理を行う前に、接続を閉じる必要があります。

エラー処理

ADO.NET エラーは、 .NET Framework でネイティブな元になる構造化例外処理のサポートを使って生成および処理されます。その結果、アプリケーションの他の場所でエラーを処理するのと同じ方法でデータ アクセス コード内のエラーを処理することになります。例外は、標準の .NET 例外処理構文や技法を使って検出および処理できます。

このセクションでは、堅牢なデータ アクセス コードを開発する方法を示し、データ アクセス エラーを処理する方法を説明します。また、SQL Server .NET データ プロバイダに関する特定の例外処理ガイダンスも提供します。

.NET の例外

.NET データ プロバイダは、データベース固有のエラー状態を標準の種類の例外に変換します。例外は、データ アクセス コードで処理する必要があります。データベース固有のエラーの詳細は、関連する例外オブジェクトのプロパティを使って利用できるようになります。

.NET のすべての種類の例外は、最終的には System 名前空間の基本 Exception クラスから派生されます。 .NET データ プロバイダは、プロバイダ固有の種類の例外をスローします。たとえば、SQL Server .NET データ プロバイダは、 SQL Server がエラー状態を返すときは常に、SqlException オブジェクトをスローします。同様に、OLE DB .NET データ プロバイダは、OleDbException 形成の例外をスローします。これらは、元になる OLE DB プロバイダが公開する詳細を含んでいます。

図 3 は、.NET データ プロバイダの例外階層を示しています。 OleDbException クラスが ExternalException、つまりすべての COM 相互運用の例外の基本クラスから派生されます。このオブジェクトの ErrorCode プロパティは、OLE DB が生成する COM HRESULT を格納します。

図 3. .NET データ プロバイダの例外階層

.NET の例外のキャッチと処理

データ アクセスの例外状態を処理するには、データ アクセス コードを try ブロック内に配置し、 catch ブロックを使用することによって、生成されるすべての例外を適切なフィルタを使ってトラップします。たとえば、SQL Server .NET データ プロバイダを使用して、データ アクセス コードを記述しているときは、以下のコードで示す SqlException という種類の例外をキャッチする必要があります。

try
{
  // データ アクセス コード
}
catch (SqlException sqlex) // より特定の例外をキャッチします
{
}
catch (Exception ex) // 特定されない例外をキャッチします
{
}

複数の catch ステートメントで異なるフィルタ条件を指定する場合、最も特定の種類から特定されない種類に並べることを覚えておいてください。この結果、最も特定の種類の catch ブロックが、指定した種類の例外に実行されます。

この SqlException クラスは、例外状態の詳細を含むプロパティを公開します。これらのプロパティには、以下のものがあります。

  • Message プロパティ : エラーを説明するテキストを保持するプロパティです。
  • Number プロパティ : エラー番号を保持するプロパティです。エラーの種類を一意に識別します。
  • State プロパティ : エラーの発生状態に関する追加情報を保持します。これは通常、特定の条件で発生する特定のエラー状態を示すために使用します。たとえば、単一のストアド プロシージャが複数行から同じエラーを生成する場合、その状態は特定の条件で発生したエラーを識別するために使用する必要があります。
  • Errors コレクション : SQL Server が生成するエラーに関する詳しいエラー情報を保持します。 Errors コレクションは常に、少なくとも 1 つの SqlError 型のオブジェクトを含みます。

以下のコードは、SQL Server .NET データ プロバイダを使って SQL Server エラー状態を処理する方法を例示しています。

using System.Data;
using System.Data.SqlClient;
using System.Diagnostics;

// データ アクセス層 (DAL) コンポーネットが公開するメソッド
public string GetProductName( int ProductID )
{
  SqlConnection conn = new SqlConnection(
        "server=(local);Integrated Security=SSPI;database=northwind");
  // try ブロック内にすべてのデータ アクセス コードを含めます
  try
  {
    conn.Open();
    SqlCommand cmd = new SqlCommand("LookupProductName", conn );
    cmd.CommandType = CommandType.StoredProcedure;

    cmd.Parameters.Add("@ProductID", ProductID );
    SqlParameter paramPN = 
         cmd.Parameters.Add("@ProductName", SqlDbType.VarChar, 40 );
    paramPN.Direction = ParameterDirection.Output;

    cmd.ExecuteNonQuery();
    // 最後のコードは、メソッドから戻る前に実行されます
    return paramPN.Value.ToString();  
  }
  catch (SqlException sqlex)
  {
    // データ アクセスの例外状態を処理します
    // 特定の例外の詳細をログに記録します
    LogException(sqlex);
    // より関連する外部の例外で現在の例外をラップし、
    // 新しい例外を再度スローします
    throw new DALException(
                  "Unknown ProductID: " + ProductID.ToString(), sqlex );
  }
  catch (Exception ex)
  {
    // 一般的な例外状態を処理します. . .
    throw ex;
  }
  finally
  {
conn.Close(); // 接続が確実に閉じられるようにします
  }
}

// SqlException をログに記録するヘルパ ルーチンは、 
// アプリケーション イベント ログに詳しく記録します
private void LogException( SqlException sqlex )
{
  EventLog el = new EventLog();
  el.Source = "CustomAppLog";
  string strMessage;
  strMessage = "Exception Number : " + sqlex.Number + 
               "(" + sqlex.Message + ") has occurred";
  el.WriteEntry( strMessage );

  foreach (SqlError sqle in sqlex.Errors)
  {
    strMessage = "Message: " + sqle.Message +
                 " Number: " + sqle.Number +
                 " Procedure: " + sqle.Procedure +
                 " Server: " + sqle.Server +
                 " Source: " + sqle.Source +
                 " State: " + sqle.State +
                 " Severity: " + sqle.Class +
                 " LineNumber: " + sqle.LineNumber;
    el.WriteEntry( strMessage );
  }
}

コードは SqlException catch ブロック内で、まず LogException ヘルパ関数を使って、例外の詳細をログに記録します。この関数は、foreach ステートメントを使用して、 Errors コレクション内のプロバイダ固有の詳細を列挙し、エラーの詳細をエラー ログに記録します。その後、catch ブロック内のコードが SQL Server 固有の例外を例外の種類 DALException 内にラップします。これが、GetProductName メソッドの呼び出し元にとってより重要になります。例外ハンドラは、throw キーワードを使用して、この例外を呼び出し元に再度反映します。

関連情報

ストアド プロシージャからエラーを生成する

Transact-SQL (T-SQL) は、 RAISERROR (スペルに注意) 関数を提供します。これは、カスタム エラーを生成し、クライアントにエラーを返すために使用できます。 ADO.NET クライアントでは、 SQL Server .NET データ プロバイダがこれらのデータベース エラーをインターセプトし、カスタム エラーを SqlError オブジェクトに変換します。

RAISERROR 関数を使用する最も簡潔な方法は、以下のコードで示すように、最初のパラメータにメッセージ テキストを含め、その後重要度パラメータや状態パラメータを指定します。

RAISERROR( 'Unknown Product ID: %s', 16, 1, @ProductID )

この例では、置き換え可能なパラメータを現在の製品 ID をエラー メッセージ テキストの一部として返すために使用しています。 2 番目のパラメータはメッセージの重要度で、3 番目のパラメータ 3 はメッセージの状態です。

関連情報

  • メッセージ テキストをハード コーディングすることを避けるために、独自のメッセージを sysmessages テーブルに追加できます。これには、sp_addmessage システム ストアド プロシージャを使用するか、 SQL Server Enterprise Manager を使用します。その後、RAISERROR 関数に渡す ID を使用して、メッセージを参照できます。定義するメッセージ ID は、以下のコードで示すように 50,000 より大きい値にする必要があります。

    RAISERROR( 50001, 16, 1, @ProductID )
    
  • RAISERROR 関数に関する完全な詳細については、 SQL Server Books Online の索引で「RAISERROR」を検索してください。

重要度のレベルを適切に使用する

エラーの重要度レベルを注意深く選択し、各レベルの影響を認識します。エラーの重要度レベルは、0 から 25 までで、 SQL Server 2000 で発生した問題点の種類を示すために使用されます。クライアント コードでは、 SqlException クラスの Errors コレクション内の SqlError オブジェクトの Class プロパティを調査することによって、エラーの重要度を取得できます。表 1 は、さまざまな重要度のレベルの影響と意味を示しています。

表 1. エラーの重要度のレベル影響と意味

重要度のレベル 接続が閉じられる SqlException を生成する 意味
10 以下 いいえ いいえ エラー状態を表す必要がない情報メッセージ。
11–16 いいえ はい ユーザーによって解決できるエラー — たとえば、修正された入力データで操作を再試行します。
17–19 いいえ はい リソース エラーまたはシステム エラー。
20–25 はい はい 致命的なシステム エラー (ハードウェア エラーなど)。クライアントの接続を終了します。

自動トランザクションを制御する

SQL Server .NET データ プロバイダは、レベル 10 を超えるレベルで発生したエラーでは SqlException をスローします。自動 (COM+) トランザクションの一部であるコンポーネントが SqlException を検出すると、そのコンポーネントはトランザクションを中断する必要があります。これは、自動処理である場合とそうでない場合があり、メソッドが AutoComplete 属性でマークされているかどうかによって異なります。

自動トランザクションのコンテキストでの SqlException の処理に関する詳細については、このドキュメントの「トランザクションの結果を決定する」セクションを参照してください。

情報メッセージを取得する

10 以下の重要度レベルは、情報メッセージを表すために使用され、SqlException は発生しません。

情報メッセージを取得する方法は以下のとおりです。

  • イベント ハンドラを作成し、 SqlConnection オブジェクトが公開する InfoMessage イベントをサブスクライブします。次のコードは、このイベントのデリゲートを示しています。

    public delegate void SqlInfoMessageEventHandler( object sender, 
                                                         SqlInfoMessageEventArgs e );
    

メッセージ データは、イベント ハンドラに渡される SqlInfoMessageEventArgs オブジェクトを使って利用できます。このオブジェクトは、Errors プロパティを公開し、 SqlError オブジェクトのセットを情報メッセージごとに 1 つずつ含みます。以下のコードは、情報メッセージを記録するために使用されるイベント ハンドラを登録する方法を例示しています。

public string GetProductName( int ProductID )
{
  SqlConnection conn = new SqlConnection(
        "server=(local);Integrated Security=SSPI;database=northwind");
  try
  {
    // メッセージ イベント ハンドラを登録します
    conn.InfoMessage += new SqlInfoMessageEventHandler( MessageEventHandler );
    conn.Open();
    // コマンド オブジェクトを設定し、それを実行します
    . . .
  }
  catch (SqlException sqlex)
  {
    // 例外を記録および処理します
    . . .
  }
  finally
  {
    conn.Close();
  }
}
// メッセージ イベント ハンドラ
void MessageEventHandler( object sender, SqlInfoMessageEventArgs e )
{
  foreach( SqlError sqle in e.Errors )
  {
    // SqlError プロパティを記録します
    . . .
  }
}

パフォーマンス

このセクションでは、多くの一般的なデータ アクセス シナリオを紹介し、シナリオごとに ADO.NET データ アクセス コードに関して最もパフォーマンスが高く、スケーラブルなソリューションについての詳しい説明を提供します。必要に応じて、パフォーマンス、機能、および開発作業を比較しています。このセクションでは、以下の機能的なシナリオを検討します。

  • 複数行を取得する。結果セットを取得し、取得した行全体を繰り返し処理します。
  • 単一行を取得する。指定した主キーを使用して単一行を取得します。
  • 単一の項目を取得する。指定した行から単一の項目を取得します。
  • データの項目の存在を判断する。特定の主キーを持つ行が存在するかどうかを調査します。これは、ある種の単一項目の照合シナリオで、単純なブール値の戻り値で十分です。

複数行を取得する

このシナリオでは、表形式のデータ セットを取得し、操作を実行するために取得した行全体を繰り返し処理することを考えます。たとえば、データ セットを取得し、非接続の形式でそのデータ セットを使って作業し、それをクライアント アプリケーションに XML ドキュメントとして (おそらく Web サービスを経由して) 渡したい場合があります。または、HTML の表のフォームにデータを表示したい場合があります。

最も適切なデータ アクセス アプローチの判断をするには、 (非接続の) DataSet オブジェクトの新たな柔軟性を必要とするか、 SqlDataReader オブジェクトが提供するパフォーマンスそのものを必要とするかを検討します。これは、B2C (企業と消費者間) Web アプリケーションでのデータ表記に理想的です。図 4 は、2 つの基本的なシナリオを示しています。

注意 DataSet を内部で作成するために使用される SqlDataAdapter は、 SqlDataReader を使用してデータにアクセスします。

図 4. 複数行のデータ アクセス シナリオ

選択肢の比較

データ ソースから複数行を取得する場合、以下の選択肢があります。

  • SqlDataAdapter オブジェクトを使用して、 DataSet または DataTable を生成します。
  • SqlDataReader を使用して、読み取り専用の、前方参照のみのデータ ストリームを提供します。
  • XmlReader を使用して、読み取り専用の、前方参照のみの XML データのデータ ストリームを提供します。

SqlDataReaderDataSet/DataTable との選択は、パフォーマンスと機能性のどちらが必要不可欠かによって異なります。 SqlDataReader は最適なパフォーマンスを提供し、 DataSet は付加機能と柔軟性を提供します。

データ連結

これら 3 つのオブジェクトはすべて、データ連結コントロールのデータ ソースとして機能できます。ただし、DataSetDataTable は、 SqlDataReader よりも広範囲のコントロールのデータ ソースとして機能できます。これは、DataSetDataTable が (IList を呼び出す) IListSource を実装するためです。それに対して、SqlDataReaderIEnumerable を実装します。多くの WinForm コントロールのデータ連結機能は、IList を実装するデータ ソースを必要とします。

この違いは、各オブジェクト型をデザインしているシナリオの種類に依存します。 DataSet (DataTable を含む) は、 Web とデスクトップ (WinForm) シナリオの両方に適している優れた非接続構造です。それに対して、データ リーダーは最適化された前方参照のみのデータ アクセスを必要とする Web アプリケーションにとって最適です。

連結する特定のコントロールの種類でのデータ ソースの必要条件を確認してください。

アプリケーション層間でデータを渡す

DataSet は、オプションで XML として操作できるデータのリレーショナル ビューを提供し、非接続のキャッシュされたデータのコピーをアプリケーション層とコンポーネント間とで受け渡せます。それにもかかわらず、 DataSet の作成に関連するパフォーマンスとメモリ オーバーヘッドを回避するので、 SqlDataReader は最適なパフォーマンスを提供します。 DataSet オブジェクトの作成により、 DataTableDataRow、および DataColumn オブジェクトなどの複数のサブオブジェクトと、これらのサブオブジェクトのコンテナとして使用されるコレクション オブジェクトを作成することになることを覚えておいてください。

DataSet を使用する

以下の場合に、SqlDataAdapter オブジェクトが作成する DataSet を使用します。

  • アプリケーション内で別のコンポーネントまたは層に渡せるように、データの非接続メモリ常駐キャッシュを必要とするとき。
  • XML または XML 以外の操作で、データのインメモリ リレーショナル ビューを必要とするとき。
  • 複数のデータベース、テーブル、またはファイルなど複数のデータ ソースから取得したデータを使って作業するとき。
  • 取得した行の一部またはすべてを更新して、 SqlDataAdapter のバッチ更新機能を使用するとき。
  • IList をサポートするデータ ソースを必要とするコントロールに対してデータ連結を行うとき。

関連情報

SqlDataAdapter を使用して、 DataSet または DataTable を生成する場合、以下の点に注意します。

  • データベース接続を明示的に開いたり閉じたりする必要はありません。 SqlDataAdapter Fill メソッドがデータベース接続を開き、メソッドから戻る前に接続を閉じます。接続が既に開かれている場合は、Fill は接続を開いたままにします。
  • 他の目的の接続を必要とする場合、 Fill メソッドを呼び出す前にそれを開くことを検討します。その結果、不要な開く/閉じる操作を回避し、パフォーマンスを向上できます。
  • 同一の SqlCommand オブジェクトを繰り返し使用して、同じコマンドを複数回実行できますが、同一の SqlCommand オブジェクトを再度使用して別のコマンドを実行しないでください。
  • SqlDataAdapter を使用して DataSet または DataTable を作成する方法を示すコード サンプルについては、付録の「SqlDataAdapter を使用して複数行を取得する方法」を参照してください。

SqlDataReader を使用する

以下の場合に、 SqlCommand オブジェクトの ExecuteReader メソッドを呼び出すことによって取得される SqlDataReader を使用します。

  • 単一のキャッシュで管理するには多すぎる大量のデータを扱うとき。
  • アプリケーションのメモリの使用量を減少したいとき。
  • DataSet に関連するオブジェクト作成のオーバーヘッドを回避したいとき。
  • IEnumerable を実装するデータ ソースをサポートするコントロールを使ってデータ連結を行うとき。
  • データ アクセスを合理化および最適化したいとき。
  • BLOB (バイナリ ラージ オブジェクト) 列を含む行を読み取るとき。 SqlDataReader を使用して、一度にすべてを取り出すのではなく、データベースから管理可能なブロックの BLOB データを取り出せます。 BLOB データの処理に関する詳細については、このドキュメントの「BLOB の処理」セクションを参照してください。

関連情報

SqlDataReader を使用する場合は、以下の点に注意します。

  • データベースへの元になる接続は、開いたままになり、データ リーダーがアクティブな間は他の目的に使用できません。 SqlDataReaderClose をできるだけ早く呼び出します。
  • 接続ごとに 1 つのデータ リーダーのみが存在できます。
  • データ リーダーを使った作業を完了するときに明示的に接続を閉じるか、または CommandBehavior.CloseConnection 列挙値を ExecuteReader メソッドに渡すことによって、接続の存続期間を SqlDataReader に結び付けます。これは、SqlDataReader を閉じるときに、接続を閉じる必要があることを示しています。
  • リーダーを使用してデータにアクセスしているとき、列の元になるデータ型がわかっている場合は、型指定するアクセサ メソッド (GetInt32GetString など) を使用します。この結果、列データを読み取るときに必要な型変換の量を削減します。
  • リーダーを閉じて残っているすべての結果を破棄したい場合、サーバーからクライアントに不必要なデータが取り出されるのを防ぐために、リーダーで Close を呼び出す前に、コマンド オブジェクトの Cancel メソッドを呼び出します。 Cancel は、結果がサーバー上で破棄され、クライアントに不必要に取り出されないことを保証します。逆に、データ リーダーで Close を呼び出すと、データ ストリームを空にするために、リーダーが残っている結果を不必要に取り出します。
  • SqlCommand オブジェクトの ExecuteReader メソッドを使用しているときに、ストアド プロシージャから返される出力または戻り値を取得したい場合、 Close メソッドを呼び出さないと、出力や戻り値を取得できません。
  • SqlDataReader を使用する方法を示すコード サンプルについては、付録の「SqlDataReader を使用して複数行を取得する方法」を参照してください。

XmlReader を使用する

以下の場合に、SqlCommand オブジェクトの ExecuteXmlReader メソッドを呼び出すことによって取得される XmlReader を使用します。

  • XML として取得したデータを処理したいが、 DataSet の作成によるパフォーマンスのオーバーヘッドを望んでいないので、データの非接続キャッシュを必要としないとき。
  • SQL Server FOR XML 句の機能を利用するとき。これを利用すると、XML フラグメント (つまり、ルート要素を持たない XML ドキュメント) を柔軟な手法でデータベースから取得できます。たとえば、このアプローチでは、正確な要素名、要素または属性中心のスキーマを使用する必要があるかどうか、 XML データを使ってスキーマを返す必要があるかどうかなどを指定します。

関連情報

XmlReader を使用する場合は、以下の点に注意します。

  • XmlReader からデータを読み取っている間は、接続が開いたままになっている必要があります。 SqlCommand オブジェクトの ExecuteXmlReader メソッドは、現在 CommandBehavior.CloseConnection 列挙値をサポートしません。そのため、リーダーを使った作業を完了するときに、接続を明示的に閉じる必要があります。
  • XmlReader を使用する方法を示すコード サンプルについては、付録の「XmlReader を使用して複数行を取得する方法」を参照してください。

単一行を取得する

このシナリオでは、データ ソースから指定した列のセットを含む単一行のデータを取得することを考えます。たとえば、顧客 ID を持っていて、関連する顧客の詳細を検索したり、または商品 ID を持っていて商品情報を取得する場合などです。

選択肢の比較

データ ソースから取得した単一の行を使ってデータ連結を行う場合、上記の複数行の取得および繰り返し処理のシナリオで説明したのと同じ方法で、 SqlDataAdapter を使用して DataSet または DataTable を作成できます。ただし、DataSet/DataTable 機能を特に必要としない場合は、これらのオブジェクトを作成すべきではありません。

単一行を取得する必要がある場合、以下の選択肢のいずれかを使用します。

  • ストアド プロシージャの出力パラメータを使用する。
  • SqlDataReader を使用する。

どちらの選択肢も、サーバーに結果セットを作成するとき、およびクライアントに DataSet を作成するときに生じる不必要なオーバーヘッドを回避します。各アプローチの相対的なパフォーマンスは、負荷レベルと、データベースの接続プールが有効かどうかによって異なります。パフォーマンス テストでは、データベースの接続プールが有効な場合、高負荷条件 (200 以上の同時接続) 下で約 30 パーセントストアド プロシージャのアプローチの方が SqlDataReader アプローチよりも高性能であることが示されました。

ストアド プロシージャの出力パラメータを使用する

以下の場合に、ストアド プロシージャの出力パラメータを使用します。

  • 接続プールが有効な複数層 Web アプリケーションから単一行を取得するとき。

関連情報

  • ストアド プロシージャの出力パラメータを使用する方法を示すコード サンプルについては、付録の「ストアド プロシージャの出力パラメータを使用して単一行を取得する方法」を参照してください。

SqlDataReader を使用する

以下の場合に、SqlDataReader を使用します。

  • データ値以外にメタデータを必要とするとき。データ リーダーの GetSchemaTable メソッドを使用して、列のメタデータを取得できます。
  • 接続プールを使用しないとき。接続プールが無効になっている場合、 SqlDataReader はあらゆる負荷条件下で優れたオプションといえます。パフォーマンス テストでは、200 個のブラウザ接続で約 20 パーセント、ストアド プロシージャのアプローチより高性能であることが示されました。

関連情報

  • クエリが単一行のみを返すことがわかっている場合は、 SqlCommand オブジェクトの ExecuteReader メソッドを呼び出すときに、 CommandBehavior.SingleRow 列挙値を使用します。 OLE DB .NET データ プロバイダが、このヒントを使用してパフォーマンスの最適化を提供する場合があります。たとえば、このプロバイダは、より負荷の高い IRowset ではなく、 IRow インターフェイス (利用できる場合) を使用することによってバインドを実行します。この引数は、SQL Server .NET データ プロバイダには効果がありません。
  • SqlDataReader オブジェクトを使用する場合、常に SqlDataReader オブジェクトの型指定するアクセサ メソッド、 GetStringGetDecimal などを使用して出力パラメータを取得します。この結果、不必要な型変換を防ぎます。
  • SqlDataReader オブジェクトを使用して単一行を取得する方法を示すコード サンプルについては、付録の「SqlDataReader を使用して単一行を取得する方法」を参照してください。

単一の項目を取得する

このシナリオでは、データの単一の項目を取得することを考えます。たとえば、ID を指定して単一の商品名を照合したり、または顧客名を指定して単一の顧客の信用格付けを照合したりする場合などがあります。このようなシナリオで単一の項目を取得するとき、通常は DataSetDataTable のオーバーヘッドの影響を受けたくはありません。

また、特定の行がデータベースに存在するかどうかを単に確認したい場合があります。たとえば、新しいユーザーが Web サイトに登録するときは、指定したユーザー名が既に存在するかどうかを確認する必要があります。これは、特殊なケースの単一項目の検索ですが、この場合は単純なブール値の戻り値で十分です。

選択肢の比較

データ ソースからデータの単一の項目を取得する場合、以下の選択肢があります。

  • ストアド プロシージャと共に、 SqlCommand オブジェクトの ExecuteScalar メソッドを使用します。
  • ストアド プロシージャの出力を使用するか、パラメータを返します。
  • SqlDataReader オブジェクトを使用します。

ExecuteScalar メソッドは、単一値のみを返すクエリ向けにデザインされているので、データ項目を直接返します。ストアド プロシージャの出力パラメータと SqlDataReader アプローチよりも必要なコードが少なくなります。

パフォーマンスの観点からは、ストアド プロシージャの出力を使用するか、ストアド プロシージャからパラメータを返すべきです。これは、ストアド プロシージャのアプローチが低負荷条件でも高負荷条件でも (100 未満の同時ブラウザ接続から 200 個のブラウザ接続まで)、一貫したパフォーマンスを提供することがテストで示されているためです。

関連情報

  • ExecuteQuery を使って実行されるクエリが、複数列や複数行を返す場合、このメソッドは先頭行の先頭列だけを返します。
  • ExecuteScalar の使用方法を示すコード サンプルについては、付録の「ExecuteScalar を使用して単一の項目を取得する方法」を参照してください。
  • ストアド プロシージャの出力を使用するか、パラメータを返して単一項目を取得する方法を示すコード サンプルについては、付録の「ストアド プロシージャの出力を使用するかパラメータを返して、単一の項目を取得する方法」を参照してください。
  • SqlDataReader オブジェクトを使用して単一項目を取得する方法を示すコード サンプルについては、付録の「SqlDataReader を使用して単一の項目を取得する方法」を参照してください。

ファイアウォール経由の接続

インターネット アプリケーションを構成して、ファイアウォール経由で SQL Server に接続したい場合がよくあります。たとえば、多くの Web アプリケーションとそれらのファイアウォールの主要なアーキテクチャ コンポーネントは、境界領域のネットワーク (DMZ または非武装地帯とも呼ばれます) で、フロントエンド Web サーバーを内部ネットークから分離するために使用されます。

ファイアウォール経由で SQL Server に接続するには、ファイアウォール、クライアント、およびサーバーに特定の構成が必要になります。 SQL Server はこの構成を支援するために、クライアント ネットワーク ユーティリティ プログラムとサーバー ネットワーク ユーティリティ プログラムを提供します。

ネットワーク ライブラリの選択

ファイアウォール経由で接続しているときは、 SQL Server TCP/IP ネットワーク ライブラリを使用して構成を簡素化します。これは、SQL Server 2000 の既定のインストールです。以前のバージョンの SQL Server を使用している場合は、クライアント ネットワーク ユーティリティとサーバー ネットワーク ユーティリティをそれぞれ使用して、クライアントとサーバーの両方で TCP/IP を既定のネットワーク ライブラリとして構成していることを確認してください。

TCP/IP ライブラリを使用することは、構成上の利点に加えて、以下のことを意味します。

  • 大量データでのパフォーマンスの向上と、スケーラビリティの向上の利点が得られます。
  • 名前付きパイプに関連する別のセキュリティの問題点を回避します。

TCP/IP では、クライアント コンピュータとサーバー コンピュータを構成する必要があります。大部分のファイアウォールは、トラフィックの流れを許可するポートのセットを制限するので、 SQL Server が使用するポート数も注意深く検討する必要があります。

サーバーを構成する

SQL Server の既定のインスタンスはポート 1433 で受信待ちします。ただし、SQL Server 2000 の名前付きインスタンスは、最初に開始されるときに動的にポート番号を割り当てます。ネットワーク管理者は、ファイアウォール上のある範囲のポート番号を開きたくないと考えます。そのため、ファイアウォールと共に SQL Server の名前付きインスタンスを使用するときは、サーバー ネットワーク ユーティリティを使用して、特定のポート番号で受信待ちするようにそのインスタンスを構成します。その後、サーバー インスタンスが受信待ちする特定の IP アドレスとポート番号にトラフィックを許可するようにファイアウォールを構成できます。

**注意 **クライアント ネットワーク ライブラリが使用するソース ポートは、1024 から 5000 までの範囲で動的に割り当てられます。これは、TCP/IP クライアント アプリケーションでは標準的な事例ですが、ファイアウォールがこの範囲内のすべてのポートからのトラフィックを許可することを意味します。 SQL Server が使用するポートに関する詳細については、 Microsoft Product Support Services Web サイトで、「INF: TCP Ports Needed for Communication to SQL Server Through a Firewall」 (英語) を参照してください。

名前付きインスタンスの動的な探索

SQL Server が受信待ちする既定のポート番号を変更する場合は、このポートに接続するクライアントを構成します。詳細については、このドキュメントの「クライアントを構成する」セクションを参照してください。

SQL Server 2000 の既定のインスタンスのポート番号を変更すると、クライアントの変更の失敗が接続エラーの原因になります。複数の SQL Server インスタンスがある場合は、最新バージョンの MDAC データ アクセス スタック (2.6) が動的探索を行い、 UDP (ユーザー データグラム プロトコル) のネゴシエーションを (UDP ポート 1434 経由で) 使用して名前付きインスタンスを見つけます。これは開発環境では機能する場合がありますが、通常はファイアウォールが UDP ネゴシエーション トラフィックをブロックするので、実稼働環境では機能しないでしょう。

これを避けるには、常に構成済みの接続先ポート番号に接続するようにクライアントを構成します。

クライアントを構成する

TCP/IP ネットワーク ライブラリを使用して SQL Server に接続するようにクライアントを構成する必要があります。さらに、クライアント ライブラリが正しい接続先ポート番号を使用することを確認する必要があります。

TCP/IP ネットワーク ライブラリを使用する

SQL Server クライアント ネットワーク ユーティリティを使って、クライアントを構成できます。一部のインストールでは、このユーティリティがクライアント (たとえば Web サーバー) にインストールされないことがあります。この場合は、以下のいずれかを行います。

  • 接続文字列を使って提供される "Network Library=dbmssocn" という名前と値のペアを使用して、ネットワーク ライブラリを指定します。文字列 "dbmssocn" は、TCP/IP (ソケット) ライブラリを識別するために使用されます。

**注意 **SQL Server .NET データ プロバイダを使用している場合、ネットワーク ライブラリの設定は既定で "dbmssocn" を使用します。

ポートを指定する

SQL Server のインスタンスが既定の 1433 以外のポートで受信待ちするように構成されている場合、以下のいずれかの方法で接続先ポート番号を指定できます。

  • クライアント ネットワーク ユーティリティを使用します。

  • 接続文字列で指定される "Server" または "Data Source" という名前と値のペアを使ってポート番号を指定します。以下の形式で文字列を使用します。

    "Data Source=ServerName,PortNumber"
    

**注意 **ServerName は、IP アドレスまたは DNS (ドメイン ネーム システム) 名になります。最適なパフォーマンスを得るには、IP アドレスを使用し、DNS 照合を防ぎます。

分散トランザクション

COM+ 分散トランザクションを使用するサービス コンポーネントおよび Microsoft 分散トランザクション コーディネータ (DTC) のサービスを開発した場合、 DTC トラフィックが独立した DTC インスタンス間および DTC と SQL Server などのリソース マネージャ間で流れることを許可するために、ファイアウォールを構成する必要がある場合もあります。

DTC ポートを開くことに関する詳細については、「[INFO] MS DTC をファイアウォール経由で動作可能にする」を参照してください。

BLOB の処理

今日、多くのアプリケーションは、より一般的な文字と数値データ以外に、グラフィックやサウンドなどのデータ形式、またはビデオなどの複雑なデータ形式さえ扱う必要があります。グラフィック、サウンド、およびビデオ形式には多くの異なる種類があります。ただし、ストレージの観点からは、それらはすべてバイナリ データの固まりと見なされ、通常は BLOB (バイナリ ラージ オブジェクト) と呼ばれます。

SQL Server は、BLOB を格納するために、 binary データ型、varbinary データ型、および image データ型を提供します。 BLOB データという名前にもかかわらず、 BLOB データはテキスト ベースのデータも参照できます。たとえば、特定の行に関連付ける任意の長いメモ フィールドを格納したい場合があります。この目的のために、SQL Server は ntext データ型と text データ型を提供しています。

一般的に、8 KB (キロバイト) 未満のバイナリ データには、varbinary データ型を使用します。このサイズを超えるバイナリ データには、image を使用します。表 2 は、各データ型の主な機能に注目しています。

表 2. データ型の機能

データ型 サイズ 説明
binary 1 バイトから 8,000 バイトまでの範囲です。ストレージ サイズは、指定した長さに 4 バイトを加算した値です。 固定長のバイナリ データ
varbinary 1 バイトから 8,000 バイトまでの範囲です。ストレージ サイズは、指定したデータの実際の長さに 4 バイトを加算した値です。 可変長のバイナリ データ
image 0 GB から 2 GB までの可変長のバイナリ データ。 大きいサイズの可変長のバイナリ データ
text 0 GB から 2 GB までの可変長のデータ。 文字データ
ntext 0 GB から 2 GB までの可変長のデータ。 Unicode 文字データ

BLOB データを格納する場所

SQL Server 7.0 以降では、データベースに格納された BLOB データを使った作業のパフォーマンスが向上しています。この理由の 1 つは、データベース ページ サイズが 8 KB に増加したことです。この結果、8 KB 未満のテキスト データまたはイメージ データは、ページの別のバイナリ ツリー構造に格納される必要がなくなり、単一行に格納できます。これは、textntext、または image データの読み取りまたは書き込みが、文字とバイナリ文字列の読み取りまたは書き込みと同程度に高速になることを意味します。 8 KB を超えると、ポインタが行内に保持され、データ自体は別のデータ ページのバイナリ ツリー構造に保持され、必然的なパフォーマンスの影響を受けます。

textntext、および image データを強制的に単一行に格納することに関する詳細については、 SQL Server Books Online で「Text 型と Image 型データの使用」トピックを参照してください。

BLOB データを処理するために一般的に使用される別のアプローチは、ファイル システムに BLOB データを格納し、適切なファイルを参照するポインタ (適切な URL [Uniform Resource Locator] リンク) をデータベース列にを格納することです。 SQL Server 7.0 より前のバージョンでは、データベース外部のファイル システムに BLOB データを格納することでパフォーマンスを向上できます。

ただし、SQL Server 2000 で機能強化された BLOB のサポートは、 ADO.NET の BLOB データの読み取りおよび書き込みに対するサポートに連結しており、データベースに BLOB データを格納することを実現可能なアプローチにします。

データベースに BLOB データを格納する利点

データベースに BLOB データを格納することは、以下のような多くの利点を提供します。

  • 行内の残りの項目と同期する BLOB データをより簡単に保持できます。
  • BLOB データがデータベースと共にバックアップされます。単一のストレージ システムを持つことで、容易に管理できます。
  • BLOB データは、SQL Server 2000 で XML のサポートを使ってアクセスでき、 XML ストリームの base 64 エンコード表現のデータを返します。
  • SQL Server フルテキスト検索 (FTS) 操作は、固定長または可変長の文字 (Unicode など) データを含む列に対して実行できます。 image フィールド内に含まれる書式化されたテキスト ベースのデータ (たとえば、Microsoft Word 文書または Microsoft Excel ドキュメント) に対しても FTS 操作を実行できます。

BLOB データをデータベースに書き込む

以下のコードはADO.NET を使用して、ファイルから取得したバイナリ データを SQL Server の image フィールドに書き込む方法を示しています。

public void StorePicture( string filename )
{
  // バイト配列にファイルを読み取ります
  FileStream fs = new FileStream( filename, FileMode.Open, FileAccess.Read );
  byte[] imageData = new Byte[fs.Length];
  fs.Read( imageData, 0, (int)fs.Length );
  fs.Close();

  SqlConnection conn = new SqlConnection("");
  SqlCommand cmd = new SqlCommand("StorePicture", conn);
  cmd.CommandType = CommandType.StoredProcedure;
  cmd.Parameters.Add("@filename", filename );
  cmd.Parameters["@filename"].Direction = ParameterDirection.Input;
  cmd.Parameters.Add("@blobdata", SqlDbType.Image);
  cmd.Parameters["@blobdata"].Direction = ParameterDirection.Input;
  // image フィールド内にバイト配列を格納します
  cmd.Parameters["@blobdata"].Value = imageData;
  try
  {
    conn.Open();
    cmd.ExecuteNonQuery();
  }
  catch
  {
    throw;
  }
  finally
  {
    conn.Close();
  }
}

BLOB データをデータベースから読み取る

ExecuteReader を使って SqlDataReader オブジェクトを作成し、 BLOB データを保持する行を読み取る場合、 CommandBehavior.SequentialAccess 列挙値を使用します。この列挙値を指定しないと、リーダーは一度に 1 行ずつサーバーからクライアントにデータを取り出します。行が BLOB 列を含んでいる場合は、これが大量のメモリを使用する結果になります。列挙値を使用することによって、より優れた制御を行えます。これは、BLOB データを参照する場合だけ取り出すためです (たとえば、読み取るバイト数を制御できる GetBytes メソッドを使用します)。以下のコードでこれを例示します。

// 以前確立されたコマンドと接続を前提にします
// コマンドがテーブルから IMAGE 列を SELECT します
conn.Open();
SqlDataReader reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess);
reader.Read();
// イメージ データのサイズを取得し、バイト配列のパラメータとして NULL を渡します
long bytesize = reader.GetBytes(0, 0, null, 0, 0);
// バイト配列を割り当てイメージ データを保持します
byte[] imageData = new byte[bytesize];
long bytesread = 0;
int curpos = 0;
while (bytesread < bytesize)
{
  // chunkSize は任意のアプリケーション定義済みの値です
  bytesread += reader.GetBytes(0, curpos, imageData, curpos, chunkSize);
  curpos += chunkSize;
}
// バイト配列の 'imageData' に、データベースからの BLOB が含まれるようになります

注意 CommandBehavior.SequentialAccess を使用するときは、完全に連続した順序で列データにアクセスする必要があります。たとえば、BLOB データが列 3 にあって、列 1 と列 2 からもデータを必要とする場合、列 3 よりも列 1 と列 2 を先に読み取る必要があります。

トランザクション

データ ソースを更新するすべての商用のアプリケーションは、事実上トランザクションのサポートを必要とします。トランザクションは、1 つ以上のデータ ソース内に含まれるシステムの状態の整合性を保証するために使用されます。これは、よく知られている ACID という頭字語の 4 つの基本的な保証を提供することによって行われます。その 4 つは、原子性、一貫性、分離性、および持続性です。

たとえば、注文書を処理する Web ベースの小売アプリケーションを考えてみましょう。それぞれの注文は、3 つのデータベースの更新に関連する以下の 3 つの個別の操作を必要とします。

  • 在庫レベルが、注文数量分だけ差し引かれる必要があります。
  • 顧客の貸方レベルが、購入額分だけ借方に記入される必要があります。
  • 新しい注文が、注文データベースに追加される必要があります。

これら 3 つの個別の操作は 1 単位として、さらに原子形式として実行されることが必要不可欠になります。これらはすべて成功するか、すべて失敗する必要があります。どんな代替手段でも、データの整合性は低下します。トランザクションはこれを保証し、さらにその他の保証を提供します。

トランザクション処理の基本についての詳しい背景は、 https://www.microsoft.com/japan/msdn/library/ja/cpguide/html/cpcontransactionprocessingfundamentals.asp を参照してください。

トランザクションの管理をデータ アクセス コードに組み込むために採用できるアプローチは数多くあります。各アプローチは、以下の 2 つの基本的なプログラミング モデルのいずれかに適合します。

  • 手動トランザクション。コンポーネントのコードまたはストアド プロシージャのそれぞれに直接、 ADO.NET または Transact-SQL のいずれかのトランザクション サポート機能を使用するコードを記述します。
  • 自動 (COM+) トランザクション。オブジェクトのトランザクションの必要条件を実行時に指定する宣言属性を .NET クラスに追加します。このモデルを利用して、同一のトランザクション内で作業を実行するように、複数のコンポーネントを容易に構成できます。

どちらの技法も、ローカル トランザクション (つまり、SQL Server 2000 などの単一のリソース マネージャに対して実行されるトランザクション)、または分散トランザクション (つまり、リモート コンピュータ上に配置されている複数のリソース マネージャに対して実行されるトランザクション) を実行するために使用できます。ただし、自動トランザクション モデルは、分散トランザクションの処理を非常に簡略化します。

自動 (COM+) トランザクションを使用して、より簡単なプログラミング モデルからの利点を得たいと考えることがあります。この利点は、データベースの更新を実行する多くのコンポーネントを持つシステムで特に顕著です。ただし、多くのシナリオで、この形式のトランザクション モデルの結果として発生する新たなオーバーヘッドやパフォーマンスの低下を回避する必要があります。

以下のセクションでは、特定のアプリケーションのシナリオに基づいて最も適切なモデルの選択を支援するためのガイダンスを提供します。

トランザクション モデルの選択

トランザクション モデルを選択する前に、トランザクションが必要か、まったく必要ないかを考える必要があります。トランザクションは、サーバー アプリケーションが利用する単一の最も負荷の高いリソースで、不必要に使用するとスケーラビリティを減少します。トランザクションの使用法を規定する以下のガイダンスを考慮してください。

  • 一連の操作間でロックを取得する必要があり、ACID ルールを強制する必要がある場合にのみトランザクションを実行します。
  • できる限り短期間トランザクションを保持し、データベース ロックを保持する時間を最小限にします。
  • トランザクションの存続期間の管理をクライアントに行わせてはいけません。
  • 個別の SQL ステートメントに対してトランザクションを使用しません。 SQL Server は、自動的に各ステートメントを個別のトランザクションとして実行します。

自動トランザクションと手動トランザクション

自動トランザクションではプログラミング モデルが多少簡略化されますが、特に複数のコンポーネントがデータベースの更新を実行している場合は、手動トランザクションが Microsoft DTC との相互作用を必要としないので、手動のローカル トランザクションの方が常により高速に処理を行います。単一のローカル リソース マネージャ (SQL Server など) に対して自動トランザクションを使用している場合でも、これが当てはまります (ただし、パフォーマンスの低下は減少します)。これは、手動のローカル トランザクションが DTC との不必要な IPC (プロセス間通信) を回避するためです。

以下の場合に、手動トランザクションを使用します。

  • 単一のデータベースに対してトランザクションを実行しています。

以下の場合に、自動トランザクションを使用します。

  • 複数のリモート データベースにまたがる単一のトランザクションを必要とします。
  • 複数のリソース マネージャ、たとえばデータベースや Windows 2000 メッセージ キュー (MSMQ とも呼ばれます) リソース マネージャを包含するために単一のトランザクションを必要とします。

**注意 **トランザクション モデルは混在して使用せずに、どちらか一方を使用してください。

パフォーマンスに十分満足できるアプリケーションのシナリオでは、自動トランザクション (単一のデータベースに対しても) を最適化してプログラミング モデルを簡素化することは簡単です。自動トランザクションは、複数のコンポーネントが同一トランザクションの一部である操作の実行を簡単にします。

手動トランザクションを使用する

手動トランザクションでは、コンポーネントのコードまたはストアド プロシージャのそれぞれに、直接 ADO.NET または Transact-SQL のいずれかのトランザクション サポート機能を使用するコードを記述します。大部分の場合に、ストアド プロシージャでトランザクションを制御するアプローチは、優れたカプセル化を提供し、パフォーマンスの観点でも ADO.NET コードを使ってトランザクションを実行することに匹敵するので、ストアド プロシージャでトランザクションを制御するアプローチを選択すべきです。

ADO.NET を使って手動トランザクションを実行する

ADO.NET は、トランザクション オブジェクトをサポートしています。トランザクション オブジェクトを使って新しいトランザクションを開始でき、その後コミットまたはロールバックする必要があるかどうかを明示的に制御できます。トランザクション オブジェクトは、単一のデータベース接続に関連付けられており、接続オブジェクトの BeginTransaction メソッドによって取得されます。このメソッドの呼び出しは、後続のコマンドがこのトランザクションのコンテキストで実行されるということを暗黙に意味しているわけではありません。コマンドの Transaction プロパティを設定して、各コマンドとトランザクションとを明示的に関連付ける必要があります。複数のコマンド オブジェクトをトランザクション オブジェクトに関連付けることができます。それにより、単一のトランザクションで単一のデータベースに対する複数の操作をグループ化します。

ADO.NET トランザクション コードの使用例については、付録の「ADO.NET の手動トランザクションをコーディングする方法」を参照してください。

関連情報

  • ADO.NET の手動トランザクションの既定の分離レベルは READ COMMITTED です。これは、データの読み取り中にデータベースが共有ロックを保持することを意味します。ただし、トランザクションの終了前にデータを変更できます。これは、繰り返し読み取ることができないデータ、または見せかけのデータになることがあります。トランザクション オブジェクトの IsolationLevel プロパティを IsolationLevel 列挙型で定義された列挙値のいずれかに設定することによって、分離レベルを変更できます。
  • トランザクションの適切な分離レベルの選択については、慎重に検討する必要があります。データの一貫性とパフォーマンスのどちらを優先するかを比較検討します。最高の分離レベルは、完全なデータの一貫性を提供しますが、システム全体のスループットを犠牲にします。より低い分離レベルを選択すると、アプリケーションがよりスケーラブルになりますが、それと同時にデータの不整合からエラーが発生する可能性が増加します。大部分の時間をデータの読み取りに費やし、たまにデータを書き込むシステムでは、より低い分離レベルが適している場合があります。
  • 適切なトランザクション分離レベルの選択に役立つ情報については、 Microsoft Press® 発行の Kalen Delaney 著の 『Inside SQL Server 2000』を参照してください。 (訳注: 日本語訳書籍があります。『アーキテクチャ徹底解説 Microsoft SQL Server 2000』)

ストアド プロシージャを使って手動トランザクションを実行する

ストアド プロシージャ内で Transact-SQL ステートメントを使用して、手動トランザクションを直接制御することもできます。たとえば、BEGIN TRANSACTION、END TRANSACTION、および ROLLBACK TRANSACTION などの Transact-SQL トランザクション ステートメントを含む単一のストアド プロシージャを使用して、トランザクション操作を実行できます。

関連情報

  • 必要に応じて、ストアド プロシージャで SET TRANSACTION ISOLATION LEVEL ステートメントを使用することにより、トランザクション分離レベルを制御できます。 Read Committed が SQL Server の既定値です。 SQL Server 分離レベルに関する詳細情報については、 SQL Server Books Online の「リレーショナル データへのアクセスおよびデータの変更」セクションの「分離レベル」を参照してください。
  • Transact-SQL トランザクション ステートメントを使用してトランザクション更新を実行する方法を示すコード サンプルについては、付録の「Transact-SQL を使用してトランザクションを実行する方法」を参照してください。

自動トランザクションを使用する

自動トランザクションは、新しいトランザクションを明示的に開始したり、そのトランザクションを明示的にコミットまたはアボートしたりする必要がないので、プログラミング モデルを簡略化します。ただし、自動トランザクションの最も重要な利点は、自動トランザクションが DTC と組み合わせて機能することです。これは、単一のトランザクションが複数の分散データ ソースに展開されることを許可します。この利点は、大規模な分散アプリケーションで重要になります。 DTC を直接プログラミングすることによって、分散トランザクションを手動で制御できますが、自動トランザクションは作業を大幅に簡素化し、コンポーネント ベースのシステム用にデザインされています。たとえば、単一のトランザクションで構成された作業を実行するために、複数のコンポーネントを宣言的な構成にすることが容易になります。

自動トランザクションは、COM+ が提供する分散トランザクションのサポート機能に依存しており、その結果サービス コンポーネント (つまり、ServicedComponent クラスから派生したコンポーネント) だけが、自動トランザクションを使用できます。

自動トランザクション用のクラスを構成する方法は以下のとおりです。

  • System.EnterpriseServices 名前空間内に配置されている ServicedComponent クラスからクラスを派生します。
  • Transaction 属性を使用して、そのクラスのトランザクションの必要条件を定義します。 TransactionOption 列挙値から提供される値は、クラスが COM+ カタログでどのように構成されるかを決定します。この属性を使って設定できるその他のプロパティには、トランザクション分離レベルやタイムアウトなどがあります。
  • トランザクションの結果を明示的に確定することを避けるために、 AutoComplete 属性などのメソッドに注釈を付けることができます。これらのメソッドが例外をスローすると、トランザクションは自動的にアボートされます。必要に応じて、依然としてトランザクションの結果を直接確定できることに注意してください。詳細については、このドキュメントの「トランザクションの結果を決定する」セクションを参照してください。

関連情報

  • COM+ 自動トランザクションに関する詳細については、 Platform SDK ドキュメントで「Automatic Transactions Through COM+」を検索してください。
  • トランザクション .NET クラスの例については、付録の「トランザクション .NET クラスをコーディングする方法」を参照してください。

トランザクション分離レベルの構成

COM+ Version 1.0、 つまり Windows 2000 で実行している COM+ のトランザクション分離レベルは、Serialized です。これは最高の分離レベルを提供しますが、このような保護はパフォーマンスを犠牲にすることになります。関連するリソース マネージャ (通常はデータベース) はトランザクションの実行中に読み取りおよび書き込みの両方のロックを保持する必要があるので、システムのスループット全体が低下します。この間、他のすべてのトランザクションはブロックされます。これが、アプリケーションのスケーラビリティに関する機能に重大な影響を与えることがあります。

Microsoft Windows Server 2003 に同梱されている COM+ Version 1.5 を利用すると、トランザクション分離レベルを COM+ カタログでコンポーネントごとに構成できます。トランザクションのルート コンポーネントに関連付けられた設定が、トランザクションの分離レベルを決定します。さらに、同一トランザクション ストリームの一部である内部サブコンポーネントは、ルート コンポーネントで定義されたトランザクション レベルよりも高いレトランザクション レベルを持つ必要がありません。サブコンポーネントがルート コンポーネントで定義されたトランザクション レベルよりも高いトランザクション レベルを持つ場合、サブコンポーネントのインスタンスが作成されるときにエラーが発生します。

.NET マネージ クラスでは、 Transaction 属性がパブリック Isolation プロパティをサポートします。このプロパティを使用して、以下のコードで例示されているように特定の分離レベルを宣言的に指定できます。

[Transaction(TransactionOption.Supported, Isolation=TransactionIsolationLevel.ReadCommitted)]
public class Account : ServicedComponent
{
  . . .
}

関連情報

  • 構成可能なトランザクション分離レベルとその他の Windows Server 2003 COM+ の機能強化に関する詳細については、 2001 年 8 月に MSDN Magazine で公開された「Windows XP: Make Your Components More Robust with COM+ 1.5 Innovations」 (英語) を参照してください。
    (訳注: MSDN マガジン September 2001 No.18 に日本語訳を収録しています。「Windows XP:COM+ 1.5 の新機軸でコンポーネントをより頑丈なものに」)

トランザクションの結果を決定する

自動トランザクションの結果は、単一のトランザクション ストリームのすべてのトランザクション コンポーネントのコンテキストで、一貫性フラグとトランザクション アボート フラグの状態の組み合わせで決定されます。トランザクションの結果は、トランザクション ストリーム内のルート コンポーネントが非アクティブになった (そして、制御が呼び出し元に返された) 時点で決定されます。これを 図 5 で例示します。この図は、標準的な銀行資金移動トランザクションを示しています。

図 5. トランザクション ストリームとコンテキスト

トランザクションの結果は、ルート オブジェクト (この例では Transfer オブジェクト) が非アクティブになるときに決定され、クライアントのメソッドの呼び出しに戻ります。すべてのコンテキスト内のすべての一貫性フラグが False に設定されている場合、またはトランザクション アボート フラグが True に設定されている場合、元になる物理 DTC トランザクションがアボートされます。

以下の 2 つの方法のいずれかを使用して、.NET からトランザクションの結果を制御できます。

  • AutoComplete 属性を持つメソッドに注釈を付けると、 .NET が自動的にトランザクションの結果を決定します。この属性では、メソッドが例外をスローすると、一貫性フラグが自動的に False に設定されます (最終的にはトランザクションがアボートされます)。例外をスローせずにメソッドから戻る場合は、一貫性フラグが True に設定されます。これは、コンポーネントがトランザクションをコミットするのに適した状態になっていることを示します。これは同一のトランザクション ストリーム内の他のオブジェクトの決定によって異なるので、このことが保証されているわけではありません。
  • ContextUtil クラスの静的 SetComplete メソッドまたは静的 SetAbort メソッドを呼び出せます。このクラスは、一貫性フラグを Ture または False のそれぞれに設定できます。

重要度が 10 よりも高い SQL Server エラーは、マネージ データ プロバイダが SqlException 型の例外をスローすることになります。メソッドが例外をキャッチおよび処理する場合、トランザクションを手動でアボートすることを決定するか、または、[AutoComplete] フラグ付きメソッドでは例外が呼び出し元に反映されることを確認してください。

[AutoComplete] メソッド

AutoComplete 属性が設定されているメソッドでは、以下のいずれかの操作を行ってください。

  • SqlException がコール スタックをバックアップするようにします。
  • 外側の例外で SqlException をラップし、呼び出し元にそれを反映します。呼び出し元にとってより意味のある例外の種類で例外をラップすることになります。

例外の反映に失敗すると、データベース エラーが発生しているにもかかわらず、トランザクションをアボートすることを決定しないオブジェクトになります。この結果、同一トランザクション ストリームを共有しているその他のオブジェクトが行ったその他の成功した操作がコミットされる可能性があることを意味します。

以下のコードは、SqlException をキャッチし、それを呼び出し元に直接反映します。このオブジェクトが非アクティブになったときに、このオブジェクトの一貫性フラグが自動的に False に設定されるので、トランザクションは最終的にはアボートします。

[AutoComplete]
void SomeMethod()
{
  try
  {
    // 接続を開き、データベース操作を実行します
    . . .
  }
  catch (SqlException sqlex )
  {
    LogException( sqlex ); // 例外の詳細をログに記録します
    throw;                 // 例外をもう一度スローします
                           // 一貫性フラグが False に設定されることになります
  }
  finally
  {
    // データベース接続を閉じます
    . . .
  }
}

非 [AutoComplete] メソッド

AutoComplete 属性が設定されていないメソッドでは、以下のことを行う必要があります。

  • catch ブロック内で ContextUtil.SetAbort を呼び出し、トランザクションをアボートすることを決定します。この結果、一貫性フラグが False に設定されます。
  • 例外が発生しない場合は、 ContextUtil.SetComplete を呼び出して、トランザクションをコミットすることを決定します。この結果、一貫性フラグが True に設定されます (これが既定の状態です)。

以下のコードはこのアプローチを例示しています。

void SomeOtherMethod()
{
  try
  {
    // 接続を開き、データベース操作を実行します
    . . .
    ContextUtil.SetComplete(); // トランザクションをコミットするように手動で決定します
  }
  catch (SqlException sqlex)
  {
    LogException( sqlex ); // 例外の詳細を記録します
    ContextUtil.SetAbort(); // トランザクションをアボートすることを手動で決定します
    // この時点で例外が処理され、呼び出し元には反映されません
  }
  finally
  {
    // データベース接続を閉じます
    . . .
  }
}

**注意 **複数の catch ブロックを持っている場合、メソッドの先頭ですぐに ContextUtil.SetAbort を呼び出し、 try ブロックの最後で ContextUtil.SetComplete を呼び出すと処理が容易になります。この方法では、各 catch ブロック内で ContextUtil.SetAbort への呼び出しを繰り返す必要はありません。これらのメソッドが決定する一貫性フラグの設定は、メソッドから戻る場合にのみに意味を持ちます。

呼び出し元のコードがトランザクションが失敗するであろうことを認識できるので、常にコール スタックをバックアップして例外 (またはラップされた例外) を呼び出し側に反映する必要があります。この結果、呼び出し元のコードが最適化することを許可します。たとえば、銀行資金移動シナリオでは、借方操作が既に失敗している場合に、移動コンポーネントが貸方操作を実行することを決定できません。

一貫性フラグを False に設定し、例外をスローせずに戻る場合、呼び出し元のコードはトランザクションが失敗するであろうことを認識する方法がありません。ブール値を返すか、ブール値の出力パラメータを返すことはできますが、一貫性を保ち、エラー状態を示すために例外をスローする必要があります。この結果、エラー処理の標準アプローチを使って、より明確でより一貫性のあるコードになります。

データのページ処理

データ全体のページ処理は、分散アプリケーションでは共通の必要条件です。たとえば、一度にリスト全体を表示できない書籍の一覧をユーザーに提示する場合があります。ユーザーはデータ上で、データの次のページや前のページを表示したり、リストの先頭または末尾にジャンプしたりといった馴染みのある操作を実行したいと考えるでしょう。

以下のセクションでは、この機能を実装するための選択肢と、各選択肢のスケーラビリティやパフォーマンスに対する影響について説明します。

選択肢の比較

データのページ処理には以下の選択肢があります。。

  • SqlDataAdapterFill メソッドを使用して、クエリからの結果のある範囲のデータを DataSet に設定します。
  • COM の相互運用性を使って ADO を使用し、サーバー側カーソルを使用します。
  • ストアド プロシージャを使用して、データのページ処理を手動で実装します。

データのページ処理に対する最適な選択肢は、以下に示す要因によって異なります。

  • スケーラビリティの必要条件
  • パフォーマンスの必要条件
  • ネットワーク帯域幅
  • データベース サーバーのメモリと能力
  • 中間層のサーバーのメモリと能力
  • ページ処理を行うクエリが返す行数
  • データ ページのサイズ

パフォーマンス テストでは、ストアド プロシージャを使用する手動のアプローチが、幅広い負荷レベル間で最適なパフォーマンスを提供することを示しています。ただし、手動のアプローチはサーバー上で作業を実行するので、サイトの機能の大部分がデータ ページ処理機能に依存している場合、サーバーの負荷レベルが重大な問題になります。このアプローチが特定の環境に適していることを確認するには、特定の必要条件に対してすべての選択肢をテストする必要があります。

さまざまな選択肢を以下で説明します。

SqlDataAdapter を使用する

以前説明したように、 SqlDataAdapter を使用して、データベースからのデータを DataSet に格納します。オーバーロードされた Fill メソッドの 1 つ (以下のコードで示されている) は、2 つの整数インデックス値を受け取ります。

public int Fill(
   DataSet dataSet,
   int startRecord,
   int maxRecords,
   string srcTable
);

startRecord 値は、 0 (ゼロ) から始まる開始レコードのインデックスを表します。 maxRecords 値は、startRecord から開始して、新しい DataSet にコピーするレコード数を指定します。

内部的には、 SqlDataAdapterSqlDataReader を使用して、クエリを実行して結果を返します。 SqlDataAdapter は結果を読み取り、 SqlDataReader から読み取ったデータに基づいて DataSet を作成します。 SqlDataAdapter は、取得したすべての結果を startRecordmaxRecords を使って新しく生成された DataSet にコピーし、不要な結果は破棄します。この結果、多くの不要なデータがネットワーク経由でデータ アクセス クライアントに取り出されることになります。つまり、これがこのアプローチの主な欠点です。

たとえば、1,000 レコードを持っていて、 900 から 950 までのレコードが必要な場合、最初の 899 レコードがネットワーク経由で取り出され、破棄されます。このオーバーヘッドは、小さな結果セットではおそらく最小限で済みますが、大きなデータ セット全体でページ処理行う場合に非常に大きくなります。

ADO を使用する

ページ処理を実装する別の選択肢は、 COM ベースの ADO を使用してページ処理を行うことです。この選択肢を採用する主な動機は、サーバー側カーソルへのアクセス許可を得ることで、これは ADO Recordset オブジェクトを使って公開されます。 Recordset カーソルの場所は、adUseServer に設定できます。 OLE DB プロバイダが (SQLOLEDB が行う) カーソルをサポートする場合、サーバー側カーソルを使用することになります。その後、このカーソルを使用して、開始レコードに直接移動できます。このとき、ネットワーク経由ですべてのレコードをデータ アクセス クライアント コードに取り出す必要はありません。

このアプローチには、以下の 2 つの主な欠点があります。

  • ほとんどの場合、 Recordset オブジェクトに返されるレコードをクライアント マネージ コードで使用するために、 DataSet に変換したいと考えるでしょう。 OleDbDataAdapterFill メソッドをオーバーロードして、 ADO Recordset オブジェクトを取り出し、それを DataSet に変換しますが、取り出しを特定のレコードから開始したり、特定のレコードで終了したりする機能がありません。唯一の現実的なオプションは、Recordset オブジェクトの開始レコードに移動し、各レコード間をループし、データを新しく手動で生成した DataSet に手動でコピーすることです。これを行うことは、 COM 相互運用呼び出しのオーバーヘッドを生じ、ネットワーク経由して余分なデータを取り出さないという利点が、特に小さい DataSet でなくなってしまいます。
  • サーバーから必要なデータを取り出している間は、接続とサーバー側カーソルが開いたままになります。カーソルは通常、負荷の高いリソースで、データベース サーバー上で開き、管理します。この選択肢はパフォーマンスを向上しますが、長時間サーバー上の重要なリソースを消費することによって、スケーラビリティを減少することになります。

手動の実装を提供する

このセクションで説明するデータ全体のページ処理を行うための最後の選択肢は、ストアド プロシージャを使用し、アプリケーションにページ処理機能を手動で実装することです。一意キーを持つテーブルは、ストアド プロシージャを比較的簡単に実装できます。一意キーを持たないテーブル (そのようなテーブルをたくさん持つべきではありませんが) では、処理はより複雑になります。

一意キーを持つテーブルでのページ処理

テーブルが一意キーを持っている場合、そのキーを WHERE 句で使用して特定の行から始まる結果セットを作成できます。これを、結果セットのサイズを制限するために使用する SET ROWCOUNT ステートメントと連結して、効果的なページ処理メカニズムを提供します。以下のストアド プロシージャ コードは、このアプローチの例です。

CREATE PROCEDURE GetProductsPaged
@lastProductID int,
@pageSize int
AS
SET ROWCOUNT @pageSize
SELECT *
FROM Products
WHERE [standard search criteria]
AND ProductID > @lastProductID
ORDER BY [Criteria that leaves ProductID monotonically increasing]
GO

このストアド プロシージャの呼び出し元が単純に lastProductID 値を管理し、連続する呼び出し間で選択したページのサイズによってその値を増減します。

一意キーを持たないテーブルでのページ処理

ページ処理に必要なテーブルが一意キーを持たない場合に、たとえば ID 列を使用して、キーを追加することを検討します。これを行うと、上記で説明したページ処理ソリューションを実装できるようになります。

結果セットの一部である 2 つ以上のその他のフィールドを組み合わせることで一意性を生成できる場合は、一意キーを持たないテーブルで効果的なページ処理ソリューションを実装できます。

たとえば、以下のテーブルを考えます。

Col1 Col2 Col3 その他の列…
A 1 W
A 1 X
A 1 Y
A 1 Z
A 2 W
A 2 X
B 1 W
B 1 X

この表では、 Col1Col2、および Col3 を組み合わせることで、一意性を生成できます。その結果、以下のストアド プロシージャで例示するアプローチを使用して、ページ処理メカニズムを実装できます。

CREATE PROCEDURE RetrieveDataPaged
@lastKey char(40),
@pageSize int
AS
SET ROWCOUNT @pageSize
SELECT
Col1, Col2, Col3, Col4, Col1+Col2+Col3 As KeyField
FROM SampleTable
WHERE [Standard search criteria]
AND Col1+Col2+Col3 > @lastKey
ORDER BY Col1 ASC, Col2 ASC, Col3 ASC
GO

クライアントは、ストアド プロシージャが返す KeyField 列の最後の値を管理し、それをストアド プロシージャに再度戻すことにより、テーブル全体のページ処理を制御します。

手動の実装はデータベース サーバーにかかる負担を増加しますが、ネットワーク経由で不要なデータを渡すことを回避します。パフォーマンス テストでは、このアプローチがある範囲の負荷レベル間で適切に機能することが示されました。ただし、サイトの作業がどの程度データのページ処理機能に関連するかによって異なりますが、サーバー上で手動のページ処理を実行することはアプリケーションのスケーラビリティに影響する可能性があります。特定のアプリケーション シナリオで最適なアプローチを見つけるために、独自の環境でパフォーマンス テストを実行する必要があります。

付録

.NET クラスのオブジェクトの構築を有効にする方法

Enterprise (COM+) Services を使用して、オブジェクトの構築用に .NET マネージ クラスを有効にするには、以下の手順を実行します。

  • System.EnterpriseServices 名前空間内に配置されている ServicedComponent クラスからクラスを派生します。

    using System.EnterpriseServices;
    public class DataAccessComponent : ServicedComponent
    
  • クラスを ConstructionEnabled 属性で装飾し、オプションで既定の構成文字列を指定します。この既定値は、COM+ カタログに保持されます。管理者は、値を保持するために、コンポーネント サービスの Microsoft 管理コンソール (MMC) スナップインを使用できます。

    [ConstructionEnabled(Default="default DSN")]
    public class DataAccessComponent : ServicedComponent
    
  • 仮想 Construct メソッドのオーバーライドされる実装を提供します。このメソッドを、オブジェクトの言語固有のコンストラクタの後に呼び出します。 COM+ カタログに保持されている構成文字列を、このメソッドへの単一の引数として指定します。

    public override void Construct( string constructString )
    {
      // Construct メソッドは、コンストラクタの後に呼び出します。
      // 構成済みの DSN を単一の引数として指定します。
    }
    
  • AssemblyKey ファイルまたは AssemblyKeyName 属性を使ってアセンブリに署名することにより、アセンブリに厳密な名前を提供します。 COM+ サービスを使って登録されるすべてのアセンブリは、厳密な名前を持つ必要があります。厳密な名前を持つアセンブリに関する詳細については、 https://www.microsoft.com/japan/msdn/library/ja/cpguide/html/cpconworkingwithstrongly-namedassemblies.asp を参照してください。

    [assembly: AssemblyKeyFile("DataServices.snk")]
    
  • 動的な登録をサポートするには、アセンブリ レベルの属性、ApplicationNameApplicationActivation を使用して、アセンブリ コンポーネント保持するために使用する COM+ アプリケーションの名前と、アプリケーションのアクティベーションの種類を指定します。アセンブリの登録に関する詳細については、 https://www.microsoft.com/japan/msdn/library/ja/cpguide/html/cpconregisteringservicedcomponents.asp を参照してください。

    // ApplicationName 属性は、アセンブリ コンポーネントを保持する 
    // COM+ アプリケーションの名前を指定します。
    [assembly : ApplicationName("DataServices")]
    
    // ApplicationActivation.ActivationOption 属性は、アセンブリ コンポーネントが
    // アクティブベーションで読み込まれる場所を指定します。
    // ライブラリ : 作成者のプロセスで実行されるコンポーネント
    // サーバー : システム プロセスで実行されるコンポーネント、dllhost.exe
    [assembly: ApplicationActivation(ActivationOption.Library)]
    

以下のコードは DataAccessComponent と呼ばれるサービス コンポーネントを示しており、これは COM+ 構成文字列を使用してデータベースの接続文字列を取得します。

using System;
using System.EnterpriseServices;

// ApplicationName 属性は、アセンブリ コンポーネントを保持する
// COM+ アプリケーションの名前を指定します。
[assembly : ApplicationName("DataServices")]

// ApplicationActivation.ActivationOption 属性は、アセンブリ コンポーネントが
// アクティブベーションで読み込まれる場所を指定します。
// ライブラリ : 作成者のプロセスで実行されるコンポーネント
// サーバー : システム プロセスで実行されるコンポーネント、dllhost.exe
[assembly: ApplicationActivation(ActivationOption.Library)]

// アセンブリに署名します。snk キー ファイルが 
// sn.exe ユーティリティを使用して作成されます。
[assembly: AssemblyKeyFile("DataServices.snk")]

[ConstructionEnabled(Default="Default DSN")]
public class DataAccessComponent : ServicedComponent
{
    private string connectionString;
    public DataAccessComponent()
    {
      // コンストラクタは、インスタンスの作成時に呼び出されます。
    }
    public override void Construct( string constructString )
    {
      // Construct メソッドは、コンストラクタの後に呼び出します。
      // 構成済みの DSN を単一の引数として指定します。
      this.connectionString = constructString;
    }
}

SqlDataAdapter を使用して複数行を取得する方法

以下のコードは、 SqlDataAdapter オブジェクトを使用して、 DataSet または DataTable を生成するコマンドを発行する方法を例示しています。 SQL Server Northwind データベースから製品カテゴリのセットを取得します。

using System.Data;
using System.Data.SqlClient;

public DataTable RetrieveRowsWithDataTable()
{
  using ( SqlConnection conn = new SqlConnection(connectionString) )
  {
    SqlCommand cmd = new SqlCommand("DATRetrieveProducts", conn);
    cmd.CommandType = CommandType.StoredProcedure;
    SqlDataAdapter da = new SqlDataAdapter( cmd );
    DataTable dt = new DataTable("Products");
    da.Fill(dt);
    return dt;
  }
}

SqlAdapter を使用して、 DataSet または DataTable を生成するには、以下の手順に従います。

  1. ストアド プロシージャを呼び出すために SqlCommand オブジェクトを作成し、これを SqlConnection オブジェクト (上記で示した) または接続文字列 (ここでは示していません) に関連付けます。
  2. 新しい SqlDataAdapter オブジェクトを作成し、それを SqlCommand オブジェクトに関連付けます。
  3. DataTable (またはオプションで DataSet) オブジェクトを作成します。コンストラクタ引数を使用して、DataTable に名前を付けます。
  4. SqlDataAdapter オブジェクトの Fill メソッドを呼び出し、取得した行を保持する DataSet または DataTable のいずれかを作成します。

SqlDataReader を使用して複数行を取得する方法

以下のコードは、複数行を取得する SqlDataReader のアプローチを例示しています。

using System.IO;
using System.Data;
using System.Data.SqlClient;

public SqlDataReader RetrieveRowsWithDataReader()
{
  SqlConnection conn = new SqlConnection(
         "server=(local);Integrated Security=SSPI;database=northwind");
  SqlCommand cmd = new SqlCommand("DATRetrieveProducts", conn );
  cmd.CommandType = CommandType.StoredProcedure;
  try
  {
    conn.Open();
    // リーダーを生成します。CommandBehavior.CloseConnection は、
    // リーダー オブジェクトが閉じられるときに、接続を閉じます。
    return( cmd.ExecuteReader( CommandBehavior.CloseConnection ) );
  }
  catch
  {
    conn.Close();
    throw;
  }
}

// コンソールを使用して製品リストを表示します
private void DisplayProducts()
{
  SqlDataReader reader = RetrieveRowsWithDataReader();
  while (reader.Read())
  {
    Console.WriteLine("{0} {1} {2}", 
                      reader.GetInt32(0).ToString(), 
                      reader.GetString(1) );
  }
  reader.Close(); // リーダーを生成するときに使用された 
                  // CommandBehavior 列挙値により接続も閉じます
}

SqlDataReader を使って行を取得するには、以下の手順に従います。

  1. ストアド プロシージャの実行に使用される SqlCommand オブジェクトを作成し、それを SqlConnection オブジェクトに関連付けます。
  2. 接続を開きます。
  3. SqlCommand オブジェクトの ExecuteReader メソッドを呼び出して、 SqlDataReader オブジェクトを生成します。
  4. ストリームからデータを読み取るために、 SqlDataReader オブジェクトの Read メソッドを呼び出して行を取得し、型指定するアクセサ メソッド (GetInt32GetString メソッドなど) を使用して列の値を取得します。
  5. リーダーを使った作業を完了するときに、 Close メソッドを呼び出します。

XmlReader を使用して複数行を取得する方法

SqlCommand オブジェクトを使用して、 XmlReader オブジェクトを生成できます。 XmlReader オブジェクトは、 XML データへの前方参照のみのストリーム ベースのアクセスを提供します。コマンド (通常はストアド プロシージャ) は、 XML ベースの結果セットを生成する必要があり、それは SQL Server 2000 では通常、有効な FOR XML 句を含む SELECT ステートメントで構成されます。以下のコードは、このアプローチを例示しています。

public void RetrieveAndDisplayRowsWithXmlReader()
{
  SqlConnection conn = new SqlConnection(connectionString);
  SqlCommand cmd = new SqlCommand("DATRetrieveProductsXML", conn );
  cmd.CommandType = CommandType.StoredProcedure;
  try
  {
    conn.Open();
    XmlTextReader xreader = (XmlTextReader)cmd.ExecuteXmlReader();
    while ( xreader.Read() )
    {
      if ( xreader.Name == "PRODUCTS" ) 
      {
        string strOutput = xreader.GetAttribute("ProductID");
        strOutput += " ";
        strOutput += xreader.GetAttribute("ProductName");
        Console.WriteLine( strOutput );
      }
    }
    xreader.Close();  
  }
  catch
  {
    throw;
  }
  finally
  {
    conn.Close();
  }
}

上記のコードは、以下のストアド プロシージャを使用します。

CREATE PROCEDURE DATRetrieveProductsXML
AS
SELECT * FROM PRODUCTS 
FOR XML AUTO
GO

XmlReader を使って XML データを取得するには、以下の手順に従います。

  1. XML の結果セットを生成するストアド プロシージャ (たとえば、SELECT ステートメントで FOR XML 句を使用) を呼び出すために SqlCommand オブジェクトを作成します。 SqlCommand オブジェクトを接続に関連付けます。
  2. SqlCommand オブジェクトの ExecuteXmlReader メソッドを呼び出し、その結果を前方参照のみの XmlTextReader オブジェクトに代入します。これは、返されるデータの XML ベースの検証を必要としないときに使用する最も効率的な種類の XmlReader オブジェクトです。
  3. XmlTextReader オブジェクトの Read メソッドを使用して、データを読み取ります。

ストアド プロシージャの出力パラメータを使用して単一行を取得する方法

名前付き出力パラメータを使用して、単一行内の取得したデータ項目を返すストアド プロシージャを呼び出せます。以下のコードは、ストアド プロシージャを使用して、 Northwind データベース内の Products テーブルに含まれている特定の製品の製品名と単価を取得します。

void GetProductDetails( int ProductID, 
                        out string ProductName, out decimal UnitPrice )
{
  SqlConnection conn = new SqlConnection(
        "server=(local);Integrated Security=SSPI;database=Northwind");

  // ストアド プロシージャの実行に使用するコマンド オブジェクトをセットアップします
  SqlCommand cmd = new SqlCommand( "DATGetProductDetailsSPOutput", conn );
  cmd.CommandType = CommandType.StoredProcedure;
  // ストアド プロシージャのパラメータを確立します
  //  @ProductID int INPUT
  //  @ProductName nvarchar(40) OUTPUT
  //  @UnitPrice money OUTPUT

  // 出力パラメータの方向を明示的に設定する必要があります
  SqlParameter paramProdID = 
         cmd.Parameters.Add( "@ProductID", ProductID );
  paramProdID.Direction = ParameterDirection.Input;
  SqlParameter paramProdName = 
         cmd.Parameters.Add( "@ProductName", SqlDbType.VarChar, 40 );
  paramProdName.Direction = ParameterDirection.Output;
  SqlParameter paramUnitPrice = 
         cmd.Parameters.Add( "@UnitPrice", SqlDbType.Money );
  paramUnitPrice.Direction = ParameterDirection.Output;
  try
  {
    conn.Open();
    // ExecuteNonQuery を使用してコマンドを実行します 
    // 行は返されませんが、マップされたすべての出力パラメータ  
    // (および可能性のある戻り値) が生成されます
    cmd.ExecuteNonQuery( );
    // ストアド プロシージャから出力パラメータを返します
    ProductName = paramProdName.Value.ToString();
    UnitPrice = (decimal)paramUnitPrice.Value;
  }
  catch
  {
    throw;
  }
  finally
  {
    conn.Close();
  }
}

ストアド プロシージャの出力パラメータを使用して単一行を取得するには、以下の手順に従います。

  1. SqlCommand オブジェクトを作成し、それを SqlConnection オブジェクトに関連付けます。
  2. SqlCommandParameters コレクションの Add メソッドを呼び出して、ストアド プロシージャのパラメータを設定します。既定では、パラメータは入力パラメータであると仮定されます。そのため、出力パラメータの方向を明示的に設定する必要があります。

**注意 **入力パラメータなどのすべてのパラメータの方向を明示的に設定することをお勧めします。

  1. 接続を開きます。
  2. SqlCommand オブジェクトの ExecuteNonQuery メソッドを呼び出します。これは、出力パラメータ (および可能性のある戻り値) を生成します。
  3. Value プロパティを使用して、適切な SqlParameter オブジェクトから出力パラメータを取得します。
  4. 接続を閉じます。

上記のコードは、以下のストアド プロシージャを呼び出します。

CREATE PROCEDURE DATGetProductDetailsSPOutput
@ProductID int,
@ProductName nvarchar(40) OUTPUT,
@UnitPrice money OUTPUT
AS
SELECT @ProductName = ProductName, 
       @UnitPrice = UnitPrice 
FROM Products 
WHERE ProductID = @ProductID
GO

SqlDataReader を使用して単一行を取得する方法

SqlDataReader オブジェクトを使用して単一行、特に返されたデータ ストリームから必要な列値を取得できます。以下のコードは、これを例示しています。

void GetProductDetailsUsingReader( int ProductID, 
                        out string ProductName, out decimal UnitPrice )
{
  SqlConnection conn = new SqlConnection(
         "server=(local);Integrated Security=SSPI;database=Northwind");

  // ストアド プロシージャの実行に使用するコマンド オブジェクトを設定します
  SqlCommand cmd = new SqlCommand( "DATGetProductDetailsReader", conn );
  cmd.CommandType = CommandType.StoredProcedure;
  // ストアド プロシージャのパラメータを確立します
  //  @ProductID int INPUT

  SqlParameter paramProdID = cmd.Parameters.Add( "@ProductID", ProductID );
  paramProdID.Direction = ParameterDirection.Input;
  try
  {
    conn.Open();
    SqlDataReader reader = cmd.ExecuteReader();
    reader.Read(); // 1 行だけ読み読み取ります

    // 返されるデータ ストリームから出力パラメータを返します
    ProductName = reader.GetString(0);
    UnitPrice = reader.GetDecimal(1);
    reader.Close();
  }
  catch
  {
    throw;
  }
  finally
  {
    conn.Close();
  }
}

SqlDataReader オブジェクトを使って単一行を返すには、以下の手順に従います。

  1. SqlCommand オブジェクトを確立します。
  2. 接続を開きます。
  3. SqlDataReader オブジェクトの ExecuteReader メソッドを呼び出します。
  4. SqlDataReader オブジェクトの型指定するアクセサ メソッド、この場合は GetStringGetDecimal を使って出力パラメータを取得します。

上記のコードは、以下のストアド プロシージャを呼び出します。

CREATE PROCEDURE DATGetProductDetailsReader
@ProductID int
AS
SELECT ProductName, UnitPrice FROM Products
WHERE ProductID = @ProductID
GO

ExecuteScalar を使用して単一の項目を取得する方法

ExecuteScalar メソッドは、単一の値だけを返すクエリ用にデザインされています。クエリが複数列や複数行を返す場合は、 ExecuteScalar は先頭行の先頭列だけを返します。

以下のコードは、特定の製品 ID の製品名を照合する方法を示しています。

void GetProductNameExecuteScalar( int ProductID, out string ProductName )
{
  SqlConnection conn = new SqlConnection(
         "server=(local);Integrated Security=SSPI;database=northwind");
  SqlCommand cmd = new SqlCommand("LookupProductNameScalar", conn );
  cmd.CommandType = CommandType.StoredProcedure;

  cmd.Parameters.Add("@ProductID", ProductID );
  try
  {
    conn.Open();
    ProductName = (string)cmd.ExecuteScalar();
  }
  catch
  {
    throw;
  }
  finally
  {
    conn.Close();
  }
}

ExecuteScalar を使用して単一の項目を取得する方法は、以下のとおりです。

  1. SqlCommand オブジェクトを確立し、ストアド プロシージャを呼び出します。
  2. 接続を開きます。
  3. ExecuteScalar メソッドを呼び出します。このメソッドはオブジェクト型を返すことに注意してください。これは、取得した最初の列の値を保持するので、適切な型にキャストする必要があります。
  4. 接続を閉じます。

上記のコードは、以下のストアド プロシージャを使用します。

CREATE PROCEDURE LookupProductNameScalar
@ProductID int
AS
SELECT TOP 1 ProductName
FROM Products
WHERE ProductID = @ProductID
GO

ストアド プロシージャの出力を使用するかパラメータを返して、単一の項目を取得する方法

ストアド プロシージャの出力を使用するか、パラメータを返すことによって、単一値を照合できます。以下のコードは、出力パラメータの使用方法を例示しています。

void GetProductNameUsingSPOutput( int ProductID, out string ProductName )
{
  SqlConnection conn = new SqlConnection(
        "server=(local);Integrated Security=SSPI;database=northwind");
  SqlCommand cmd = new SqlCommand("LookupProductNameSPOutput", conn );
  cmd.CommandType = CommandType.StoredProcedure;

  SqlParameter paramProdID = cmd.Parameters.Add("@ProductID", ProductID );
  ParamProdID.Direction = ParameterDirection.Input;
  SqlParameter paramPN = 
         cmd.Parameters.Add("@ProductName", SqlDbType.VarChar, 40 );
  paramPN.Direction = ParameterDirection.Output;
  try
  {
    conn.Open();
    cmd.ExecuteNonQuery();
    ProductName = paramPN.Value.ToString();  
  }
  catch
  {
    throw;
  }
  finally
  {
    conn.Close();
  }
}

ストアド プロシージャの出力パラメータを使用して単一の値を取得するには、以下の手順に従います。

  1. SqlCommand オブジェクトを確立し、ストアド プロシージャを呼び出します。
  2. SqlParametersSqlCommandParameters コレクションに追加することによって、すべての入力パラメータと 1 つの出力パラメータを設定します。
  3. 接続を開きます。
  4. SqlCommand オブジェクトの ExecuteNonQuery メソッドを呼び出します。
  5. 接続を閉じます。
  6. 出力 SqlParameterValue プロパティを使用して、出力値を取得します。

上記のコードは、以下のストアド プロシージャを使用します。

CREATE PROCEDURE LookupProductNameSPOutput 
@ProductID int,
@ProductName nvarchar(40) OUTPUT
AS
SELECT @ProductName = ProductName
FROM Products
WHERE ProductID = @ProductID
GO

以下のコードは、特定の行が存在するかどうかを示す戻り値の使用を例示しています。コーディングの観点からは、 SqlParameter の方向を ParameterDirection.ReturnValue に明示的に設定する必要があることを除いて、ストアド プロシージャの出力パラメータを使用する場合に似ています。

bool CheckProduct( int ProductID )
{
  SqlConnection conn = new SqlConnection(
       "server=(local);Integrated Security=SSPI;database=northwind");
  SqlCommand cmd = new SqlCommand("CheckProductSP", conn );
  cmd.CommandType = CommandType.StoredProcedure;

  cmd.Parameters.Add("@ProductID", ProductID );
  SqlParameter paramRet = 
         cmd.Parameters.Add("@ProductExists", SqlDbType.Int );
  paramRet.Direction = ParameterDirection.ReturnValue;
  try
  {
    conn.Open();
    cmd.ExecuteNonQuery();
  }
  catch
  {
    throw;
  }
  finally
  {
    conn.Close();
  }
  return (int)paramRet.Value == 1;
}

特定の行が存在するかどうかを、ストアド プロシージャの戻り値を使用して確認するには、以下の手順に従います。

  1. SqlCommand オブジェクトを確立し、ストアド プロシージャを呼び出します。
  2. アクセスされる行の主キー値を含む入力パラメータを設定します。
  3. 1 つの戻り値のパラメータを設定します。 SqlParameter オブジェクトを SqlCommandParameters コレクションに追加し、その方向を ParameterDirection.ReturnValue に設定します。
  4. 接続を開きます。
  5. SqlCommand オブジェクトの ExecuteNonQuery メソッドを呼び出します。
  6. 接続を閉じます。
  7. 戻り値 SqlParameterValue プロパティを使用して、戻り値を取得します。

上記のコードは、以下のストアド プロシージャを使用します。

CREATE PROCEDURE CheckProductSP 
@ProductID int
AS
IF EXISTS( SELECT ProductID
           FROM Products
           WHERE ProductID = @ProductID )
  return 1
ELSE
  return 0
GO

SqlDataReader を使用して単一の項目を取得する方法

コマンド オブジェクトの ExecuteReader メソッドを呼び出すことによって、 SqlDataReader オブジェクトを使用して、単一の出力値を取得できます。これは、SqlDataReader Read メソッドを呼び出た後、必要な値をリーダーのアクセサ メソッドを使って取得するので、やや多くのコードを必要とします。以下のコードは、SqlDataReader オブジェクトの使用方法を例示しています。

bool CheckProductWithReader( int ProductID )
{
  SqlConnection conn = new SqlConnection(
         "server=(local);Integrated Security=SSPI;database=northwind");
  SqlCommand cmd = new SqlCommand("CheckProductExistsWithCount", conn );
  cmd.CommandType = CommandType.StoredProcedure;

  cmd.Parameters.Add("@ProductID", ProductID );
  cmd.Parameters["@ProductID"].Direction = ParameterDirection.Input;
  try
  {
    conn.Open();
    SqlDataReader reader = cmd.ExecuteReader(
                                CommandBehavior.SingleResult );
    reader.Read();

    bool bRecordExists = reader.GetInt32(0) > 0;
    reader.Close();
    return bRecordExists;
  }
  catch
  {
    throw;
  }
  finally
  {
    conn.Close(); 
  }

}

上記のコードは、以下のストアド プロシージャを使用します。

CREATE PROCEDURE CheckProductExistsWithCount 
@ProductID int
AS
SELECT COUNT(*) FROM Products
WHERE ProductID = @ProductID
GO

ADO.NET の手動トランザクションをコーディングする方法

以下のコードは、トランザクションで資金移動操作を保護するために、 SQL Server .NET データ プロバイダが提供するトランザクション サポートを利用する方法を示しています。この操作は、同一のデータベースに存在する 2 つのアカウント間でお金を転送します。

public void TransferMoney( string toAccount, string fromAccount, decimal amount )
{
 using ( SqlConnection conn = new SqlConnection(
 "server=(local);Integrated Security=SSPI;database=SimpleBank" ) )
  {
    SqlCommand cmdCredit = new SqlCommand("Credit", conn );
    cmdCredit.CommandType = CommandType.StoredProcedure;
    cmdCredit.Parameters.Add( new SqlParameter("@AccountNo", toAccount) );
    cmdCredit.Parameters.Add( new SqlParameter("@Amount", amount ));

    SqlCommand cmdDebit = new SqlCommand("Debit", conn );
    cmdDebit.CommandType = CommandType.StoredProcedure;
    cmdDebit.Parameters.Add( new SqlParameter("@AccountNo", fromAccount) );
    cmdDebit.Parameters.Add( new SqlParameter("@Amount", amount ));

    conn.Open();
    // 新しいトランザクションを開始します
    using ( SqlTransaction trans = conn.BeginTransaction() )
    {
      // 2 つのコマンド オブジェクトを同一のトランザクションに関連付けます
      cmdCredit.Transaction = trans;
      cmdDebit.Transaction = trans;
      try
      {
        cmdCredit.ExecuteNonQuery();
        cmdDebit.ExecuteNonQuery();
        // 両方のコマンド (credit と debit) は成功しました
        trans.Commit();
      }
      catch( Exception ex )
      {
        // トランザクションが失敗しました
        trans.Rollback();
        // 例外の詳細を記録します . . .
        throw ex;
      }
    }
  }
}

Transact-SQL を使用してトランザクションを実行する方法

以下のストアド プロシージャは、 Transact-SQL ストアド プロシージャ内でトランザクション化された資金移動操作を実行する方法を例示しています。

CREATE PROCEDURE MoneyTransfer
@FromAccount char(20),
@ToAccount char(20),
@Amount money
AS
BEGIN TRANSACTION
-- PERFORM DEBIT OPERATION
UPDATE Accounts
SET Balance = Balance - @Amount
WHERE AccountNumber = @FromAccount
IF @@RowCount = 0
BEGIN
  RAISERROR('Invalid From Account Number', 11, 1)
  GOTO ABORT
END
DECLARE @Balance money
SELECT @Balance = Balance FROM ACCOUNTS
WHERE AccountNumber = @FromAccount
IF @BALANCE < 0
BEGIN
  RAISERROR('Insufficient funds', 11, 1)
  GOTO ABORT
END
-- PERFORM CREDIT OPERATION
UPDATE Accounts 
SET Balance = Balance + @Amount 
WHERE AccountNumber = @ToAccount
IF @@RowCount = 0
BEGIN
  RAISERROR('Invalid To Account Number', 11, 1)
  GOTO ABORT
END
COMMIT TRANSACTION
RETURN 0
ABORT:
  ROLLBACK TRANSACTION
GO

このストアド プロシージャは、 BEGIN TRANSACTIONCOMMIT TRANSACTION、および ROLLBACK TRANSACTION ステートメントを使用して、トランザクションを手動で制御します。

トランザクション .NET クラスをコーディングする方法

以下のサンプル コードは、自動トランザクション用に構成されている 3 つのサービス .NET マネージ クラスを示しています。各クラスは、Transaction 属性に注釈を付けています。 Transaction 属性は、新しいトランザクション ストリームが開始される必要があるかどうか、またはオブジェクトがその中間の呼び出し元のストリームを共有する必要があるかどうかを判断する値を持ちます。これらのコンポーネントは、銀行資金移動を実行するために組み合わせて機能します。 Transfer クラスは、 RequiresNew トランザクション属性で構成されており、 Debit クラスと Credit クラスは Required トランザクション属性で構成されます。その結果、実行時に 3 つのオブジェクトはすべて同一のトランザクションを共有します。

using System;
using System.EnterpriseServices;

[Transaction(TransactionOption.RequiresNew)]
public class Transfer : ServicedComponent
{
  [AutoComplete]
  public void Transfer( string toAccount, 
                        string fromAccount, decimal amount )
  {
    try
    {
      // 借方操作を実行します
      Debit debit = new Debit();
      debit.DebitAccount( fromAccount, amount );
      // 貸方操作を実行します
      Credit credit = new Credit();
      credit.CreditAccount( toAccount, amount );
    }
    catch( SqlException sqlex )
    {
      // 例外の詳細を処理および記録します .
      // 例外をラップおよび反映します
      throw new TransferException( "Transfer Failure", sqlex );    
    }
  }
}
[Transaction(TransactionOption.Required)]
public class Credit : ServicedComponent
{
  [AutoComplete]
  public void CreditAccount( string account, decimal amount )
  {
    SqlConnection conn = new SqlConnection(
            "Server=(local); Integrated Security=SSPI"; database="SimpleBank");
    SqlCommand cmd = new SqlCommand("Credit", conn );
    cmd.CommandType = CommandType.StoredProcedure;
    cmd.Parameters.Add( new SqlParameter("@AccountNo", account) );
    cmd.Parameters.Add( new SqlParameter("@Amount", amount ));
    try
    {
      conn.Open();
      cmd.ExecuteNonQuery();
    }
    catch (SqlException sqlex)
    {
      // ここで例外の詳細を記録します .
      throw; // 例外を反映します
    }
  }
}
[Transaction(TransactionOption.Required)]
public class Debit : ServicedComponent
{
  public void DebitAccount( string account, decimal amount )
  {
    SqlConnection conn = new SqlConnection(
            "Server=(local); Integrated Security=SSPI"; database="SimpleBank");
    SqlCommand cmd = new SqlCommand("Debit", conn );
    cmd.CommandType = CommandType.StoredProcedure;
    cmd.Parameters.Add( new SqlParameter("@AccountNo", account) );
    cmd.Parameters.Add( new SqlParameter("@Amount", amount ));
    try
    {
      conn.Open();
      cmd.ExecuteNonQuery();
    }
    catch (SqlException sqlex)
    {
      // ここで例外の詳細を記録します .
      throw; // 例外を呼び出し元に反映し直します
    }
  }
}

協力者

原稿の寄稿とレビューを行ってくれた以下の協力者に感謝します。

Bill Vaughn、Mike Pizzo、Doug Rothaus、Kevin White、Blaine Dokter、David Schleifer、Graeme Malcolm (Content Master)、Bernard Chen (Sapient)、Matt Drucker (Turner Broadcasting)、および Steve Kirk。

質問やコメントや提案がある方がいらっしゃいますか? このドキュメントの内容に関するフィードバックについては、 devfdbck@microsoft.com に電子メールでご連絡ください。

.NET の能力を学習し、役に立てたいと思いますか ? 開発の推奨事例を学習するために、 Microsoft Technology Centers の技術専門家といっしょに作業してください。詳細情報については、 https://www.microsoft.com/business/services/mtc.asp を参照してください。