MVC 5在Visual Basic - 处理并发中具有EF 6

本教程是一系列12的第十个,它教导您如何使用今天福彩字谜总汇框架构建MVC 5应用程序进行数据访问和Visual Basic。在早期的教程中,您学习了如何更新数据。本教程显示如何在多个用户同时更新相同的今天福彩字谜总汇时处理冲突。您将更改与部门今天福彩字谜总汇一起使用的网页,以便处理并发错误。

原辅导系列,由汤姆Dykstra生产 里克安德森 ( @rickandmsft. )使用C#语言编写。我的版本尽可能靠近原件,只改变编码语言。叙述文本从原始文本保持不变,并与Microsoft权限一起使用。

本教程系列教授如何使用今天福彩字谜总汇框架6创建ASP.NET MVC 5应用程序 Visual Studio 2013 Express for web。该系列使用代码第一工作流程。有关如何在代码第一,数据库首先选择的信息的信息,请参阅 今天福彩字谜总汇框架开发工作流程.

教程系列总共包括12个部分。它们旨在顺序顺序排列,因为每个部分都在上一节中赋予的知识。通过这些部分的进步反映在Visual Studio Express for Web项目下载,该部分伴随着具有您通过该系列构建的Web应用程序的每个部分。

下载代码

可用本节的代码 这里。拯救 。压缩 文件到方便的位置,然后提取内容。确保您安装了一个版本的Visual Studio 2013(Express for Web,Professional,Premium或Ultimate),然后双击 .sln. 文件。项目在IDE中打开后,按 Shift + Ctrl + B 建立解决方案。这将确保从Nuget恢复所有包,并且可能需要一段时间根据您的互联网连接速度。

通过该系列的导航路径如下:

  1. 创建今天福彩字谜总汇框架数据模型
  2. 实施基本的CRUD功能
  3. 排序,过滤和分页
  4. 连接弹性和命令拦截
  5. 代码首次迁移和部署
  6. 创建更复杂的数据模型
  7. 阅读相关数据
  8. 更新相关数据
  9. 异步和存储过程
  10. 处理并发
  11. 实现 - 继承
  12. 高级今天福彩字谜总汇框架方案

并发冲突

当一个用户显示今天福彩字谜总汇数据以便编辑它时,发生并发冲突,然后在将第一用户的改变写入数据库之前,另一个用户更新相同的今天福彩字谜总汇的数据。如果您未启用此类冲突的检测,则更新数据库上次覆盖其他用户的更改。在许多应用中,这种风险是可接受的:如果用户少,或者很少的更新,或者如果覆盖某些更改,则是不是真的至关重要,则对并发的编程成本可能超过了益处。在这种情况下,您不必配置应用程序以处理并发冲突。

悲观并发(锁定)

如果您的应用程序确实需要防止并发方案中的意外数据丢失,那么这样做的方式就是使用数据库锁。这就是所谓的 悲观的并发。例如,在从数据库中读取行之前,请请求锁定只读或更新访问。如果您锁定了更新访问权限,则允许其他用户锁定用于只读或更新访问的行,因为它们会获得更改过程中的数据副本。如果锁定只需要只读访问权限,其他人也可以将其锁定为只读访问,但不是更新。

管理锁具有缺点。它可以复杂于程序。它需要重大的数据库管理资源,并且由于应用程序的用户数量增加,它可能会导致性能问题。由于这些原因,并非所有数据库管理系统都支持悲观的并发性。今天福彩字谜总汇框架没有为其提供内置支持,本教程并不显示您如何实现它。

乐观并发

涉及悲观并发的替代方案是 乐观并发。乐观的并发意味着允许并发冲突发生,然后如果他们这样做,就会适当地反应。例如,John运行部门编辑页面,更改 预算 英国部门的金额从350,000.00英镑到0.00英镑。

MVC5与EF6.

在约翰点击之前 保存,Jane运行相同的页面并更改 开始日期 Field 2007-09-01至2014-06-07。

MVC5与EF6.

