ASP.NET 4 Web アプリケーションでの Entity Framework 4.0 を使用したコンカレンシーの処理

著者: Tom Dykstra

このチュートリアル シリーズは、Entity Framework 4.0 の概要チュートリアル シリーズで作成された Contoso University Web アプリケーションを基にして作られています。 前のチュートリアルを終えていない場合は、このチュートリアルの始めに、前のチュートリアルで作成するはずであったアプリケーションをダウンロードできます。 完全なチュートリアル シリーズで作成されるアプリケーションをダウンロードすることもできます。 チュートリアルに関する質問がある場合は、ASP.NET Entity Framework フォーラムに質問を投稿できます。

前のチュートリアルでは、ObjectDataSource コントロールと Entity Framework を使ってデータの並べ替えとフィルター処理を行う方法を学習しました。 このチュートリアルでは、Entity Framework を使う ASP.NET Web アプリケーションでコンカレンシーを処理するためのオプションを示します。 インストラクターのオフィスの割り当ての更新だけを行う新しい Web ページを作成します。 そのページと、前に作成した Departments ページでの、コンカレンシーの問題を処理します。

Image06

Image01

コンカレンシーの競合

コンカレンシーの競合は、最初のユーザーがレコードを編集し、そのユーザーの変更がデータベースに書き込まれる前に、別のユーザーが同じレコードを編集すると発生します。 そのような競合を検出するように Entity Framework を設定しないと、最後にデータベースを更新したユーザーが他のユーザーの変更を上書きします。 多くのアプリケーションでは、このリスクは許容できるものであり、コンカレンシーの競合の可能性を処理するようにアプリケーションを構成する必要はありません。 (ユーザーや更新が少ない場合、または一部の変更が上書きされても大きな問題でない場合は、コンカレンシーのプログラミングのコストが利点を上回る可能性があります。)コンカレンシーの競合について心配する必要がない場合は、このチュートリアルをスキップしてかまいません。シリーズの残りの 2 つのチュートリアルは、このチュートリアルで作成するものに依存しません。

ペシミスティック コンカレンシー (ロック)

コンカレンシーで偶発的にデータが失われる事態をアプリケーションで回避する必要があれば、その方法としてデータベース ロックがあります。 これはペシミスティック コンカレンシーと呼ばれています。 たとえば、データベースから行を読む前に、読み取り専用か更新アクセスでロックを要求します。 更新アクセスで行をロックすると、他のユーザーはその行を読み取り専用または更新アクセスでロックできなくなります。変更中のデータのコピーが与えられるためです。 読み取り専用で行をロックすると、他のユーザーはその行を読み取り専用でロックできますが、更新アクセスではロックできません。

ロックの管理には、いくつかの欠点があります。 プログラムが複雑になります。 データベース管理に大量のリソースが必要であり、アプリケーションのユーザー数が増えるにつれてパフォーマンスの問題が発生する可能性があります (つまり、スケーリングがうまくいきません)。 そのような理由から、一部のデータベース管理システムはペシミスティック コンカレンシーに対応していません。 Entity Framework にも組み込まれておらず、このチュートリアルでは実装方法を説明しません。

オプティミスティック コンカレンシー

ペシミスティック コンカレンシーの代わりとなるのがオプティミスティック コンカレンシーです。 オプティミスティック コンカレンシーでは、コンカレンシーの競合の発生を許し、発生したら適切に対処します。 たとえば、John が Department.aspx ページを実行し、History 学科の [Edit] リンクをクリックして、[Budget] の金額を $1,000,000.00 から $125,000.00 に減らします。 (John は競合する学科を管理し、自分の学科のためにお金を融通しようと考えます。)

Image07

John が [Update] をクリックする前に、Jane が同じページを実行し、History 学科の [Edit] リンクをクリックして、[Start Date] フィールドを 1/10/2011 から 1/1/1999 に変更します。 (ジェーンは History 学科を管理して、その継続期間を長くしようと考えています。)

Image08

John が先に [Update] をクリックし、その後で Jane が [更新] をクリックします。 現在、Jane のブラウザーの一覧では、[Budget] の金額が $1,000,000.00 と表示されていますが、John が金額が $125,000.00 に変更しているため、これは正しくありません。

