Руководство. Обработка параллелизма с помощью EF в приложении ASP.NET MVC 5

В предыдущих руководствах вы узнали, как обновлять данные. В этом руководстве показано, как использовать оптимистичный параллелизм для обработки конфликтов, когда несколько пользователей одновременно обновляют одну сущность. Вы изменяете веб-страницы, работающие с сущностью Department , так, чтобы они обрабатывали ошибки параллелизма. На следующих рисунках показаны страницы "Edit" (Редактирование) и "Delete" (Удаление), включая некоторые сообщения, которые отображаются при возникновении конфликта параллелизма.

Снимок экрана: страница

Снимок экрана: страница

В этом учебнике рассмотрены следующие задачи.

  • Дополнительные сведения о конфликтах параллелизма
  • Добавление оптимистичного параллелизма
  • Изменение контроллера отдела
  • Проверка обработки параллелизма
  • Обновление страницы удаления

Предварительные требования

Конфликты параллелизма

Конфликт параллелизма возникает, когда один пользователь отображает данные сущности, чтобы изменить их, а другой пользователь обновляет данные той же сущности до того, как изменение первого пользователя будет записано в базу данных. Если не включить обнаружение таких конфликтов, то пользователь, обновляющий базу данных последним, перезаписывает изменения другого пользователя. Во многих приложениях такой риск допустим: при небольшом числе пользователей или обновлений, а также в случае, если перезапись некоторых изменений не является критической, стоимость реализации параллелизма может перевесить его преимущества. В этом случае вам не нужно настраивать приложение для обработки конфликтов параллелизма.

Пессимистичный параллелизм (блокировка)

Если приложению нужно предотвратить случайную потерю данных в сценариях параллелизма, одним из способов сделать это являются блокировки базы данных. Это называется пессимистичным параллелизмом. Например, перед чтением строки из базы данных вы запрашиваете блокировку для доступа для обновления или только для чтения. Если заблокировать строку для обновления, другие пользователи не могут заблокировать ее для обновления или только для чтения, так как получат копию данных, которые находятся в процессе изменения. Если заблокировать строку только для чтения, другие пользователи также могут заблокировать ее только для чтения, но не для обновления.

Управление блокировками имеет недостатки. Оно может оказаться сложным с точки зрения программирования. Оно требует значительных ресурсов управления базами данных, а также может вызвать проблемы с производительностью по мере увеличения числа пользователей приложения. Поэтому не все системы управления базами данных поддерживают пессимистичный параллелизм. Платформа Entity Framework не предоставляет встроенной поддержки, и в этом руководстве не показано, как ее реализовать.

Оптимистическая блокировка

Альтернативой пессимистичному параллелизму является оптимистичный параллелизм. Оптимистическая блокировка допускает появление конфликтов параллелизма, а затем обрабатывает их соответствующим образом. Например, Джон запускает страницу Редактирование отделов и изменяет сумму бюджета для английского отдела с 350 000,00 долл. США на 0,00 долл. США.

Прежде чем нажать кнопку Сохранить, Джейн запустит ту же страницу и изменит поле Дата начала с 01.09.2007 на 8.08.2013.