约翰点击 保存 首先,当浏览器返回索引页面时,请看到他的变化,然后点击 保存。接下来会发生什么是由处理并发冲突的方式决定。部分选项包括以下内容:

  • 您可以跟踪用户已修改的属性并仅更新数据库中的相应列。在示例方案中,没有数据将丢失,因为两个用户更新了不同的属性。下次有人浏览英国部门时,他们将看到约翰和简的变化 - 2014-06-07的开始日期,预算为0.00英镑。

    这种更新方法可以减少可能导致数据丢失的冲突数,但如果对今天福彩字谜总汇的相同属性进行竞争更改,则无法避免数据丢失。今天福彩字谜总汇框架是否有效地取决于您实现更新代码的方式。它通常在Web应用程序中往往不实用,因为它可能需要维护大量状态,以便跟踪今天福彩字谜总汇的所有原始属性值以及新值。维护大量状态可能会影响应用程序性能,因为它要么需要服务器资源,要么必须包含在网页本身(例如,隐藏的字段中)或cookie中。

  • 你可以让简的变化覆盖约翰的改变。下次有人浏览英语部门时,他们将看到2014-06-07和恢复的35万英镑的价值。这被称为a 客户获胜 or 最后一次获胜 设想。 (客户端的所有值都优先于数据存储中的内容。)如在本节的简介中所指出的,如果您不进行任何编码进行并发处理,则会自动发生。

  • 您可以防止Jane的更改在数据库中更新。通常,您将显示一条错误消息,显示其当前数据状态,并允许她重新申请她的更改,如果她仍然想要制作它们。这被称为a 商店获胜 设想。 (数据存储值优先于客户端提交的值。)您将在本教程中实现商店WINS方案。此方法可确保在没有提醒到发生的内容的情况下没有覆盖任何更改。

检测并发冲突

您可以通过处理来解决冲突 OptimisticConcurrencyException. 今天福彩字谜总汇框架抛出的例外情况。为了知道何时抛出这些例外,今天福彩字谜总汇框架必须能够检测冲突。因此,您必须适当地配置数据库和数据模型。有些用于启用冲突检测的选项包括以下内容:

  • 在数据库表中,包含可用于确定行已更改时的跟踪列。然后,您可以将今天福彩字谜总汇框架配置为在SQLupdate或DELETE命令的WHERE子句中包含该列。

    追踪列的数据类型通常是 Rowversion.。这 Rowversion. 值是每次更新行时递增的顺序编号。在A. 更新 or 删除 command, the 在哪里 子句包括跟踪列的原始值(原始行版本)。如果更新的行已被另一个用户更改,则该值 Rowversion. 列与原始值不同,所以 更新 or 删除 声明找不到由于的行为更新 在哪里 条款。当今天福彩字谜总汇框架发现没有任何行已更新时 更新 or 删除 命令(即,当受影响的行数为零时),它将其解释为并发冲突。

  • 配置今天福彩字谜总汇框架以包括表中每个列的原始值 在哪里 clause of 更新 and 删除 commands.

    如在第一个选项中,如果行驶中的行中的任何内容都是改变的,因为第一次读取行,那么 在哪里 子句不会返回要更新的行,今天福彩字谜总汇框架将其作为并发冲突解释为其。对于具有许多列的数据库表,这种方法可能导致非常大 在哪里 条文,并且可以要求您保持大量状态。如前所述,保持大量状态可能会影响应用程序性能。因此,通常不建议使用这种方法,而不是本教程中使用的方法。

    如果您确实希望实现这种方法并发,则必须将您想要通过添加来跟踪并发性的今天福彩字谜总汇中的所有非主键属性 ConcurrencyCheck. 属于他们。该更改使今天福彩字谜总汇框架能够在SQL中包含所有列 在哪里 clause of 更新 statements.

在本教程的其余部分中,您将添加一个 Rowversion. 跟踪财产 部门 今天福彩字谜总汇,创建一个控制器和视图,并测试以验证所有内容是否正常工作。

为部门今天福彩字谜总汇添加乐观的并发属性

在 模特\ Department.VB.,添加名为的跟踪属性 Rowversion.:

进口 System.ComponentModel.DataAnnotations
进口 System.ComponentModel.DataAnnotations.Schema

Namespace Models
    Public Class 部门
        Public Property 部门 As Integer

        <StringLength(50, MinimumLength:=3)>
        Public Property Name As String

        <DataType(DataType.Currency)>
        <Column(TypeName:="money")>
        Public Property 预算 As Decimal

        <DataType(DataType.Date)>
        <DisplayFormat(DataFormatString:="{0:yyyy-MM-dd}", ApplyFormatInEditMode:=True)>
        <Display(Name:="Start Date")>
        Public Property StartDate As DateTime

        Public Property 在structorID As Integer?

        <时间戳>
        Public Property Rowversion. As Byte()

        Public Overridable Property 行政人员 As 在structor
        Public Overridable Property Courses As ICollection(Of Course)

    End Class
