包装事务内的数据库修改 (VB)

作者 :Scott Mitchell

下载 PDF

本教程是介绍更新、删除和插入数据批处理的四个教程中的第一个。 在本教程中,我们将了解数据库事务如何允许批量修改作为原子操作进行,这可确保所有步骤都成功或所有步骤都失败。

简介

插入、更新和删除数据概述 教程开始,GridView 提供了对行级编辑和删除的内置支持。 只需单击几下鼠标,即可创建一个丰富的数据修改接口,而无需编写一行代码,只要你满足于按行编辑和删除即可。 但是,在某些情况下,这是不够的,我们需要为用户提供编辑或删除一批记录的能力。

例如,大多数基于 Web 的电子邮件客户端使用网格列出每条消息,其中每一行都包含一个复选框,以及) (主题、发件人等的电子邮件信息。 此界面允许用户通过检查多个邮件,然后单击“删除所选邮件”按钮来删除这些邮件。 在用户通常编辑许多不同的记录的情况下,批处理编辑界面是理想的选择。 批处理编辑界面将呈现每一行及其编辑界面,而不是强制用户单击“编辑”,进行更改,然后针对需要修改的每个记录单击“更新”。 用户可以快速修改需要更改的行集,然后单击“全部更新”按钮保存这些更改。 在这组教程中,我们将介绍如何创建用于插入、编辑和删除数据批次的接口。

执行批处理操作时,请务必确定批处理中的某些操作是否可以成功,而其他操作则失败。 请考虑批量删除接口 - 如果第一个选定记录已成功删除,但第二个记录因外键约束冲突而失败,会发生什么情况? 删除的第一条记录应该回滚还是第一条记录保持删除状态是否可接受?

如果希望批处理操作被视为 原子操作(其中所有步骤都成功或所有步骤都失败),则需要扩充数据访问层,以包括对 数据库事务的支持。 数据库事务保证在事务保护下执行的 、 UPDATEDELETE 语句集INSERT的原子性,并且是大多数新式数据库系统都支持的一项功能。

本教程介绍如何扩展 DAL 以使用数据库事务。 后续教程将介绍实现网页以批量插入、更新和删除接口。 让我们开始吧!

注意

修改批处理事务中的数据时,并不总是需要原子性。 在某些情况下,某些数据修改成功,而同一批中的其他数据修改失败是可以接受的,例如从基于 Web 的电子邮件客户端中删除一组电子邮件时。 如果在删除过程中出现数据库错误,则删除未出错处理的记录可能是可以接受的。 在这种情况下,无需修改 DAL 以支持数据库事务。 但是,还有其他批处理操作方案,其中原子性至关重要。 当客户将资金从一个银行账户转移到另一个银行账户时,必须执行两项操作:必须从第一个帐户中扣除资金,然后添加到第二个帐户。 虽然银行可能不介意第一步成功,但第二步失败,其客户会感到沮丧是可以理解的。 我鼓励你完成本教程并实现 DAL 的增强功能,以支持数据库事务,即使你不打算在以下三个教程中构建的批量插入、更新和删除接口中使用它们。

事务概述

大多数数据库都支持事务,这些 事务允许将多个数据库命令分组到单个逻辑工作单元中。 构成事务的数据库命令保证为原子命令,这意味着所有命令都将失败或全部成功。

通常,事务通过 SQL 语句使用以下模式实现:

  1. 指示事务的开始时间。
  2. 执行构成事务的 SQL 语句。
  3. 如果步骤 2 中的某个语句出错,请回滚事务。
  4. 如果步骤 2 中的所有语句都未出错,请提交事务。

在编写 SQL 脚本或创建存储过程时,可以手动输入用于创建、提交和回滚事务的 SQL 语句,或者通过编程方式使用命名空间中的 System.TransactionsADO.NET 或类。 在本教程中,我们将仅介绍使用 ADO.NET 管理事务。 在将来的教程中,我们将介绍如何在数据访问层中使用存储过程,届时我们将探索用于创建、回滚和提交事务的 SQL 语句。 同时,有关详细信息,请参阅管理SQL Server存储过程中的事务

注意

TransactionScope命名空间中的 System.Transactions 类使开发人员能够以编程方式包装事务范围内的一系列语句,并包括对涉及多个源的复杂事务的支持,例如两个不同的数据库,甚至异类类型的数据存储,例如 Microsoft SQL Server 数据库、Oracle 数据库和 Web 服务。 我决定在本教程中使用 ADO.NET 事务而不是 类, TransactionScope 因为 ADO.NET 更具体地用于数据库事务,并且在许多情况下,资源消耗要少得多。 此外,在某些情况下, TransactionScope 类使用 Microsoft 分布式事务处理协调器 (MSDTC) 。 围绕 MSDTC 的配置、实现和性能问题使其成为一个相当专业和高级的主题,超出了这些教程的范围。

在 ADO.NET 中使用 SqlClient 提供程序时,通过调用 SqlConnection方法BeginTransaction启动事务,该方法返回 对象SqlTransaction。 构成事务的数据修改语句放置在块 try...catch 中。 如果块中的try语句中发生错误,则执行会传输到块,catch在该块中,事务可以通过 对象 s Rollback 方法回滚SqlTransaction。 如果所有语句都成功完成,则调用 SqlTransaction 块末尾try的对象 s Commit 方法将提交事务。 以下代码片段演示了此模式。

' Create the SqlTransaction object
Dim myTransaction As SqlTransaction = SqlConnectionObject.BeginTransaction();
Try
    '
    ' ... Perform the database transaction�s data modification statements...
    '
    ' If we reach here, no errors, so commit the transaction
    myTransaction.Commit()
Catch
    ' If we reach here, there was an error, so rollback the transaction
    myTransaction.Rollback()
    Throw
End Try

默认情况下,类型化数据集中的 TableAdapter 不使用事务。 为了提供对事务的支持,我们需要扩充 TableAdapter 类,以包括使用上述模式在事务范围内执行一系列数据修改语句的其他方法。 在步骤 2 中,我们将了解如何使用分部类添加这些方法。

步骤 1:创建使用批处理数据网页

在开始探索如何扩充 DAL 以支持数据库事务之前,让我们先花点时间创建本教程所需的 ASP.NET 网页和后面的三个网页。 首先添加名为 BatchData 的新文件夹,然后添加以下 ASP.NET 页,将每个页面与 Site.master 母版页相关联。

  • Default.aspx
  • Transactions.aspx
  • BatchUpdate.aspx
  • BatchDelete.aspx
  • BatchInsert.aspx

为 SqlDataSource-Related 教程添加 ASP.NET 页面

图 1:为 SqlDataSource-Related 教程添加 ASP.NET 页

与其他文件夹一样, Default.aspx 将使用 SectionLevelTutorialListing.ascx 用户控件列出其部分中的教程。 因此,将此用户控件Default.aspx从解决方案资源管理器拖到页面设计视图中,将其添加到 。

将 SectionLevelTutorialListing.ascx 用户控件添加到 Default.aspx

图 2:将 SectionLevelTutorialListing.ascx 用户控件添加到 Default.aspx (单击以查看全尺寸图像)

最后,将这四个页面作为条目添加到 Web.sitemap 文件中。 具体而言,在自定义站点地图 <siteMapNode>之后添加以下标记:

<siteMapNode title="Working with Batched Data" 
    url="~/BatchData/Default.aspx" 
    description="Learn how to perform batch operations as opposed to 
                 per-row operations.">
    
    <siteMapNode title="Adding Support for Transactions" 
        url="~/BatchData/Transactions.aspx" 
        description="See how to extend the Data Access Layer to support 
                     database transactions." />
    <siteMapNode title="Batch Updating" 
        url="~/BatchData/BatchUpdate.aspx" 
        description="Build a batch updating interface, where each row in a 
                      GridView is editable." />
    <siteMapNode title="Batch Deleting" 
        url="~/BatchData/BatchDelete.aspx" 
        description="Explore how to create an interface for batch deleting 
                     by adding a CheckBox to each GridView row." />
    <siteMapNode title="Batch Inserting" 
        url="~/BatchData/BatchInsert.aspx" 
        description="Examine the steps needed to create a batch inserting 
                     interface, where multiple records can be created at the 
                     click of a button." />
</siteMapNode>

更新 Web.sitemap后,请花点时间通过浏览器查看教程网站。 左侧的菜单现在包含使用批处理数据教程的项。

站点地图现在包括使用批处理数据教程的条目

图 3:站点地图现在包含使用批处理数据教程的条目

步骤 2:更新数据访问层以支持数据库事务

正如我们在第 一个教程创建数据访问层中所述,DAL 中的类型化数据集由 DataTable 和 TableAdapter 组成。 DataTable 保存数据,而 TableAdapters 提供从数据库将数据读取到 DataTables、使用对 DataTable 所做的更改更新数据库等的功能。 回想一下,TableAdapters 提供了两种用于更新数据的模式,我称之为 Batch Update 和 DB-Direct。 使用 Batch 更新模式时,向 TableAdapter 传递 DataSet、DataTable 或 DataRows 集合。 此数据是枚举的,对于每个插入、修改或删除的行, InsertCommand将执行 、 UpdateCommandDeleteCommand 。 使用 DB-Direct 模式时,TableAdapter 改为传递插入、更新或删除单个记录所需的列的值。 然后,DB Direct 模式方法使用这些传入值来执行相应的 InsertCommandUpdateCommandDeleteCommand 语句。

无论使用的更新模式如何,TableAdapters 自动生成的方法都不使用事务。 默认情况下,TableAdapter 执行的每个插入、更新或删除操作都被视为单个离散操作。 例如,假设 BLL 中的某些代码使用 DB-Direct 模式将十条记录插入数据库中。 此代码将调用 TableAdapter 方法 Insert 十次。 如果前五次插入成功,但第六次插入导致异常,则前五个插入的记录将保留在数据库中。 同样,如果使用 Batch 更新模式对 DataTable 中插入、修改和删除的行执行插入、更新和删除操作,则如果前几次修改成功,但后来的修改遇到错误,则先前完成的修改将保留在数据库中。

在某些情况下,我们希望确保一系列修改的原子性。 为此,我们必须在事务的保护伞下添加执行 InsertCommandUpdateCommandDeleteCommand 的新方法,手动扩展 TableAdapter。 在 创建数据访问层 中,我们介绍了如何使用 分部类 扩展类型化数据集中 DataTable 的功能。 此方法还可以与 TableAdapters 一起使用。

类型化数据集 Northwind.xsd 位于 App_Code 文件夹子 DAL 文件夹中。 在名为 的 DAL 文件夹中创建一个 TransactionSupport 子文件夹,并添加一个名为 ProductsTableAdapter.TransactionSupport.vb 的新类文件 (请参阅图 4) 。 此文件将保存 的部分实现, ProductsTableAdapter 其中包括使用事务执行数据修改的方法。

添加名为 TransactionSupport 的文件夹和名为 ProductsTableAdapter.TransactionSupport.vb 的类文件

图 4:添加名为 TransactionSupport 的文件夹和名为 的类文件 ProductsTableAdapter.TransactionSupport.vb

在 文件中输入以下代码 ProductsTableAdapter.TransactionSupport.vb

Imports System.Data
Imports System.Data.SqlClient
Namespace NorthwindTableAdapters
    Partial Public Class ProductsTableAdapter
        Private _transaction As SqlTransaction
        Private Property Transaction() As SqlTransaction
            Get
                Return Me._transaction
            End Get
            Set(ByVal Value As SqlTransaction)
                Me._transaction = Value
            End Set
        End Property
        Public Sub BeginTransaction()
            ' Open the connection, if needed
            If Me.Connection.State <> ConnectionState.Open Then
                Me.Connection.Open()
            End If
            ' Create the transaction and assign it to the Transaction property
            Me.Transaction = Me.Connection.BeginTransaction()
            ' Attach the transaction to the Adapters
            For Each command As SqlCommand In Me.CommandCollection
                command.Transaction = Me.Transaction
            Next
            Me.Adapter.InsertCommand.Transaction = Me.Transaction
            Me.Adapter.UpdateCommand.Transaction = Me.Transaction
            Me.Adapter.DeleteCommand.Transaction = Me.Transaction
        End Sub
        Public Sub CommitTransaction()
            ' Commit the transaction
            Me.Transaction.Commit()
            ' Close the connection
            Me.Connection.Close()
        End Sub
        Public Sub RollbackTransaction()
            ' Rollback the transaction
            Me.Transaction.Rollback()
            ' Close the connection
            Me.Connection.Close()
        End Sub
    End Class
End Namespace

Partial 此处的类声明中的关键字 (keyword) 向编译器指示,在 中添加的成员将添加到 ProductsTableAdapter 命名空间中的 NorthwindTableAdapters 类中。 Imports System.Data.SqlClient请注意文件顶部的 语句。 由于 TableAdapter 配置为使用 SqlClient 提供程序,因此它在内部使用 SqlDataAdapter 对象向数据库发出其命令。 因此,我们需要使用 SqlTransaction 类开始事务,然后提交或回滚事务。 如果使用 Microsoft SQL Server 以外的数据存储,则需要使用适当的提供程序。

这些方法提供启动、回滚和提交事务所需的构建基块。 它们被标记为 Public,以便从 内部 ProductsTableAdapter、DAL 中的另一个类或体系结构中的另一层(如 BLL)使用它们。 BeginTransaction 如果需要) 打开 TableAdapter 的内部 SqlConnection (,启动事务并将其 Transaction 分配给 属性,并将事务附加到内部 SqlDataAdapter 对象 SqlCommandCommitTransactionRollbackTransaction 分别在Transaction关闭内部Connection对象Commit之前调用 对象 和 Rollback 方法。

步骤 3:添加在事务保护下更新和删除数据的方法

完成这些方法后,我们准备向 或 BLL 添加方法 ProductsDataTable ,以在事务的保护下执行一系列命令。 以下方法使用 Batch 更新模式通过事务更新 ProductsDataTable 实例。 它通过调用 方法启动事务, BeginTransaction 然后使用 Try...Catch 块发出数据修改语句。 如果对 Adapter 对象 方法的 Update 调用导致异常,则执行将传输到块, catch 在该块中将回滚事务并重新引发异常。 回想一下, Update 方法通过枚举提供的 ProductsDataTable 的行并执行必要的 InsertCommandUpdateCommandDeleteCommand 来实现批处理更新模式。 如果这些命令中的任何一个导致错误,则会回滚事务,撤消在事务生存期内所做的先前修改。 Update如果语句完成且没有错误,则事务将全部提交。

Public Function UpdateWithTransaction _
    (ByVal dataTable As Northwind.ProductsDataTable) As Integer
    
    Me.BeginTransaction()
    Try
        ' Perform the update on the DataTable
        Dim returnValue As Integer = Me.Adapter.Update(dataTable)
        ' If we reach here, no errors, so commit the transaction
        Me.CommitTransaction()
        Return returnValue
    Catch
        ' If we reach here, there was an error, so rollback the transaction
        Me.RollbackTransaction()
        Throw
    End Try
End Function

UpdateWithTransaction通过 中的ProductsTableAdapter.TransactionSupport.vb分部类将 方法添加到 ProductsTableAdapter 类。 或者,此方法可以添加到业务逻辑层 类 ProductsBLL ,但略有语法更改。 也就是说,、 和 中的Me.CommitTransaction()关键字 (keyword) 需要替换为 Adapter (召回,该Adapter召回是) 类型的ProductsTableAdapter属性ProductsBLL的名称。 MeMe.BeginTransaction()Me.RollbackTransaction()

方法 UpdateWithTransaction 使用 Batch 更新模式,但也可以在事务范围内使用一系列 DB-Direct 调用,如以下方法所示。 方法DeleteProductsWithTransaction接受 类型Integer为 的输入 List(Of T) ,这是ProductID要删除的 。 方法通过调用 BeginTransaction 来启动事务,然后在 块中Try循环访问所提供的列表,为每个ProductID值调用 DB-Direct 模式Delete方法。 如果对 Delete 的任何调用失败,控制权将 Catch 转移到事务回滚并重新引发异常的块。 如果对 的所有调用 Delete 都成功,则提交事务。 将此方法添加到 ProductsBLL 类。

Public Sub DeleteProductsWithTransaction _
    (ByVal productIDs As System.Collections.Generic.List(Of Integer))
    
    ' Start the transaction
    Adapter.BeginTransaction()
    Try
        ' Delete each product specified in the list
        For Each productID As Integer In productIDs
            Adapter.Delete(productID)
        Next
        ' Commit the transaction
        Adapter.CommitTransaction()
    Catch
        ' There was an error - rollback the transaction
        Adapter.RollbackTransaction()
        Throw
    End Try
End Sub

跨多个 TableAdapter 应用事务

本教程中检查的与事务相关的代码允许将针对 ProductsTableAdapter 的多个语句视为原子操作。 但是,如果需要以原子方式对不同数据库表进行多次修改,该怎么办? 例如,删除类别时,我们可能首先希望将其当前产品重新分配给其他类别。 重新分配产品和删除类别的这两个步骤应作为原子操作执行。 ProductsTableAdapter但 仅包含用于修改Products表的方法,CategoriesTableAdapter而 仅包含用于修改表的方法Categories。 那么,事务如何包含这两个 TableAdapter 呢?

一种选择是将一个方法添加到名为 DeleteCategoryAndReassignProducts(categoryIDtoDelete, reassignToCategoryID) 的 ,CategoriesTableAdapter并让该方法调用存储过程,该过程既重新分配产品,又删除存储过程中定义的事务范围内的类别。 我们将在未来的教程中了解如何在存储过程中开始、提交和回滚事务。

另一个选项是在 DAL 中创建包含 方法的 DeleteCategoryAndReassignProducts(categoryIDtoDelete, reassignToCategoryID) 帮助程序类。 此方法将创建 和 的 CategoriesTableAdapter 实例, ProductsTableAdapter 然后将这两个 TableAdapters Connection 属性设置为同一 SqlConnection 实例。 此时,两个 TableAdapter 中的任一个都将通过调用 BeginTransaction来启动事务。 将根据需要在提交或回滚事务的块中 Try...Catch 调用用于重新分配产品和删除类别的 TableAdapters 方法。

步骤 4:将UpdateWithTransaction方法添加到业务逻辑层

在步骤 3 中,我们向 ProductsTableAdapter DAL 中的 添加了一个 UpdateWithTransaction 方法。 我们应该向 BLL 添加相应的方法。 虽然表示层可以直接调用 DAL 来调用 UpdateWithTransaction 方法,但这些教程已努力定义一个分层体系结构,使 DAL 与表示层隔离。 因此,我们有必要继续采用这种方法。

ProductsBLL打开类文件并添加名为 UpdateWithTransaction 的方法,该方法只需调用相应的 DAL 方法。 现在中 ProductsBLL应有两个新方法: UpdateWithTransaction刚刚添加的 和 DeleteProductsWithTransaction步骤 3 中添加的 。

Public Function UpdateWithTransaction _
    (ByVal products As Northwind.ProductsDataTable) As Integer
    
    Return Adapter.UpdateWithTransaction(products)
End Function
Public Sub DeleteProductsWithTransaction _
    (ByVal productIDs As System.Collections.Generic.List(Of Integer))
    
    ' Start the transaction
    Adapter.BeginTransaction()
    Try
        ' Delete each product specified in the list
        For Each productID As Integer In productIDs
            Adapter.Delete(productID)
        Next
        ' Commit the transaction
        Adapter.CommitTransaction()
    Catch
        ' There was an error - rollback the transaction
        Adapter.RollbackTransaction()
        Throw
    End Try
End Sub

注意

这些方法不包括 DataObjectMethodAttribute 分配给 类中 ProductsBLL 大多数其他方法的属性,因为我们将直接从 ASP.NET 页代码隐藏类调用这些方法。 回想一下, DataObjectMethodAttribute 它用于标记应在 ObjectDataSource 的“配置数据源”向导中以及哪个选项卡下显示哪些方法, (SELECT、UPDATE、INSERT 或 DELETE) 。 由于 GridView 缺少对批量编辑或删除的任何内置支持,因此我们必须以编程方式调用这些方法,而不是使用无代码声明性方法。

步骤 5:从表示层以原子方式更新数据库数据

为了说明事务在更新一批记录时的影响,让我们创建一个用户界面,该界面列出 GridView 中的所有产品,并包含按钮 Web 控件,单击该控件时,该控件将重新分配产品 CategoryID 值。 具体而言,类别重新分配将进行,以便为前几个产品分配有效 CategoryID 值,而其他产品则被故意分配不存在 CategoryID 的值。 如果尝试使用与现有类别 CategoryID不匹配的产品CategoryID更新数据库,将发生外键约束冲突,并引发异常。 此示例中看到的是,使用事务时,外键约束冲突引发的异常将导致回滚以前的有效 CategoryID 更改。 但是,如果不使用事务,对初始类别的修改将保留。

首先打开 文件夹中的页面Transactions.aspxBatchData,并将 GridView 从“工具箱”拖到Designer。 将其 ID 设置为 Products ,并从其智能标记中将其绑定到名为 ProductsDataSource的新 ObjectDataSource。 将 ObjectDataSource 配置为从 ProductsBLL 类方法 GetProducts 中提取其数据。 这是只读的 GridView,因此请将“更新”、“插入”和“删除”选项卡中的下拉列表设置为 (“无”) 并单击“完成”。

将 ObjectDataSource 配置为使用 ProductsBLL 类的 GetProducts 方法

图 5:将 ObjectDataSource 配置为使用 ProductsBLLGetProducts 方法 (单击以查看全尺寸图像)

将“更新”、“插入”和“删除”选项卡中的 Drop-Down Lists 设置为“ (None”)

图 6:将“更新”、“插入”和“删除”选项卡中的 Drop-Down Lists 设置为“ (无”) (单击以查看全尺寸图像)

完成“配置数据源”向导后,Visual Studio 将为产品数据字段创建 BoundFields 和 CheckBoxField。 删除除 、、 和 之外ProductID的所有字段,并将 和 CategoryName BoundFields HeaderText 属性分别重命名ProductName为 Product 和 CategoryName Category。 CategoryIDProductName 在智能标记中,检查“启用分页”选项。 进行这些修改后,GridView 和 ObjectDataSource 的声明性标记应如下所示:

<asp:GridView ID="Products" runat="server" AllowPaging="True" 
    AutoGenerateColumns="False" DataKeyNames="ProductID" 
    DataSourceID="ProductsDataSource">
    <Columns>
        <asp:BoundField DataField="ProductID" HeaderText="ProductID" 
            InsertVisible="False" ReadOnly="True" 
            SortExpression="ProductID" />
        <asp:BoundField DataField="ProductName" HeaderText="Product" 
            SortExpression="ProductName" />
        <asp:BoundField DataField="CategoryID" HeaderText="CategoryID" 
            SortExpression="CategoryID" />
        <asp:BoundField DataField="CategoryName" HeaderText="Category" 
            SortExpression="CategoryName" />
    </Columns>
</asp:GridView>
<asp:ObjectDataSource ID="ProductsDataSource" runat="server" 
    OldValuesParameterFormatString="original_{0}"
    SelectMethod="GetProducts" TypeName="ProductsBLL">
</asp:ObjectDataSource>

接下来,在 GridView 上方添加三个按钮 Web 控件。 将第一个 Button 的 Text 属性设置为“刷新网格”,将第二个设置为“修改类别 (WITH TRANSACTION) ”,将第三个属性设置为“修改类别” (WITHOUT TRANSACTION) 。

<p>
    <asp:Button ID="RefreshGrid" runat="server" Text="Refresh Grid" />
</p>
<p>
    <asp:Button ID="ModifyCategoriesWithTransaction" runat="server"
        Text="Modify Categories (WITH TRANSACTION)" />
</p>
<p>
    <asp:Button ID="ModifyCategoriesWithoutTransaction" runat="server"
        Text="Modify Categories (WITHOUT TRANSACTION)" />
</p>

此时,Visual Studio 中的“设计”视图应类似于图 7 中显示的屏幕截图。

页面包含 GridView 和三个按钮 Web 控件

图 7:页面包含 GridView 和三个按钮 Web 控件 (单击以查看全尺寸图像)

为三个 Button 事件 Click 中的每个事件创建事件处理程序,并使用以下代码:

Protected Sub RefreshGrid_Click _
    (ByVal sender As Object, ByVal e As System.EventArgs) _
    Handles RefreshGrid.Click
    
    Products.DataBind()
End Sub
Protected Sub ModifyCategoriesWithTransaction_Click _
    (ByVal sender As Object, ByVal e As System.EventArgs) _
    Handles ModifyCategoriesWithTransaction.Click
    
    ' Get the set of products
    Dim productsAPI As New ProductsBLL()
    Dim productsData As Northwind.ProductsDataTable = productsAPI.GetProducts()
    ' Update each product's CategoryID
    For Each product As Northwind.ProductsRow In productsData
        product.CategoryID = product.ProductID
    Next
    ' Update the data using a transaction
    productsAPI.UpdateWithTransaction(productsData)
    ' Refresh the Grid
    Products.DataBind()
End Sub
Protected Sub ModifyCategoriesWithoutTransaction_Click _
    (ByVal sender As Object, ByVal e As System.EventArgs) _
    Handles ModifyCategoriesWithoutTransaction.Click
    
    ' Get the set of products
    Dim productsAPI As New ProductsBLL()
    Dim productsData As Northwind.ProductsDataTable = productsAPI.GetProducts()
    ' Update each product's CategoryID
    For Each product As Northwind.ProductsRow In productsData
        product.CategoryID = product.ProductID
    Next
    ' Update the data WITHOUT using a transaction
    Dim productsAdapter As New NorthwindTableAdapters.ProductsTableAdapter()
    productsAdapter.Update(productsData)
    ' Refresh the Grid
    Products.DataBind()
End Sub

刷新按钮 Click 事件处理程序只是通过调用 Products GridView s DataBind 方法将数据重新绑定到 GridView。

第二个事件处理程序重新分配产品 CategoryID ,并使用 BLL 中的新事务方法执行事务保护下的数据库更新。 请注意,将每个产品 CategoryID 任意设置为与其 ProductID相同的值。 这适用于前几个产品,因为这些产品的值 ProductID 恰好映射到有效的 CategoryID 。 但是,ProductID一旦 开始变得太大,s 和 CategoryID s 的ProductID巧合重叠将不再适用。

第三 Click 个事件处理程序以相同的方式更新产品 CategoryID ,但使用 ProductsTableAdapter 默认 Update 方法将更新发送到数据库。 此方法 Update 不会将一系列命令包装在事务中,因此这些更改是在首次遇到外键约束冲突错误之前进行的。

若要演示此行为,请通过浏览器访问此页面。 最初应看到数据的第一页,如图 8 所示。 接下来,单击“修改类别 (WITH TRANSACTION) ”按钮。 这将导致回发并尝试更新所有产品 CategoryID 值,但会导致外键约束冲突 (见图 9) 。

产品显示在可分页的 GridView 中

图 8:产品显示在可分页的 GridView 中 (单击以查看全尺寸图像)

重新分配类别会导致外键约束冲突

图 9:重新分配类别导致外键约束冲突 (单击以查看全尺寸图像)

现在,点击浏览器的“后退”按钮,然后单击“刷新网格”按钮。 刷新数据时,应会看到与图 8 中所示完全相同的输出。 也就是说,即使某些产品 CategoryID 已更改为合法值并在数据库中更新,但在发生外键约束冲突时,它们也会回滚。

现在,请尝试单击“修改类别 (不带事务) 按钮。 这将导致相同的外键约束冲突错误 (见图 9) ,但这次不会回滚值更改为合法值的产品 CategoryID 。 点击浏览器的“后退”按钮,然后点击“刷新网格”按钮。 如图 10 所示, CategoryID 前 8 个产品的 已重新分配。 例如,在图 8 中,Chang 的 为 CategoryID 1,但在图 10 中,它已重新分配到 2。

某些产品类别 ID 值已更新,而其他未更新

图 10:某些产品 CategoryID 值已更新,而其他产品值未 (单击以查看全尺寸图像)

总结

默认情况下,TableAdapter 方法不会在事务范围内包装执行的数据库语句,但通过一些工作,我们可以添加创建、提交和回滚事务的方法。 在本教程中,我们在 类中 ProductsTableAdapter 创建了三个此类方法: BeginTransactionCommitTransactionRollbackTransaction。 我们了解了如何将这些方法与块结合使用 Try...Catch ,使一系列数据修改语句成为原子语句。 具体而言,我们在 中ProductsTableAdapter创建了 UpdateWithTransaction 方法,该方法使用 Batch Update 模式对提供的 ProductsDataTable行执行必要的修改。 我们还向 BLL 中的 类添加了 DeleteProductsWithTransaction 方法,该类接受 值的 作为ProductIDList其输入,并为每个 ProductID调用 DB-Direct 模式方法DeleteProductsBLL。 这两种方法首先创建事务,然后在块中 Try...Catch 执行数据修改语句。 如果发生异常,则会回滚事务,否则将提交该事务。

步骤 5 演示了事务批处理更新与忽略使用事务的批处理更新的效果。 在接下来的三个教程中,我们将在本教程的基础之上构建,并创建用于执行批量更新、删除和插入的用户界面。

编程快乐!

深入阅读

有关本教程中讨论的主题的详细信息,请参阅以下资源:

关于作者

斯科特·米切尔是七本 ASP/ASP.NET 书籍的作者和 4GuysFromRolla.com 的创始人,自 1998 年以来一直在使用 Microsoft Web 技术。 Scott 担任独立顾问、培训师和作家。 他的最新一本书是 山姆斯在 24 小时内 ASP.NET 2.0。 可以在 上mitchell@4GuysFromRolla.com联系他,也可以通过他的博客(可在 中找到http://ScottOnWriting.NET)。

特别感谢

本教程系列由许多有用的审阅者审阅。 本教程的主要审阅者是戴夫·加德纳、希尔顿·吉塞诺和特蕾莎·墨菲。 有兴趣查看我即将发布的 MSDN 文章? 如果是,请在 处mitchell@4GuysFromRolla.com放置一行。