このシナリオでは、次のようないくつかのアクションを実行できます。

  • ユーザーが変更したプロパティを追跡記録し、それに該当する列だけをデータベースで更新できます。 例のシナリオでは、2 人のユーザーが異なるプロパティを更新したため、データは失われません。 次に誰かが History 学科を閲覧すると、1/1/1999 と $125,000.00 が表示されます。

    これは Entity Framework の既定の動作であり、データ損失につながる可能性のある競合の数を大幅に減らすことができます。 ただし、この動作では、エンティティの同じプロパティに対して競合する変更が行われた場合は、データの損失を回避できません。 さらに、この動作は常に可能なわけではありません。ストアド プロシージャをエンティティ型にマップした場合、データベースでエンティティが変更されると、エンティティのすべてのプロパティが更新されます。

  • Jane の変更で John の変更を上書きするように指定できます。 Jane が [Update] をクリックすると、[Budget] の金額は $1,000,000.00 に戻ります。 これは Client Wins (クライアント側に合わせる) シナリオまたは Last in Wins (最終書き込み者優先) シナリオと呼ばれています。 (クライアントからの値がすべて、データ ストアの値より優先されます)

  • データベースで Jane の変更が更新されないようにできます。 通常は、エラー メッセージを表示し、データの現在の状態を伝えて、彼女がそれでもそれを行うことを望む場合は、変更を再入力できるようにします。 入力を保存し、入力し直さなくても再適用できるようにすると、プロセスをさらに自動化できます。 これは Store Wins (ストア側に合わせる) シナリオと呼ばれています。 (クライアントが送信した値よりデータストアの値が優先されます。)

コンカレンシーの競合の検出

Entity Framework では、Entity Framework がスローする OptimisticConcurrencyException 例外を処理して競合を解決できます。 このような例外がスローされるタイミングを認識する目的で、Entity Framework は競合を検出できなければなりません。 そのため、データベースとデータ モデルを適宜構成する必要があります。 競合検出を有効にするためのオプションには次のようなものがあります。

  • データベースに、行が変更されたことを判断するために使用できるテーブル列を追加します。 その後、Entity Framework を構成し、SQL の Update または Delete コマンドの Where 句にその列を追加できます。

    それが、OfficeAssignment テーブルの Timestamp 列の目的です。

    Image09

    Timestamp 列のデータ型も Timestamp という名前です。 ただし、列に実際に含まれるのは日付または時刻の値ではありません。 そうではなく、値は連続する番号であり、行が更新されるたびにインクリメントされます。 Update または Delete コマンドの Where 句には、元の Timestamp の値が含まれます。 更新対象の行が別のユーザーによって変更されている場合、Timestamp の値は元の値と異なるので、Where 句は更新する行を返しません Entity Framework は、現在の Update または Delete コマンドによって行が更新されていないことを検出すると (つまり、影響を受けた行の数が 0 のとき)、それをコンカレンシーの競合として解釈します。

  • Entity Framework を構成し、テーブルで Update コマンドと Delete コマンドの Where 句にすべての列の元の値を追加します。

    最初のオプションと同様に、行が最初に読み取られてから行に変更があった場合、Where 句は更新する行を返さず、Entity Framework はそれをコンカレンシーの競合として解釈します。 この方法は、Timestamp フィールドを使うのと同じくらい効果的ですが、効率的でない場合があります。 データベース テーブルに多くの列がある場合、結果的に Where 句が非常に多くなることがあり、Web アプリで大量の状態を保持することが必要になる可能性があります。 大量の状態を保持するには、サーバーのリソースが必要になったり (セッション状態など)、Web ページ自体に含めることが必要になったり (ビュー状態など) するため、アプリケーションのパフォーマンスが影響を受ける可能性があります。

このチュートリアルでは、追跡プロパティを持たないエンティティ (Department エンティティ) と、追跡プロパティを持つエンティティ (OfficeAssignment エンティティ) のオプティミスティック同時実行制御の競合に対するエラー処理を追加します。

追跡プロパティを使用しないオプティミスティック同時実行制御の処理

