在事务中封装数据库修改62

简介

正如我们在数据插入、更新和删除概述 教程中探讨的那样,GridView 提供内置功能支持行级编辑和删除功能。您只需单击几次鼠标就可以创建丰富的数据修改界面而不用写一行代码,前提是您进行的编辑和删除是基于每行数据的。然而,在某些情况下,这还不够,我们需要让用户能够编辑或删除批量记录。

例如,大多数基于 web 的电子邮件客户端都使用一个网格列出每条消息,除了包含邮件的信息(主题、发送者等等)外,还包括一个复选框。该界面允许用户删除多个消息,方法是先勾选上它们,再单击“Delete Selected Messages ”按钮。在用户通常编辑多条不同记录的情况下,一个批量编辑界面则是一项理想的选择。用不着让用户对每条要修改的记录,单击Edit ,进行修改,然后单击 Update ,批量编辑界面里每条记录都有其各自的编辑界面。用户可以快速地修改需要更改的那几行,然后单击“Update All” 按钮保存这些更改。在本系列教程中,我们将探讨如何创建插入、编辑和删除批量数据的界面。

执行批操作时,重要的是确定是否会出现批操作中的某些操作可能成功而其它操作可能失败的情况。考虑一个批量删除界面:如果选择的第一条记录删除成功,但第二条由于违反外健约束而失败时,会出现什么情况?第一条记录的删除操作应该回滚?还是可以接受删除第一条记录?

如果想把批操作作为 原子操作 对待,即所有的操作步骤要么都成功要么都失败,则需要对数据访问层进行扩展以支持 数据库事务。数据库事务确保INSERT 、UPDATE 和 DELETE 语句集的原子数在事务的保护下执行,大多数当代数据库系统都支持数据库事务特性。

在本教程中,我们将探讨如何扩展DAL 以使用数据库事务。后面的教程将详细探讨为批量插入、更新和删除界面的web 页面实现。 让我们开始吧 !

注意 : 在批事务里修改数据时,原子数并非是必要的。在某些情况下,在同一批处理中,某些数据修改成功某些修改失败是可以接受的,比如,从基于web 的邮件客户端删除一批邮件时。如果在删除过程中出现了数据库错误,对于没有发生错误的记录,仍然保持删除是可以接受的。对于这种情况,没有必要修改DAL 以支持数据库事务。不过在其它批操作情况下,原子数是至关重要的。某个客户想把资金从一个银行账户转到另一个账户时,必须执行两个操作:将第一个账户上的资金扣除,然后添加到第二个账户。如果第一步成功但第二步失败,银行可以不介意,不过客户肯定要心烦意乱了。在后面的三篇教程中,我们将创建插入、更新和删除的批处理界面,就算您不打算在这些页面中使用数据库事务,我也希望您照着本教程,对DAL 进行扩展以支持数据库事务。

事务概述

大多数数据库都支持事务,它可以把多个数据库命令当成一个逻辑单元进行处理。保证组成事务的数据库命令是原子级表示所有的命令要么都失败要么都成功。

一般来说,事务通过 SQL 语句来实现,使用如下的模式:

  1. 指示事务的开始。
  2. 执行构成事务的 SQL 语句。
  3. 如果步骤 2 中的一个语句出错,则回退该事务。
  4. 如果步骤 2 中的所有语句都没有错误, 则提交该事务。

用于创建、提交和回退事务的 SQL 语句可通过手动输入,比如写 SQL 脚本或创建存储过程,或使用 ADO.NET 或System.Transactions 命名空间 的类通过编程的方式来构建。在本教程中,我们仅探讨使用ADO.NET 管理事务。在后面的教程中,我们将探讨如何在数据库访问层使用存储过程,那时,我们再仔细探讨创建、回退和提交事务的SQL 语句。同时,更多信息请参考 在 SQL Server 存储过程中管理事务 。

注意 :System.Transactions 命名空间的 TransactionScope 类 允许开发人员通过编程的方式将一系列语句封装到事务中,它支持涉及多个源的复杂事务,比如两个不同的数据库,甚至不同类型的数据存储,比如Microsoft SQL Server 数据库、Oracle 数据库和 Web service 。本教程我们使用 ADO.NET 事务而非TransactionScope 类,因为 ADO.NET 对于数据库事务更明确,且在多数情况下占用的资源更少。此外,在某些情况下,TransactionScope 类要使用Microsoft Distributed Transaction Coordinator (MSDTC) 。有关MSDTC 的配置、实现和性能问题是相当专业和高深的课题,不在这些教程的范围内。

