原文: 學習ASP.NET Core Razor 程式設計系列十八——并發解決方案 學習ASP.NET Core Razor 程式設計系列目錄 學習ASP.NET Core Razor 程式設計系列一 學習ASP.NET Core Razor 程式設計系列二——添加一個實體 學習ASP.NET Core Razor 程式設計系列三——建立資料表及建立項目基本頁面 學習ASP.NET Core Razor 程式設計系列四——Asp.Net Core Razor清單模闆頁面 學習ASP.NET Core Razor 程式設計系列五——Asp.Net Core Razor建立模闆頁面 學習ASP.NET Core Razor 程式設計系列六——資料庫初始化 學習ASP.NET Core Razor 程式設計系列七——修改清單頁面 學習ASP.NET Core Razor 程式設計系列八——并發處理 學習ASP.NET Core Razor 程式設計系列九——增加查詢功能 學習ASP.NET Core Razor 程式設計系列十——添加新字段 學習ASP.NET Core Razor 程式設計系列十一——把新字段更新到資料庫 學習ASP.NET Core Razor 程式設計系列十二——在頁面中增加校驗 學習ASP.NET Core Razor 程式設計系列十三——檔案上傳功能(一) 學習ASP.NET Core Razor 程式設計系列十四——檔案上傳功能(二) 學習ASP.NET Core Razor 程式設計系列十五——檔案上傳功能(三) 學習ASP.NET Core Razor 程式設計系列十六——排序 學習ASP.NET Core Razor 程式設計系列十七——分組 在文章(
)中對于并發錯誤,我們隻是簡單粗暴的進行了異常捕獲,然後抛出了異常。在本文中我們來看兩個解決并發的方法。
樂觀并發的解決方案有以下三種:
1) 可以跟蹤使用者已修改的屬性,并僅更新資料庫中相應的列。
在這種情況下,資料不會丢失。 兩個使用者更新了不同的字段内容(例如:書名與出版社)。下次有人浏覽書籍資訊時,将看到書名和出版社兩個人的更改。 這種更新方法可以減少導緻資料丢失的沖突數。這種方法需要維持重要狀态,以便跟蹤所有資料庫值與目前值,增加了應用複雜,可能會影響應用性能。通常不适用于 Web 應用。
2) 可讓後送出的使用者更改覆寫之前使用者送出的更改。
這種方法稱為“用戶端優先”或“最後一個優先”方案。 (用戶端的所有值優先于資料存儲的值。)如果不對并發處理進行任何編碼,則自動執行“用戶端優先”。
3) 可以阻止在資料庫中更新後一使用者送出的更改。
這種方法,需要顯示錯誤資訊,顯示目前資料和資料庫中的資料,允許使用者重新修改,并儲存。這稱為“存儲優先”方案。 (資料存儲值優先于用戶端送出的值。)
一、用戶端優化
接下去我們來看看“用戶端優先”方案。 此方法確定後一使用者的送出為準,覆寫資料庫中的資料。
樂觀并發允許發生并發沖突,并在并發沖突發生時作出正确反應。 例如,管理者通路用書籍資訊編輯頁面,将“Publishing”字段值修改為“清華大學出版社”。
1.首先,我們使用Visual Studio 2017打開Books\Edit.cshmtl.cs檔案,看一下OnPostAsync()方法,代碼如下。如下圖。
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
_context.Attach(Book).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!_context.Book.Any(e => e.ID == Book.ID))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToPage("./Index");
}
2.在Visual Studio 2017中按F5運作應用程式。在浏覽器中浏覽書籍資訊,并在書籍清單頁面中選擇一條書籍資訊。我們假設有兩個使用者要對此條書籍資訊進行編輯。首先是管理者,對此條書籍資訊修改了“Publishing”的資訊。如下圖。

