Traducción: Introducción a Entity Framework 6 de MVC5 (10) - Manejo de concurrencia para aplicaciones ASP.NET MVC

Manejo de concurrencia para aplicaciones ASP.NET MVC

Esta es una traducción del tutorial oficial de Microsoft Comenzando con Entity Framework 6 Code First usando la serie MVC 5. Aquí está el décimo artículo: Manejo de concurrencia para aplicaciones MVC ASP.NET

原文 :Manejo de concurrencia con Entity Framework 6 en una aplicación ASP.NET MVC 5


En el tutorial anterior, aprendió a actualizar datos. En este tutorial, mostraremos cómo manejar conflictos cuando varios usuarios actualizan la misma entidad al mismo tiempo.

Modificará las páginas web para manejar entidades del Departamento para que puedan manejar errores concurrentes. La captura de pantalla siguiente muestra las páginas de índice y eliminación, así como algunos mensajes de error de conflicto de concurrencia.

departamentos

departamento de edición

Conflicto de concurrencia

Cuando un usuario muestra los datos de la entidad y los edita, otro usuario actualiza los datos de la misma entidad antes de que los cambios del primer usuario se escriban en la base de datos, se producirá un conflicto concurrente. Si no habilita esta detección de conflictos, el usuario que actualizó por última vez la base de datos sobrescribirá los cambios realizados por otros usuarios en la base de datos. En la mayoría de las aplicaciones, este riesgo es aceptable: si solo hay unos pocos usuarios o pocas actualizaciones, o el problema de la cobertura de actualización de datos realmente no es muy importante, el costo de implementar conflictos concurrentes puede ser mayor que Beneficios En este caso, no necesita configurar la aplicación para manejar conflictos de concurrencia.

Concurrencia pesimista (bloqueado)

Si su aplicación necesita evitar la pérdida accidental de datos debido a la concurrencia, una forma de hacerlo es usar bloqueos de la base de datos. Esto se llama concurrencia pesimista. Por ejemplo, antes de leer una fila de la base de datos, solicita un bloqueo de acceso de solo lectura o actualizado. Si bloquea el acceso de actualización a una fila, ningún otro usuario puede bloquear la fila, ya sea de solo lectura o actualizada. Porque los datos que obtienen son solo una copia del proceso de cambio. Si bloquea el acceso de solo lectura a una fila, otros también pueden bloquearlo para el acceso de solo lectura, pero no puede actualizarlo.

Los bloqueos de gestión también tienen inconvenientes. Hará la programación más complicada. Y requiere recursos de administración de bases de datos, un gran número, y puede causar problemas de rendimiento, como el aumento en el número de usuarios de la aplicación. Por estas razones, no todos los sistemas de gestión de bases de datos admiten concurrencia pesimista. Entity Framework tiene soporte incorporado para la concurrencia pesimista, y este tutorial no discutirá cómo implementarlo.

Concurrencia optimista

La alternativa a la concurrencia pesimista es la concurrencia optimista. La concurrencia optimista significa que se producen conflictos de concurrencia en ejecución y luego responde adecuadamente a los cambios que ocurren. Por ejemplo, Passerby A cambió el presupuesto en inglés de 350,000.00 a 0.00 en la página de edición del departamento.

departamentos

Antes de que el transeúnte guardara el cambio, el transeúnte B también abrió la página y cambió el campo de fecha de inicio al 2 de mayo de 2014.

departamento de edición

Passerby primero hizo clic en Guardar, vio los cambios que hizo en la página de índice y Passerby B también hizo clic en Guardar. Lo que suceda después depende de cómo maneje los conflictos de concurrencia. Aquí hay algunas opciones:

  • Puede rastrear los atributos modificados del usuario y actualizar solo las columnas correspondientes en la base de datos. En el ejemplo, no se perderán datos, ya que dos atributos diferentes son actualizados por dos usuarios diferentes. El transeúnte C verá los cambios realizados por A y B al mismo tiempo cuando navegue por la página, la fecha de inicio del 8/8/2013 y el presupuesto de 0 yuanes.
    Este método de actualización puede reducir conflictos, pero aún puede causar pérdida de datos si realiza cambios en el mismo atributo. El uso de esta forma para hacer que Entity Framework funcione depende de cómo implemente su código de actualización. Esta no suele ser la mejor práctica en aplicaciones web reales. Porque requerirá mantener una gran cantidad de estado para rastrear todos los atributos originales y los nuevos valores de la entidad. Mantener una gran cantidad de estado puede afectar el rendimiento de la aplicación. Porque esto requiere más recursos del servidor.

  • Puede dejar que los cambios de B anulen los cambios de A. Cuando C navega por la página, verá la fecha de inicio del 8/8/2013 y el presupuesto restaurado de 350,000.00 yuanes. Esto se llama cliente-todo-toma o último-toma-todo. (El valor del cliente tiene prioridad sobre el valor guardado primero, sobrescribiendo todos los datos).

  • También puede evitar que los cambios de B se guarden en la base de datos. Por lo general, se muestra un mensaje de error que muestra la diferencia entre los datos que se sobrescriben para permitir que el usuario vuelva a enviar los cambios si el usuario desea hacerlo. Esto se llama almacenamiento-toma-todo. (El valor guardado tiene prioridad sobre el valor enviado por el cliente) Implementará esta solución en este tutorial para asegurarse de que los cambios de otros usuarios no se sobrescriban antes de avisar al usuario.

