本教程介紹如何處理多個用戶并發(fā)更新同一實(shí)體(同時)時出現(xiàn)的沖突。 如果遇到無法解決的問題,請下載或查看已完成的應(yīng)用。 下載說明。
在以下情況下,會發(fā)生并發(fā)沖突:
如果未啟用并發(fā)檢測,當(dāng)發(fā)生并發(fā)更新時:
樂觀并發(fā)允許發(fā)生并發(fā)沖突,并在并發(fā)沖突發(fā)生時作出正確反應(yīng)。 例如,Jane 訪問院系編輯頁面,將英語系的預(yù)算從 350,000.00 美元更改為 0.00 美元。
在 Jane 單擊“保存”之前,John 訪問了相同頁面,并將開始日期字段從 2007/1/9 更改為 2013/1/9。
Jane 先單擊“保存”,并在瀏覽器顯示索引頁時看到她的更改。
John 單擊“編輯”頁面上的“保存”,但頁面的預(yù)算仍顯示為 350,000.00 美元。 接下來的情況取決于并發(fā)沖突的處理方式。
樂觀并發(fā)包括以下選項(xiàng):
當(dāng)屬性配置為并發(fā)令牌時:
數(shù)據(jù)庫和數(shù)據(jù)模型必須配置為支持引發(fā) DbUpdateConcurrencyException。
可使用 ConcurrencyCheck 特性在屬性級別檢測并發(fā)沖突。 該特性可應(yīng)用于模型上的多個屬性。 有關(guān)詳細(xì)信息,請參閱數(shù)據(jù)注釋 - ConcurrencyCheck。
本教程中不使用 [ConcurrencyCheck] 特性。
要檢測并發(fā)沖突,請將 rowversion 跟蹤列添加到模型。 rowversion:
數(shù)據(jù)庫生成 rowversion 序號,該數(shù)字隨著每次行的更新遞增。 在 Update 或 Delete 命令中,Where 子句包括 rowversion 的提取值。 如果要更新的行已更改:
在 EF Core 中,如果未通過 Update 或 Delete 命令更新行,則引發(fā)并發(fā)異常。
在 Models/Department.cs 中,添加名為 RowVersion 的跟蹤屬性:
C#
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
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; }
public int? InstructorID { get; set; }
[Timestamp]
public byte[] RowVersion { get; set; }
public Instructor Administrator { get; set; }
public ICollection<Course> Courses { get; set; }
}
}
Timestamp 特性指定此列包含在 Update 和 Delete 命令的 Where 子句中。 該特性稱為 Timestamp,因?yàn)橹鞍姹镜?SQL Server 在 SQL rowversion 類型將其替換之前使用 SQL timestamp 數(shù)據(jù)類型。
Fluent API 還可指定跟蹤屬性:
C#
modelBuilder.Entity<Department>()
.Property<byte[]>("RowVersion")
.IsRowVersion();
以下代碼顯示更新 Department 名稱時由 EF Core 生成的部分 T-SQL:
SQL
SET NOCOUNT ON;
UPDATE [Department] SET [Name] = @p0
WHERE [DepartmentID] = @p1 AND [RowVersion] = @p2;
SELECT [RowVersion]
FROM [Department]
WHERE @@ROWCOUNT = 1 AND [DepartmentID] = @p1;
前面突出顯示的代碼顯示包含 RowVersion 的 WHERE 子句。 如果數(shù)據(jù)庫 RowVersion 不等于 RowVersion 參數(shù)(@p2),則不更新行。
以下突出顯示的代碼顯示驗(yàn)證更新哪一行的 T-SQL:
SQL
SET NOCOUNT ON;
UPDATE [Department] SET [Name] = @p0
WHERE [DepartmentID] = @p1 AND [RowVersion] = @p2;
SELECT [RowVersion]
FROM [Department]
WHERE @@ROWCOUNT = 1 AND [DepartmentID] = @p1;
@@ROWCOUNT 返回受上一語句影響的行數(shù)。 在沒有行更新的情況下,EF Core 引發(fā) DbUpdateConcurrencyException。
在 Visual Studio 的輸出窗口中可看見 EF Core 生成的 T-SQL。
添加 RowVersion 屬性可更改數(shù)據(jù)庫模型,這需要遷移。
生成項(xiàng)目。 在命令窗口中輸入以下命令:
console
dotnet ef migrations add RowVersion
dotnet ef database update
前面的命令:
按照為“學(xué)生”模型搭建基架中的說明操作,并對模型類使用 Department
。
上述命令為 Department 模型創(chuàng)建基架。 在 Visual Studio 中打開項(xiàng)目。
生成項(xiàng)目。
基架引擎為索引頁創(chuàng)建 RowVersion 列,但不應(yīng)顯示該字段。 本教程中顯示 RowVersion 的最后一個字節(jié),以幫助理解并發(fā)。 不能保證最后一個字節(jié)是唯一的。 實(shí)際應(yīng)用不會顯示 RowVersion 或 RowVersion 的最后一個字節(jié)。
更新索引頁:
以下標(biāo)記顯示更新后的頁面:
HTML
@page
@model ContosoUniversity.Pages.Departments.IndexModel
@{
ViewData["Title"] = "Departments";
}
<h2>Departments</h2>
<p>
<a asp-page="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Department[0].Name)
</th>
<th>
@Html.DisplayNameFor(model => model.Department[0].Budget)
</th>
<th>
@Html.DisplayNameFor(model => model.Department[0].StartDate)
</th>
<th>
@Html.DisplayNameFor(model => model.Department[0].Administrator)
</th>
<th>
RowVersion
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Department) {
<tr>
<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>
<td>
@item.RowVersion[7]
</td>
<td>
<a asp-page="./Edit" asp-route-id="@item.DepartmentID">Edit</a> |
<a asp-page="./Details" asp-route-id="@item.DepartmentID">Details</a> |
<a asp-page="./Delete" asp-route-id="@item.DepartmentID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
使用以下代碼更新 pages\departments\edit.cshtml.cs:
C#
using ContosoUniversity.Data;
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using System.Threading.Tasks;
namespace ContosoUniversity.Pages.Departments
{
public class EditModel : PageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;
public EditModel(ContosoUniversity.Data.SchoolContext context)
{
_context = context;
}
[BindProperty]
public Department Department { get; set; }
// Replace ViewData["InstructorID"]
public SelectList InstructorNameSL { get; set; }
public async Task<IActionResult> OnGetAsync(int id)
{
Department = await _context.Departments
.Include(d => d.Administrator) // eager loading
.AsNoTracking() // tracking not required
.FirstOrDefaultAsync(m => m.DepartmentID == id);
if (Department == null)
{
return NotFound();
}
// Use strongly typed data rather than ViewData.
InstructorNameSL = new SelectList(_context.Instructors,
"ID", "FirstMidName");
return Page();
}
public async Task<IActionResult> OnPostAsync(int id)
{
if (!ModelState.IsValid)
{
return Page();
}
var departmentToUpdate = await _context.Departments
.Include(i => i.Administrator)
.FirstOrDefaultAsync(m => m.DepartmentID == id);
// null means Department was deleted by another user.
if (departmentToUpdate == null)
{
return await HandleDeletedDepartment();
}
// Update the RowVersion to the value when this entity was
// fetched. If the entity has been updated after it was
// fetched, RowVersion won't match the DB RowVersion and
// a DbUpdateConcurrencyException is thrown.
// A second postback will make them match, unless a new
// concurrency issue happens.
_context.Entry(departmentToUpdate)
.Property("RowVersion").OriginalValue = Department.RowVersion;
if (await TryUpdateModelAsync<Department>(
departmentToUpdate,
"Department",
s => s.Name, s => s.StartDate, s => s.Budget, s => s.InstructorID))
{
try
{
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
catch (DbUpdateConcurrencyException ex)
{
var exceptionEntry = ex.Entries.Single();
var clientValues = (Department)exceptionEntry.Entity;
var databaseEntry = exceptionEntry.GetDatabaseValues();
if (databaseEntry == null)
{
ModelState.AddModelError(string.Empty, "Unable to save. " +
"The department was deleted by another user.");
return Page();
}
var dbValues = (Department)databaseEntry.ToObject();
await setDbErrorMessage(dbValues, clientValues, _context);
// Save the current RowVersion so next postback
// matches unless an new concurrency issue happens.
Department.RowVersion = (byte[])dbValues.RowVersion;
// Must clear the model error for the next postback.
ModelState.Remove("Department.RowVersion");
}
}
InstructorNameSL = new SelectList(_context.Instructors,
"ID", "FullName", departmentToUpdate.InstructorID);
return Page();
}
private async Task<IActionResult> HandleDeletedDepartment()
{
Department deletedDepartment = new Department();
// ModelState contains the posted data because of the deletion error and will overide the Department instance values when displaying Page().
ModelState.AddModelError(string.Empty,
"Unable to save. The department was deleted by another user.");
InstructorNameSL = new SelectList(_context.Instructors, "ID", "FullName", Department.InstructorID);
return Page();
}
private async Task setDbErrorMessage(Department dbValues,
Department clientValues, SchoolContext context)
{
if (dbValues.Name != clientValues.Name)
{
ModelState.AddModelError("Department.Name",
$"Current value: {dbValues.Name}");
}
if (dbValues.Budget != clientValues.Budget)
{
ModelState.AddModelError("Department.Budget",
$"Current value: {dbValues.Budget:c}");
}
if (dbValues.StartDate != clientValues.StartDate)
{
ModelState.AddModelError("Department.StartDate",
$"Current value: {dbValues.StartDate:d}");
}
if (dbValues.InstructorID != clientValues.InstructorID)
{
Instructor dbInstructor = await _context.Instructors
.FindAsync(dbValues.InstructorID);
ModelState.AddModelError("Department.InstructorID",
$"Current value: {dbInstructor?.FullName}");
}
ModelState.AddModelError(string.Empty,
"The record you attempted to edit "
+ "was modified by another user after you. 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.");
}
}
}
要檢測并發(fā)問題,請使用來自所提取實(shí)體的 rowVersion 值更新 OriginalValue。 EF Core 使用包含原始 RowVersion 值的 WHERE 子句生成 SQL UPDATE 命令。 如果沒有行受到 UPDATE 命令影響(沒有行具有原始 RowVersion 值),將引發(fā) DbUpdateConcurrencyException 異常。
C#
public async Task<IActionResult> OnPostAsync(int id)
{
if (!ModelState.IsValid)
{
return Page();
}
var departmentToUpdate = await _context.Departments
.Include(i => i.Administrator)
.FirstOrDefaultAsync(m => m.DepartmentID == id);
// null means Department was deleted by another user.
if (departmentToUpdate == null)
{
return await HandleDeletedDepartment();
}
// Update the RowVersion to the value when this entity was
// fetched. If the entity has been updated after it was
// fetched, RowVersion won't match the DB RowVersion and
// a DbUpdateConcurrencyException is thrown.
// A second postback will make them match, unless a new
// concurrency issue happens.
_context.Entry(departmentToUpdate)
.Property("RowVersion").OriginalValue = Department.RowVersion;
在前面的代碼中,Department.RowVersion 為實(shí)體提取后的值。 使用此方法調(diào)用 FirstOrDefaultAsync 時,OriginalValue 為數(shù)據(jù)庫中的值。
以下代碼獲取客戶端值(向此方法發(fā)布的值)和數(shù)據(jù)庫值:
C#
try
{
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
catch (DbUpdateConcurrencyException ex)
{
var exceptionEntry = ex.Entries.Single();
var clientValues = (Department)exceptionEntry.Entity;
var databaseEntry = exceptionEntry.GetDatabaseValues();
if (databaseEntry == null)
{
ModelState.AddModelError(string.Empty, "Unable to save. " +
"The department was deleted by another user.");
return Page();
}
var dbValues = (Department)databaseEntry.ToObject();
await setDbErrorMessage(dbValues, clientValues, _context);
// Save the current RowVersion so next postback
// matches unless an new concurrency issue happens.
Department.RowVersion = (byte[])dbValues.RowVersion;
// Must clear the model error for the next postback.
ModelState.Remove("Department.RowVersion");
}
以下代碼為每列添加自定義錯誤消息,這些列中的數(shù)據(jù)庫值與發(fā)布到 OnPostAsync 的值不同:
C#
private async Task setDbErrorMessage(Department dbValues,
Department clientValues, SchoolContext context)
{
if (dbValues.Name != clientValues.Name)
{
ModelState.AddModelError("Department.Name",
$"Current value: {dbValues.Name}");
}
if (dbValues.Budget != clientValues.Budget)
{
ModelState.AddModelError("Department.Budget",
$"Current value: {dbValues.Budget:c}");
}
if (dbValues.StartDate != clientValues.StartDate)
{
ModelState.AddModelError("Department.StartDate",
$"Current value: {dbValues.StartDate:d}");
}
if (dbValues.InstructorID != clientValues.InstructorID)
{
Instructor dbInstructor = await _context.Instructors
.FindAsync(dbValues.InstructorID);
ModelState.AddModelError("Department.InstructorID",
$"Current value: {dbInstructor?.FullName}");
}
ModelState.AddModelError(string.Empty,
"The record you attempted to edit "
+ "was modified by another user after you. 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.");
}
以下突出顯示的代碼將 RowVersion 值設(shè)置為從數(shù)據(jù)庫檢索的新值。 用戶下次單擊“保存”時,將僅捕獲最后一次顯示編輯頁后發(fā)生的并發(fā)錯誤。
C#
try
{
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
catch (DbUpdateConcurrencyException ex)
{
var exceptionEntry = ex.Entries.Single();
var clientValues = (Department)exceptionEntry.Entity;
var databaseEntry = exceptionEntry.GetDatabaseValues();
if (databaseEntry == null)
{
ModelState.AddModelError(string.Empty, "Unable to save. " +
"The department was deleted by another user.");
return Page();
}
var dbValues = (Department)databaseEntry.ToObject();
await setDbErrorMessage(dbValues, clientValues, _context);
// Save the current RowVersion so next postback
// matches unless an new concurrency issue happens.
Department.RowVersion = (byte[])dbValues.RowVersion;
// Must clear the model error for the next postback.
ModelState.Remove("Department.RowVersion");
}
ModelState 具有舊的 RowVersion 值,因此需使用 ModelState.Remove 語句。 在 Razor 頁面中,當(dāng)兩者都存在時,字段的 ModelState 值優(yōu)于模型屬性值。
使用以下標(biāo)記更新 Pages/Departments/Edit.cshtml:
HTML
@page "{id:int}"
@model ContosoUniversity.Pages.Departments.EditModel
@{
ViewData["Title"] = "Edit";
}
<h2>Edit</h2>
<h4>Department</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<input type="hidden" asp-for="Department.DepartmentID" />
<input type="hidden" asp-for="Department.RowVersion" />
<div class="form-group">
<label>RowVersion</label>
@Model.Department.RowVersion[7]
</div>
<div class="form-group">
<label asp-for="Department.Name" class="control-label"></label>
<input asp-for="Department.Name" class="form-control" />
<span asp-validation-for="Department.Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Department.Budget" class="control-label"></label>
<input asp-for="Department.Budget" class="form-control" />
<span asp-validation-for="Department.Budget" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Department.StartDate" class="control-label"></label>
<input asp-for="Department.StartDate" class="form-control" />
<span asp-validation-for="Department.StartDate" class="text-danger">
</span>
</div>
<div class="form-group">
<label class="control-label">Instructor</label>
<select asp-for="Department.InstructorID" class="form-control"
asp-items="@Model.InstructorNameSL"></select>
<span asp-validation-for="Department.InstructorID" class="text-danger">
</span>
</div>
<div class="form-group">
<input type="submit" value="Save" class="btn btn-default" />
</div>
</form>
</div>
</div>
<div>
<a asp-page="./Index">Back to List</a>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
前面的標(biāo)記:
在英語系打開編輯的兩個瀏覽器實(shí)例:
兩個瀏覽器選項(xiàng)卡顯示相同信息。
在第一個瀏覽器選項(xiàng)卡中更改名稱,然后單擊“保存”。
瀏覽器顯示更改值并更新 rowVersion 標(biāo)記后的索引頁。 請注意更新后的 rowVersion 標(biāo)記,它在其他選項(xiàng)卡的第二回發(fā)中顯示。
在第二個瀏覽器選項(xiàng)卡中更改不同字段。
單擊“保存” 。 可看見所有不匹配數(shù)據(jù)庫值的字段的錯誤消息:
此瀏覽器窗口將不會更改名稱字段。 將當(dāng)前值(語言)復(fù)制并粘貼到名稱字段。 退出選項(xiàng)卡。客戶端驗(yàn)證將刪除錯誤消息。
再次單擊“保存”。 保存在第二個瀏覽器選項(xiàng)卡中輸入的值。 在索引頁中可以看到保存的值。
使用以下代碼更新“刪除”頁模型:
C#
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Threading.Tasks;
namespace ContosoUniversity.Pages.Departments
{
public class DeleteModel : PageModel
{
private readonly ContosoUniversity.Data.SchoolContext _context;
public DeleteModel(ContosoUniversity.Data.SchoolContext context)
{
_context = context;
}
[BindProperty]
public Department Department { get; set; }
public string ConcurrencyErrorMessage { get; set; }
public async Task<IActionResult> OnGetAsync(int id, bool? concurrencyError)
{
Department = await _context.Departments
.Include(d => d.Administrator)
.AsNoTracking()
.FirstOrDefaultAsync(m => m.DepartmentID == id);
if (Department == null)
{
return NotFound();
}
if (concurrencyError.GetValueOrDefault())
{
ConcurrencyErrorMessage = "The record you attempted to delete "
+ "was modified by another user after you selected delete. "
+ "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.";
}
return Page();
}
public async Task<IActionResult> OnPostAsync(int id)
{
try
{
if (await _context.Departments.AnyAsync(
m => m.DepartmentID == id))
{
// Department.rowVersion value is from when the entity
// was fetched. If it doesn't match the DB, a
// DbUpdateConcurrencyException exception is thrown.
_context.Departments.Remove(Department);
await _context.SaveChangesAsync();
}
return RedirectToPage("./Index");
}
catch (DbUpdateConcurrencyException)
{
return RedirectToPage("./Delete",
new { concurrencyError = true, id = id });
}
}
}
}
刪除頁檢測提取實(shí)體并更改時的并發(fā)沖突。 提取實(shí)體后,Department.RowVersion 為行版本。 EF Core 創(chuàng)建 SQL DELETE 命令時,它包括具有 RowVersion 的 WHERE 子句。 如果 SQL DELETE 命令導(dǎo)致零行受影響:
使用以下代碼更新 Pages/Departments/Delete.cshtml:
HTML
@page "{id:int}"
@model ContosoUniversity.Pages.Departments.DeleteModel
@{
ViewData["Title"] = "Delete";
}
<h2>Delete</h2>
<p class="text-danger">@Model.ConcurrencyErrorMessage</p>
<h3>Are you sure you want to delete this?</h3>
<div>
<h4>Department</h4>
<hr />
<dl class="dl-horizontal">
<dt>
@Html.DisplayNameFor(model => model.Department.Name)
</dt>
<dd>
@Html.DisplayFor(model => model.Department.Name)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Department.Budget)
</dt>
<dd>
@Html.DisplayFor(model => model.Department.Budget)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Department.StartDate)
</dt>
<dd>
@Html.DisplayFor(model => model.Department.StartDate)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Department.RowVersion)
</dt>
<dd>
@Html.DisplayFor(model => model.Department.RowVersion[7])
</dd>
<dt>
@Html.DisplayNameFor(model => model.Department.Administrator)
</dt>
<dd>
@Html.DisplayFor(model => model.Department.Administrator.FullName)
</dd>
</dl>
<form method="post">
<input type="hidden" asp-for="Department.DepartmentID" />
<input type="hidden" asp-for="Department.RowVersion" />
<div class="form-actions no-color">
<input type="submit" value="Delete" class="btn btn-default" /> |
<a asp-page="./Index">Back to List</a>
</div>
</form>
</div>
上述標(biāo)記進(jìn)行以下更改:
創(chuàng)建測試系。
在測試系打開刪除的兩個瀏覽器實(shí)例:
兩個瀏覽器選項(xiàng)卡顯示相同信息。
在第一個瀏覽器選項(xiàng)卡中更改預(yù)算,然后單擊“保存”。
瀏覽器顯示更改值并更新 rowVersion 標(biāo)記后的索引頁。 請注意更新后的 rowVersion 標(biāo)記,它在其他選項(xiàng)卡的第二回發(fā)中顯示。
從第二個選項(xiàng)卡中刪除測試部門。并發(fā)錯誤顯示來自數(shù)據(jù)庫的當(dāng)前值。 單擊“刪除”將刪除實(shí)體,除非 RowVersion 已更新,院系已刪除。
請參閱繼承了解如何繼承數(shù)據(jù)模型。
更多建議: