Gestione dei conflitti di concorrenza (EF6)

La concorrenza ottimistica implica il tentativo ottimistico di salvare l'entità nel database nella speranza che i dati non siano stati modificati dopo il caricamento dell'entità. Se si scopre che i dati sono stati modificati, viene generata un'eccezione ed è necessario risolvere il conflitto prima di tentare di salvare di nuovo. Questo argomento illustra come gestire tali eccezioni in Entity Framework. Le tecniche illustrate in questo argomento si applicano in modo analogo ai modelli creati con Code First ed EF Designer.

Questo post non è il posto appropriato per una discussione completa della concorrenza ottimistica. Le sezioni seguenti presuppongono alcune conoscenze sulla risoluzione della concorrenza e mostrano i modelli per le attività comuni.

Molti di questi modelli usano gli argomenti descritti in Uso dei valori delle proprietà.

La risoluzione dei problemi di concorrenza quando si usano associazioni indipendenti (in cui la chiave esterna non è mappata a una proprietà nell'entità) è molto più difficile rispetto a quando si usano associazioni di chiavi esterne. Pertanto, se si intende eseguire la risoluzione della concorrenza nell'applicazione, è consigliabile eseguire sempre il mapping delle chiavi esterne nelle entità. Tutti gli esempi seguenti presuppongono che si usino associazioni di chiavi esterne.

Un'eccezione DbUpdateConcurrencyException viene generata da SaveChanges quando viene rilevata un'eccezione di concorrenza ottimistica durante il tentativo di salvare un'entità che usa associazioni di chiavi esterne.

Risoluzione delle eccezioni di concorrenza ottimistica con ricaricamento (prevale il database)

Il metodo Reload può essere usato per sovrascrivere i valori correnti dell'entità con i valori ora presenti nel database. L'entità viene in genere restituita all'utente in qualche forma e deve tentare di apportare di nuovo le modifiche e salvarla nuovamente. Ad esempio:

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

Un buon modo per simulare un'eccezione di concorrenza consiste nell'impostare un punto di interruzione nella chiamata SaveChanges e quindi modificare un'entità salvata nel database usando un altro strumento, ad esempio SQL Server Management Studio. È anche possibile inserire una riga prima di SaveChanges per aggiornare il database direttamente usando SqlCommand. Ad esempio:

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

Il metodo Entries in DbUpdateConcurrencyException restituisce le istanze DbEntityEntry per le entità che non sono riuscite ad aggiornare. Questa proprietà restituisce attualmente sempre un singolo valore per i problemi di concorrenza. Può restituire più valori per le eccezioni di aggiornamento generale. Un'alternativa per alcune situazioni potrebbe essere quella di ottenere voci per tutte le entità che potrebbero dover essere ricaricate dal database e chiamare il ricaricamento per ognuna di queste entità.

Risoluzione delle eccezioni di concorrenza ottimistica quando il client vince

L'esempio precedente che usa Ricaricamento viene talvolta definito database wins o store wins perché i valori nell'entità vengono sovrascritti dai valori del database. In alcuni casi è possibile eseguire l'operazione opposta e sovrascrivere i valori nel database con i valori attualmente presenti nell'entità. Questa operazione viene talvolta chiamata client wins e può essere eseguita recuperando i valori correnti del database e impostandoli come valori originali per l'entità. (Vedere Utilizzo dei valori delle proprietà per informazioni sui valori correnti e originali. Per esempio:

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

Risoluzione personalizzata delle eccezioni di concorrenza ottimistica

In alcuni casi è possibile combinare i valori attualmente presenti nel database con i valori attualmente presenti nell'entità. Questo richiede in genere una logica personalizzata o un'interazione dell'utente. Ad esempio, è possibile presentare un modulo all'utente contenente i valori correnti, i valori nel database e un set predefinito di valori risolti. L'utente modifica quindi i valori risolti in base alle esigenze e questi valori risolti vengono salvati nel database. Questa operazione può essere eseguita usando gli oggetti DbPropertyValues restituiti da CurrentValues e GetDatabaseValues nella voce dell'entità. Ad esempio:

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

Risoluzione personalizzata delle eccezioni di concorrenza ottimistica tramite oggetti

Il codice precedente usa le istanze DbPropertyValues per passare valori correnti, di database e risolti. In alcuni casi può essere più semplice usare istanze del tipo di entità per questo tipo. Questa operazione può essere eseguita usando i metodi ToObject e SetValues di DbPropertyValues. Ad esempio:

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