End Namespace

这 时间戳 属性指定此列将包含在其中 在哪里 clause of 更新 并删除发送到数据库的命令。调用该属性 时间戳 因为以前版本的SQL Server使用了SQL 时间戳 数据类型在SQL之前 Rowversion. 更换它。 .NET类型为 Rowversion. is a byte array. 

如果您愿意使用Fluent API,您可以使用 iSconcurrengyToken. 指定跟踪属性的方法,如下例所示:

Protected Overrides Sub OnModelCreating(ByVal modelBuilder As DbModelBuilder)
    modelBuilder.Conventions.Remove(Of PluralizingTableNameConvention)()
    modelBuilder.Entity(Of Course)() _
        .HasMany(Function(c) c.Instructors).WithMany(Function(i) i.Courses) _
        .Map(Function(t) t.MapLeftKey("CourseID") _
        .MapRightKey("InstructorID") _
         .ToTable("CourseInstructor"))
     modelBuilder.Entity(Of 部门)().MapToStoredProcedures()
     modelBuilder.Entity(Of 部门)() _
         .Property(Function(p) p.RowVersion).IsConcurrencyToken()
End Sub

通过添加您更改数据库模型的属性,因此您需要执行另一个迁移。在Package Manager控制台(PMC)中,输入以下命令:

Add-Migration RowVersion 
Update-Database

修改部门控制器

在 控制器\ Departmencontroller.vb..,添加A. 进口 statement:

进口 System.Data.Entity.Infrastructure

在里面 Departmencontroller.vb. 文件,更改所有四个出现"LastName" to "FullName"因此,部门管理员下拉列表将包含讲师的全名,而不是姓氏。

viewbag..InstructorID = New SelectList(db.Instructors, "ID", "全名", department.InstructorID)

替换现有代码 HttpPost Edit 具有以下代码的方法:

<httppost()>
<ValidateAntiForgeryToken()>
Async Function 编辑(<Bind(Include:="DepartmentID,Name,Budget,StartDate,RowVersion,InstructorID")> ByVal 部门 As 部门) As Task(Of ActionResult)
    Try
        If ModelState.IsValid 这n
            db.Entry(department).State = 今天福彩字谜总汇State.Modified
            Await db.SaveChangesAsync()
            Return RedirectToAction("Index")
        End If
    Catch ex As dbupdateconcurrencyException.
        Dim entry = ex.Entries.Single()
        Dim clientValues = DirectCast(entry.Entity, 部门)
        Dim databaseEntry = entry.GetDatabaseValues()
        If databaseEntry Is Nothing Then
            ModelState.AddModelError(String.Empty, "Unable to save changes. The department was deleted by another user.")
        Else
            Dim databaseValues = DirectCast(databaseEntry.ToObject(), 部门)

            If databaseValues.Name <> clientValues.Name 这n
                ModelState.AddModelError("Name", "Current value: " & databaseValues.Name)
            End If
            If databaseValues.Budget <> clientValues.Budget 这n
                ModelState.AddModelError("Budget", "Current value: " & String.Format("{0:c}", databaseValues.Budget))
            End If
            If databaseValues.StartDate <> clientValues.StartDate 这n
                ModelState.AddModelError("StartDate", "Current value: " & String.Format("{0:d}", databaseValues.StartDate))
            End If
            If databaseValues.InstructorID <> clientValues.InstructorID 这n
                ModelState.AddModelError("InstructorID", "Current value: " & db.Instructors.Find(databaseValues.InstructorID).FullName)
            End If
            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 cancelled 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
        End If
    Catch dex As RetryLimitExceededException
        'Log the error 
        ModelState.AddModelError(String.Empty, "Unable to save changes. Try again, and if the problem persists contact your system administrator.")
    End Try

    viewbag..InstructorID = New SelectList(db.Instructors, "ID", "FullName", department.InstructorID)
    Return View(department)
End Function

视图将在隐藏字段中存储原始的RowVersion值。当模型粘合剂创造 部门 实例,该对象将具有原始 Rowversion. 属性值和其他属性的新值,由用户在编辑页面上输入。然后当今天福彩字谜总汇框架创建一个SQL时 更新 命令,该命令将包括一个 在哪里 寻找有原始的行的条款 Rowversion. value.

