Controlar los conflictos de simultaneidad (EF6)

La simultaneidad optimista implica intentar guardar la entidad en la base de datos con la esperanza de que los datos no hayan cambiado desde que se cargó la entidad. Si resulta que los datos han cambiado, se produce una excepción y debe resolver el conflicto antes de intentar guardar otra vez. En este tema se explica cómo controlar estas excepciones en Entity Framework. Las técnicas que se muestran en este tema se aplican igualmente a los modelos creados con Code First y EF Designer.

Esta publicación no es el mejor lugar para una discusión a fondo sobre la simultaneidad optimista. En las secciones siguientes se asume cierto conocimiento de la resolución de simultaneidad y se muestran patrones para tareas comunes.

Muchos de estos patrones usan los temas descritos en Trabajar con valores de propiedad.

La resolución de problemas de simultaneidad cuando se usan asociaciones independientes (en las que la clave externa no está asignada a una propiedad de la entidad) es mucho más difícil que cuando se usan asociaciones de clave externa. Por lo tanto, si va a resolver la simultaneidad en la aplicación, se recomienda asignar siempre claves externas a las entidades. En todos los ejemplos siguientes se supone que usa asociaciones de clave externa.

SaveChanges produce una excepción de simultaneidad DbUpdateConcurrencyException cuando se detecta una excepción de simultaneidad optimista al intentar guardar una entidad que usa asociaciones de clave externa.

Resolver excepciones de simultaneidad optimista con Reload (la base de datos gana)

El método Reload puede usarse para sobrescribir los valores actuales de la entidad con los valores que hay en la base de datos. A continuación, normalmente se devuelve la entidad al usuario de alguna forma y este debe intentar volver a realizar sus cambios y guardar. Por ejemplo:

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

Una buena manera de simular una excepción de simultaneidad es establecer un punto de interrupción en la llamada a SaveChanges y, a continuación, modificar una entidad que se guarde en la base de datos mediante otra herramienta como SQL Server Management Studio. También puede insertar una línea antes de SaveChanges para actualizar la base de datos directamente mediante SqlCommand. Por ejemplo:

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

El método Entries de DbUpdateConcurrencyException devuelve las instancias de DbEntityEntry para las entidades que no se pudieron actualizar. (Actualmente, esta propiedad siempre devuelve un único valor para problemas de simultaneidad. Puede devolver varios valores para excepciones de actualización generales). Una alternativa para algunas situaciones podría ser obtener entradas para todas las entidades que puedan necesitar volver a cargarse desde la base de datos y llamar a la recarga para cada una de ellas.

Resolver excepciones de simultaneidad optimista en las que la base de datos gana

El ejemplo anterior que usa Reload a veces se llama “gana la base de datos” o “gana el almacén” porque los valores de la entidad se sobrescriben con los valores de la base de datos. Es posible que en ocasiones quiera hacer lo contrario y sobrescribir los valores de la base de datos con los valores actuales de la entidad. Esto se llama a veces “el cliente gana” y puede hacerse obteniendo los valores actuales de la base de datos y estableciéndolos como valores originales para la entidad. (Consulte Trabajar con valores de propiedad para obtener información sobre los valores actuales y originales). Por ejemplo:

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

Resolución personalizada de excepciones de simultaneidad optimista

Es posible que en ocasiones quiera combinar los valores de la base de datos con los valores actuales de la entidad. Esto normalmente requiere lógica personalizada o interacción del usuario. Por ejemplo, puede presentar un formulario al usuario que contiene los valores actuales, los de la base de datos y un conjunto predeterminado de valores resueltos. Entonces, el usuario editaría los valores resueltos según sea necesario y estos serían los que se guardarían en la base de datos. Esto se puede hacer mediante los objetos DbPropertyValues devueltos desde CurrentValues y GetDatabaseValues en la entrada de la entidad. Por ejemplo:

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

Resolución personalizada de excepciones de simultaneidad optimista mediante objetos

El código anterior usa instancias de DbPropertyValues para pasar valores actuales, de base de datos y resueltos. A veces puede ser más fácil usar instancias de su tipo de entidad para esto. Esto se puede hacer mediante los métodos ToObject y SetValues de DbPropertyValues. Por ejemplo:

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