在 ADO.NET 中使用SqlClient 提供程序时,通过调用 SqlConnection 类 的 BeginTransaction 方法 启动事务,该方法返回一个SqlTransaction 对象 。构成事务的数据修改语句放在try...catch 块内。如果 try 块内的某个语句出错,执行跳到 catch 块,在此,通过 SqlTransaction 对象的 Rollback 方法 回退事务。如果所有的语句都成功完成,将在try 块后面调用 SqlTransaction 对象的Commit 方法 提交事务。下面的代码片段说明了该模式。有关在ADO.NET 中使用事务的更多语法和示例,请参考 维护与事务的数据库移植性 。

// Create the SqlTransaction object 
SqlTransaction myTransaction = 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; 
}

默认情况下,Typed DataSet 的TableAdapters 不使用事务。要支持事务,我们需要对TableAdapter 类进行扩展,使其包括使用上述模式来执行事务范围内数据修改语句的其他方法。在步骤2 中,我们将探讨如何使用部分类来添加这些方法。

步骤1 :创建批处理数据 Web 页面

在探讨如何扩展 DAL 以支持数据库事务之前,先花点时间来创建一些ASP.NET web 页面,我们在本教程和后面三篇教程中都要用到它们。先添加一个名为BatchData 的新文件夹,然后添加以下 ASP.NET 页面,并将每个页面都与 Site.master 母版页相关联。

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

图1 :为 SqlDataSource 相关的教程添加 ASP.NET 页面

与其它文件夹一样,Default.aspx 将使用SectionLevelTutorialListing.ascx 用户控件列出本部分的教程。因此,从Solution Explorer 中将该用户控件拖到页面的 Design 视图中,从而向Default.aspx 添加该控件。

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

最后 , 将这四个页面作为条目添加到 Web.sitemap 文件中。 具体来说,将下面的标记添加到“Customizing the Site Map” <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 中的Typed DataSet 由 DataTables 和 TableAdapters 构成。DataTables 保存数据,而TableAdapters 提供从数据库中读取数据到 DataTables ,并根据 DataTables 的改动更新数据库等功能。我们知道,TableAdapters 提供两种更新数据的模式,我称之为 Batch Update 和 DB-Direct 。在 Batch Update 模式下,给TableAdapter 传入 DataSet 、DataTable 或 DataRows 集。枚举这些数据,且对每个要插入、修改或删除的行执行InsertCommand 、UpdateCommand 或 DeleteCommand 。在DB-Direct 模式下,给 TableAdapter 传入的是插入、更新或修改一个记录时所需的那些列的值。DB Direct 模式方法然后使用这些传入的值来执行适当的InsertCommand 、UpdateCommand 或 DeleteCommand 语句。

不管使用哪种更新模式,TableAdapters 的自动生成方法都不使用事务。默认情况下,TableAdapter 执行的每个插入、更新或删除操作都被看作是独立的、互不相干的操作。比如,假定BLL 中某个代码使用 DB-Direct 模式来向数据库插入十条记录。该代码将调用TableAdapter 的 Insert 方法十次。如果前五条记录插入成功,但第六条遇到异常,则前五条插入的记录仍然保存在数据库中。同样,如果使用Batch Update 模式来执行插入、更新或删除 DataTable 中的行,如果前几次修改成功,但后面一次遇到错误,则之前完成的修改仍在数据库中。

在某些情况下,我们想在进行一系列修改时引入原子数。为此,我们必须手动扩展TableAdapter :添加在事务的保护下执行 InsertCommand 、UpdateCommand 和DeleteCommands 的新方法。在创建数据访问层 中,我们讨论了使用 部分类 来扩展Typed DataSet 的 DataTables 功能。该技术同样适用于 TableAdapters 。

Typed DataSet Northwind.xsd 位于App_Code 文件夹的 DAL 子文件夹中。在 DAL 文件夹中创建一个名为 TransactionSupport 的子文件夹,并在里面添加一个名为ProductsTableAdapter.TransactionSupport.cs 的新的类文件(见图 4 )。该文件将保持ProductsTableAdapter 的部分实现,ProductsTableAdapter 包含使用事务执行数据修改的方法。