追跡 (Timestamp) プロパティを持たない Department エンティティに対するオプティミスティック同時実行制御を実装するには、次のタスクを行います。

  • データ モデルを変更して、Department エンティティのコンカレンシー追跡を有効にします。
  • SchoolRepository クラスの SaveChanges メソッドで、コンカレンシー例外を処理します。
  • Departments.aspx ページで、試みた変更が失敗したことをユーザーに示すメッセージを表示して、コンカレンシー例外を処理します。 その後、ユーザーは現在の値を確認し、それでも必要な場合は変更を再試行できます。

データ モデルでのコンカレンシー追跡の有効化

Visual Studio で、このシリーズの前のチュートリアルで作業した Contoso University Web アプリケーションを開きます。

SchoolModel.edmx を開き、データ モデル デザイナーで Department エンティティの Name プロパティを右クリックして、[プロパティ] をクリックします。 [プロパティ] ウィンドウで、ConcurrencyMode プロパティを Fixed に変更します。

Image16

他の主キーではないスカラー プロパティ (BudgetStartDateAdministrator) に対して同じことを行います。(ナビゲーション プロパティに対してこれを行うことはできません)。これは、データベース内の Department エンティティを更新する Update または Delete SQL コマンドを Entity Framework が生成するたびに、(元の値を含む) これらの列を Where 句に含める必要があることを指定します。 Update または Delete コマンドの実行時に行が見つからない場合、Entity Framework はオプティミスティック同時実行制御例外をスローします。

データ モデルを保存して閉じます。

DAL でのコンカレンシー例外の処理

SchoolRepository.cs を開き、System.Data 名前空間に対する次の using ステートメントを追加します。

using System.Data;

オプティミスティック同時実行制御例外を処理する次の新しい SaveChanges メソッドを追加します。

public void SaveChanges()
{
    try
    {
        context.SaveChanges();
    }
    catch (OptimisticConcurrencyException ocex)
    {
        context.Refresh(RefreshMode.StoreWins, ocex.StateEntries[0].Entity);
        throw ocex;
    }
}

このメソッドが呼び出されたときにコンカレンシー エラーが発生した場合、メモリ内のエンティティのプロパティ値は、データベース内の現在の値に置き換えられます。 Web ページで処理できるように、コンカレンシー例外が再スローされます。

DeleteDepartmentUpdateDepartment メソッドで、context.SaveChanges() の既存の呼び出しを SaveChanges() の呼び出しに置き換えて、新しいメソッドを呼び出すようにします。

プレゼンテーション層でのコンカレンシー例外の処理

Departments.aspx を開き、DepartmentsObjectDataSource コントロールに OnDeleted="DepartmentsObjectDataSource_Deleted" 属性を追加します。 コントロールの開始タグは、次の例のようになります。

<asp:ObjectDataSource ID="DepartmentsObjectDataSource" runat="server" 
        TypeName="ContosoUniversity.BLL.SchoolBL" DataObjectTypeName="ContosoUniversity.DAL.Department" 
        SelectMethod="GetDepartmentsByName" DeleteMethod="DeleteDepartment" UpdateMethod="UpdateDepartment"
        ConflictDetection="CompareAllValues" OldValuesParameterFormatString="orig{0}" 
        OnUpdated="DepartmentsObjectDataSource_Updated" SortParameterName="sortExpression" 
        OnDeleted="DepartmentsObjectDataSource_Deleted" >

次の例に示すように、DepartmentsGridView コントロールの DataKeyNames 属性ですべてのテーブル列を指定します。 これにより非常に大きなビュー状態フィールドが作成されることに注意してください。これは、コンカレンシーの競合を追跡する方法として一般に追跡フィールドの使用が推奨される理由の 1 つです。

<asp:GridView ID="DepartmentsGridView" runat="server" AutoGenerateColumns="False"
        DataSourceID="DepartmentsObjectDataSource" 
        DataKeyNames="DepartmentID,Name,Budget,StartDate,Administrator" 
        OnRowUpdating="DepartmentsGridView_RowUpdating"
        OnRowDataBound="DepartmentsGridView_RowDataBound"
        AllowSorting="True" >

Departments.aspx.cs を開き、System.Data 名前空間に対する次の using ステートメントを追加します。

using System.Data;