Detectar conflictos concurrentes

Puede resolver el conflicto manejando la excepción OptimisticConcurrencyException generada por Entity Framework. Para saber cuándo y dónde se generan estas excepciones, Entity Framework debe poder detectar conflictos. Por lo tanto, debe configurar correctamente la base de datos y el modelo de datos, incluidos los siguientes:

  • En la tabla de datos, contiene una columna para el seguimiento de las modificaciones. Luego puede configurar Entity Framework para incluir esta columna cuando se actualiza o elimina para su detección.
    El tipo de datos de la columna de seguimiento suele ser la versión de fila. El valor de la versión de fila es un número secuencial que se incrementa cada vez que se actualiza. En el comando actualizar o eliminar, la instrucción Where contendrá el valor original de la columna de seguimiento. Si otro usuario cambia la fila que se está actualizando, el valor en la versión de la fila será inconsistente con el original. Por lo tanto, las declaraciones de actualización y eliminación no pueden encontrar la fila que se actualizará. Cuando no se actualizan filas en el momento de la actualización o eliminación, Entity Framework reconocerá el comando como un conflicto concurrente.

  • Configure Entity Framework para incluir el valor original de cada columna de la tabla de datos en la cláusula Where de los comandos de actualización y eliminación.
    Similar al primer método, si la fila de datos se cambia después de ser leída por primera vez, la cláusula Where no encontrará la fila que se actualizará, y el cuadro de entidad se interpretará como un conflicto concurrente. Para una tabla de base de datos con varias columnas, este método puede resultar en una oración Where muy grande y requerir que mantenga una gran cantidad de estados. Como se mencionó anteriormente, mantener una gran cantidad de estados puede afectar el rendimiento de la aplicación. Por lo tanto, este método generalmente no se recomienda y no se utilizará en este tutorial.
    Si desea implementar este método para lograr la concurrencia, debe agregar la función ConcurrencyCheck a todos los atributos clave no primarios de la entidad. Este cambio permite que Entity Framework incluya todas las columnas marcadas en la cláusula Where de la declaración de actualización.

En el resto de este tutorial, agregará una versión de fila para rastrear los atributos de la entidad del Departamento.

Agregue los atributos requeridos para la concurrencia optimista a la entidad del Departamento

En Models \ Department.cs, agregue un atributo de seguimiento denominado RowCersion:

public class Department
{
    public int DepartmentID { get; set; }

    [StringLength(50, MinimumLength = 3)]
    public string Name { get; set; }

    [DataType(DataType.Currency)]
    [Column(TypeName = "money")]
    public decimal Budget { get; set; }