3.在管理者單擊“Save”按鈕之前,Test使用者通路了相同頁面,并将“出版日期”修改為了“2018-01-08”。如下圖。
4.Test使用者先單擊“儲存”,并在浏覽器的書籍資訊清單頁面中看到了他修改的出版日期資料儲存到了資料庫。如下圖。
5.此時,管理者單擊“編輯”頁面上的“儲存”,但頁面的上的“出版日期”還是“2018-01-13”,按照“用戶端優化”規則會把Test使用者的修改覆寫掉。如下圖。
二、存儲優先
接下去我們來看看“存儲優先”方案。 此方法可確定使用者在未收到警報時不會覆寫任何更改。
首先我們來了解三組值:
- “目前值”是應用程式嘗試寫入資料庫的值。
- “原始值”是在進行任何編輯之前最初從資料庫中檢索的值。
- “資料庫值”是目前存儲在資料庫中的值。
處理并發沖突的正常方法是:
1)在
SaveChanges
期間捕獲
DbUpdateConcurrencyException
。
2)使用
DbUpdateConcurrencyException.Entries
為受影響的實體準備一組新更改。
3)重新整理并發令牌的原始值以反映資料庫中的目前值。
4)重試該過程,直到不發生任何沖突。
下面的示例,使用時間戳作為行級版本号。
1. 在Visual Studio 2017的“解決方案資料總管”中使用滑鼠左鍵輕按兩下打開 Models /Book.cs檔案, 對User實體添加跟蹤屬性RowVersion,并在其上添加Timestamp特性。代碼如下:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
namespace RazorMvcBooks.Models
{
public class Book
{
public int ID { get; set; }
[Required]
[StringLength(50, MinimumLength = 2)]
public string Name { get; set; }
[Display(Name = "出版日期")]
[DataType(DataType.Date)]
public DateTime ReleaseDate { get; set; }
[Range(1,200)]
[DataType(DataType.Currency)]
public decimal Price { get; set; }
public string Author { get; set; }
[ Required]
public string Publishing { get; set; }
[Timestamp]
public byte[] RowVersion { get; set; }
}
}
2.在Visual Studio 2017中選擇“菜單>Nuget包管理器>程式包管理器控制台”,然後在打開的程式包管理器控制台依次執行以下指令
Add-Migration RowVer
Update-Database
3.在SQL Server Management Studio中檢視Book表。如下圖。
4.在Visual Studio 2017的“解決方案資料總管”中使用滑鼠左鍵輕按兩下打開 Pages/Books/Edit.cshtml.cs檔案,對OnPostAsync方法進行修改。Entity Framework Core 使用包含原始 RowVersion 值的 WHERE 子句生成 SQL UPDATE 指令。如果沒有行受到 UPDATE 指令影響(沒有行具有原始 RowVersion 值),将引發 DbUpdateConcurrencyException 異常。代碼如下:
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
var updBook = _context.Book.AsNoTracking().Where(u => u.ID == Book.ID).First();
// 如果為null,則目前使用者資訊已經被 删除
if (updBook == null)
{
return HandDeleteBook();
}
_context.Attach(Book).State = EntityState.Modified;
if (await TryUpdateModelAsync<Book>(
Book,
"Book",
s => s.Name, s =>s.Publishing, s => s.ReleaseDate, s => s.Price))
{
try
{
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
catch (DbUpdateConcurrencyException ex)
{
var exceptionEntry = ex.Entries.Single();
var clientValues = (Book)exceptionEntry.Entity;
var databaseEntry = exceptionEntry.GetDatabaseValues();
if (databaseEntry == null)
{
ModelState.AddModelError(string.Empty, "儲存失敗!.目前使用者資訊已經被删除");
return Page();
}
var dbValues = (Book)databaseEntry.ToObject();
setDbErrorMessage(dbValues, clientValues, _context);
//用資料庫中的 RowVersion 值設定為目前實體對象用戶端界面中的RowVersion值。 使用者下次單擊“儲存”時,将僅捕獲最後一次顯示編輯頁後發生的并發錯誤。
Book.RowVersion = (byte[])dbValues.RowVersion;
//ModelState 具有舊的 RowVersion 值,是以需使用 ModelState.Remove 語句。 在 Razor 頁面中,
//當兩者都存在時,字段的 ModelState 值優于模型屬性值。
ModelState.Remove("Book.RowVersion");
}
}
return Page();
}
private PageResult HandDeleteBook()
{
Book deletedDepartment = new Book();
ModelState.AddModelError(string.Empty,
"儲存失敗!.目前書籍資訊已經被删除!");
return Page();
}
6.在Edit.cshtml.cs檔案,添加setDbErrorMessage方法。為每列添加自定義錯誤消息,當這些列中的資料庫值與用戶端界面上的值不同時,給出相應的錯誤資訊。代碼如下:
private void setDbErrorMessage(Book dbValues,
Book clientValues, BookContext context)
{
if (dbValues.Name != clientValues.Name)
{
ModelState.AddModelError("Book.Name",
$"資料庫值: {dbValues.Name}");
}
if (dbValues.Publishing != clientValues.Publishing)
{
ModelState.AddModelError("Book.Publishing",
$"資料庫值: {dbValues.Publishing}");
}
if (dbValues.ReleaseDate != clientValues.ReleaseDate)
{
ModelState.AddModelError("Book.ReleaseDate",
$"資料庫值: {dbValues.ReleaseDate}");
}
if (dbValues.Price != clientValues.Price)
{
ModelState.AddModelError("Book.Price",
$"資料庫值: {dbValues.Price}");
}
ModelState.AddModelError(string.Empty,"您嘗試編輯的書籍資訊記錄被另一個使用者修改了。編輯操作被取消,"
+ "資料庫中的目前值已經顯示。如果仍想編輯此記錄,請單擊“儲存”按鈕。");
}
7.在Visual Studio 2017的“解決方案資料總管”中使用滑鼠左鍵輕按兩下打開 Pages/Books/Edit.cshtml檔案, <form method="post">标簽下面添加添加隐藏的行版本。必須添加 RowVersion,以便回發綁定值。
<input type="hidden" asp-for="Book.RowVersion" />
8.在Visual Studio 2017中按F5運作應用程式。使用兩個浏覽器打開同一條書籍資訊記錄進行編輯,此時兩個浏覽器顯示的書籍資訊是一樣的。浏覽器1中的書籍資訊界面。在修改了“Publishing”的資料由“清華大學出版社”修改為“機械工業出版社”,然後點選“Save”按鈕。如下圖。
9.在浏覽器中單擊“儲存”之後,浏覽器會自動跳轉到書籍資訊清單頁面中看到了所修改的“Publishing”資料儲存到了資料庫。如下圖。
10.在第二個浏覽器中,修改“出版日期”的值,由“2018-01-13”改為“2018-01-08”。如下圖。
11.然後使用單擊“ Save”按鈕。此時由于用戶端界面上的資訊與資料庫中的值不一樣,是以會出現錯誤提示資訊。如下圖。
12. 把“Publishing”修改為“機械工業出版社”,再次單擊“儲存”,将第二個浏覽器中輸入的值儲存到資料庫。 浏覽器自動跳轉到書籍資訊清單,可以看到儲存的值。如下圖。
13.當然如果你不做任何修改,再次點選儲存,也會把目前頁面上的資料儲存到資料庫中。如下圖。