Джон сначала нажимает кнопку Сохранить и видит изменения, когда браузер вернется на страницу Индекс, а затем Джейн нажимает кнопку Сохранить. Дальнейший ход событий определяется порядком обработки конфликтов параллелизма. Некоторые параметры перечислены ниже:

  • Вы можете отслеживать, для какого свойства пользователь изменил и обновил только соответствующие столбцы в базе данных. В этом примере сценария данные не будут потеряны, так как эти два пользователя обновляли разные свойства. В следующий раз, когда кто-то просматривает английский отдел, он увидит изменения Джона и Джейн — дата начала 08.08.2013 и бюджет в 0 долларов.

    Этот метод обновления помогает снизить число конфликтов, которые могут привести к потере данных, но не позволяет избежать такой потери, когда конкурирующие изменения вносятся в одно свойство сущности. То, работает ли Entity Framework в таком режиме, зависит от того, как вы реализуете код обновления. В веб-приложении это часто нецелесообразно, так как может потребоваться обрабатывать большой объем состояний, чтобы отслеживать все исходные значения свойств для сущности, а также новые значения. Обработка большого объема состояний может повлиять на производительность приложения, так как требует ресурсов сервера или должна быть включена непосредственно в веб-страницу (например, в скрытые поля) или файл cookie.

  • Вы можете позволить, чтобы изменения Джейн перезаписывали изменения Джона. В следующий раз, когда кто-то просматривает английский отдел, он увидит 8/8/2013 и восстановленное значение $350,000.00. Такой подход называется победой клиента или сохранением последнего внесенного изменения. (Все значения из клиента имеют приоритет над данными в хранилище.) Как отмечено во введении к этому разделу, если вы не пишете код для обработки параллелизма, она выполняется автоматически.

  • Вы можете предотвратить обновление изменений Джейн в базе данных. Как правило, вы отображаете сообщение об ошибке, показываете текущее состояние данных и разрешаете ей повторно применить изменения, если она по-прежнему хочет их внести. Это называется победой хранилища. (Значения в хранилище имеют приоритет над данными, передаваемыми клиентом.) В этом руководстве вы реализуете сценарий победы хранилища. Данный метод гарантирует, что никакие изменения не перезаписываются без оповещения пользователя о случившемся.

Обнаружение конфликтов параллелизма

Вы можете разрешать конфликты, обрабатывая исключения OptimisticConcurrencyException , создаваемые Entity Framework. Чтобы определить, когда именно нужно выдавать исключения, платформа Entity Framework должна быть в состоянии обнаруживать конфликты. Поэтому нужно соответствующим образом настроить базу данных и модель данных. Ниже приведены некоторые варианты для реализации обнаружения конфликтов:

  • Включите в таблицу базы данных столбец отслеживания, который позволяет определять, когда была изменена строка. Затем можно настроить Entity Framework для включения этого столбца в Where предложение SQL Update или Delete команд.

    Типом данных столбца отслеживания обычно является rowversion. Значение rowversion — это последовательное число, которое увеличивается при каждом обновлении строки. В команде UpdateWhere или Delete предложение включает исходное значение столбца отслеживания (исходная версия строки). Если обновляемая строка была изменена другим пользователем, значение в rowversion столбце отличается от исходного значения, поэтому Update оператору или Delete не удается найти строку для обновления из-за Where предложения . Когда Entity Framework обнаруживает, что ни один из строк не был обновлен командой Update или Delete (то есть, если число затронутых строк равно нулю), она интерпретирует это как конфликт параллелизма.

  • Настройте Entity Framework для включения исходных значений каждого столбца в таблице в Where предложение Update команд и Delete .

    Как и в первом варианте, если что-либо в строке изменилось с момента первого чтения строки, Where предложение не вернет строку для обновления, которую Entity Framework интерпретирует как конфликт параллелизма. Для таблиц базы данных с большим количеством столбцов такой подход может привести к созданию очень больших Where предложений и может потребовать поддержания большого количества состояний. Как было указано ранее, обслуживание большого объема состояний может негативно повлиять на производительность приложения. Поэтому в общем случае данный подход не рекомендуется, кроме того, он не применяется и в этом руководстве.

    Если вы хотите реализовать этот подход к параллелизму, необходимо пометить все свойства, не относящиеся к первичному ключу, в сущности, для которой требуется отслеживать параллелизм, добавив к ним атрибут ConcurrencyCheck . Это изменение позволяет Entity Framework включать все столбцы в предложение SQL WHERE инструкций UPDATE .

В оставшейся части этого руководства вы добавите свойство отслеживания rowversion в Department сущность, создадите контроллер и представления и проверите, что все работает правильно.

Добавление оптимистичного параллелизма

В Models\Department.cs добавьте свойство отслеживания с именем RowVersion:

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

Атрибут Timestamp указывает, что этот столбец будет включен в Where предложение Update команды и Delete , отправляемые в базу данных. Атрибут называется Timestamp, так как в предыдущих версиях SQL Server использовался тип данных timestamp SQL, прежде чем sql rowversion заменил его. Тип .NET для rowversion является массивом байтов.

