トランザクション内のデータベース変更をラップする (C#)

作成者: Scott Mitchell

PDF のダウンロード

このチュートリアルは、データのバッチの更新、削除、挿入について確認する 4 つのチュートリアルのうち最初のものです。 このチュートリアルでは、データベース トランザクションを使用してバッチ変更をアトミック操作として実行する方法について説明します。この方法により、すべてのステップが成功するか、すべてのステップが失敗することが保証されます。

はじめに

データの挿入、更新、削除の概要」チュートリアルから始めて確認したように、GridView には行レベルの編集と削除が組み込まれています。 行単位で編集と削除を行うコンテンツであれば、コード行を記述することなく、マウスを数回クリックするだけで、豊富なデータ変更インターフェイスを作成できます。 ただし、特定のシナリオでは、これでは不十分であるため、レコードのバッチを編集または削除する機能をユーザーに提供する必要があります。

たとえば、ほとんどの Web ベースのメール クライアントでは、各行にメールの情報 (件名、送信者など) と共にチェックボックスが含まれる各メッセージを一覧表示するために、グリッドを使用します。 このインターフェイスを使用すると、ユーザーは複数のメッセージをチェックし、[選択したメッセージの削除] ボタンをクリックすることで、これらのメッセージを削除できます。 バッチ編集インターフェイスは、ユーザーが一般的に多くの異なるレコードを編集する状況に最適です。 [編集] をクリックし、変更を行ない、変更する必要があるレコードごとに [更新] をクリックすることをユーザーに強制するのではなく、バッチ編集インターフェイスにより、その編集インターフェイスを使用して各行がレンダリングされます。 ユーザーは、変更する必要がある一連の行をすばやく変更し、[すべて更新] ボタンをクリックしてこれらの変更を保存できます。 この一連のチュートリアルでは、データのバッチを挿入、編集、および削除するためのインターフェイスを作成する方法について説明します。

バッチ操作を実行するときは、バッチ内の一部の操作が成功する一方、他の操作が失敗することが可能であるどうかを判断することが重要です。 インターフェイスを削除するバッチについて考えてみましょう。最初に選択したレコードが正常に削除されたが、たとえば、外部キー制約違反が原因で 2 つ目のレコードが失敗した場合はどうなるでしょうか? 最初のレコードの削除をロールバックすべきですか、または最初のレコードを削除することが許容されますか?

バッチ操作をアトミック操作 (すべてのステップが成功するか、すべてのステップが失敗する操作) として扱う場合は、データベース トランザクションのサポートが含まれるようにデータ アクセス層を拡張する必要があります。 データベース トランザクションは、トランザクションの傘下で実行される一連の INSERTUPDATEDELETE ステートメントのアトミック性を保証し、ほとんどの最新のデータベース システムでサポートされる機能です。

このチュートリアルでは、DAL を拡張してデータベース トランザクションを使用する方法について説明します。 以降のチュートリアルでは、インターフェイスのバッチの挿入、更新、削除のための Web ページの実装について説明します。 では、始めましょう。

Note

バッチ トランザクション内のデータを変更する場合、アトミック性が常に必要であるとは限りません。 一部のシナリオでは、Web ベースのメール クライアントから一連のメールを削除する場合など、一部のデータ変更が成功し、同じバッチ内のその他の変更が失敗することが許容される場合があります。 削除プロセスの途中でデータベース エラーが発生した場合、エラーなしで処理されたレコードが削除されたまま残ることが許容される可能性が高いです。 このような場合、データベース トランザクションをサポートするために DAL を変更する必要はありません。 ただし、アトミック性が不可欠であるその他のバッチ操作シナリオがあります。 顧客が 1 つの銀行口座から別の銀行口座に資金を移動する場合は、2 つの操作を実行する必要があります。すなわち、資金を最初の口座から差し引き、次に 2 番目の口座に追加する必要があります。 銀行は最初のステップが成功したが 2 番目のステップが失敗しても気にしないかもしれませんが、顧客は当然ながら狼狽します。 次の 3 つのチュートリアルで構築するインターフェイスのバッチの挿入、更新、削除でデータベース トランザクションを使用することを計画していない場合でも、このチュートリアルを実行し、データベース トランザクションをサポートするための DAL の機能強化を実装することをお勧めします。

トランザクションの概要

ほとんどのデータベースにはトランザクションのサポートが含まれています。これにより、複数のデータベース コマンドを 1 つの作業論理ユニットにグループ化できます。 トランザクションを構成するデータベース コマンドはアトミックであること、つまり、すべてのコマンドが失敗するか、すべてのコマンドが成功することが保証されています。

一般に、トランザクションは、次のパターンを使用して SQL ステートメントを介して実装されます。

  1. トランザクションの開始点を示します。
  2. トランザクションを構成する SQL ステートメントを実行します。
  3. ステップ 2 のステートメントのいずれかでエラーが発生した場合は、トランザクションをロールバックします。
  4. ステップ 2 のすべてのステートメントがエラーなしで完了した場合は、トランザクションをコミットします。

トランザクションの作成、コミット、ロールバックに使用する SQL ステートメントは、SQL スクリプトの記述時またはストアド プロシージャの作成時に手動で入力するか、ADO.NET または System.Transactions 名前空間内のクラスを使用するプログラムによる方法を介して入力できます。 このチュートリアルでは、ADO.NET を使用したトランザクションの管理のみについて説明します。 今後のチュートリアルでは、データ アクセス層でストアド プロシージャを使用する方法について説明します。その際、トランザクションを作成、ロールバック、コミットするための SQL ステートメントについて説明します。

Note

System.Transactions 名前空間の TransactionScopeクラスを使用すると、開発者は、トランザクションのスコープ内で一連のステートメントをプログラムでラップできます。このクラスには、2 つの異なるデータベースや、さらには Microsoft SQL Server データベース、Oracle データベース、Web サービスなどの異種データ ストアなど、複数のソースが含まれる複雑なトランザクションに対するサポートが含まれます。 このチュートリアルでは、TransactionScope クラスの代わりに ADO.NET トランザクションを使用することに決めました。なぜなら、ADO.NET はデータベース トランザクションに対してより具体的で明確であり、多くの場合、リソースの負荷がはるかに少ないからです。 また、特定のシナリオでは、TransactionScope クラスは Microsoft 分散トランザクション コーディネーター (MSDTC) を使用します。 MSDTC に関する構成、実装、パフォーマンスの問題のため、トピックはこれらのチュートリアルの範囲を超えて、かなり特殊で高度な内容になっています。

ADO.NET で SqlClient プロバイダーを操作する場合、トランザクションは、SqlConnection クラスBeginTransaction メソッドに対する呼び出しを介して開始され、これにより、SqlTransaction オブジェクトが返されます。 トランザクションを構成するデータ変更ステートメントは、try...catch ブロック内に配置されます。 try ブロック内のステートメントでエラーが発生した場合、実行は、SqlTransaction オブジェクトの Rollback メソッドを使用してトランザクションをロールバックできる catch ブロックに転送されます。 すべてのステートメントが正常に完了すると、try ブロックの末尾にある SqlTransaction オブジェクトの Commit メソッドに対する呼び出しにより、トランザクションがコミットされます。 次のコード スニペットは、このパターンを示しています。 「データベースとトランザクションとの整合性の維持」を参照してください。

// Create the SqlTransaction object
SqlTransaction myTransaction = SqlConnectionObject.BeginTransaction();
try
{
    /*
     * ... Perform the database transaction�s data modification statements...
     */
    // If we reach here, no errors, so commit the transaction
    myTransaction.Commit();
}
catch
{
    // If we reach here, there was an error, so rollback the transaction
    myTransaction.Rollback();
    throw;
}

既定では、型指定された DataSet 内の TableAdapter はトランザクションを使用しません。 トランザクションをサポートするには、TableAdapter クラスを拡張して、上記のパターンを使用してトランザクションのスコープ内で一連のデータ変更ステートメントを実行する追加のメソッドを含める必要があります。 ステップ 2 では、部分クラスを使用してこれらのメソッドを追加する方法について説明します。

ステップ 1: バッチ処理されたデータの操作に関する Web ページを作成する

DAL を拡張してデータベース トランザクションをサポートする方法を調べ始める前に、まず、少し時間を取って、このチュートリアルとその後に続く 3 つのチュートリアルに必要となる ASP.NET Web ページを作成してみましょう。 まず、BatchData という名前の新しいフォルダーを追加することから始めてから、次の ASP.NET ページを追加し、各ページを Site.master マスター ページに関連付けます。

  • Default.aspx
  • Transactions.aspx
  • BatchUpdate.aspx
  • BatchDelete.aspx
  • BatchInsert.aspx

Add the ASP.NET Pages for the SqlDataSource-Related Tutorials

図 1: SqlDataSource 関連のチュートリアルの ASP.NET ページを追加する

その他のフォルダーと同様に、Default.aspxSectionLevelTutorialListing.ascx ユーザー コントロールを使用して、セクション内のチュートリアルを一覧表示します。 そのため、このユーザー コントロールをソリューション エクスプローラーからページのデザイン ビューにドラッグして Default.aspx に追加します。

Add the SectionLevelTutorialListing.ascx User Control to Default.aspx

図 2: SectionLevelTutorialListing.ascx ユーザー コントロールを Default.aspx に追加する (クリックするとフルサイズの画像が表示されます)

最後に、これら 4 つのページをエントリとして Web.sitemap ファイルに追加します。 具体的には、サイト マップ <siteMapNode> をカスタマイズした後に次のマークアップを追加します。

<siteMapNode title="Working with Batched Data" 
    url="~/BatchData/Default.aspx" 
    description="Learn how to perform batch operations as opposed to 
                 per-row operations.">
    
    <siteMapNode title="Adding Support for Transactions" 
        url="~/BatchData/Transactions.aspx" 
        description="See how to extend the Data Access Layer to support 
                     database transactions." />
    <siteMapNode title="Batch Updating" 
        url="~/BatchData/BatchUpdate.aspx" 
        description="Build a batch updating interface, where each row in a 
                      GridView is editable." />
    <siteMapNode title="Batch Deleting" 
        url="~/BatchData/BatchDelete.aspx" 
        description="Explore how to create an interface for batch deleting 
                     by adding a CheckBox to each GridView row." />
    <siteMapNode title="Batch Inserting" 
        url="~/BatchData/BatchInsert.aspx" 
        description="Examine the steps needed to create a batch inserting 
                     interface, where multiple records can be created at the 
                     click of a button." />
</siteMapNode>

Web.sitemap を更新した後、少し時間を取って、ブラウザーを介してチュートリアル Web サイトを表示します。 左側のメニューに、バッチ処理されたデータの操作に関するチュートリアルの項目が含まれるようになりました。

The Site Map Now Includes Entries for the Working with Batched Data Tutorials

図 3: サイト マップに、バッチ処理されたデータの操作に関するチュートリアルのエントリが含まれるようになった

ステップ 2: データベース トランザクションをサポートするためにデータ アクセス層を更新する

最初のチュートリアル「データ アクセス層を作成する」で説明したように、DAL の型指定された DataSet は DataTable と TableAdapter で構成されています。 DataTable はデータを保持しますが、TableAdapter はデータベースから DataTable にデータを読み取る機能や、DataTable に加えられた変更を使用してデータベースを更新する機能などを提供します。 TableAdapter には、データを更新するために、バッチ更新と DB ダイレクトと呼ばれる 2 つのパターンが用意されていることを思い出してください。 バッチ更新パターンでは、TableAdapter に DataSet、DataTable、または DataRows のコレクションが渡されます。 このデータは列挙され、挿入、変更、削除される行ごとに InsertCommandUpdateCommandDeleteCommand が実行されます。 DB ダイレクト パターンの場合、代わりに TableAdapter に、単一レコードの挿入、更新、または削除に必要な列の値が渡されます。 その後、DB ダイレクト パターン メソッドは、渡されたこれらの値を使用して、適切な InsertCommandUpdateCommand、または DeleteCommand ステートメントを実行します。

使用される更新パターンに関係なく、TableAdapter 自動生成メソッドはトランザクションを使用しません。 既定では、TableAdapter によって実行される各挿入、更新、または削除は、単一の個別操作として扱われます。 たとえば、データベースに 10 個のレコードを挿入するために、BLL の一部のコードで DB ダイレクト パターンが使用されるとします。 このコードは、TableAdapter の Insert メソッドを 10 回呼び出します。 最初の 5 回の挿入が成功したが、6 回目の挿入で例外が発生した場合、最初に挿入された 5 個のレコードがデータベース内に残ります。 同様に、バッチ更新パターンを使用して DataTable 内の挿入、変更、削除済み行に対する挿入、更新、削除を実行した場合、最初のいくつかの変更が成功したが、後でその 1 つにエラーが発生した場合、完了した以前の変更はデータベース内に残ります。

特定のシナリオでは、一連の変更にわたってアトミック性を確保する必要があります。 これを実現するには、トランザクションの傘下で InsertCommandUpdateCommandDeleteCommand を実行する新しいメソッドを追加することで、TableAdapter を手動で拡張する必要があります。 「データ アクセス層を作成する」では、部分クラスを使用して、型指定された DataSet 内の DataTable の機能を拡張する方法を確認しました。 この手法は、TableAdapter でも使用できます。

型指定された DataSet Northwind.xsd は、App_Code フォルダーの DAL サブフォルダーにあります。 DAL フォルダー内に TransactionSupport という名前のサブフォルダーを作成し、ProductsTableAdapter.TransactionSupport.cs という名前の新しいクラス ファイルを追加します (図 4 を参照)。 このファイルには、トランザクションを使用してデータ変更を実行するためのメソッドが含まれる ProductsTableAdapter の部分的な実装が保持されます。

Add a Folder Named TransactionSupport and a Class File Named ProductsTableAdapter.TransactionSupport.cs

図 4:TransactionSupport という名前のフォルダーと ProductsTableAdapter.TransactionSupport.cs という名前のクラス ファイルを追加する

ProductsTableAdapter.TransactionSupport.cs ファイルに次のコードを入力します。

using System;
using System.Data;
using System.Data.SqlClient;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
namespace NorthwindTableAdapters
{
    public partial class ProductsTableAdapter
    {
        private SqlTransaction _transaction;
        private SqlTransaction Transaction
        {
            get
            {                
                return this._transaction;
            }
            set
            {
                this._transaction = value;
            }
        }
        public void BeginTransaction()
        {
            // Open the connection, if needed
            if (this.Connection.State != ConnectionState.Open)
                this.Connection.Open();
            // Create the transaction and assign it to the Transaction property
            this.Transaction = this.Connection.BeginTransaction();
            // Attach the transaction to the Adapters
            foreach (SqlCommand command in this.CommandCollection)
            {
                command.Transaction = this.Transaction;
            }
            this.Adapter.InsertCommand.Transaction = this.Transaction;
            this.Adapter.UpdateCommand.Transaction = this.Transaction;
            this.Adapter.DeleteCommand.Transaction = this.Transaction;
        }
        public void CommitTransaction()
        {
            // Commit the transaction
            this.Transaction.Commit();
            // Close the connection
            this.Connection.Close();
        }
        public void RollbackTransaction()
        {
            // Rollback the transaction
            this.Transaction.Rollback();
            // Close the connection
            this.Connection.Close();
        }
   }
}

ここでのクラス宣言の partial キーワードは、内部に追加されるメンバーが NorthwindTableAdapters 名前空間内の ProductsTableAdapter クラスに追加されることをコンパイラに示します。 ファイルの上部にある using System.Data.SqlClient ステートメントに注目してください。 TableAdapter は SqlClient プロバイダーを使用するように構成されているため、内部的には SqlDataAdapter オブジェクトを使用してデータベースに対するコマンドを発行します。 その結果、SqlTransaction クラスを使用してトランザクションを開始してから、これをコミットまたはロールバックする必要があります。 Microsoft SQL Server 以外のデータ ストアを使用している場合、適切なプロバイダーを使用する必要があります。

これらのメソッドは、トランザクションの開始、ロールバック、コミットに必要な構成要素を提供します。 これらは public としてマークされ、ProductsTableAdapter 内から、DAL 内の別のクラスから、またはアーキテクチャ内の別のレイヤー (BLL など) から使用できるようになります。 BeginTransaction は TableAdapter の内部 SqlConnection を開き (必要な場合)、トランザクションを開始し、これを Transaction プロパティに割り当て、トランザクションを内部の SqlDataAdapterSqlCommand オブジェクトにアタッチします。 CommitTransactionRollbackTransaction は、内部の Connection オブジェクトを閉じる前に、Transaction オブジェクトの Commit および Rollback メソッドをそれぞれ呼び出します。

ステップ 3: トランザクションの傘下にあるデータを更新および削除するメソッドを追加する

これらのメソッドが完了したら、トランザクションの傘下で一連のコマンドを実行するメソッドを ProductsDataTable または BLL に追加する準備ができます。 次のメソッドは、バッチ更新パターンを使用して、トランザクションを使用して ProductsDataTable インスタンスを更新します。 これは、BeginTransaction メソッドを呼び出してトランザクションを開始してから、try...catch ブロックを使用してデータ変更ステートメントを発行します。 Adapter オブジェクトの Update メソッドを呼び出した結果、例外が発生した場合、実行は catch ブロックに転送され、ここで、トランザクションはロールバックされ、例外は再スローされます。 Update メソッドは指定された ProductsDataTable の行を列挙し、必要な InsertCommandUpdateCommandDeleteCommand を実行することでバッチ更新パターンを実装することを思い出してください。 これらのコマンドのいずれかがエラーになった場合、トランザクションはロールバックされ、トランザクションの有効期間中に行われた以前の変更が元に戻されます。 Update ステートメントがエラーなしで完了すると、トランザクション全体がコミットされます。

public int UpdateWithTransaction(Northwind.ProductsDataTable dataTable)
{
    this.BeginTransaction();
    try
    {
        // Perform the update on the DataTable
        int returnValue = this.Adapter.Update(dataTable);
        // If we reach here, no errors, so commit the transaction
        this.CommitTransaction();
        return returnValue;
    }
    catch
    {
        // If we reach here, there was an error, so rollback the transaction
        this.RollbackTransaction();
        throw;
    }
}

ProductsTableAdapter.TransactionSupport.cs 内の部分クラスを使用して UpdateWithTransaction メソッドを ProductsTableAdapter クラスに追加します。 または、このメソッドをビジネス ロジック レイヤーの ProductsBLL クラスに追加し、構文的な変更をいくつか加えることもできます。 つまり、this.BeginTransaction()this.CommitTransaction()this.RollbackTransaction() 内のこのキーワードを Adapter に置き換える必要があります (Adapter は型 ProductsTableAdapterProductsBLL 内のプロパティの名前であることを思い出してください)。

UpdateWithTransaction メソッドはバッチ更新パターンを使用しますが、次のメソッドに示すように、一連の DB ダイレクト呼び出しをトランザクションのスコープ内で使用することもできます。 DeleteProductsWithTransaction メソッドは、型 intList<T> を入力として受け取ります。これは削除対象の ProductID です。 このメソッドは、BeginTransaction への呼び出しを介してトランザクションを開始し、try ブロック内で、提供されたリストを反復処理しながら ProductID 値ごとに DB ダイレクト パターンの Delete メソッドを呼び出します。 Delete への呼び出しのいずれかが失敗した場合、コントロールは catch ブロックに転送され、ここで、トランザクションはロールバックされ、例外は再スローされます。 Delete への呼び出しがすべて成功した場合、トランザクションはコミットされます。 このメソッドを ProductsBLL クラスに追加します。

public void DeleteProductsWithTransaction
    (System.Collections.Generic.List<int> productIDs)
{
    // Start the transaction
    Adapter.BeginTransaction();
    try
    {
        // Delete each product specified in the list
        foreach (int productID in productIDs)
        {
            Adapter.Delete(productID);
        }
        // Commit the transaction
        Adapter.CommitTransaction();
    }
    catch
    {
        // There was an error - rollback the transaction
        Adapter.RollbackTransaction();
        throw;
    }
}

複数の TableAdapter 全体にトランザクションを適用する

このチュートリアルで説明するトランザクション関連のコードでは、アトミック操作として扱う ProductsTableAdapter に対して複数のステートメントを使用できます。 しかし、異なるデータベース テーブルに対する複数の変更をアトミックに実行する必要がある場合はどうでしょうか? たとえば、カテゴリを削除するときに、最初に現在の製品をその他のカテゴリに再割り当てすることが必要な場合があります。 製品の再割り当てとカテゴリの削除の 2 つのステップは、アトミック操作として実行する必要があります。 ただし、ProductsTableAdapter には Products テーブルを変更するためのメソッドのみが含まれ、CategoriesTableAdapter には Categories テーブルを変更するためのメソッドのみが含まれます。 では、どうすれば 1 つのトランザクションで両方の TableAdapter を網羅できるでしょうか?

1 つのオプションでは、DeleteCategoryAndReassignProducts(categoryIDtoDelete, reassignToCategoryID) という名前の CategoriesTableAdapter にメソッドを追加し、そのメソッドにストアド プロシージャを呼び出させます。このストアド プロシージャは、このストアド プロシージャ内に定義されているトランザクションのスコープ内で、製品の再割り当てとカテゴリの削除の両方を実行します。 今後のチュートリアルでは、ストアド プロシージャ内でトランザクションを開始、コミット、ロールバックする方法について説明します。

もう 1 つのオプションでは、DeleteCategoryAndReassignProducts(categoryIDtoDelete, reassignToCategoryID) メソッドが含まれるヘルパー クラスを DAL に作成します。 このメソッドは、CategoriesTableAdapterProductsTableAdapter のインスタンスを作成し、これら 2 つの TableAdapter の Connection プロパティを同じ SqlConnection インスタンスに設定します。 その時点で、2 つの TableAdapter の 1 つが、BeginTransaction への呼び出しを使用してトランザクションを開始します。 製品の再割り当てとカテゴリの削除用の TableAdapter メソッドは、必要に応じてコミットまたはロールバックされたトランザクションを使用して try...catch ブロックで呼び出されます。

ステップ 4: ビジネス ロジック レイヤーに UpdateWithTransaction メソッドを追加する

ステップ 3 では、DAL 内の ProductsTableAdapterUpdateWithTransaction メソッドを追加しました。 BLL には、対応するメソッドを追加する必要があります。 プレゼンテーション レイヤーは下位の DAL を直接呼び出して UpdateWithTransaction メソッドを呼び出すことができますが、これらのチュートリアルでは、プレゼンテーション レイヤーから DAL を分離する複数レイヤー型アーキテクチャを定義しようと努めました。 したがって、このアプローチを続ける必要があります。

ProductsBLL クラス ファイルを開き、対応する下位の DAL メソッドを単純に呼び出す UpdateWithTransaction という名前のメソッドを追加します。 これで、ProductsBLL 内には、追加したばかりの UpdateWithTransaction とステップ 3 で追加した DeleteProductsWithTransaction の 2 つの新しいメソッドがあるはずです。

public int UpdateWithTransaction(Northwind.ProductsDataTable products)
{
    return Adapter.UpdateWithTransaction(products);
}
public void DeleteProductsWithTransaction
    (System.Collections.Generic.List<int> productIDs)
{
    // Start the transaction
    Adapter.BeginTransaction();
    try
    {
        // Delete each product specified in the list
        foreach (int productID in productIDs)
            Adapter.Delete(productID);
        // Commit the transaction
        Adapter.CommitTransaction();
    }
    catch
    {
        // There was an error - rollback the transaction
        Adapter.RollbackTransaction();
        throw;
    }
}

Note

これらのメソッドには、ProductsBLL クラス内のその他のほとんどのメソッドに割り当てられた DataObjectMethodAttribute 属性は含まれていません。なぜなら、これらのメソッドは ASP.NET ページの分離コード クラスから直接呼び出されるためです。 DataObjectMethodAttribute は、ObjectDataSource の [データ ソースの構成] ウィザード 内に、およびどのタブ (SELECT、UPDATE、INSERT、または DELETE) に、どのようなメソッドを表示すべきかに関するフラグを設定するために使用されることを思い出してください。 GridView にはバッチ編集または削除に対するサポートが組み込まれていないため、コード不要の宣言型アプローチを使用するのではなく、これらのメソッドをプログラムで呼び出す必要があります。

ステップ 5: プレゼンテーション レイヤーからデータベース データをアトミックに更新する

レコードのバッチを更新するときにトランザクションが与える影響を示すために、GridView 内のすべての製品を一覧表示し、クリックされたときに製品の CategoryID 値を再割り当てする Button Web コントロールが含まれるユーザー インターフェイスを作成しましょう。 特に、カテゴリの再割り当ては、最初の複数の製品に有効な CategoryID 値が割り当てられ、その他の製品には存在しない CategoryID 値が意図的に割り当てられるよう進行します。 CategoryID が既存のカテゴリの CategoryID と一致しない製品を使用してデータベースを更新しようとすると、外部キー制約違反が発生し、例外が発生します。 この例でわかることは、トランザクションを使用する場合、外部キー制約違反から発生した例外が原因で、以前の有効な CategoryID の変更がロールバックされるということです。 ただし、トランザクションを使用しない場合、初期カテゴリに対する変更は残ります。

BatchData フォルダー内の Transactions.aspx ページを開くことから始めて、ツールボックスからデザイナーに GridView をドラッグします。 その IDProducts に設定し、スマート タグから、ProductsDataSource という名前の新しい ObjectDataSource にバインドします。 ProductsBLL クラスの GetProducts メソッドからデータをプルするように ObjectDataSource を構成します。 これは読み取り専用の GridView になるため、[UPDATE]、[INSERT]、[DELETE] の各タブのドロップダウン リストを [(None)] に設定し、[完了] をクリックします。

Figure 5: Configure the ObjectDataSource to Use the ProductsBLL Class s GetProducts Method

図 5: ProductsBLL クラスの GetProducts メソッドを使用するように ObjectDataSource を構成する (クリックするとフルサイズの画像が表示されます)

Set the Drop-Down Lists in the UPDATE, INSERT, and DELETE Tabs to (None)

図 6: [UPDATE]、[INSERT]、[DELETE] の各タブのドロップダウン リストを [(None)] に設定する (クリックするとフルサイズの画像が表示されます)

[データ ソースの構成] ウィザードが完了すると、Visual Studio により、製品データ フィールドの BoundField と CheckBoxField が作成されます。 ProductIDProductNameCategoryIDCategoryName を除くこれらのフィールドをすべて削除し、ProductName および CategoryName BoundField の HeaderText プロパティの名前を Product と Category にそれぞれ変更します。 スマート タグで、[ページングを有効にする] オプションにチェックを付けます。 これらの変更を行った後、GridView と ObjectDataSource の宣言型マークアップは次のようになります。

<asp:GridView ID="Products" runat="server" AllowPaging="True" 
    AutoGenerateColumns="False" DataKeyNames="ProductID" 
    DataSourceID="ProductsDataSource">
    <Columns>
        <asp:BoundField DataField="ProductID" HeaderText="ProductID" 
            InsertVisible="False" ReadOnly="True" 
            SortExpression="ProductID" />
        <asp:BoundField DataField="ProductName" HeaderText="Product" 
            SortExpression="ProductName" />
        <asp:BoundField DataField="CategoryID" HeaderText="CategoryID" 
            SortExpression="CategoryID" />
        <asp:BoundField DataField="CategoryName" HeaderText="Category" 
            SortExpression="CategoryName" />
    </Columns>
</asp:GridView>
<asp:ObjectDataSource ID="ProductsDataSource" runat="server" 
    OldValuesParameterFormatString="original_{0}"
    SelectMethod="GetProducts" TypeName="ProductsBLL">
</asp:ObjectDataSource>

次に、GridView の上に 3 つの Button Web コントロールを追加します。 最初の Button の Text プロパティを Refresh Grid に設定し、2 番目を Modify Categories (WITH TRANSACTION) に設定し、3 番目を Modify Categories (WITHOUT TRANSACTION) に設定します。

<p>
    <asp:Button ID="RefreshGrid" runat="server" Text="Refresh Grid" />
</p>
<p>
    <asp:Button ID="ModifyCategoriesWithTransaction" runat="server"
        Text="Modify Categories (WITH TRANSACTION)" />
</p>
<p>
    <asp:Button ID="ModifyCategoriesWithoutTransaction" runat="server"
        Text="Modify Categories (WITHOUT TRANSACTION)" />
</p>

この時点で、Visual Studio のデザイン ビューは図 7 に示すスクリーン ショットのようになります。

The Page Contains a GridView and Three Button Web Controls

図 7: ページに GridView と 3 つの Button Web コントロールが含まれている (クリックするとフルサイズの画像が表示されます)

3 つのボタンの Click イベントのイベント ハンドラーを作成し、次のコードを使用します。

protected void RefreshGrid_Click(object sender, EventArgs e)
{
    Products.DataBind();
}
protected void ModifyCategoriesWithTransaction_Click(object sender, EventArgs e)
{
    // Get the set of products
    ProductsBLL productsAPI = new ProductsBLL();
    Northwind.ProductsDataTable products = productsAPI.GetProducts();
    // Update each product's CategoryID
    foreach (Northwind.ProductsRow product in products)
    {
        product.CategoryID = product.ProductID;
    }
    // Update the data using a transaction
    productsAPI.UpdateWithTransaction(products);
    // Refresh the Grid
    Products.DataBind();
}
protected void ModifyCategoriesWithoutTransaction_Click(object sender, EventArgs e)
{
    // Get the set of products
    ProductsBLL productsAPI = new ProductsBLL();
    Northwind.ProductsDataTable products = productsAPI.GetProducts();
    // Update each product's CategoryID
    foreach (Northwind.ProductsRow product in products)
    {
        product.CategoryID = product.ProductID;
    }
    // Update the data WITHOUT using a transaction
    NorthwindTableAdapters.ProductsTableAdapter productsAdapter = 
        new NorthwindTableAdapters.ProductsTableAdapter();
    productsAdapter.Update(products);
    // Refresh the Grid
    Products.DataBind();
}

更新用ボタンの Click イベント ハンドラーは、Products GridView の DataBind メソッドを呼び出すことで、単純にデータを GridView に再バインドします。

2 番目のイベント ハンドラーは、製品の CategoryID を再割り当てし、BLL の新しいトランザクション メソッドを使用して、トランザクションの傘下のデータベースの更新を実行します。 各製品の CategoryID はその ProductID と同じ値に恣意的に設定されることに注意してください。 これは最初のいくつかの製品に対して正常に動作します。なぜなら、これらの製品には、有効な CategoryID にマッピングされる ProductID 値があるからです。 しかし、ProductID が大きくなりすぎ始めると、ProductIDCategoryID のこの偶然の重複は適用されなくなります。

3 番目の Click イベント ハンドラーは、製品の CategoryID を同じ方法で更新しますが、ProductsTableAdapter の既定の Update メソッドを使用してデータベースに更新を送信します。 この Update メソッドはトランザクション内の一連のコマンドをラップしないため、最初に発生した外部キー制約違反エラーが永続化される前に、これらの変更が行われます。

この動作を実際に確認するには、ブラウザーを介してこのページにアクセスしてください。 最初に、図 8 に示すように、データの最初のページが表示されます。 次に、[Modify Categories (WITH TRANSACTION)] ボタンをクリックします。 これによりポストバックが発生し、すべての製品の CategoryID 値の更新が試みられますが、外部キー制約違反が発生します (図 9 を参照)。

The Products are Displayed in a Pageable GridView

図 8: 製品がページング可能な GridView に表示される (クリックするとフルサイズの画像が表示されます)

Reassigning the Categories Results in a Foreign Key Constraint Violation

図 9: カテゴリの結果を再割り当てすると、外部キー制約違反が発生する (クリックするとフルサイズの画像が表示されます)

ブラウザーの [戻る] ボタンをクリックし、[Refresh Grid] ボタンをクリックします。 データを更新すると、図 8 に示すものとまったく同じ出力が表示されます。 つまり、一部の製品の CategoryID が有効な値に変更され、データベースで更新された場合でも、外部キー制約違反が発生したときにこれらはロールバックされました。

ここで、[Modify Categories (WITHOUT TRANSACTION)] ボタンをクリックしてみてください。 この結果、同じ外部キー制約違反エラーが発生します (図 9 を参照)。ただし、今回は、CategoryID 値が有効な値に変更された製品はロールバックされません。 ブラウザーの [戻る] ボタンをクリックし、[Refresh Grid] ボタンをクリックしてください。 図 10 に示すように、最初の 8 個の製品の CategoryID が再割り当てされています。 たとえば、図 8 では、Chang の CategoryID は 1 でしたが、図 10 では 2 に再割り当てされています。

Some Products CategoryID Values were Updated While Others Were Not

図 10: 一部の製品の CategoryID 値は更新されたが、その他の製品では更新されなかった (クリックするとフルサイズの画像が表示されます)

まとめ

既定では、TableAdapter のメソッドは、トランザクションのスコープ内で実行されたデータベース ステートメントをラップしません。しかし、少しの作業で、トランザクションを作成、コミット、ロールバックするメソッドを追加できます。 このチュートリアルでは、このようなメソッドとして BeginTransactionCommitTransactionRollbackTransaction の 3 つのメソッドを ProductsTableAdapter クラス内に作成しました。 これらのメソッドを try...catch ブロックと共に使用して一連のデータ変更ステートメントをアトミックにする方法について説明しました。 特に、バッチ更新パターンを使用して指定された行に必要な変更を実行する UpdateWithTransaction メソッドを ProductsTableAdapter 内に作成しました。 また、BLL 内の ProductsBLL クラスに DeleteProductsWithTransaction メソッドを追加しました。このメソッドは、ProductID 値の List を入力として受け入れ、ProductID ごとに DB ダイレクト パターン メソッド Delete を呼び出します。 どちらのメソッドも最初に、トランザクションを作成してから try...catch ブロック内でデータ変更ステートメントを実行します。 例外が発生した場合、トランザクションはロールバックされ、それ以外の場合はコミットされます。

ステップ 5 では、トランザクション バッチ更新の影響と、トランザクションの使用を怠ったバッチ更新の影響を示しました。 次の 3 つのチュートリアルでは、このチュートリアルで築いた基盤に基づいて構築し、バッチ更新、削除、挿入を実行するためのユーザー インターフェイスを作成します。

プログラミングに満足!

もっと読む

この記事で説明したトピックの詳細については、次のリソースを参照してください。

著者について

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

特別な感謝

このチュートリアル シリーズは、多くの役に立つ校閲者によってレビューされました。 このチュートリアルのレビュー リーダーは、Dave Gardner、Hilton Giesenow、Teresa Murphy でした。 今後の MSDN の記事を確認することに関心がありますか? その場合は、 にmitchell@4GuysFromRolla.com行をドロップしてください。