次の新しいメソッドを追加します。コンカレンシー例外を処理するため、データ ソース コントロールの UpdatedDeleted イベント ハンドラーからこれを呼び出します。

private void CheckForOptimisticConcurrencyException(ObjectDataSourceStatusEventArgs e, string function)
{
    if (e.Exception.InnerException is OptimisticConcurrencyException)
    {
        var concurrencyExceptionValidator = new CustomValidator();
        concurrencyExceptionValidator.IsValid = false;
        concurrencyExceptionValidator.ErrorMessage = 
            "The record you attempted to edit or delete was modified by another " +
            "user after you got the original value. The edit or delete operation was canceled " +
            "and the other user's values have been displayed so you can " +
            "determine whether you still want to edit or delete this record.";
        Page.Validators.Add(concurrencyExceptionValidator);
        e.ExceptionHandled = true;
    }
}

このコードは例外の種類をチェックし、コンカレンシー例外の場合は、ValidationSummary コントロールにメッセージを表示する CustomValidator コントロールを動的に作成します。

前に追加した Updated イベント ハンドラーから新しいメソッドを呼び出します。 さらに、同じメソッドを呼び出す新しい Deleted イベント ハンドラーを作成します (ただし、他には何も行いません)。

protected void DepartmentsObjectDataSource_Updated(object sender, ObjectDataSourceStatusEventArgs e)
{
    if (e.Exception != null)
    {
        CheckForOptimisticConcurrencyException(e, "update");
        // ...
    }
}

protected void DepartmentsObjectDataSource_Deleted(object sender, ObjectDataSourceStatusEventArgs e)
{
    if (e.Exception != null)
    {
        CheckForOptimisticConcurrencyException(e, "delete");
    }
}

Departments ページでのオプティミスティック同時実行制御のテスト

Departments.aspx ページを実行します。

A screenshot that shows the Departments page.

行の [Edit] をクリックして、[Budget] 列の値を変更します。 (既存の School データベースのレコードには無効なデータが含まれているため、このチュートリアル用に作成したレコードのみを編集できることに注意してください。Economics 学科のレコードは、実験しても安全です。)

Image18

新しいブラウザー ウィンドウを開いて、ページをもう一度実行します (最初のブラウザー ウィンドウのアドレス ボックスから 2 番目のブラウザー ウィンドウに URL をコピーします)。

A screenshot that shows a new browser window ready for input.

先ほど編集したのと同じ行の [Edit] をクリックして、[Budget] の値を別のものに変更します。

Image19

2 番目のブラウザー ウィンドウで、[Update] をクリックします。 [Budget] の金額がこの新しい値に正常に変更されます。

Image20

1 番目のブラウザー ウィンドウで、[Update] をクリックします。 更新は失敗します。 [Budget] の額は、2 番目のブラウザー ウィンドウで設定した値を使って再表示され、エラー メッセージが表示されます。

Image21

追跡プロパティを使用するオプティミスティック同時実行制御の処理

追跡プロパティを持つエンティティのオプティミスティック同時実行制御を処理するには、次のタスクを行います。

  • OfficeAssignment エンティティを管理するため、データ モデルにストアド プロシージャを追加します。 (追跡プロパティとストアド プロシージャを一緒に使う必要はありません。ここでは、説明のためだけにグループ化しています。)
  • DAL でのオプティミスティック同時実行制御例外を処理するコードを含む CRUD メソッドを、OfficeAssignment エンティティの DAL と BLL に追加します。
  • オフィス割り当て Web ページを作成します。
  • 新しい Web ページでオプティミスティック同時実行制御をテストします。

データ モデルへの OfficeAssignment ストアド プロシージャの追加

モデル デザイナーで SchoolModel.edmx ファイルを開き、デザイン サーフェイスを右クリックして、[データベースからモデルを更新] をクリックします。 [データベース オブジェクトの選択] ダイアログ ボックスの [追加] タブで、[ストアド プロシージャ] を展開し、3 つの OfficeAssignment ストアド プロシージャ (次のスクリーンショットを参照) を選んでから、[完了] をクリックします。 (これらのストアド プロシージャは、スクリプトを使ってデータベースをダウンロードまたは作成した時点で、そこに既に存在していました)。

Image02

