コンカレンシーの競合の処理 (EF6)

オプティミスティック同時実行制御では、エンティティが読み込まれてからデータが変更されていないことを期待して、オプティミスティックに、データベースへのエンティティの保存を試みます。 データが変更されていることが判明すると、例外がスローされるので、再度保存を試みる前に競合を解決する必要があります。 このトピックでは、Entity Framework でこのような例外を処理する方法について説明します。 このトピックで紹介するテクニックは、Code First および EF Designer で作成されたモデルに等しく使用できます。

この投稿は、オプティミスティック同時実行制御の詳細の確認には適していません。 以下のセクションでは、コンカレンシーの解決についてある程度の知識があることを前提に、一般的なタスクのパターンを示しています。

これらのパターンの多くは、「プロパティ値の操作」で説明するトピックを利用しています。

独立した関連付けを使用している場合 (外部キーがエンティティのプロパティにマップされていない場合) のコンカレンシーの問題の解決は、外部キーの関連付けを使用している場合よりもはるかに難しくなります。 そのため、アプリケーションでコンカレンシーの解決を行う場合は、常に外部キーをエンティティにマップすることをお勧めします。 以下の例はすべて、外部キーの関連付けを使用していることを前提としています。

外部キーの関連付けを使用するエンティティを保存しようとしているときに、オプティミスティック同時実行制御の例外が検出されると、SaveChanges によって DbUpdateConcurrencyException がスローされます。

Reload を使用したオプティミスティック同時実行制御の例外の解決 (データベース優先)

Reload メソッドを使用して、エンティティの現在の値をデータベースの現在の値で上書きできます。 通常、エンティティはなんらかの形でユーザーに返されるので、ユーザーは再度変更を加えて再保存する必要があります。 次に例を示します。

using (var context = new BloggingContext())
{
    var blog = context.Blogs.Find(1);
    blog.Name = "The New ADO.NET Blog";

    bool saveFailed;
    do
    {
        saveFailed = false;

        try
        {
            context.SaveChanges();
        }
        catch (DbUpdateConcurrencyException ex)
        {
            saveFailed = true;

            // Update the values of the entity that failed to save from the store
            ex.Entries.Single().Reload();
        }

    } while (saveFailed);
}

コンカレンシー例外をシミュレートする場合は、SaveChanges 呼び出しにブレークポイントを設定し、SQL Server Management Studio などの別のツールを使用して、データベースに保存されるエンティティを変更することをお勧めします。 SaveChanges の前に、SqlCommand を使用してデータベースを直接更新する行を挿入することもできます。 次に例を示します。

context.Database.SqlCommand(
    "UPDATE dbo.Blogs SET Name = 'Another Name' WHERE BlogId = 1");

DbUpdateConcurrencyException の Entries メソッドは、更新に失敗したエンティティの DbEntityEntry インスタンスを返します。 (現在、このプロパティは、コンカレンシーの問題に対して常に単一の値を返します。一般的な更新例外に対しては複数の値を返す場合があります。)状況によっては、データベースからの再読み込みが必要になる可能性のあるすべてのエンティティのエントリを取得し、それぞれに対して再読み込みを呼び出すこともできます。

オプティミスティック同時実行制御の例外の解決 (クライアント優先)

Reload を使用する上記の例は、エンティティの値がデータベースの値で上書きされるため、データベース優先またはストア優先と呼ばれることもあります。 逆に、データベースの値をエンティティの現在の値で上書きしたい場合があります。 これはクライアント優先と呼ばれることもあり、データベースの現在の値を取得し、それをエンティティの元の値として設定することで実行できます (現在の値と元の値については、「プロパティ値の操作」を参照してください)。次に例を示します。

using (var context = new BloggingContext())
{
    var blog = context.Blogs.Find(1);
    blog.Name = "The New ADO.NET Blog";

    bool saveFailed;
    do
    {
        saveFailed = false;
        try
        {
            context.SaveChanges();
        }
        catch (DbUpdateConcurrencyException ex)
        {
            saveFailed = true;

            // Update original values from the database
            var entry = ex.Entries.Single();
            entry.OriginalValues.SetValues(entry.GetDatabaseValues());
        }

    } while (saveFailed);
}