图4 :添加一个名为 TransactionSupport 的文件夹以及一个名为 ProductsTableAdapter.TransactionSupport.cs 的类文件

在ProductsTableAdapter.TransactionSupport.cs 文件中输入下面的代码:

using System; 
using System.Data; 
using System.Data.SqlClient; 
using System.Configuration; 
using System.Web; 
using System.Web.Security; 
using System.Web.UI; 
using System.Web.UI.WebControls; 
using System.Web.UI.WebControls.WebParts; 
using System.Web.UI.HtmlControls; 
 
namespace NorthwindTableAdapters 

    public partial class ProductsTableAdapter 
    { 
        private SqlTransaction _transaction; 
        private SqlTransaction Transaction 
        { 
            get 
            {                 
                return this._transaction; 
            } 
            set 
            { 
                this._transaction = value; 
            } 
        } 
 
 
        public void BeginTransaction() 
        { 
            // Open the connection, if needed 
            if (this.Connection.State != ConnectionState.Open) 
                this.Connection.Open(); 
 
            // Create the transaction and assign it to the Transaction property 
            this.Transaction = this.Connection.BeginTransaction(); 
 
            // Attach the transaction to the Adapters 
            foreach (SqlCommand command in this.CommandCollection) 
            { 
                command.Transaction = this.Transaction; 
            } 
 
            this.Adapter.InsertCommand.Transaction = this.Transaction; 
            this.Adapter.UpdateCommand.Transaction = this.Transaction; 
            this.Adapter.DeleteCommand.Transaction = this.Transaction; 
        } 
 
 
        public void CommitTransaction() 
        { 
            // Commit the transaction 
            this.Transaction.Commit(); 
 
            // Close the connection 
            this.Connection.Close(); 
        } 
 
 
        public void RollbackTransaction() 
        { 
            // Rollback the transaction 
            this.Transaction.Rollback(); 
 
            // Close the connection 
            this.Connection.Close(); 
        } 
   } 
}

类声明中的 partial 关键字告诉编译器增加的成员要添加到NorthwindTableAdapters 命名空间的 ProductsTableAdapter 类。注意文件上面 System.Data.SqlClient 语句的使用。因为TableAdapter 被配置为使用 SqlClient 提供程序,其内部使用一个SqlDataAdapter 对象来向数据库发布命令。因此,我们需要使用SqlTransaction 类来启动事务,然后提交或回退事务。如果使用的是非Microsoft SQL Server 的数据存储,您需要使用适当的提供程序。

这些方法提供开始、回退和提交事务所需的构建块。它们被标记为public ,我们可以在 ProductsTableAdapter 中,或 DAL 的其它类中,或架构中的其它层比如 BLL 中调用这些方法。BeginTransaction 打开TableAdapter 的内部 SqlConnection (如果需要),启动事务并将其赋值为Transaction 属性,并将事务分配给内部 SqlDataAdapter 的 SqlCommand 对象。在关闭内部的 Connection 对象之前,CommitTransaction 和RollbackTransaction 分别调用 Transaction 对象的 Commit 和Rollback 方法。

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

完成这些方法后,我们就可以向ProductsDataTable 或BLL 添加方法,以便在事务保护下执行一系列命令。下面的方法使用Batch Update 模式通过事务更新一个ProductsDataTable 实例。它通过调用BeginTransaction 方法来启动一个事务,然后使用try...catch 块来发布数据更改语句。如果调用Adapter 对象的 Update 方法出现异常,执行将会跳转到catch 块,在此,事务将回退,异常被抛弃。记住,Update 方法实现 Batch Update 模式时,将枚举提供的 ProductsDataTable 的所有行,并执行必要的 InsertCommand 、UpdateCommand 和 DeleteCommands 。如果这些命令中的一个出现错误,事务将回退,撤销事务期间所作的更改。Update 语句完成且没有错误时,才会完整提交事务。