OfficeAssignment エンティティを右クリックして、[ストアド プロシージャ マッピング] を選びます。

Image03

InsertUpdateDelete の各関数を、対応するストアド プロシージャを使うように設定します。 Update 関数の OrigTimestamp パラメーターで、[プロパティ]Timestamp に設定し、[元の値を使用する] オプションをオンにします。

Image04

Entity Framework は、UpdateOfficeAssignment ストアド プロシージャを呼び出すときに、Timestamp 列の元の値を OrigTimestamp パラメーターで渡します。 このストアド プロシージャは、Where 句でこのパラメーターを使います。

ALTER PROCEDURE [dbo].[UpdateOfficeAssignment]
    @InstructorID int,
    @Location nvarchar(50),
    @OrigTimestamp timestamp
    AS
    UPDATE OfficeAssignment SET Location=@Location 
    WHERE InstructorID=@InstructorID AND [Timestamp]=@OrigTimestamp;
    IF @@ROWCOUNT > 0
    BEGIN
        SELECT [Timestamp] FROM OfficeAssignment 
            WHERE InstructorID=@InstructorID;
    END

このストアド プロシージャは、Entity Framework がメモリ内の OfficeAssignment エンティティと対応するデータベース行の同期を維持できるよう、更新後の Timestamp 列の新しい値も選択します。

(オフィス割り当て削除用ストアド プロシージャには、OrigTimestamp パラメーターがないことに注意してください。このため、Entity Framework では、エンティティを削除する前にそれが変更されていないことを確認できません)。

データ モデルを保存して閉じます。

DAL への OfficeAssignment メソッドの追加

ISchoolRepository.cs を開き、OfficeAssignment エンティティ セットに次の CRUD メソッドを追加します。

IEnumerable<OfficeAssignment> GetOfficeAssignments(string sortExpression);
void InsertOfficeAssignment(OfficeAssignment OfficeAssignment);
void DeleteOfficeAssignment(OfficeAssignment OfficeAssignment);
void UpdateOfficeAssignment(OfficeAssignment OfficeAssignment, OfficeAssignment origOfficeAssignment);

次の新しいメソッドを SchoolRepository.cs に追加します。 UpdateOfficeAssignment メソッドでは、context.SaveChanges の代わりにローカルの SaveChanges メソッドを呼び出します。

public IEnumerable<OfficeAssignment> GetOfficeAssignments(string sortExpression)
{
    return new ObjectQuery<OfficeAssignment>("SELECT VALUE o FROM OfficeAssignments AS o", context).Include("Person").OrderBy("it." + sortExpression).ToList();
}

public void InsertOfficeAssignment(OfficeAssignment officeAssignment)
{
    context.OfficeAssignments.AddObject(officeAssignment);
    context.SaveChanges();
}

public void DeleteOfficeAssignment(OfficeAssignment officeAssignment)
{
    context.OfficeAssignments.Attach(officeAssignment);
    context.OfficeAssignments.DeleteObject(officeAssignment);
    context.SaveChanges();
}

public void UpdateOfficeAssignment(OfficeAssignment officeAssignment, OfficeAssignment origOfficeAssignment)
{
    context.OfficeAssignments.Attach(origOfficeAssignment);
    context.ApplyCurrentValues("OfficeAssignments", officeAssignment);
    SaveChanges();
}

テスト プロジェクトで MockSchoolRepository.cs を開き、次の OfficeAssignment コレクションと CRUD メソッドをそれに追加します。 (モック リポジトリでリポジトリ インターフェイスが実装されている必要があります。そうでないと、ソリューションはコンパイルされません。)

List<OfficeAssignment> officeAssignments = new List<OfficeAssignment>();
        
public IEnumerable<OfficeAssignment> GetOfficeAssignments(string sortExpression)
{
    return officeAssignments;
}

public void InsertOfficeAssignment(OfficeAssignment officeAssignment)
{
    officeAssignments.Add(officeAssignment);
}

public void DeleteOfficeAssignment(OfficeAssignment officeAssignment)
{
    officeAssignments.Remove(officeAssignment);
}

public void UpdateOfficeAssignment(OfficeAssignment officeAssignment, OfficeAssignment origOfficeAssignment)
{
    officeAssignments.Remove(origOfficeAssignment);
    officeAssignments.Add(officeAssignment);
}