    [DataType(DataType.Date)]
    [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
    [Display(Name = "Start Date")]
    public DateTime StartDate { get; set; }

    [Display(Name = "Administrator")]
    public int? InstructorID { get; set; }

    [Timestamp]
    public byte[] RowVersion { get; set; }

    public virtual Instructor Administrator { get; set; }
    public virtual ICollection<Course> Courses { get; set; }
}

El atributo Timestamp especifica que la columna se incluirá en la cláusula Where del comando de actualización o eliminación enviado a la base de datos. Este atributo se denomina marca de tiempo porque la versión anterior de SQL Server usaba el tipo de datos de Marca de tiempo de SQL. La versión de línea del tipo .Net es una matriz de bytes.

Si prefiere usar la API fluida, puede usar el método IsConcurrencyToken para especificar atributos de seguimiento, como en el siguiente ejemplo:

modelBuilder.Entity<Department>()
    .Property(p => p.RowVersion).IsConcurrencyToken();

Ahora que ha cambiado el modelo de la base de datos, debe realizar otra migración. En la consola del administrador de paquetes, ingrese el siguiente comando:

Add-Migration RowVersion
Update-Database

Modificar controlador de departamento

En DepartmentController.cs, agregue la instrucción using:

using System.Data.Entity.Infrastructure;

Cambie todo "Apellido" en el archivo a "Nombre completo" para que la lista desplegable use el nombre completo del maestro en lugar del apellido.

ViewBag.InstructorID = new SelectList(db.Instructors, "ID", "FullName");

Reemplace el método de edición de HttpPost con el siguiente código:

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Edit(int? id, byte[] rowVersion)
{
    string[] fieldsToBind = new string[] { "Name", "Budget", "StartDate", "InstructorID", "RowVersion" };

    if (id == null)
    {
        return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
    }

    var departmentToUpdate = await db.Departments.FindAsync(id);
    if (departmentToUpdate == null)
    {
        Department deletedDepartment = new Department();
        TryUpdateModel(deletedDepartment, fieldsToBind);
        ModelState.AddModelError(string.Empty,
            "Unable to save changes. The department was deleted by another user.");
        ViewBag.InstructorID = new SelectList(db.Instructors, "ID", "FullName", deletedDepartment.InstructorID);
        return View(deletedDepartment);
    }

    if (TryUpdateModel(departmentToUpdate, fieldsToBind))
    {
        try
        {
            db.Entry(departmentToUpdate).OriginalValues["RowVersion"] = rowVersion;
            await db.SaveChangesAsync();

            return RedirectToAction("Index");
        }
        catch (DbUpdateConcurrencyException ex)
        {
            var entry = ex.Entries.Single();
            var clientValues = (Department)entry.Entity;
            var databaseEntry = entry.GetDatabaseValues();
            if (databaseEntry == null)
            {
                ModelState.AddModelError(string.Empty,
                    "Unable to save changes. The department was deleted by another user.");
            }
            else
            {
                var databaseValues = (Department)databaseEntry.ToObject();

                if (databaseValues.Name != clientValues.Name)
                    ModelState.AddModelError("Name", "Current value: "
                        + databaseValues.Name);
                if (databaseValues.Budget != clientValues.Budget)
                    ModelState.AddModelError("Budget", "Current value: "
                        + String.Format("{0:c}", databaseValues.Budget));
                if (databaseValues.StartDate != clientValues.StartDate)
                    ModelState.AddModelError("StartDate", "Current value: "
                        + String.Format("{0:d}", databaseValues.StartDate));
                if (databaseValues.InstructorID != clientValues.InstructorID)
                    ModelState.AddModelError("InstructorID", "Current value: "
                        + db.Instructors.Find(databaseValues.InstructorID).FullName);
                ModelState.AddModelError(string.Empty, "The record you attempted to edit "
                    + "was modified by another user after you got the original value. The "
                    + "edit operation was canceled and the current values in the database "
                    + "have been displayed. If you still want to edit this record, click "
                    + "the Save button again. Otherwise click the Back to List hyperlink.");
                departmentToUpdate.RowVersion = databaseValues.RowVersion;
            }
        }
        catch (RetryLimitExceededException /* dex */)
        {
            //Log the error (uncomment dex variable name and add a line here to write a log.
            ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator.");
        }
    }
    ViewBag.InstructorID = new SelectList(db.Instructors, "ID", "FullName", departmentToUpdate.InstructorID);
    return View(departmentToUpdate);
}

La vista almacena el valor original de RowVersion en un campo oculto. Cuando la carpeta del modelo crea una instancia del sistema, el objeto tendrá el valor original del atributo RowVersion y el nuevo valor de otros atributos, como el usuario ingresado en la página de edición. Luego, Entity Framework crea un comando de actualización, que incluirá el valor RowVersion en la cláusula Where para la consulta.

Si no se actualizan filas (no se encuentran filas que coincidan con el valor original de RowVersion), Entity Framework generará una excepción DbUpdateConcurrencyException y obtendrá la entidad de Departamento afectada del objeto de excepción en el bloque de código de captura.

var entry = ex.Entries.Single();

La propiedad Entity del objeto tiene el nuevo valor introducido por el usuario. También puede llamar al método GetDatabaseValues ​​para leer el valor original de la base de datos.

var clientValues = (Department)entry.Entity;
var databaseEntry = entry.GetDatabaseValues();

Si alguien elimina la fila de la base de datos, el método GetDataBaseValue devolverá nulo; de lo contrario, debe convertir el objeto devuelto en la clase Departamento para acceder a las propiedades en el Departamento.

if (databaseEntry == null)
{
    ModelState.AddModelError(string.Empty,
        "Unable to save changes. The department was deleted by another user.");
}
else
{
    var databaseValues = (Department)databaseEntry.ToObject();

A continuación, el código agregará un mensaje de error personalizado para cada columna de la base de datos y los valores de entrada del usuario diferentes:

if (databaseValues.Name != currentValues.Name)
    ModelState.AddModelError("Name", "Current value: " + databaseValues.Name);
    // ...

Un largo mensaje de error explica al usuario lo que sucedió:

ModelState.AddModelError(string.Empty, "The record you attempted to edit "
    + "was modified by another user after you got the original value. The"
    + "edit operation was canceled and the current values in the database "
    + "have been displayed. If you still want to edit this record, click "
    + "the Save button again. Otherwise click the Back to List hyperlink.");

Finalmente, el código establece el valor RowVersion del objeto Departamento en el nuevo valor recuperado de la base de datos. El nuevo valor se almacena en un campo oculto cuando se vuelve a mostrar la página de edición. La próxima vez que el usuario haga clic en Guardar, la página de edición que se vuelve a mostrar continuará detectando errores concurrentes.

En Views \ Department \ Edit.cshtml, agregue un campo oculto RowVersion después del campo oculto DepartmentID.

@model ContosoUniversity.Models.Department

@{
    ViewBag.Title = "Edit";
}

<h2>Edit</h2>

@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()

    <div class="form-horizontal">
        <h4>Department</h4>
        <hr />
        @Html.ValidationSummary(true)
        @Html.HiddenFor(model => model.DepartmentID)
        @Html.HiddenFor(model => model.RowVersion)

Probar el procesamiento concurrente optimista

Ejecute la aplicación, haga clic en la pestaña Departamento y copie una pestaña, y abra las dos páginas Departamento repetidamente.

departamentos

Abra la página de edición del mismo departamento en dos ventanas al mismo tiempo, edite una de las páginas y guárdela.

departamento de edición

departamento de edición

Verá que el valor se ha guardado en la base de datos.

departamentos

Modifique y guarde los campos en la segunda ventana.

departamento de edición

Verá el mensaje de error de concurrencia:

departamento de edición

Haga clic en Guardar nuevamente, y el valor de su base de datos en el segundo navegador sobrescribirá el guardado en la primera ventana de la base de datos.

departamentos

Actualizar eliminar página

Para eliminar páginas, Entity Framework utiliza un método similar para detectar conflictos concurrentes. Cuando el método Delete de HttpGet muestra la vista de confirmación, el valor original de RowVersion se incluye en el campo oculto de la vista. Cuando el usuario confirma la eliminación, el valor es suficiente para pasarlo y llamarlo en el método Delete de HttpPost. Cuando Entity Framework crea el comando SQL Delete, el valor original de RowVersion se incluirá en la cláusula Where. Si no hay líneas afectadas después de ejecutar el comando, se lanzará una excepción concurrente. Se llama al método Delete de HttpGet y el indicador se establecerá en true para volver a mostrar la página de confirmación y mostrar el error. Pero al mismo tiempo, tenga en cuenta que si otro usuario elimina la fila, también se verá afectada una fila 0. En este caso, mostraremos un mensaje de error diferente.

En DepartmentController.cs, reemplace el método Delete de HttpGet con el siguiente código:

public async Task<ActionResult> Delete(int? id, bool? concurrencyError)
{
    if (id == null)
    {
        return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
    }
    Department department = await db.Departments.FindAsync(id);
    if (department == null)
    {
        if (concurrencyError.GetValueOrDefault())
        {
            return RedirectToAction("Index");
        }
        return HttpNotFound();
    }

    if (concurrencyError.GetValueOrDefault())
    {
        ViewBag.ConcurrencyErrorMessage = "The record you attempted to delete "
            + "was modified by another user after you got the original values. "
            + "The delete operation was canceled and the current values in the "
            + "database have been displayed. If you still want to delete this "
            + "record, click the Delete button again. Otherwise "
            + "click the Back to List hyperlink.";
    }

    return View(department);
}

Este método acepta un parámetro opcional que indica si la página se volverá a mostrar cuando ocurra un error de conflicto concurrente. Si este indicador es verdadero, se enviará un error a la vista usando ViewBag.

Reemplace el método de eliminación HttpPost (llamado DeleteConfirmed) con el siguiente código:

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Delete(Department department)
{
    try
    {
        db.Entry(department).State = EntityState.Deleted;
        await db.SaveChangesAsync();
        return RedirectToAction("Index");
    }
    catch (DbUpdateConcurrencyException)
    {
        return RedirectToAction("Delete", new { concurrencyError = true, id=department.DepartmentID });
    }
    catch (DataException /* dex */)
    {
        //Log the error (uncomment dex variable name after DataException and add a line here to write a log.
        ModelState.AddModelError(string.Empty, "Unable to delete. Try again, and if the problem persists contact your system administrator.");
        return View(department);
    }
}

En su código de andamio no modificado, este método recibe una ID de registro

public async Task<ActionResult> DeleteConfirmed(int id)

Cambió este parámetro y usó la carpeta modelo para crear una entidad Departamento, que le da acceso al valor de la propiedad RowVersion.

public async Task<ActionResult> Delete(Department department)

Al mismo tiempo, cambió el nombre del método de DeleteConfirmed a Delete. El código de andamio para el método Delete de HttpPost usa el nombre Delete, porque esto le dará al método HttpPost una firma única. (El CLR requiere que los métodos tengan diferentes parámetros para la sobrecarga. Ahora la firma es única. Puede mantener la convención MVC y usar el mismo nombre de método en los métodos HttpPost y HttpGet).

Si se detecta un error concurrente, el código vuelve a mostrar la página de confirmación de eliminación y proporciona un indicador para indicar que se muestra un mensaje de error concurrente.

En Vistas \ Departamento \ Eliminar.cshtml, agregue campos de mensajes de error y campos ocultos a la vista. Reemplace el código de andamio con lo siguiente:

@model ContosoUniversity.Models.Department

@{
    ViewBag.Title = "Delete";
}

<h2>Delete</h2>

<p class="error">@ViewBag.ConcurrencyErrorMessage</p>

<h3>Are you sure you want to delete this?</h3>
<div>
    <h4>Department</h4>
    <hr />
    <dl class="dl-horizontal">
        <dt>
            Administrator
        </dt>

        <dd>
            @Html.DisplayFor(model => model.Administrator.FullName)
        </dd>

        <dt>
            @Html.DisplayNameFor(model => model.Name)
        </dt>

        <dd>
            @Html.DisplayFor(model => model.Name)
        </dd>

        <dt>
            @Html.DisplayNameFor(model => model.Budget)
        </dt>

        <dd>
            @Html.DisplayFor(model => model.Budget)
        </dd>

        <dt>
            @Html.DisplayNameFor(model => model.StartDate)
        </dt>

        <dd>
            @Html.DisplayFor(model => model.StartDate)
        </dd>

    </dl>

    @using (Html.BeginForm()) {
        @Html.AntiForgeryToken()
        @Html.HiddenFor(model => model.DepartmentID)
        @Html.HiddenFor(model => model.RowVersion)

        <div class="form-actions no-color">
            <input type="submit" value="Delete" class="btn btn-default" /> |
            @Html.ActionLink("Back to List", "Index")
        </div>
    }
</div>

El código agrega un mensaje de error entre h2 y h3:

<p class="error">@ViewBag.ConcurrencyErrorMessage</p>

Reemplazar Apellido con Nombre Completo:

<dt>
  Administrator
</dt>
<dd>
  @Html.DisplayFor(model => model.Administrator.FullName)
</dd>

Finalmente, agrega campos ocultos después de la declaración Html.BeginForm:

@Html.HiddenFor(model => model.DepartmentID)
@Html.HiddenFor(model => model.RowVersion)

Ejecute la aplicación, abra la página de índice del sistema, haga clic con el botón derecho en el hipervínculo de eliminación de ciencias naturales y elija abrir en una nueva ventana. Luego haga clic en Editar en la primera ventana para modificar el presupuesto y guardarlo.

departamento de edición

Los cambios se han guardado en la base de datos.

departamentos

Haga clic en el botón Eliminar en la segunda ventana y verá un mensaje de error concurrente.

eliminar departamento

Si vuelve a hacer clic en Eliminar en este momento, la entidad se eliminará y se lo redirigirá a la página de índice.

eliminar departamento

Resumen

En esta sección presentamos cómo lidiar con conflictos concurrentes. Para obtener más información sobre cómo manejar conflictos concurrentes, consulte MSDN. En la siguiente sección presentaremos cómo implementar la tabla de entidades Instructor y Estudiante, cada nivel de herencia.

Información del autor

tom-dykstra Tom Dykstra -Tom Dykstra es un programador senior y escritor en el equipo de Microsoft Web Platform and Tools.

Publicó 40 artículos originales · 25 alabanzas · 100,000+ vistas

Supongo que te gusta

Origin blog.csdn.net/yym373872996/article/details/69486444
Recomendado
Clasificación