public int UpdateWithTransaction(Northwind.ProductsDataTable dataTable) 

    this.BeginTransaction(); 
 
    try 
    { 
        // Perform the update on the DataTable 
        int returnValue = this.Adapter.Update(dataTable); 
 
        // If we reach here, no errors, so commit the transaction 
        this.CommitTransaction(); 
 
        return returnValue; 
    } 
    catch 
    { 
        // If we reach here, there was an error, so rollback the transaction 
        this.RollbackTransaction(); 
 
        throw; 
    } 
}

通过ProductsTableAdapter.TransactionSupport.cs 的部分类将 UpdateWithTransaction 方法添加到ProductsTableAdapter 类。另外,也可将该方法添加到业务逻辑层的ProductsBLL 类,不过要对语法稍作修改。也就是说,将this.BeginTransaction() 、this.CommitTransaction() 和this.RollbackTransaction() 中的关键字 “this” 替换为 “Adapter” (我们知道,Adapter 是ProductsBLL 中类型 ProductsTableAdapter 的属性名称)。

UpdateWithTransaction 方法使用Batch Update 模式,不过也可在事务范围内调用DB-Direct ,如下面的方法所示。DeleteProductsWithTransaction 方法接受一个 int 类型的 List<T> 输入,即要删除 ProductIDs 。该方法通过调用 BeginTransaction 来启动事务,然后在 try 块对提供列表中的每个 ProductID 值调用 DB-Direct 模式 Delete 方法。如果任何一个 Delete 调用失败,控件将跳转到 catch 块,以回退事务并抛出异常。如果所有的Delete 调用都成功,则提交事务。将该方法添加到ProductsBLL 类。

public void DeleteProductsWithTransaction 
    (System.Collections.Generic.List<int> productIDs) 

    // Start the transaction 
    Adapter.BeginTransaction(); 
 
    try 
    { 
        // Delete each product specified in the list 
        foreach (int productID in productIDs) 
        { 
            Adapter.Delete(productID); 
        } 
 
        // Commit the transaction 
        Adapter.CommitTransaction(); 
    } 
    catch 
    { 
        // There was an error - rollback the transaction 
        Adapter.RollbackTransaction(); 
 
        throw; 
    } 
}

跨多个 TableAdapters 应用事务

本教程中探讨的事务相关代码允许将ProductsTableAdapter 中的多个语句作为一个原子操作进行处理。如果我们对不同的数据库表进行的多处修改需要执行原子操作又会怎样呢?比如,删除一个类别时,在删除之前我们想把该类别下的产品分配给其它类别。这两个步骤,即重新分配产品和删除类别,应该作为一个原子操作来执行。但是ProductsTableAdapter 只包含修改 Products 表的方法,而 CategoriesTableAdapter 只包含修改Categories 表的方法。那么一个事务怎样才能包含这两个TableAdapters 呢?

一个办法是向 CategoriesTableAdapter 添加一个名为DeleteCategoryAndReassignProducts(categoryIDtoDelete,reassignToCategoryID) 的方法,该方法调用一个存储过程,该存储过程中定义的事务可重新分配产品和删除类别。我们将在后面的教程中探讨在存储过程中如何开始、提交和回退事务。

另一个办法是在 DAL 中创建一个helper 类,该类包含 DeleteCategoryAndReassignProducts(categoryIDtoDelete,reassignToCategoryID) 方法。该方法将创建CategoriesTableAdapter 和 ProductsTableAdapter 的实例,并将这两个 TableAdapters 的Connection 属性设置为相同的 SqlConnection 实例。这样,这两个 TableAdapters 都将调用 BeginTransaction 来启动事务。在try...catch 中调用 TableAdapters 的重新分配产品和删除类别的方法,并按需要提交或回退事务。

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

在步骤 3 中,我们向DAL 的 ProductsTableAdapter 添加了一个 UpdateWithTransaction 方法。我们应该向 BLL 添加相应的方法。虽然表示层可直接向DAL 调用 UpdateWithTransaction 方法,但是这些教程仍然定义了一个分层结构,将DAL 和表示层分隔开。因此,我们可继续该方法。

打开 ProductsBLL 类文件,添加一个名为UpdateWithTransaction 的方法,该方法简单地向下调用相应的DAL 方法。现在 ProductsBLL 中应该有两个方法:您刚才添加的UpdateWithTransaction 以及在步骤3 中添加的DeleteProductsWithTransaction 。