BLL への OfficeAssignment メソッドの追加

メイン プロジェクトで SchoolBL.cs を開き、OfficeAssignment エンティティ セットの次の CRUD メソッドをそれに追加します。

public IEnumerable<OfficeAssignment> GetOfficeAssignments(string sortExpression)
{
    if (string.IsNullOrEmpty(sortExpression)) sortExpression = "Person.LastName";
    return schoolRepository.GetOfficeAssignments(sortExpression);
}

public void InsertOfficeAssignment(OfficeAssignment officeAssignment)
{
    try
    {
        schoolRepository.InsertOfficeAssignment(officeAssignment);
    }
    catch (Exception ex)
    {
        //Include catch blocks for specific exceptions first,
        //and handle or log the error as appropriate in each.
        //Include a generic catch block like this one last.
        throw ex;
    }
}

public void DeleteOfficeAssignment(OfficeAssignment officeAssignment)
{
    try
    {
        schoolRepository.DeleteOfficeAssignment(officeAssignment);
    }
    catch (Exception ex)
    {
        //Include catch blocks for specific exceptions first,
        //and handle or log the error as appropriate in each.
        //Include a generic catch block like this one last.
        throw ex;
    }
}

public void UpdateOfficeAssignment(OfficeAssignment officeAssignment, OfficeAssignment origOfficeAssignment)
{
    try
    {
        schoolRepository.UpdateOfficeAssignment(officeAssignment, origOfficeAssignment);
    }
    catch (Exception ex)
    {
        //Include catch blocks for specific exceptions first,
        //and handle or log the error as appropriate in each.
        //Include a generic catch block like this one last.
        throw ex;
    }
}

OfficeAssignments Web ページの作成

Site.Master マスター ページを使う新しい Web ページを作成して、OfficeAssignments.aspx という名前を付けます。 次のマークアップを Content2 という名前の Content コントロールに追加します。

<h2>Office Assignments</h2>
    <asp:ObjectDataSource ID="OfficeAssignmentsObjectDataSource" runat="server" TypeName="ContosoUniversity.BLL.SchoolBL"
        DataObjectTypeName="ContosoUniversity.DAL.OfficeAssignment" SelectMethod="GetOfficeAssignments"
        DeleteMethod="DeleteOfficeAssignment" UpdateMethod="UpdateOfficeAssignment" ConflictDetection="CompareAllValues"
        OldValuesParameterFormatString="orig{0}"
        SortParameterName="sortExpression"  OnUpdated="OfficeAssignmentsObjectDataSource_Updated">
    </asp:ObjectDataSource>
    <asp:ValidationSummary ID="OfficeAssignmentsValidationSummary" runat="server" ShowSummary="true"
        DisplayMode="BulletList" Style="color: Red; width: 40em;" />
    <asp:GridView ID="OfficeAssignmentsGridView" runat="server" AutoGenerateColumns="False"
        DataSourceID="OfficeAssignmentsObjectDataSource" DataKeyNames="InstructorID,Timestamp"
        AllowSorting="True">
        <Columns>
            <asp:CommandField ShowEditButton="True" ShowDeleteButton="True" ItemStyle-VerticalAlign="Top">
                <ItemStyle VerticalAlign="Top"></ItemStyle>
            </asp:CommandField>
            <asp:TemplateField HeaderText="Instructor" SortExpression="Person.LastName">
                <ItemTemplate>
                    <asp:Label ID="InstructorLastNameLabel" runat="server" Text='<%# Eval("Person.LastName") %>'></asp:Label>,
                    <asp:Label ID="InstructorFirstNameLabel" runat="server" Text='<%# Eval("Person.FirstMidName") %>'></asp:Label>
                </ItemTemplate>
            </asp:TemplateField>
            <asp:DynamicField DataField="Location" HeaderText="Location" SortExpression="Location"/>
        </Columns>
        <SelectedRowStyle BackColor="LightGray"></SelectedRowStyle>
    </asp:GridView>

DataKeyNames 属性のマークアップで、Timestamp プロパティとレコード キー (InstructorID) が指定されていることに注意してください。 DataKeyNames 属性でプロパティを指定すると、コントロールはそれをコントロール ステート (ビュー状態に似ています) に保存して、ポストバック処理の間に元の値を使用できるようにします。

