在ASP.NET 2.0中操作数据之六十一:在事务里对数据库修改进行封装
正如我们在第16章《概述插入、更新和删除数据》里探讨的那样,GridView控件内建的功能支持对每行数据的编辑和删除功能,你只需要稍稍动一下鼠标就可以创建丰富的数据
导言:
正如我们在第16章《概述插入、更新和删除数据》里探讨的那样,GridView控件内建的功能支持对每行数据的编辑和删除功能,你只需要稍稍动一下鼠标就可以创建丰富的数据修改界面而不用写一行代码.但是,在某些情况下,这还不够,我们需要让用户能够成批地处理数据.
比如,很多基于web(web-based)的电子邮件客户端,将所有邮件出来,每条邮件除了包含邮件信息(主题、发送者等)外,还包含一个checkbox控件。这些界面允许用户同时删除多个邮件,用户只需要选中邮件,再点"删除所选邮件"按钮.当用户要编辑多条不同的记录的时候,提供一个批编辑界面是比较理想的.我们用不着让用户每次都选中一条要编辑的记录,再做相关的修改,最后点“更新”按钮,在批编辑界面里每条记录都有各自的编辑选项,用户可以快速地编辑多条记录再点“Update All”按钮来保存对他们所做的修改.本系列我们将考察如何创建对数据进行添加、编辑、删除批处理的界面.
如果想对批处理执行atomic operation(原子操作), 那么首先,所做的操作要么都执行成功要么都失败,另外还要对数据访问层进行扩充以支持database transactions(数据库事务)。数据库事务确保INSERT, UPDATE, 和 DELETE语句执行的atomicity(原子数)置于数据库事务的保护之下.另外,绝大多数的当代数据库系统都支持数据库事务.
在本系列我们先看如何扩充数据访问层以支持数据库事务,接下来我们看如何创建页面以包含添加、更新、删除数据的批处理界面,让我们开始吧.
注意:在批处理事务里修改数据时,原子数(atomicity)并非总数必要的。在批处理的某些情况下,某些修改成功某些修改失败是可以接受的。比如删除电子邮件时,有些邮件在删除过程中发生了数据库错误,有些邮件没有发生错误,对这种没有发生错误的邮件,批处理照样将其删除掉.对这种情况,我们没有必要设置数据访问层DAL支持数据库事务.不过在其它某些情况下,原子数是至关重要的.比如某个客户想把资金从一个银行帐户转移到另一个银行帐号,下面2个操作必须执行成功:首先,将第一个帐号的资金扣除,然后将资金转入第二个帐号.如果第一步执行成功,第二步执行失败,银行当然高兴,客户怕是要发疯了.在后面的文章里我们将创建添加、更新、删除的批处理界面,就算你不打算在这些页面里使用数据库事务,我也希望你照着本篇文章,对数据访问层进行扩展一支持数据库事务.
事务概述
绝大多数的数据库都支持事务,它可以将多个数据库命令当成一个逻辑单位进行处理.这些包含事务的命令要么都执行成功要么都执行失败.
一般来说,事务通过SQL命令来执行,使用如下的模式:
1.声明事务开始
2.执行构成事务的那些SQL命令
3.如果在第二步中的任何一个命令出错,执行事务回滚(rollback the transaction)
4.如果在第二步中的所有命令成功执行,提交事务
这些SQL命令可以通过手写的方式输入,比如写SQL脚本、创建存储过程、也可以通过编程的方式来构建,比如使用ADO.NET技术或调用System.Transactions namespace命名空间的类.在本文,我们仅仅考察用ADO.NET技术管理事务.在后面的教程我们看如何在数据访问层Data Access Layer里使用存储过程,到那时,我们再来考察这些创建、回滚、提交事物的SQL命令。另外,要获得更多信息请参考文章《Managing Transactions in SQL Server Stored Procedures》(http://www.4guysfromrolla.com/webtech/080305-1.shtml)
注意:System.Transactions namespace命名空间的TransactionScope class类允许开发者通过编程的方式获取事务里的一系列命令,且允许事务包含多个数据源,甚至类型不同,比如:Microsoft SQL Server database, 或Oracle database,甚至Web service.本教程我们使用ADO.NET技术而非TransactionScope class类,是因为ADO.NET指定数据库事务更详细,且在很多情况下占用资源更少.此外,在某些情况下,TransactionScope class类要用到Microsoft Distributed Transaction Coordinator (MSDTC),围绕MSDTC的配置、执行和性能问题是比较专业、高级的问题稍微超出了本教程的范围.
在ADO.NET里,通过调用SqlConnection class类的BeginTransaction method方法启动事务, 该方法返回一个SqlTransaction object对象.将构成事务的数据操作命令放在try...catch区域,如果在try区域的某个命令出错的话,程序将转到catch区域,在此,通过SqlTransaction object对象的Rollback method方法执行事务回滚。如果所有的命令执行成功,将调用位于try区域底部的SqlTransaction object对象的Commit method方法来提交事务.下面的代码片段揭示了该模式。要想看在ADO.NET里使用事务的更多例子,请参阅文章《Maintaining Database Consistency with Transactions》(http://aspnet.4guysfromrolla.com/articles/072705-1.aspx).
// 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 classes类进行扩展,以包含额外的方法以使用上述模式来执行事务。在第二步,我们看如何使用一个partial classes类来添加这些方法.
第一步:创建批处理数据的页面
在我们考察如何扩展数据访问层DAL以支持数据库事务之前,让我们花点时间来创建一些ASP.NET web页面,我们在本章及后面三章将用到它们.
添加一个名为BatchData的新文件夹,再添加如下的 ASP.NET页面, 务必套用Site.master模板页.
Default.aspx
Transactions.aspx
BatchUpdate.aspx
BatchDelete.aspx
BatchInsert.aspx
图1:添加相关的页面
就像其它文件夹里的Default.aspx页面一样,用SectionLevelTutorialListing.ascx用户控件来列出本部分的章节。将其从解决资源管理器里拖到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>
完成后,花几分钟在浏览器里登录页面,左面的菜单列出了本部分的各项
图3:Site Map现在包含了本章节
第二步:更新数据访问层以支持数据库事务
就像我们在第一章《创建一个数据访问层》探讨的一样,位于数据访问层的强类型数据集(Typed DataSet)由DataTables 和 TableAdapters构成. DataTables保存数据,而TableAdapters提供相应的方法从数据库读取数据,并根据DataTables的改动对数据库做相应的更新,等等.记得TableAdapters有2种更新数据的模式——Batch Update 和 DB-Direct.就Batch Update模式而言, TableAdapter可以传入DataSet, DataTable, 或DataRows集,遍历这些数据对要添加、修改、删除的行执行相应的InsertCommand, UpdateCommand, or DeleteCommand方法。就DB-Direct模式而言,TableAdapter传入的是那些需要进行添加、更新、删除操作的某条记录的列的值,再使用这些值执行相关的InsertCommand, UpdateCommand, 或DeleteCommand命令.
TableAdapter自动生成的方法并不使用事务.默认状态下,TableAdapter执行的每一个insert, update, 或delete操作都看作是单独的、互不相干的.假定在业务逻辑层BLL里使用DB-Direct模式来向数据库添加十条记录,代码将分十次调用TableAdapter的Insert方法. 如果前5条记录添加正常,而在添加第六条记录时发生异常,前5条记录仍然保存在数据库.同样的,用Batch Update模式来操作的话,效果亦然.
在某些情况下,我们想确保在进行一系列的改动时引入原子数(atomicity).为此,我们必须手动扩展TableAdapter,通过添加一些新的方法将InsertCommand, UpdateCommand, 和DeleteCommands命令置于事务之下.在第一章《创建一个数据访问层》里,我们考察了使用部分类(partial classes)对强类型数据集(Typed DataSet)里的DataTable的函数进行扩充.该技术同样适用于TableAdapter.
强类型数据集Northwind.xsd位于App_Code文件夹的DAL子文件夹里.在DAL文件夹里再创建一个名为TransactionSupport的子文件夹,再在里面添加一个新类,名为ProductsTableAdapter.TransactionSupport.cs (见图4).该类包含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向编译器表明代码里添加的成员(members)是添加到命名空间NorthwindTableAdapters里的ProductsTableAdapter class类.我们注意到在文件的顶部有一个using System.Data.SqlClient声明,这是因为TableAdapter被设置为使用SqlClient provider,在其内部使用一个SqlDataAdapter object对象来向数据库发出命令.因此,我们需要使用SqlTransaction class类来启动事务,然后提交或回滚事务.如果没有使用Microsoft SQL Server数据库的话,你需要调用恰当的provider.
这些方法被标记为public,我们可以在ProductsTableAdapter里,或数据访问层DAL的其它类,甚至是其它层比如业务逻辑层BLL来调用这些法.
BeginTransaction()方法打开了TableAdapter的内部的SqlConnection(如果需要的话), 开启事务并赋值给Transaction属性,并将事务分配(attache)给SqlDataAdapter的SqlCommand objects对象.CommitTransaction()和 RollbackTransaction()方法在关闭内部的Connection object对象前分别调用Transaction object对象的Commit 和 Rollback方法.
添加上述代码后,我们将在ProductsDataTable 或业务逻辑层BLL里添加方法以执行一系列的置于事务之下的命令. 下面的代码在Batch Update pattern模式里使用一个事务来更新一个ProductsDataTable instance实例.它调用BeginTransaction method方法来启动一个事务,然后用一个try...catch模块来发布数据更改命令.如果调用Adapter object对象的Update方法出现异常,那么将转到catch区域,对事务进行回滚.记得执行Batch Update pattern模式的Update方法将遍历ProductsDataTable里的所有行(rows),执行相应的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; } }
将上述的UpdateWithTransaction()方法添加到文件ProductsTableAdapter.TransactionSupport.cs里的ProductsTableAdapter class类。另外,还可以将该方法添加到业务逻辑层的ProductsBLL class类,不过要做些许修改:即将this.BeginTransaction(), this.CommitTransaction(), and this.RollbackTransaction()三中方法里的关键字“this”替换为“Adapter”(我们知道,ProductsBLL类里的ProductsTableAdapter的name属性即是Adapter).
UpdateWithTransaction()方法使用的是Batch Update模式,不过也可在事务里调用DB-Direct模式,就像下面的代码显示的那样.DeleteProductsWithTransaction()方法接受一个int类型的List<T>,也就是要删除的ProductIDs.该方法通过调用BeginTransaction来启动事务,然后在try模块里对每一个ProductID值调用DB-Direct模式的Delete方法.如果任何一个对Delete的调用出错,将转到catch 模块,事务将会回滚;如果所有对Delete的调用成功,那就提交事务。添加该方法给ProductsBLL class类.
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里的多个命令采用原子操作.如果我们是对多个不同的数据库表进行改动,并对这些改动执行原子操作那又怎么办呢?比如:当删除一个category时,在删除之前我们想把该种类对应的products分配给其它的category.对这种2步操作——分配products和删除category——应该执行原子操作.但是ProductsTableAdapter只包含修改Products表的方法;而CategoriesTableAdapter只包含修改Categories表的方法.那么怎样使用一个包含这2个TableAdapters的事务呢?
其中一个办法是向CategoriesTableAdapter添加一个名为DeleteCategoryAndReassignProducts(categoryIDtoDelete, reassignToCategoryID)的方法.再定义一个方法来调用一个存储过程,使用事务来达到分配products和删除category的目的.我们将在后面考察在一个存储过程里开始、提交和回滚事务.
另一个方法是在数据访问层里添加一个类,来包含DeleteCategoryAndReassignProducts(categoryIDtoDelete, reassignToCategoryID)方法.该方法创建CategoriesTableAdapter 和 the ProductsTableAdapter的实例,并将这2个TableAdapters的Connection属性设置为相同的SqlConnection实例。这样,它们都将调用BeginTransaction来开启事务.然后在try...catch模块里执行分配products和删除category的方法,最后提交或回滚事务.
第四步:向业务逻辑层添加UpdateWithTransaction方法
在第三步我们向数据访问层DAL里的ProductsTableAdapter添加了一个UpdateWithTransaction方法,我们将向业务逻辑层添加相应的方法.虽然表现层可以直接向DAL调用UpdateWithTransaction方法,但是我们在这里仍然将它们分隔开。
打开ProductsBLL class类,添加一个名为UpdateWithTransaction的方法,该方法仅仅简单地调用对应的DAL方法.现在ProductsBLL类里有2个方法:UpdateWithTransaction方法——我们才添加的;以及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; } }
注意:根ProductsBLL类里的大部分方法不同,上述方法并不包含DataObjectMethodAttribute属性。这是因为我们将直接在ASP.NET页面的后台代码里调用这些方法,记得DataObjectMethodAttribute方法的作用是指出哪些方法应该出现在ObjectDataSource控件的设置数据源向导的某些标签(SELECT, UPDATE, INSERT, 或DELETE)里.由于GridView控件缺乏内置的支持“批编辑”或“批删除”的功能,我们将通过编辑的方式来调用这些方法.
第五步:在表现层更新数据库数据
为演示更新一批记录时事务的作用,我们将创建一个用户界面来将所有产品用一个GridView控件显示出来,并包含一个Button Web控件。当点击该按钮时为product重新赋值一个有效的CategoryID值。具体来说,对头几个products分配一个有效的CategoryID值;而剩下的分配一个无效的(non-existent)CategoryID值,当我们试图对这样的一个product——其CategoryID值与现有的category的CategoryID不匹配——进行更新时,将违反外键约束,进而抛出一个异常.在本文的示例里你将看到,在使用事务时,当违反外键约束抛出一个异常时将导致前面的正确分配CategoryID值的操作产生回滚.如果不使用事务的话,这些正确的操作将执行成功.
首先,打开BatchData文件夹里的Transactions.aspx页面,从工具箱拖一个GridView控件到页面。设置其ID为Products,从其智能标签里将其绑定到一个名为ProductsDataSource的ObjectDataSource控件,设置该控件调用ProductsBLL class类的GetProducts()方法。由于该GridView是“只读”的,在UPDATE, INSERT, 和DELETE标签里选“(None)”,点完成。
图5:设置ObjectDataSource使用ProductsBLL Class类的GetProducts方法
图6:在UPDATE, INSERT, 和DELETE标签里选“(None)”
完成设置后,Visual Studio将自动的添加BoundFields以及一个CheckBoxField,删除ProductID, ProductName, CategoryID,和CategoryName以外的其它列;并且分别将ProductName 和 CategoryName列的HeaderText属性重命名为“Product” 和 “Category”.在智能标签里启用“分页”功能.做完这些修改后,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控件上添加3个Button Web控件,设置第一个按钮的Text属性 为“Refresh Grid”;第二个按钮的Text属性为“Modify Categories (WITH TRANSACTION)”;第三个按钮的Text属性为“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的设计模式里,界面看起来和下面的截屏差不多:
图7:页面包含一个GridView控件和三个Button Web控件
为这3个按钮的Click events事件创建事件处理器,如下:
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事件处理器仅仅调用Products GridView的DataBind方法将数据重新绑定到ridView控件.
第二个事件处理器对products的CategoryID属性重新赋值,并调用BLL层里的新的事务方法来执行数据库更新.我们注意到将每个产品的ProductID值赋给其CategoryID属性,对最开头的几个产品而言没有任何问题,但随着ProductID值越变越大,CategoryID的值也越变越大,而Category表里定义的种类毕竟有限,于是问题就出来了。
第三个事件处理器也是将ProductID值赋给CategoryID属性,只是用ProductsTableAdapter的默认的Update方法来更新数据库. 该Update方法并没有使用事务来封装这些命令,所以只要是没有违背外键约束的更新都会执行成功.
在浏览器里登录该页面进行验证.最开始你将看到如图8所示的画面,然后点“Modify Categories (WITH TRANSACTION)”.这将导致页面回传并试题更新所有products的CategoryID值,这将导致违背外键约束(见图9).
图8:Products将显示在一个分页的GridView控件里
图9:导致违背外键约束
现在点击浏览器的Back按钮,再点击“Refresh Grid”按钮,此时你看到的界面和图8的界面一摸一样。这是因为发生了违背外键约束,导致回滚,所有的操作失败.
再点“Modify Categories (WITHOUT TRANSACTION)”按钮,这同样将违背外键约束(见图9),不过这一次,那些对CategoryID属性赋以有效值的操作不会回滚.点击浏览器的Back按钮,再点“Refresh Grid”按钮。就像图10显示的那样,最开始的8个产品的CategoryID值已经发生了更改,比如,在图8里Chang的CategoryID值为1,而在图10里就变成了2了.
图10:某些Product的CategoryID值发生了改变,而其它的没有
结语:
默认情况下,TableAdapter的方法没有使用事务来执行数据库命令,不过只需多做一点工作我们就可以添加一些用于创建、提交、回滚事务的方法.在本教程,我们在ProductsTableAdapter class类里创建了这3个方法:BeginTransaction, CommitTransaction,和RollbackTransaction.我们考察了如何在try...catch模块里使用这些方法来执行一系列的修改命令.具体来说,我们在ProductsTableAdapter里创建了UpdateWithTransaction方法,该方法运用Batch Update模式对ProductsDataTable里的每行记录执行必要的更改操作;我们也对BLL里的ProductsBLL class类添加了DeleteProductsWithTransaction方法,它将一系列ProductID值作为输入参数,并使用DB-Direct模式将每个产品删除.这些方法开始都创建一个事务,再在try...catch模块里执行数据更改命令.如果抛出异常,则回滚事务,否则提交事务.
第五步演示了事务的作用。在接下来的3章我们将以本章为基础,创建批更新、批删除、批添加的用户界面.
祝编程快乐!
作者简介
本系列教程作者 Scott Mitchell,著有六本ASP/ASP.NET方面的书,是4GuysFromRolla.com的创始人,自1998年以来一直应用 微软Web技术。大家可以点击查看全部教程《[翻译]Scott Mitchell 的ASP.NET 2.0数据教程》,希望对大家的学习ASP.NET有所帮助。