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

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

Department_Edit_page_2_after_clicking_Save

Department_Edit_page_2_after_clicking_Save

Изучив это руководство, вы:

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

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

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

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

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

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

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

Оптимистический параллелизм

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

Прежде чем Джон нажмет кнопку " сохранить", Мария выполняет ту же страницу и изменит поле " Дата начала " с 9/1/2007 на 8/8/2013.

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

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

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

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

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

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

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

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

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

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

В моделс\департмент.КСдобавьте свойство отслеживания с именем 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 команды, отправляемые в базу данных. Атрибут называется меткой времени , так как предыдущие версии SQL Server использовали тип данных timestamp SQL до того, как он заменил значение rowversion SQL. Тип для rowversion — массив байтов.

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

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

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

Add-Migration RowVersion
Update-Database

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

В контроллерс\департментконтроллер.КСдобавьте инструкцию using:

using System.Data.Entity.Infrastructure;

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

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

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

[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, только если отдел был удален после выполнения 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" (Редактирование).

В виевс\департмент\едит.кштмлДобавьте скрытое поле, чтобы сохранить значение свойства 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 (Сохранить).

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

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

Department_Edit_page_2_after_clicking_Save

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

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

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

В DepartmentController.CSзамените метод HttpGet Delete следующим кодом:

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.

Замените код в методе HttpPost Delete (с именем 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. Шаблонный код с именем HttpPost метод Delete DeleteConfirmed, чтобы присвоить методу HttpPost уникальную сигнатуру. (Среда CLR требует, чтобы перегруженные методы имели различные параметры метода.) Теперь, когда сигнатуры уникальны, можно придерживаться соглашения MVC и использовать одно и то же имя для методов HttpPost и HttpGet DELETE.

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

В виевс\департмент\делете.кштмлзамените код шаблона следующим кодом, который добавляет поле сообщения об ошибке и скрытые поля для свойств 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>

Он заменяет LastName FullName в поле 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.

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

Изучив это руководство, вы:

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

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