Timestamp の値を保存しなかった場合、Entity Framework には SQL Update コマンドの Where 句に使うものがありません。 そのため、更新するものが何も見つかりません。 その結果、Entity Framework は、OfficeAssignment エンティティが更新されるたびにオプティミスティック同時実行制御例外をスローします。

OfficeAssignments.aspx.cs を開き、データ アクセス層に次の using ステートメントを追加します。

using ContosoUniversity.DAL;

動的データ機能を有効にする次の Page_Init メソッドを追加します。 また、コンカレンシー エラーを調べるため、ObjectDataSource コントロールの Updated イベントに次のハンドラーを追加します。

protected void Page_Init(object sender, EventArgs e)
{
    OfficeAssignmentsGridView.EnableDynamicData(typeof(OfficeAssignment));
}

protected void OfficeAssignmentsObjectDataSource_Updated(object sender, ObjectDataSourceStatusEventArgs e)
{
    if (e.Exception != null)
    {
        var concurrencyExceptionValidator = new CustomValidator();
        concurrencyExceptionValidator.IsValid = false;
        concurrencyExceptionValidator.ErrorMessage = "The record you attempted to " +
            "update has been modified by another user since you last visited this page. " +
            "Your update was canceled to allow you to review the other user's " +
            "changes and determine if you still want to update this record.";
        Page.Validators.Add(concurrencyExceptionValidator);
        e.ExceptionHandled = true;
    }
}

OfficeAssignments ページでのオプティミスティック同時実行制御のテスト

OfficeAssignments.aspx ページを実行します。

A screenshot that shows the Office Assignments page.

行の [Edit] をクリックして、[Location] 列の値を変更します。

Image11

新しいブラウザー ウィンドウを開いて、ページをもう一度実行します (最初のブラウザー ウィンドウから 2 番目のブラウザー ウィンドウに URL をコピーします)。

A screenshot that shows a new browser window.

先ほど編集したのと同じ行の [Edit] をクリックして、[Location] の値を別のものに変更します。

Image12

2 番目のブラウザー ウィンドウで、[Update] をクリックします。

Image13

最初のブラウザー ウィンドウに切り替えて、[Update] をクリックします。

Image15

エラー メッセージが表示され、[Location] の値は 2 番目のブラウザー ウィンドウでの変更後の値を示すように更新されています。

EntityDataSource コントロールでのコンカレンシーの処理

EntityDataSource コントロールに含まれる組み込みのロジックは、データ モデルでのコンカレンシーの設定を認識し、それに応じて更新と削除の操作を処理します。 ただし、どのような例外でも同じですが、ユーザーにわかりやすいエラー メッセージを提供するには、OptimisticConcurrencyException 例外を自分で処理する必要があります。

次に、コンカレンシーの競合が発生した場合に更新と削除の操作を許可し、エラー メッセージを表示するように、Courses.aspx ページを構成します (EntityDataSource コントロールを使用)。 Course エンティティにはコンカレンシー追跡列がないため、Department エンティティで行ったのと同じ方法を使って、キーではないすべてのプロパティの値を追跡します。

SchoolModel.edmx ファイルを開きます。 Course エンティティのキー以外のプロパティ (TitleCreditsDepartmentID) で、[コンカレンシー モード] プロパティを Fixed に設定します。 次に、データ モデルを保存して閉じます。