Если вы предпочитаете использовать fluent API, можно использовать метод IsConcurrencyToken , чтобы указать свойство отслеживания, как показано в следующем примере:

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

Добавив свойство, вы изменили модель базы данных, поэтому нужно выполнить еще одну миграцию. Введите в консоли диспетчера пакетов (PMC) следующие команды:

Add-Migration RowVersion
Update-Database

Изменение контроллера отдела

В файле Controllers\DepartmentController.cs добавьте using оператор :

using System.Data.Entity.Infrastructure;

В файле DepartmentController.cs измените все четыре вхождения "LastName" на "FullName", чтобы в раскрывающихся списках администратора отдела отображалось полное имя преподавателя, а не только фамилия.

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

Замените существующий HttpPostEdit код для метода следующим кодом:

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

Если метод FindAsync возвращает значение null, кафедра была удалена другим пользователем. Приведенный код использует опубликованные значения формы для создания сущности отдела, чтобы можно было повторно отобразить страницу Правка с сообщением об ошибке. Кроме того, повторно создать сущность кафедры не нужно, если вы выводите только сообщение об ошибке без повторного отображения полей кафедры.

Представление сохраняет исходное RowVersion значение в скрытом поле, а метод получает его в параметре rowVersion . Перед вызовом SaveChanges нужно поместить это исходное значение свойства RowVersion в коллекцию OriginalValues для сущности. Затем, когда Entity Framework создает команду SQL UPDATE , эта команда будет включать WHERE предложение, которое ищет строку с исходным RowVersion значением.

Если команда не влияет на UPDATE строки (строки не имеют исходного RowVersion значения), Entity Framework создает DbUpdateConcurrencyException исключение, а код в блоке catch получает затронутую Department сущность из объекта исключения.

var entry = ex.Entries.Single();

Этот объект содержит новые значения, введенные пользователем в его Entity свойстве, и вы можете получить значения, считанные из базы данных, вызвав GetDatabaseValues метод .

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

Метод GetDatabaseValues возвращает значение NULL, если кто-то удалил строку из базы данных; в противном случае необходимо привести возвращенный объект к классу Department , чтобы получить доступ к свойствам Department . (Так как вы уже проверили удаление, будет иметь значение NULL только в том случае, databaseEntry если отдел был удален после FindAsync выполнения и до SaveChanges выполнения.)

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 != currentValues.Name)
    ModelState.AddModelError("Name", "Current value: " + databaseValues.Name);
    // ...

В более длинном сообщении об ошибке объясняется, что произошло и что с этим делать:

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.");

Наконец, код задает RowVersion для Department объекта новое значение, полученное из базы данных. Это новое значение RowVersion будет сохранено в скрытом поле при повторном отображении страницы "Edit" (Редактирование). Когда пользователь в следующий раз нажимает кнопку Save (Сохранить), перехватываются только те ошибки параллелизма, которые возникли с момента повторного отображения страницы "Edit" (Редактирование).

В Views\Department\Edit.cshtml добавьте скрытое поле, чтобы сохранить RowVersion значение свойства сразу после скрытого 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)

Проверка обработки параллелизма

Запустите сайт и щелкните Отделы.

Щелкните правой кнопкой мыши гиперссылку Изменить для отдела английского языка и выберите Открыть на новой вкладке, а затем щелкните гиперссылку Изменить для английского отдела. На двух вкладках отображаются одинаковые сведения.

Измените поле на первой вкладке браузера и нажмите кнопку Save (Сохранить).

В браузере отображается страница индекса с измененным значением.

Измените поле на второй вкладке браузера и нажмите кнопку Сохранить. Отображается сообщение об ошибке:

Снимок экрана: страница

Снова нажмите кнопку Save (Сохранить). Значение, введенное на второй вкладке браузера, сохраняется вместе с исходным значением данных, измененных в первом браузере. Сохраненные значения отображаются при открытии страницы индекса.

Обновление страницы удаления

