Behandeln von Parallelitätskonflikten (EF6)

Optimistische Parallelität beinhaltet das optimistische Bestreben, Ihre Entität in der Datenbank zu speichern, in der Hoffnung, dass sich die Daten seit dem Laden der Entität nicht geändert haben. Wenn sich herausstellt, dass sich die Daten geändert haben, wird eine Ausnahme ausgelöst, und Sie müssen den Konflikt lösen, bevor Sie erneut versuchen, zu speichern. In diesem Thema wird erläutert, wie solche Ausnahmen in Entity Framework behandelt werden. Die in diesem Thema gezeigten Techniken gelten gleichermaßen für Modelle, die mit Code First und EF Designer erstellt wurden.

Dieser Beitrag ist nicht der richtige Ort für eine vollständige Diskussion über optimistische Parallelität. In den folgenden Abschnitten wird von einigen Kenntnissen der Parallelitätsauflösung ausgegangen und Muster für allgemeine Aufgaben angezeigt.

Viele dieser Muster verwenden die themen, die in der Arbeit mit Eigenschaftswerten behandelt werden.

Das Beheben von Parallelitätsproblemen, wenn Sie unabhängige Zuordnungen verwenden (bei denen der Fremdschlüssel nicht einer Eigenschaft in Ihrer Entität zugeordnet ist) ist viel schwieriger, als wenn Sie Fremdschlüsselzuordnungen verwenden. Daher wird empfohlen, Fremdschlüssel immer in Ihren Entitäten zuzuordnen, wenn Sie die Parallelitätsauflösung in Ihrer Anwendung ausführen werden. In allen folgenden Beispielen wird davon ausgegangen, dass Sie Fremdschlüsselzuordnungen verwenden.

Eine DbUpdateConcurrencyException wird von SaveChanges ausgelöst, wenn beim Versuch, eine Entität zu speichern, die Fremdschlüsselzuordnungen verwendet, eine optimistische Parallelitätsausnahme erkannt wird.

Auflösen optimistischer Parallelitätsausnahmen mit Reload (Datenbank gewinnt)

Die Reload-Methode kann verwendet werden, um die aktuellen Werte der Entität mit den Werten zu überschreiben, die sich jetzt in der Datenbank befinden. Die Entität wird dann in der Regel dem Benutzer in irgendeiner Form zurückgesendet, und er muss versuchen, seine Änderungen erneut vorzunehmen und erneut zu speichern. Beispiel:

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

Eine gute Möglichkeit zum Simulieren einer Parallelitäts ausnahme besteht darin, einen Haltepunkt für den SaveChanges-Aufruf festzulegen und dann eine Entität zu ändern, die in der Datenbank mithilfe eines anderen Tools wie SQL Server Management Studio gespeichert wird. Sie können auch eine Zeile einfügen, bevor SaveChanges die Datenbank direkt mithilfe von SqlCommand aktualisiert. Beispiel:

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

Die Entries-Methode für DbUpdateConcurrencyException gibt die DbEntityEntry-Instanzen für die Entitäten zurück, die nicht aktualisiert werden konnten. (Diese Eigenschaft gibt derzeit immer einen einzelnen Wert für Parallelitätsprobleme zurück. Es kann mehrere Werte für allgemeine Update-Ausnahmen zurückgeben.) Eine Alternative für einige Situationen kann das Abrufen von Einträgen für alle Entitäten sein, die möglicherweise aus der Datenbank neu geladen werden müssen, und das erneute Laden für jedes dieser Elemente.

Auflösen optimistischer Parallelitätsausnahmen, wenn der Client gewinnt

Das obige Beispiel, in dem 'Reload' verwendet wird, wird manchmal als 'Datenbank gewinnt' oder 'Store gewinnt' bezeichnet, da die Werte in der Entität durch Werte aus der Datenbank überschrieben werden. Manchmal möchten Sie das Gegenteil tun und die Werte in der Datenbank mit den Werten überschreiben, die sich derzeit in der Entität befinden. Dies wird manchmal als "Client gewinnt" bezeichnet und kann erreicht werden, indem die aktuellen Datenbankwerte abgerufen und als ursprüngliche Werte für die Entität festgelegt werden. (Informationen zu aktuellen und ursprünglichen Werten finden Sie unter Arbeiten mit Eigenschaftswerten .) Zum Beispiel:

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

Benutzerdefinierte Auflösung optimistischer Parallelitäts-Ausnahmen

Manchmal möchten Sie die Werte, die sich derzeit in der Datenbank befinden, mit den Werten kombinieren, die sich derzeit in der Entität befinden. Dies erfordert in der Regel eine benutzerdefinierte Logik oder Benutzerinteraktion. Beispielsweise können Sie dem Benutzer ein Formular mit den aktuellen Werten, den Werten in der Datenbank und einem Standardsatz aufgelöster Werte präsentieren. Der Benutzer würde dann die aufgelösten Werte nach Bedarf bearbeiten, und es wären diese aufgelösten Werte, die in der Datenbank gespeichert werden. Dazu können die von CurrentValues und GetDatabaseValues zurückgegebenen DbPropertyValues-Objekte für den Eintrag der Entität verwendet werden. Beispiel:

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.
}

Benutzerdefinierte Auflösung optimistischer Parallelitätsausnahmen mithilfe von Objekten.

Der obige Code verwendet DbPropertyValues-Instanzen zum Übergeben aktueller, datenbank- und aufgelöster Werte. Manchmal ist es möglicherweise einfacher, Instanzen Ihres Entitätstyps dafür zu verwenden. Dies kann mithilfe der ToObject- und SetValues-Methoden von DbPropertyValues erfolgen. Beispiel:

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.
}