Gestion des conflits d'accès concurrentiel (EF6)

L’accès concurrentiel optimiste implique une tentative optimiste d’enregistrement de votre entité dans la base de données avec l’espoir que les données qui s’y trouvent n’ont pas changé depuis le chargement de l’entité. S’il s’avère que les données ont changé, une exception est levée et vous devez résoudre le conflit avant de tenter à nouveau d’enregistrer. Cette rubrique explique comment gérer ces exceptions dans Entity Framework. Les techniques présentées dans cette rubrique s’appliquent également aux modèles créés avec Code First et EF Designer.

Ce billet n’est pas l’endroit approprié pour une discussion complète de l’accès concurrentiel optimiste. Les sections ci-dessous supposent une certaine connaissance de la résolution d’accès concurrentiel et affichent des modèles pour des tâches courantes.

La plupart de ces modèles utilisent les rubriques abordées dans Utilisation des valeurs de propriété.

La résolution des problèmes d’accès concurrentiel quand vous utilisez des associations indépendantes (où la clé étrangère n’est pas mappée sur une propriété dans votre entité) est beaucoup plus difficile que lorsque vous utilisez des associations de clés étrangères. Ainsi, si vous effectuez une résolution d’accès concurrentiel dans votre application, il est conseillé de toujours mapper des clés étrangères dans vos entités. Tous les exemples suivants supposent que vous utilisez des associations de clés étrangères.

Une exception DbUpdateConcurrencyException est levée par SaveChanges quand une exception d’accès concurrentiel optimiste est détectée lors d’une tentative d’enregistrer une entité qui utilise des associations de clés étrangères.

Résolution des exceptions d’accès concurrentiel optimiste avec Reload (victoires de base de données)

La méthode Reload peut être utilisée pour remplacer les valeurs actuelles de l’entité avec les valeurs se trouvant désormais dans la base de données. L’entité est ensuite généralement remise à l’utilisateur sous une certaine forme et elle doit essayer d’apporter à nouveau ses modifications et re-enregistrer. Par exemple :

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

Une bonne façon de simuler une exception d’accès concurrentiel consiste à définir un point d’arrêt sur l’appel SaveChanges, puis à modifier une entité en cours d’enregistrement dans la base de données à l’aide d’un autre outil, comme SQL Server Management Studio. Vous pouvez également insérer une ligne avant SaveChanges pour mettre à jour la base de données directement avec SqlCommand. Par exemple :

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

La méthode Entries sur DbUpdateConcurrencyException retourne les instances DbEntityEntry pour les entités dont la mise à jour a échoué. (Cette propriété retourne toujours une valeur unique pour les problèmes d’accès concurrentiel. Elle peut retourner plusieurs valeurs pour les exceptions de mise à jour générale.) Dans certaines situations, une alternative peut être d’obtenir des entrées pour toutes les entités qui peuvent avoir besoin d’être rechargées à partir de la base de données et d’appeler Reload pour chacune de ces entités.

Résolution des exceptions d’accès concurrentiel optimiste en tant que victoires de client

L’exemple ci-dessus qui utilise Reload s’appelle parfois victoires de base de données ou victoires de magasin, car les valeurs de l’entité sont remplacées par des valeurs de la base de données. Il arrive parfois que vous souhaitiez faire le contraire et remplacer les valeurs de la base de données par les valeurs actuellement dans l’entité. Cela s’appelle parfois victoires de client et cette méthode peut être effectuée en obtenant les valeurs de base de données actuelles, puis en les définissant comme valeurs d’origine pour l’entité. (Consultez Utilisation des valeurs de propriété pour plus d’informations sur les valeurs actuelles et d’origine.) Exemple :

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

Résolution personnalisée d’exceptions d’accès concurrentiel optimiste

Il arrive parfois que vous souhaitiez combiner les valeurs actuelles de la base de données avec les valeurs actuellement dans l’entité. Cela nécessite généralement une logique personnalisée ou une interaction utilisateur. Par exemple, vous pouvez présenter à l’utilisateur un formulaire contenant les valeurs actuelles, les valeurs de la base de données et un ensemble par défaut de valeurs résolues. L’utilisateur modifie ensuite au besoin les valeurs résolues et ce sont ces valeurs résolues qui sont enregistrées dans la base de données. Pour ce faire, utilisez les objets DbPropertyValues retournés par CurrentValues et GetDatabaseValues sur l’entrée de l’entité. Par exemple :

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

Résolution personnalisée des exceptions d’accès concurrentiel optimiste à l’aide d’objets

Le code ci-dessus utilise des instances DbPropertyValues pour faire circuler des valeurs actuelles, de base de données et résolues. Il est parfois plus facile d’utiliser des instances de votre type d’entité dans ce cas. Pour ce faire, utilisez les méthodes ToObject et SetValues de DbPropertyValues. Par exemple :

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