如果没有行受影响 更新 命令(没有行有原件 Rowversion. 值),今天福彩字谜总汇框架抛出了一个 dbupdateconcurrencyException. 例外,捕获块中的代码获取受影响的 部门 来自异常对象的今天福彩字谜总汇。

Dim entry = ex.Entries.Single()

此对象具有用户输入的新值 今天福彩字谜总汇 属性,您可以通过调用来获取从数据库中读取的值 getDatabasevalues. method.

 

Dim clientValues = DirectCast(entry.Entity, 部门) 
Dim databaseEntry = entry.GetDatabaseValues()

这 getDatabasevalues. method returns 没有什么 如果有人从数据库中删除了行;否则,您必须将对象返回给 部门 课程才能访问 部门 properties.

If databaseEntry Is Nothing Then 
    ModelState.AddModelError(String.Empty, "Unable to save changes. The department was deleted by another user.")

更长的错误消息解释发生了什么以及该怎么办?

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 cancelled 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. value of the 部门 对象从数据库中检索的新值。这个新的 Rowversion. 重新显示编辑页面时,值将存储在隐藏字段中,然后下次用户点击时 保存,只捕获自编辑页面的重新显示以来发生的并发错误。

在 观点\部门\ edit.vbhtml,添加一个隐藏的字段以保存 Rowversion. 属性值,立即追随隐藏的字段 部门 property:

@Using (Html.BeginForm())
    @Html.AntiForgeryToken()
    
    @<div class="form-horizontal">
        <h4>部门</h4>
        <hr />
        @Html.ValidationSummary(true)
        @Html.HiddenFor(Function(model) model.DepartmentID)
        @Html.HiddenFor(Function(model) model.RowVersion)

测试乐观并发处理

运行网站并单击 部门:

MVC5与EF6.

右键单击 编辑 英语部门的超链接,选择 在新标签中打开, then click the 编辑 英国部门的超链接。两个选项卡显示相同的信息。

MVC5与EF6.

在第一个浏览器选项卡中更改一个字段,然后单击 保存.

MVC5与EF6.

浏览器显示具有更改值的索引页面。

MVC5与EF6.

在第二次浏览器选项卡中更改一个字段,然后单击 保存.

MVC5与EF6.

点击 保存 在第二个浏览器选项卡中。您会看到错误消息:

MVC5与EF6.

点击 保存 再次。您在第二次浏览器选项卡中输入的值以及您在第一个浏览器中更改的数据的原始值。出现索引页面时,您会看到保存的值。

MVC5与EF6.

更新删除页面

对于删除页面,今天福彩字谜总汇框架检测以类似方式编辑部门的其他人引起的并发冲突。当。。。的时候 httpget删除 方法显示确认视图,视图包括原始版本 Rowversion. 隐藏字段中的值。然后可获得该值 httppost删除 当用户确认删除时调用的方法。当今天福彩字谜总汇框架创建SQL时 删除 命令,它包括一个 在哪里 与原始的条款 Rowversion. 价值。如果该命令导致影响零行(意味着行被改变后) 删除 显示确认页面),抛出并发异常,以及 httpGet. 删除 将错误标志设置为true调用方法,以便在使用错误消息中重新显示确认页面。零行也可能受到影响,因为该行被另一个用户删除,因此在这种情况下,将显示不同的错误消息。

在 Departmencontroller.vb., 更换 httpget删除 具有以下代码的方法:

Async Function 删除(ByVal id As Integer?, ByVal concurrencyError As Boolean?) As Task(Of ActionResult)
    If IsNothing(id) 这n
        Return New HttpStatusCodeResult(HttpStatusCode.BadRequest)
    End If
    Dim 部门 As 部门 = Await db.Departments.FindAsync(id)
    If IsNothing(department) 这n
        If concurrencyError = True Then
            Return RedirectToAction("Index")
        End If
        Return HttpNotFound()
    End If
    If concurrencyError.GetValueOrDefault() 这n
        If IsNothing(department) 这n
            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."
        End If
    End If
    Return View(department)
End Function

该方法接受可选参数,该参数指示在并发错误后是否正在重新显示页面。如果此标志为真,则使用a将错误消息发送到视图 viewbag. property.

替换代码 httppost删除 method (named deleteconfirmed)以下代码:

