Поделиться через


Обработка параллелизма с entity Framework в приложении MVC ASP.NET (7 из 10)

Том Дайкстра

Пример веб-приложения Contoso University демонстрирует создание ASP.NET приложений MVC 4 с помощью Entity Framework 5 Code First и Visual Studio 2012. Сведения о серии руководств см. в первом руководстве серии.

Примечание.

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

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

Снимок экрана: страница отделов Университета Contoso перед изменениями.

Снимок экрана: страница университета с сообщением о том, что операция отменена, так как значение было изменено другим пользователем.

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

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

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

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

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

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

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

Changing_English_dept_budget_to_100000

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

Changing_English_dept_start_date_to_1999

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

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

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

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

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

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

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

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

    Тип данных столбца отслеживания обычно является строковой версией. Значение 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 .

В оставшейся части этого руководства вы добавите свойство отслеживания строк в 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)]
    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 использовали тип данных метки времени SQL до замены строки SQL. Тип .Net для rowversion — это массив байтов. Если вы предпочитаете использовать api fluent, можно использовать метод IsConcurrencyToken для указания свойства отслеживания, как показано в следующем примере:

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

См. статью о проблеме GitHub Replace IsConcurrencyToken by IsRowVersion.

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

Add-Migration RowVersion
Update-Database

Создание контроллера отдела

Department Создайте контроллер и просмотры так же, как и другие контроллеры, используя следующие параметры:

Add_Controller_dialog_box_for_Department_controller

В Controllers\DepartmentController.cs добавьте инструкцию using :

using System.Data.Entity.Infrastructure;

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

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

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

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(
   [Bind(Include = "DepartmentID, Name, Budget, StartDate, RowVersion, InstructorID")]
    Department department)
{
   try
   {
      if (ModelState.IsValid)
      {
         db.Entry(department).State = EntityState.Modified;
         db.SaveChanges();
         return RedirectToAction("Index");
      }
   }
   catch (DbUpdateConcurrencyException ex)
   {
      var entry = ex.Entries.Single();
      var clientValues = (Department)entry.Entity;
      var databaseValues = (Department)entry.GetDatabaseValues().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.");
      department.RowVersion = databaseValues.RowVersion;
   }
   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 save changes. Try again, and if the problem persists contact your system administrator.");
   }

   ViewBag.InstructorID = new SelectList(db.Instructors, "InstructorID", "FullName", department.InstructorID);
   return View(department);
}

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

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

var entry = ex.Entries.Single();
var clientValues = (Department)entry.Entity;
var databaseValues = (Department)entry.GetDatabaseValues().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()
    @Html.ValidationSummary(true)

    <fieldset>
        <legend>Department</legend>

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

        <div class="editor-label">
            @Html.LabelFor(model => model.Name)
        </div>

В Представлениях\Department\Index.cshtml замените существующий код следующим кодом, чтобы переместить ссылки строк влево и изменить заголовки страницы и заголовки столбцов вместо FullName LastName столбца "Администратор ".

@model IEnumerable<ContosoUniversity.Models.Department>

@{
    ViewBag.Title = "Departments";
}

<h2>Departments</h2>

<p>
    @Html.ActionLink("Create New", "Create")
</p>
<table>
    <tr>
        <th></th>
        <th>Name</th>
        <th>Budget</th>
        <th>Start Date</th>
        <th>Administrator</th>
    </tr>

@foreach (var item in Model) {
    <tr>
        <td>
            @Html.ActionLink("Edit", "Edit", new { id=item.DepartmentID }) |
            @Html.ActionLink("Details", "Details", new { id=item.DepartmentID }) |
            @Html.ActionLink("Delete", "Delete", new { id=item.DepartmentID })
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.Name)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.Budget)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.StartDate)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.Administrator.FullName)
        </td>
    </tr>
}

</table>

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

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

Снимок экрана: страница отделов университета Contoso.

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

Department_Edit_page_before_changes

Измените поле в первом окне браузера и нажмите кнопку "Сохранить".

Department_Edit_page_1_after_change

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

Departments_Index_page_after_first_budget_edit

Измените любое поле во втором окне браузера и нажмите кнопку "Сохранить".

Department_Edit_page_2_after_change

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

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

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

Department_Index_page_with_change_from_second_browser

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

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

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

public ActionResult Delete(int id, bool? concurrencyError)
{
    Department department = db.Departments.Find(id);

    if (concurrencyError.GetValueOrDefault())
    {
        if (department == null)
        {
            ViewBag.ConcurrencyErrorMessage = "The record you attempted to delete "
                + "was deleted by another user after you got the original values. "
                + "Click the Back to List hyperlink.";
        }
        else
        {
            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 ActionResult Delete(Department department)
{
    try
    {
        db.Entry(department).State = EntityState.Deleted;
        db.SaveChanges();
        return RedirectToAction("Index");
    }
    catch (DbUpdateConcurrencyException)
    {
        return RedirectToAction("Delete", new { concurrencyError=true } );
    }
    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 ActionResult DeleteConfirmed(int id)

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

public ActionResult Delete(Department department)

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

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

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

@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>
<fieldset>
    <legend>Department</legend>

    <div class="display-label">
         @Html.DisplayNameFor(model => model.Name)
    </div>
    <div class="display-field">
        @Html.DisplayFor(model => model.Name)
    </div>

    <div class="display-label">
         @Html.DisplayNameFor(model => model.Budget)
    </div>
    <div class="display-field">
        @Html.DisplayFor(model => model.Budget)
    </div>

    <div class="display-label">
         @Html.DisplayNameFor(model => model.StartDate)
    </div>
    <div class="display-field">
        @Html.DisplayFor(model => model.StartDate)
    </div>

    <div class="display-label">
         @Html.DisplayNameFor(model => model.Administrator.FullName)
    </div>
    <div class="display-field">
        @Html.DisplayFor(model => model.Administrator.FullName)
    </div>
</fieldset>
@using (Html.BeginForm()) {
    @Html.AntiForgeryToken()
   @Html.HiddenFor(model => model.DepartmentID)
    @Html.HiddenFor(model => model.RowVersion)
    <p>
        <input type="submit" value="Delete" /> |
        @Html.ActionLink("Back to List", "Index")
    </p>
}

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

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

Он заменяет LastName FullName его в Administrator поле:

<div class="display-label">
    @Html.LabelFor(model => model.InstructorID)
</div>
<div class="display-field">
    @Html.DisplayFor(model => model.Administrator.FullName)
</div>

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

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

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

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

Department_Edit_page_after_change_before_delete

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

Departments_Index_page_after_budget_edit_before_delete

Во втором окне нажмите кнопку "Удалить".

Department_Delete_confirmation_page_before_concurrency_error

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

Department_Delete_confirmation_page_with_concurrency_error

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

Итоги

На этом заканчивается введение в обработку конфликтов параллелизма. Сведения о других способах обработки различных сценариев параллелизма см . в статье "Шаблоны оптимистического параллелизма" и "Работа со значениями свойств" в блоге команды Entity Framework. В следующем руководстве показано, как реализовать наследование таблиц на иерархию для Instructor сущностей и Student сущностей.

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