オプティミスティック同時実行制御の例外のカスタム解決

データベースの現在の値をエンティティの現在の値と組み合わせることが必要な場合があります。 通常、これにはカスタム ロジックまたはユーザー操作が必要です。 たとえば、現在の値、データベースの値、解決済みの値の既定のセットを含むフォームをユーザーに提示できます。 その後、ユーザーは必要に応じて解決済みの値を編集します。これらの解決済みの値がデータベースに保存されます。 これは、エンティティのエントリの CurrentValues と GetDatabaseValues から返された DbPropertyValues オブジェクトを使用して実行できます。 次に例を示します。

using (var context = new BloggingContext())
{
    var blog = context.Blogs.Find(1);
    blog.Name = "The New ADO.NET Blog";

    bool saveFailed;
    do
    {
        saveFailed = false;
        try
        {
            context.SaveChanges();
        }
        catch (DbUpdateConcurrencyException ex)
        {
            saveFailed = true;

            // Get the current entity values and the values in the database
            var entry = ex.Entries.Single();
            var currentValues = entry.CurrentValues;
            var databaseValues = entry.GetDatabaseValues();

            // Choose an initial set of resolved values. In this case we
            // make the default be the values currently in the database.
            var resolvedValues = databaseValues.Clone();

            // Have the user choose what the resolved values should be
            HaveUserResolveConcurrency(currentValues, databaseValues, resolvedValues);

            // Update the original values with the database values and
            // the current values with whatever the user choose.
            entry.OriginalValues.SetValues(databaseValues);
            entry.CurrentValues.SetValues(resolvedValues);
        }
    } while (saveFailed);
}

public void HaveUserResolveConcurrency(DbPropertyValues currentValues,
                                       DbPropertyValues databaseValues,
                                       DbPropertyValues resolvedValues)
{
    // Show the current, database, and resolved values to the user and have
    // them edit the resolved values to get the correct resolution.
}

オブジェクトを使用したオプティミスティック同時実行制御の例外のカスタム解決

上記のコードでは、DbPropertyValues インスタンスを使用して、現在の値、データベースの値、解決済みの値を渡しています。 これにエンティティ型のインスタンスを使用する方が簡単な場合もあります。 これは、DbPropertyValues の ToObject および SetValues メソッドを使用して実行できます。 次に例を示します。

using (var context = new BloggingContext())
{
    var blog = context.Blogs.Find(1);
    blog.Name = "The New ADO.NET Blog";

    bool saveFailed;
    do
    {
        saveFailed = false;
        try
        {
            context.SaveChanges();
        }
        catch (DbUpdateConcurrencyException ex)
        {
            saveFailed = true;

            // Get the current entity values and the values in the database
            // as instances of the entity type
            var entry = ex.Entries.Single();
            var databaseValues = entry.GetDatabaseValues();
            var databaseValuesAsBlog = (Blog)databaseValues.ToObject();

            // Choose an initial set of resolved values. In this case we
            // make the default be the values currently in the database.
            var resolvedValuesAsBlog = (Blog)databaseValues.ToObject();

            // Have the user choose what the resolved values should be
            HaveUserResolveConcurrency((Blog)entry.Entity,
                                       databaseValuesAsBlog,
                                       resolvedValuesAsBlog);

            // Update the original values with the database values and
            // the current values with whatever the user choose.
            entry.OriginalValues.SetValues(databaseValues);
            entry.CurrentValues.SetValues(resolvedValuesAsBlog);
        }

    } while (saveFailed);
}

public void HaveUserResolveConcurrency(Blog entity,
                                       Blog databaseValues,
                                       Blog resolvedValues)
{
    // Show the current, database, and resolved values to the user and have
    // them update the resolved values to get the correct resolution.
}