Обработка параллелизма с entity Framework в приложении MVC ASP.NET (7 из 10)
Пример веб-приложения Contoso University демонстрирует создание ASP.NET приложений MVC 4 с помощью Entity Framework 5 Code First и Visual Studio 2012. Сведения о серии руководств см. в первом руководстве серии.
Примечание.
Если вы не сможете устранить проблему, скачайте завершенную главу и попытайтесь воспроизвести проблему. Как правило, решение проблемы можно найти, сравнивая код с завершенным кодом. Некоторые распространенные ошибки и способы их устранения см. в статье об ошибках и обходных решениях.
В предыдущих двух руководствах вы работали с связанными данными. В этом руководстве показано, как обрабатывать параллелизм. Вы создадите веб-страницы, которые работают с сущностью Department
, а страницы, которые редактируют и удаляют Department
сущности, будут обрабатывать ошибки параллелизма. На следующих рисунках показаны страницы индекса и удаления, включая некоторые сообщения, отображаемые при возникновении конфликта параллелизма.
Конфликты параллелизма
Конфликт параллелизма возникает, когда один пользователь отображает данные сущности, чтобы изменить их, а другой пользователь обновляет данные той же сущности до того, как изменение первого пользователя будет записано в базу данных. Если не включить обнаружение таких конфликтов, то пользователь, обновляющий базу данных последним, перезаписывает изменения другого пользователя. Во многих приложениях такой риск допустим: при небольшом числе пользователей или обновлений, а также в случае, если перезапись некоторых изменений не является критической, стоимость реализации параллелизма может перевесить его преимущества. В этом случае вам не нужно настраивать приложение для обработки конфликтов параллелизма.
Пессимистичная параллелизм (блокировка)
Если приложению нужно предотвратить случайную потерю данных в сценариях параллелизма, одним из способов сделать это являются блокировки базы данных. Это называется пессимистической параллелизмом. Например, перед чтением строки из базы данных вы запрашиваете блокировку для доступа для обновления или только для чтения. Если заблокировать строку для обновления, другие пользователи не могут заблокировать ее для обновления или только для чтения, так как получат копию данных, которые находятся в процессе изменения. Если заблокировать строку только для чтения, другие пользователи также могут заблокировать ее только для чтения, но не для обновления.
Управление блокировками имеет недостатки. Оно может оказаться сложным с точки зрения программирования. Для этого требуются значительные ресурсы управления базами данных, и это может привести к проблемам с производительностью, так как число пользователей приложения увеличивается (т. е. оно не хорошо масштабируется). Поэтому не все системы управления базами данных поддерживают пессимистичный параллелизм. Entity Framework не поддерживает встроенную поддержку, и в этом руководстве не показано, как реализовать его.
Оптимистическая блокировка
Альтернатива пессимистическому параллелизму является оптимистической параллелизмом. Оптимистическая блокировка допускает появление конфликтов параллелизма, а затем обрабатывает их соответствующим образом. Например, Джон запускает страницу редактирования отделов, изменяет сумму бюджета для английского отдела с $ 35000,000 до $ 0,00.
Перед нажатием кнопки "Сохранить" Джейн запускает ту же страницу и изменяет поле "Дата начала" с 9.1.2007 на 8.8.2013.
Джон нажимает кнопку "Сохранить сначала" и видит его изменение, когда браузер возвращается на страницу индекса, а затем Джейн нажимает кнопку "Сохранить". Дальнейший ход событий определяется порядком обработки конфликтов параллелизма. Некоторые параметры перечислены ниже:
Вы можете отслеживать, для какого свойства пользователь изменил и обновил только соответствующие столбцы в базе данных. В этом примере сценария данные не будут потеряны, так как эти два пользователя обновляли разные свойства. В следующий раз, когда кто-то просматривает английский отдел, они увидят изменения Джона и Джейн - дата начала 8/8/2013 и бюджет нулевых долларов.
Этот метод обновления помогает снизить число конфликтов, которые могут привести к потере данных, но не позволяет избежать такой потери, когда конкурирующие изменения вносятся в одно свойство сущности. То, работает ли Entity Framework в таком режиме, зависит от того, как вы реализуете код обновления. В веб-приложении это часто нецелесообразно, так как может потребоваться обрабатывать большой объем состояний, чтобы отслеживать все исходные значения свойств для сущности, а также новые значения. Сохранение большого количества состояния может повлиять на производительность приложения, так как требует ресурсов сервера или должно быть включено в саму веб-страницу (например, в скрытых полях).
Вы можете позволить Джейн изменить изменение Джона. В следующий раз, когда кто-то просматривает английский отдел, они увидят 8/8/2013 и восстановленное значение $ 350 000,000. Такой подход называется победой клиента или сохранением последнего внесенного изменения. (Значения клиента имеют приоритет над тем, что находится в хранилище данных.) Как отмечалось в этом разделе, если вы не выполняете код для обработки параллелизма, это произойдет автоматически.
Вы можете предотвратить обновление изменений Джейн в базе данных. Как правило, вы будете отображать сообщение об ошибке, показывать ей текущее состояние данных и разрешать ей повторно применять изменения, если она все еще хочет сделать их. Это называется победой хранилища. (Значения хранилища данных имеют приоритет над значениями, отправленными клиентом.) Вы реализуете сценарий Магазина Wins в этом руководстве. Данный метод гарантирует, что никакие изменения не перезаписываются без оповещения пользователя о случившемся.
Обнаружение конфликтов параллелизма
Вы можете устранить конфликты, обрабатывая исключения ОптимистичногоConcurrencyException , вызываемые Entity Framework. Чтобы определить, когда именно нужно выдавать исключения, платформа Entity Framework должна быть в состоянии обнаруживать конфликты. Поэтому нужно соответствующим образом настроить базу данных и модель данных. Ниже приведены некоторые варианты для реализации обнаружения конфликтов:
Включите в таблицу базы данных столбец отслеживания, который позволяет определять, когда была изменена строка. Затем можно настроить Entity Framework для включения этого столбца в
Where
предложение SQLUpdate
или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
Создайте контроллер и просмотры так же, как и другие контроллеры, используя следующие параметры:
В 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>
Тестирование обработки оптимистического параллелизма
Запустите сайт и щелкните " Отделы":
Щелкните правой кнопкой мыши гиперссылку "Изменить " для Ким Abercrombie и выберите "Открыть" на новой вкладке, а затем щелкните гиперссылку "Изменить " для Ким Abercrombie. В двух окнах отображаются одни и те же сведения.
Измените поле в первом окне браузера и нажмите кнопку "Сохранить".
В браузере отображается страница индекса с измененным значением.
Измените любое поле во втором окне браузера и нажмите кнопку "Сохранить".
Нажмите кнопку " Сохранить " во втором окне браузера. Отображается сообщение об ошибке:
Щелкните Сохранить еще раз. Значение, введенное во втором браузере, сохраняется вместе с исходным значением измененных данных в первом браузере. Сохраненные значения отображаются при открытии страницы индекса.
Обновление страницы удаления
Для страницы "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 (Сохранить):
Страница индекса подтверждает изменение.
Во втором окне нажмите кнопку "Удалить".
Вы видите сообщение об ошибке параллелизма, а значения кафедры обновляются с использованием актуальных сведений из базы данных.
Если нажать кнопку Delete (Удалить) еще раз, вы будете перенаправлены на страницу индекса, которая показывает, что кафедра была удалена.
Итоги
На этом заканчивается введение в обработку конфликтов параллелизма. Сведения о других способах обработки различных сценариев параллелизма см . в статье "Шаблоны оптимистического параллелизма" и "Работа со значениями свойств" в блоге команды Entity Framework. В следующем руководстве показано, как реализовать наследование таблиц на иерархию для Instructor
сущностей и Student
сущностей.
Ссылки на другие ресурсы Entity Framework можно найти на карте содержимого доступа к данным ASP.NET.