public int UpdateWithTransaction(Northwind.ProductsDataTable products) 

    return Adapter.UpdateWithTransaction(products); 

 
 
public void DeleteProductsWithTransaction 
    (System.Collections.Generic.List<int> productIDs) 

    // Start the transaction 
    Adapter.BeginTransaction(); 
 
    try 
    { 
        // Delete each product specified in the list 
        foreach (int productID in productIDs) 
            Adapter.Delete(productID); 
 
        // Commit the transaction 
        Adapter.CommitTransaction(); 
    } 
    catch 
    { 
        // There was an error - rollback the transaction 
        Adapter.RollbackTransaction(); 
 
        throw; 
    } 
}

注意 : 这些方法不包含DataObjectMethodAttribute 属性,而 ProductsBLL 类中的大部分方法都有该属性。这是因为我们将直接从ASP.NET 页面的内含代码中调用这些方法。记住,DataObjectMethodAttribute 用于指出哪些方法应该出现在 ObjectDataSource 的Configure Data Source 向导中以及出现在哪个选项卡中(SELECT 、UPDATE 、INSERT 和DELETE )。由于 GridView 缺乏内置的支持批量编辑或删除的功能,我们将必须通过编程的方式来调用这些方法,而不是使用无需代码的声明方式。

步骤5 :原子级更新表示层的数据库数据

为了说明更新一批记录时事务的作用,我们创建一个用户界面,在一个GridView 中列出所有的产品,并包含一个 Web 按钮控件,单击时该控件重新分配产品的CategoryID 值。特别地,类别再分配将继续,给前几个产品分配一个有效的CategoryID 值,而专门给剩下的分配一个不存在的CategoryID 值。在我们试图对其 CategoryID 与现有类别的 CategoryID 不匹配的产品进行更新时,将违反外健约束,从而抛出一个异常。在本示例中您将看到,使用事务时,违反外健约束时抛出的异常将导致回退前面所做的有效CategoryID 改动。然而,如果不使用事务,对原始类别的修改将生效。

首先,打开 BatchData 文件夹中的Transactions.aspx 页面,从 Toolbox 拖一个 GridView 到 Designer 中。将其ID 赋值为 Products ,从其智能标记中,将它绑定到一个名为ProductsDataSource 的新 ObjectDataSource 。 配置 ObjectDataSource , 从ProductsBLL 类的 GetProducts 方法中拖取数据。 这是一个只读的GridView ,因此将 UPDATE 、INSERT 和 DELETE 选项卡中的下拉列表设置为 “(None)” ,然后单击 Finish 。

图5 :配置ObjectDataSource 以使用 ProductsBLL 类的 GetProducts 方法

图6 :将 UPDATE 、INSERT 和 DELETE 选项卡中的下拉列表设置为 “(None)”

完成 Configure Data Source 向导后,Visual Studio 将为各产品数据字段创建 BoundFields 和 CheckBoxField 。删除除 ProductID 、ProductName 、CategoryID 和 CategoryName 外的所有字段,分别将 ProductName 和 CategoryName BoundFields 的 HeaderText 属性重命名为 “Product” 和 “Category” 。在智能标记中,勾选上 “Enable Paging” 选项。做完这些修改后,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 按钮控件。将第一个按钮的 Text 属性赋值为 “Refresh Grid” ,第二个的属性赋值为 “Modify Categories (WITH TRANSACTION)” ,第三个的属性赋值为“Modify Categories (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 的Design 视图看起来和图 7 中的截屏差不多:

图7 :页面包含一个 GridView 和三个 Web 按钮控件

分别创建这三个按钮的 Click 事件的 event handler ,并使用下面的代码:

protected void RefreshGrid_Click(object sender, EventArgs e) 

    Products.DataBind(); 

 
protected void ModifyCategoriesWithTransaction_Click(object sender, EventArgs e) 

    // Get the set of products 
    ProductsBLL productsAPI = new ProductsBLL(); 
    Northwind.ProductsDataTable products = productsAPI.GetProducts(); 
 
    // Update each product's CategoryID 
    foreach (Northwind.ProductsRow product in products) 
    { 
        product.CategoryID = product.ProductID; 
    } 
 
    // Update the data using a transaction 
    productsAPI.UpdateWithTransaction(products); 
 
    // Refresh the Grid 
    Products.DataBind(); 

 
protected void ModifyCategoriesWithoutTransaction_Click(object sender, EventArgs e) 

    // Get the set of products 
    ProductsBLL productsAPI = new ProductsBLL(); 
    Northwind.ProductsDataTable products = productsAPI.GetProducts(); 
 
    // Update each product's CategoryID 
    foreach (Northwind.ProductsRow product in products) 
    { 
        product.CategoryID = product.ProductID; 
    } 
 
    // Update the data WITHOUT using a transaction 
    NorthwindTableAdapters.ProductsTableAdapter productsAdapter =  
        new NorthwindTableAdapters.ProductsTableAdapter(); 
    productsAdapter.Update(products); 
 
    // Refresh the Grid 
    Products.DataBind(); 
}