Для страницы "Delete" (Удаление) платформа Entity Framework обнаруживает конфликты параллелизма, вызванные схожим изменением кафедры. HttpGetDelete Когда метод отображает представление подтверждения, представление включает исходное RowVersion значение в скрытое поле. Затем это значение становится доступным HttpPostDelete для метода, вызываемого при подтверждении удаления пользователем. Когда Entity Framework создает команду SQL DELETE , она включает WHERE предложение с исходным RowVersion значением. Если команда приводит к нулю затронутых строк (это означает, что строка была изменена после отображения страницы подтверждения удаления), создается исключение параллелизма и HttpGet Delete вызывается метод с флагом ошибки, равным true , чтобы повторно отобразить страницу подтверждения с сообщением об ошибке. Кроме того, возможно, что были затронуты нулевые строки, так как строка была удалена другим пользователем, поэтому в этом случае отображается другое сообщение об ошибке.

В файле DepartmentController.cs замените HttpGetDelete метод следующим кодом:

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

Этот метод принимает необязательный параметр, который указывает, отображается ли страница повторно после ошибки параллелизма. Если этот флаг имеет значение true, в представление отправляется сообщение об ошибке с помощью ViewBag свойства .

Замените код в методе HttpPostDelete (с именем DeleteConfirmed) следующим кодом:

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

В шаблонном коде, который вы только что заменили, этот метод принимал только идентификатор записи:

public async Task<ActionResult> DeleteConfirmed(int id)

Вы изменили этот параметр на экземпляр сущности Department, созданный связывателем модели. Это дает доступ к значению RowVersion свойства в дополнение к ключу записи.

public async Task<ActionResult> Delete(Department department)

Вы также изменили имя метода действия с DeleteConfirmed на Delete. Шаблонный код с именем HttpPostDelete метода DeleteConfirmed для предоставления методу HttpPost уникальной сигнатуры. ( Среда CLR требует, чтобы перегруженные методы имели разные параметры метода.) Теперь, когда сигнатуры уникальны, можно придерживаться соглашения MVC и использовать одно и то же имя для HttpPost методов и HttpGet delete.

При перехвате ошибки параллелизма код повторно отображает страницу подтверждения удаления и предоставляет флаг, указывающий, что нужно отобразить сообщение об ошибке параллелизма.

В Views\Department\Delete.cshtml замените шаблонный код следующим кодом, который добавляет поле сообщения об ошибке и скрытые поля для свойств DepartmentID и RowVersion. Изменения выделены.

@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>

Этот код добавляет сообщение об ошибке h2 между заголовками и h3 :

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

Он заменяет LastNameFullName на в Administrator поле :

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

Наконец, он добавляет скрытые DepartmentID поля для свойств и RowVersion после Html.BeginForm оператора :

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

Запустите страницу "Индекс отделов". Щелкните правой кнопкой мыши гиперссылку Удалить для отдела английского языка и выберите Открыть на новой вкладке, а затем на первой вкладке щелкните гиперссылку Изменить для английского отдела.

В первом окне измените одно из значений и нажмите кнопку Сохранить.

На странице Индекс подтверждается изменение.

На второй вкладке нажмите кнопку Delete (Удалить).

Вы видите сообщение об ошибке параллелизма, а значения кафедры обновляются с использованием актуальных сведений из базы данных.

Department_Delete_confirmation_page_with_concurrency_error

Если нажать кнопку Delete (Удалить) еще раз, вы будете перенаправлены на страницу индекса, которая показывает, что кафедра была удалена.

Получите код

Скачивание завершенного проекта

Дополнительные ресурсы

Ссылки на другие ресурсы Entity Framework можно найти в ASP.NET доступ к данным — рекомендуемые ресурсы.

Дополнительные сведения о других способах обработки различных сценариев параллелизма см. в разделах Шаблоны оптимистичного параллелизма и Работа со значениями свойств на сайте MSDN. В следующем руководстве показано, как реализовать наследование таблицы на иерархию для сущностей Instructor и Student .

Дальнейшие действия

В этом учебнике рассмотрены следующие задачи.

  • Дополнительные сведения о конфликтах параллелизма
  • Добавлен оптимистичный параллелизм
  • Измененный контроллер отдела
  • Протестированная обработка параллелизма
  • Обновление страницы удаления

Перейдите к следующей статье, чтобы узнать, как реализовать наследование в модели данных.