-
学习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.当然如果你不做任何修改,再次点击保存,也会把当前页面上的数据保存到数据库中。如下图。
出处:https://www.cnblogs.com/chillsrc/p/9687312.html