refresh 按钮的Click event handler 仅调用 Products GridView 的 DataBind 方法,以将数据重新绑定到 GridView 。

第二个 event handler 重新分配产品的CategoryID ,并使用 BLL 的新事务方法在事务的保护下执行数据库更新。注意,每个产品的CategoryID 都被强制设置为与其 ProductID 相同的值。对前面几个产品而言没有问题,因为这些产品的ProductID 值碰巧映射到一个有效的 CategoryID 。但随着 ProductID 值越变越大,这种 ProductID 和 CategoryID 间的一致就不再适用了。

第三个 Click event handler 以同样的方式更新产品的 CategoryID ,只是它使用 ProductsTableAdapter 的默认 Update 方法向数据库发送更新。该 Update 方法并没有将一系列命令封装到事务中,因此在首次违反外健约束之前进行的那些改动仍将生效。

可通过浏览器访问该页面来进行验证。刚开始看到的第一个数据页应该如图8 所示。然后单击 “Modify Categories (WITH TRANSACTION)” 按钮。这将导致一次回传,并试图更新所有产品的CategoryID 值,但这将导致违反外健约束(见图 9 )。

图8 :在可分页的 GridView 中显示产品

图9 :重新分配类别导致违反外健约束

现在单击浏览器的 Back 按钮,然后单击“Refresh Grid” 按钮。刷新数据后所看到的数据应该与图8 中所示的完全相同。也就是,即使某些产品的CategoryID 被改变为有一个有效的值并在数据库中进行了更新,违反了外健约束时仍会将它们回退。

现在试着单击 “Modify Categories (WITHOUT TRANSACTION)” 按钮。这将出现同样的违反外健约束错误(见图9 ),但这次,将不回退那些被赋予有效值的CategoryID 值的产品。单击浏览器的 Back 按钮,然后单击 “Refresh Grid” 按钮。如图10 所示, 前八个产品的CategoryID 已被修改。例如,图 8 中 Chang 的 CategoryID 为 1 ,但在图 10 中就变为 2 了。

图10 :某些产品的 CategoryID 值进行了更新,而其它的没有

小结

默认情况下,TableAdapter 的方法没有将执行的数据库语句封装在事务范围内,不过只需稍加操作就可以添加一些用于创建、提交和回退事务的方法。在本教程中,我们在ProductsTableAdapter 类中创建了三个这样的方法:BeginTransaction 、CommitTransaction 和RollbackTransaction 。我们探讨了如何在 try...catch 块内使用这些方法来执行一系列数据修改语句的原子操作。具体来说,我们在ProductsTableAdapter 中创建了 UpdateWithTransaction 方法,该方法使用 Batch Update 模式对提供的ProductsDataTable 中的各行进行必要的更改。我们也向 BLL 的ProductsBLL 类添加了 DeleteProductsWithTransaction 方法,它将一系列 ProductID 值作为输入参数,并对每个 ProductID 调用 DB-Direct 模式方法 Delete 。两个方法都从创建一个事务开始,然后在try...catch 块内执行数据更改语句。如果出现异常,则回退事务;相反,则提交事务。

步骤 5 说明了使用事务进行批量更新以及不使用事务进行批量更新的效果。在接下来的三篇教程中,我们将以本教程为基础,创建执行批量更新、删除和插入的用户界面。

快乐编程!

posted @ 2016-05-02 00:28  迅捷之风  阅读(306)  评论(0编辑  收藏  举报