Courses.aspx ページを開き、次の変更を行います。

  • CoursesEntityDataSource コントロールで、EnableUpdate="true"EnableDelete="true" 属性を追加します。 コントロールの開始タグは、次の例のようになります。

    <asp:EntityDataSource ID="CoursesEntityDataSource" runat="server" 
            ContextTypeName="ContosoUniversity.DAL.SchoolEntities" EnableFlattening="false" 
            AutoGenerateWhereClause="True" EntitySetName="Courses"
            EnableUpdate="true" EnableDelete="true">
    
  • CoursesGridView コントロールで、DataKeyNames 属性の値を "CourseID,Title,Credits,DepartmentID" に変更します。 次に、[Edit][Delete] ボタンを表示する Columns 要素に CommandField 要素を追加します (<asp:CommandField ShowEditButton="True" ShowDeleteButton="True" />)。 GridView コントロールは次の例のようになります。

    <asp:GridView ID="CoursesGridView" runat="server" AutoGenerateColumns="False" 
            DataKeyNames="CourseID,Title,Credits,DepartmentID"
            DataSourceID="CoursesEntityDataSource" >
            <Columns>
                <asp:CommandField ShowEditButton="True" ShowDeleteButton="True" />
                <asp:BoundField DataField="CourseID" HeaderText="CourseID" ReadOnly="True" SortExpression="CourseID" />
                <asp:BoundField DataField="Title" HeaderText="Title" SortExpression="Title" />
                <asp:BoundField DataField="Credits" HeaderText="Credits" SortExpression="Credits" />
            </Columns>
        </asp:GridView>
    

ページを実行し、前に Departments ページで行ったようにして競合状態を作成します。 2 つのブラウザー ウィンドウでページを実行し、各ウィンドウで同じ行の [Edit] をクリックして、それぞれ異なる変更を行います。 一方のウィンドウで [Update] をクリックしてから、もう一方のウィンドウで [Update] をクリックします。 2 回目に [Update] をクリックすると、ハンドルされないコンカレンシー例外の結果として発生するエラー ページが表示されます。

Image22

このエラーを、ObjectDataSource コントロールでのそれの処理方法とよく似た方法で処理します。 Courses.aspx ページを開き、CoursesEntityDataSource コントロールで DeletedUpdated イベントのハンドラーを指定します。 コントロールの開始タグは、次の例のようになります。

<asp:EntityDataSource ID="CoursesEntityDataSource" runat="server" 
        ContextTypeName="ContosoUniversity.DAL.SchoolEntities" EnableFlattening="false"
        AutoGenerateWhereClause="true" EntitySetName="Courses" 
        EnableUpdate="true" EnableDelete="true" 
        OnDeleted="CoursesEntityDataSource_Deleted" 
        OnUpdated="CoursesEntityDataSource_Updated">

CoursesGridView コントロールの前に、次の ValidationSummary コントロールを追加します。

<asp:ValidationSummary ID="CoursesValidationSummary" runat="server" 
        ShowSummary="true" DisplayMode="BulletList"  />

Courses.aspx.cs で、System.Data 名前空間の using ステートメントを追加し、コンカレンシー例外をチェックするメソッドを追加して、EntityDataSource コントロールの UpdatedDeleted イベントーのハンドラーを追加します。 コードは次のようになります。

using System.Data;
protected void CoursesEntityDataSource_Updated(object sender, EntityDataSourceChangedEventArgs e)
{
    CheckForOptimisticConcurrencyException(e, "update");
}

protected void CoursesEntityDataSource_Deleted(object sender, EntityDataSourceChangedEventArgs e)
{
    CheckForOptimisticConcurrencyException(e, "delete");
}

private void CheckForOptimisticConcurrencyException(EntityDataSourceChangedEventArgs e, string function)
{
    if (e.Exception != null && e.Exception is OptimisticConcurrencyException)
    {
        var concurrencyExceptionValidator = new CustomValidator();
        concurrencyExceptionValidator.IsValid = false;
        concurrencyExceptionValidator.ErrorMessage = 
            "The record you attempted to edit or delete was modified by another " +
            "user after you got the original value. The edit or delete operation was canceled " +
            "and the other user's values have been displayed so you can " +
            "determine whether you still want to edit or delete this record.";
        Page.Validators.Add(concurrencyExceptionValidator);
        e.ExceptionHandled = true;
    }
}

このコードと、ObjectDataSource コントロールに対して行ったことの唯一の違いは、この場合のコンカレンシー例外は、その例外の InnerException プロパティではなく、イベント引数オブジェクトの Exception プロパティで発生するということです。

ページを実行し、コンカレンシーの競合をもう一度発生させます。 今回はエラー メッセージが表示されます。

Image23

コンカレンシーの競合処理の入門編はこれで終わりです。 次のチュートリアルでは、Entity Framework を使う Web アプリのパフォーマンスを向上させる方法に関するガイダンスを提供します。