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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

В оставшейся части этого руководства вы добавите свойство отслеживания строк в 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 указывает, что этот столбец будет включен в предложение Update и Delete команды, отправляемые в Where базу данных. Атрибут называется меткой времени, так как предыдущие версии SQL Server использовали тип данных метки времени SQL до замены строки SQL. Тип .NET для rowversion является массивом байтов.

Если вы предпочитаете использовать текучий 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 . (Так как вы уже проверили удаление, databaseEntry значение null будет иметь значение NULL, только если отдел был удален после 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 для повторного воспроизведения страницы подтверждения с сообщением об ошибке. Также возможно, что 0 строк были затронуты, так как строка была удалена другим пользователем, поэтому в этом случае отображается другое сообщение об ошибке.

В Файле 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 удаления.

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

В 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 сущностей.

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

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

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

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