<httppost()>
<ValidateAntiForgeryToken()>
Async Function 删除(ByVal 部门 As 部门) As Task(Of ActionResult)
    Try
        db.Entry(department).State = 今天福彩字谜总汇State.Deleted
        Await db.SaveChangesAsync()
        Return RedirectToAction("Index")
    Catch ex As dbupdateconcurrencyException.
        Return RedirectToAction("Delete", New With {.concurrencyError = True, .id = department.DepartmentID})
    Catch dex As DataException
        'Log the error 
        ModelState.AddModelError(String.Empty, "Unable to delete. Try again, and if the problem persists contact your system administrator.")
        Return View(department)
    End Try
End Function

在刚刚替换的脚手架代码中,此方法仅接受记录ID:

Async Function deleteconfirmed(ByVal id As 在teger) As Task(Of ActionResult)

您已将此参数更改为a 部门 模型粘合剂创建的今天福彩字谜总汇实例。这使您可以访问 Rowversion. 属性值除了记录键。

Async Function 删除(ByVal 部门 As 部门) As Task(Of ActionResult)

您还更改了操作方法名称 deleteconfirmed to 删除。脚手架代码命名为 httppost删除 method deleteconfirmed to give the httppost  方法是一个独特的签名。 (CLR需要重载的方法具有不同的方法参数。)现在签名是唯一的,您可以坚持使用MVC约定并使用相同的名称 httppost and httpGet. delete methods.

如果捕获并发错误,则代码重新显示删除确认页面并提供标志,指示它应该显示并发错误消息。

在 观点\部门\ delete.vbhtml,用以下代码替换脚手架代码,该代码添加了错误消息字段和隐藏字段 部门Rowversion. 特性。更改突出显示。

@ModelType ContosoUniversity.Models.部门
@Code
    ViewBag.Title = "Delete"
End Code

<h2>删除</h2>

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

<h3>Are you sure you want to delete this?</h3>
<div>
    <h4>部门</h4>
    <hr />
    <dl class="dl-horizontal">
        <dt>
            行政人员
        </dt>

        <dd>
            @Html.DisplayFor(Function(model) model.Administrator.全名)
        </dd>

        <dt>
            @Html.DisplayNameFor(Function(model) model.Name)
        </dt>

        <dd>
            @Html.DisplayFor(Function(model) model.Name)
        </dd>

        <dt>
            @Html.DisplayNameFor(Function(model) model.Budget)
        </dt>

        <dd>
            @Html.DisplayFor(Function(model) model.Budget)
        </dd>

        <dt>
            @Html.DisplayNameFor(Function(model) model.StartDate)
        </dt>

        <dd>
            @Html.DisplayFor(Function(model) model.StartDate)
        </dd>

    </dl>
    @Using (Html.BeginForm())
        @Html.AntiForgeryToken()
        @Html.HiddenFor(Function(model) model.DepartmentID)
        @Html.HiddenFor(Function(model) model.RowVersion)
        
        @<div class="form-actions no-color">
            <input type="submit" value="Delete" class="btn btn-default" /> |
            @Html.ActionLink("Back to List", "Index")
        </div>
    End Using
</div>

此代码会在其中添加错误消息 h2 and h3 headings:

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

它取代了  with 全名 in the 行政人员 field:

<dt>
    行政人员
</dt>
<dd> 
    @Html.DisplayFor(Function(model) model.Administrator.全名) 
</dd>

最后,它为此添加了隐藏的字段 部门 and Rowversion. properties after the html.beginform. statement:

@Html.HiddenFor(Function(model) model.DepartmentID) 
@Html.HiddenFor(Function(model) model.RowVersion)

运行部门索引页面。右键单击 删除 英语部门的超链接,选择 在新标签中打开, 然后在第一个标签中单击 Edit 英国部门的超链接。

在第一个窗口中,更改其中一个值,然后单击 保存 :

MVC5与EF6.

索引页确认更改。

MVC5与EF6.

在第二个选项卡中,单击 删除.

MVC5与EF6.

您可以看到并发错误消息,并且刷新部门值与当前数据库中的内容刷新。

MVC5与EF6.

如果你点击 删除 同样,您再次被重定向到索引页面,该页面显示该部门已被删除。

概括

这完成了处理并发冲突的介绍。有关处理各种并发方案的其他方法的信息,请参阅 乐观的并发模式 and 使用财产价值 on MSDN. The 下一个教程 展示如何为教师和学生今天福彩字谜总汇实现每个层次结构继承。