分享
 
 
 

ADO.NET的开发场景及传统ADO的处理

王朝c#·作者佚名  2006-11-24
窄屏简体版  字體: |||超大  

当转为使用ADO.NET时,您将需要了解如何应对以前知道用ADO处理而现在必须用ADO.NET解决的场景。就像使用Visual Basic、C++和ASP开发的N层解决方案经常要依赖ADO来满足数据访问需要一样,Windows?窗体、Web窗体和Web服务也要依赖ADO.NET。我曾经从使用传统ADO开发的角度讨论了如何使用ADO.NET来处理一些数据访问的场景。其中的一些主题包括将行集保留为XML、处理只进流水游标和执行Command对象的多种方式。在文中,我将继续讨论使用ADO.NET的开发场景,以及使用传统的ADO技术是如何处理的。我将从经常使用的传统ADO的只进、静态、键集和动态游标的情况开始讨论。我还要讨论如何处理并发性问题,以及断开连接的行集如何从ADO演变到ADO.NET。然后,我会说明如何将使用传统的ADO处理批量更新的代码转换为使用ADO.NET(用DataAdapter及其四个命令对象)的代码。

分散Recordset的功能

在ADO.NET中,ADO Recordset的大多数功能都分到了三个主要对象中:DataReader、DataSet和DataAdapter(参见图1)。

图1 ADO Recordset

ADO.NET DataReader对象被设计成服务器端的只进、只读游标。ADO.NET DataSet对象是行集断开连接的存储工具。它存储记录,但不持有与数据源的连接,事实上,它并不关心其行集从哪个数据源派生。DataSet在内存中存储时是一个二进制对象,但是它可以轻松地从XML序列化和序列化为XML。这与ADO Recordset对象从其相关联的Connection对象断开连接(通过Recordset的ActiveConnection属性)时将CursorType设置为adOpenStatic、将CursorLocation设置为adUseClient是类似的。ADO.NET DataAdapter对象是连接与DataSet对象之间的桥梁,它可以通过一个连接从数据源加载一个DataSet,并用DataSet中存储的已更改的内容更新数据源。ADO Recordset的行为取决于其属性的设置(包括CursorType和CursorLocation属性)。在ADO.NET中构建了不同的对象来处理这些特定的情况,而无需用一个对象来应对所有情况。

流水游标

传统的ADO公开了四个不同类型的游标,可以改变ADO Recordset对象的运作方式。ADO Recordset对象的行为方式因其CursorType属性的设置情况互不相同,可能有非常大的差异。例如,通过将CursorType设置为adOpenForwardOnly,Recordset可保持与其数据源的连接,而且必须按只进方向进行遍历。然而,当您将CursorType属性设置为adOpenDynamic时,Recordset可向前或者向后遍历,甚至可以使游标跳到某个特定行。通过其CursorType和CursorLocation属性,ADO Recordset对象采用了一种将许多解决方案包装到单个对象中的方式。而ADO.NET采用的方法则不相同,它设计由不同的对象和方法来处理各种特定情况。在传统的ADO中,只进、只读游标是通过将CursorType设置为adOpenForwardOnly、将CursorLocation设置为adUseServer(这也是默认设置)实现的,这将使Recordset对象采用只进的服务器端游标形式。MoveNext方法将Recordset重定位到下一行,而MovePrevious方法则根本不允许使用,但是可以调用Recordset的MoveFirst方法。这有些容易引起误解,因为该方法并不将Recordset重定位到当前行集的开始;相反,它调用原来的SQL语句,从头开始重新填充Recordset,因此会再次移动到第一个记录。每次执行传统ADO Recordset的MoveFirst方法时(CursorType设置为adOpenForwardOnly),通过打开SQL事件探查器工具,观察SQL的执行情况,可以很容易地看到这一点。在需要逐个遍历成千上万(乃至更多)行中的每行或者在需要小一些的行集但是只需遍历一次(可能为了加载到一个选取列表)时,这种游标是非常常用的:

'-- Forward-only Firehose Cursor in ASP and ADO

Set oRs.ActiveConnection = oCn

oRs.LockType = adLockReadOnly

oRs.CursorType = adOpenForwardOnly

oRs.CursorLocation = adUseServer

oRs.Open sSQL

最接近这种传统的ADO流水游标的等效物是ADO.NET DataReader对象。和传统的只进ADO Recordset一样,DataReader在打开的同时保持着与其数据源的连接,而且只能以前进的方向进行遍历。但是,它们还是有区别的。区别之一是,专门为某个data provider(如SQL Server?(SqlDataReader类)或者ODBC数据源(OdbcDataReader类))编写单独的DataReader类型。ADO.NET DataReader对象非常高效,这是因为它是专门为实现只进、只读游标这一目的而构建的。传统ADO的只进游标是通过相同的Recordset对象(作为一个断开连接的Recordset甚至是一个数据源敏感的Recordset)实现的。而DataReader只是为了作为轻型流水游标而设计的。

//-- ASP.NET and ADO.NET in C#

SqlDataReader oDr = oCmd.ExecuteReader();

数据敏感游标

传统ADO的只进游标现在由DataReader对象处理。但是,对基础数据库中的更改非常敏感的键集和动态服务器端游标(CursorType = adOpenKeySet和CursorType = adOpenDynamic)又怎么样呢?当前版本的ADO.NET并没有公开多方向、可滚动、可更新的服务器端游标。但是,ADO.NET确实提供了许多方式避免使用这些类型的服务器端游标,因为有其他推荐的技术可以考虑。除了使用DataSet结合DataAdapter来检索和更新数据以外,还有一些好的替代方案在客户端使用可滚动的服务器端游标。例如,可以使用存储过程,在运行服务器端SQL处理时这是非常高效的。还可以通过服务器端只进游标用DataReader检索数据,然后用Command对象分别进行更新。但是,经常有比使用可更新的服务器端游标更高效的修改数据的方式。

在一个N层环境中,服务器端可滚动游标的问题之一在于,它们需要在服务器上保持状态。因此,如果应用程序在中间层使用服务器端可滚动游标,客户端层就需要维持与可滚动游标所在的业务层的连接。这样,在同样使用可滚动游标的客户端应用程序屏幕存在期间,业务对象都需要保留。各种文档中已经很好地记录了这些可伸缩性问题,但是,在一些情况下服务器端游标还是很有价值的,如需要只进流水游标的情况。例如,在应用程序需要注重数据并发性问题的时候就要用到服务器端游标。使用传统的ADO,Recordset可以用一个动态游标打开,这种游标使Recordset能够获悉所有其他用户进行的插入、更改和删除操作。这种服务器端游标对于其他用户所做的更改非常敏感,可以在应用程序出现并发性问题的时候用于采取主动措施。例如,传统ADO中的动态游标经常用于这样的场合:应用程序准备在数据库中保存更改,但是又需要确保首先知道是否有其他人更改了同一条记录。当用户更改动态Recordset中一个记录值的时候,该值将自动在服务器端动态Recordset中更新:

'-- Dynamic Cursor in ASP and ADO

Set oRs.ActiveConnection = oCn

oRs.LockType = adLockOptimistic

oRs.CursorType = adOpenDynamic

oRs.CursorLocation = adUseServer

oRs.Open sSQL

服务器端游标与并发性

并发性问题在许多企业级应用程序中是合理而且常见的问题,但是使用服务器端游标以解决这一问题的代价却可能非常之高。那么在ADO.NET中有什么替代方式处理并发性问题呢?常见的技术是允许应用程序对数据库提交更改,然后在遇到并发性问题时引发特殊类型的异常。有一个特殊类型的异常名为DBConcurrencyException,它是从Exception基类派生而来的。因为DataSet将每列的原始值和拟更改值存储到一行中,它知道如何将值与数据库进行比较以自动寻找并发性问题。例如,假定用户将firstname字段的值由Lloyd更改为Lamar,并将更改提交给数据库,存储在DataSet中的更改将通过DataAdapter的Update方法传递给数据库。在数据真正保存之前,如果数据库中的名字不再是Lloyd,现在变成了Lorenzo,将会引发一个DBConcurrencyException异常。此异常可以以最适合应用程序需要的任何方式进行捕获和处理,例如,可以采取“最后者优先”的规则,使用户可以选择取消更改、改写或者强制更新或其他技术。

public DataSet SaveData(DataSet oDs)

{

string sMethodName = "[public void SaveData(DataSet oDs)]";

//==========================================================

//-- Establish local variables

//==========================================================

string sProcName;

string sConnString = "Server=(local);Database=Northwind;Integrated

Security=SSPI";

SqlDataAdapter oDa = new SqlDataAdapter();

SqlTransaction oTrn = null;

SqlConnection oCn = null;

SqlCommand oInsCmd = null;

SqlCommand oUpdCmd = null;

SqlCommand oDelCmd = null;

try

{

//==========================================================

//-- Set up the Connection

//==========================================================

oCn = new SqlConnection(sConnString);

//==========================================================

//-- Open the Connection and create the Transaction

//==========================================================

oCn.Open();

oTrn = oCn.BeginTransaction();

//==========================================================

//-- Set up the INSERT Command

//==========================================================

sProcName = "prInsert_Order";

oInsCmd = new SqlCommand(sProcName, oCn, oTrn);

oInsCmd.CommandType = CommandType.StoredProcedure;

oInsCmd.Parameters.Add(new SqlParameter("@sCustomerID", SqlDbType.NChar, 5, "CustomerID"));

oInsCmd.Parameters.Add(new SqlParameter("@dtOrderDate",

SqlDbType.DateTime, 8,"OrderDate"));

oInsCmd.Parameters.Add(new SqlParameter("@sShipCity",

SqlDbType.NVarChar, 30, "ShipCity"));

oInsCmd.Parameters.Add(new SqlParameter("@sShipCountry", SqlDbType.NVarChar, 30, "ShipCountry"));

oDa.InsertCommand = oInsCmd;

//==========================================================

//-- Set up the UPDATE Command

//==========================================================

sProcName = "prUpdate_Order";

oUpdCmd = new SqlCommand(sProcName, oCn, oTrn);

oUpdCmd.CommandType = CommandType.StoredProcedure;

oUpdCmd.Parameters.Add(new SqlParameter("@nOrderID", SqlDbType.Int, 4, "OrderID"));

oUpdCmd.Parameters.Add(new SqlParameter("@dtOrderDate", SqlDbType.DateTime, 8,"OrderDate"));

oUpdCmd.Parameters.Add(new SqlParameter("@sShipCity", SqlDbType.NVarChar, 30, "ShipCity"));

oUpdCmd.Parameters.Add(new SqlParameter("@sShipCountry", SqlDbType.NVarChar, 30, "ShipCountry"));

oDa.UpdateCommand = oUpdCmd;

//==========================================================

//-- Set up the DELETE Command

//==========================================================

sProcName = "prDelete_Order";

oDelCmd = new SqlCommand(sProcName, oCn, oTrn);

oDelCmd.CommandType = CommandType.StoredProcedure;

oDelCmd.Parameters.Add(new SqlParameter("@nOrderID", SqlDbType.Int, 4, "OrderID"));

oDa.DeleteCommand = oDelCmd;

//==========================================================

//-- Save all changes to the database

//==========================================================

oDa.Update(oDs.Tables["Orders"]);

oTrn.Commit();

oCn.Close();

}

catch (DBConcurrencyException exDBConcurrency)

{

//=======================================================

//-- Roll back the transaction

//=======================================================

oTrn.Rollback();

//--------------------------------------------------------

//-- May want to rethrow the Exception at this point.

//-- This depends on how you want to handle the concurrency

//-- issue.

//--------------------------------------------------------

//-- throw(exDBConcurrency);

}

catch (Exception ex)

{

//==========================================================

//-- Roll back the transaction

//==========================================================

oTrn.Rollback();

//--------------------------------------------------------

//-- Rethrow the Exception

//--------------------------------------------------------

throw;

}

finally

{

oInsCmd.Dispose();

oUpdCmd.Dispose();

oDelCmd.Dispose();

oDa.Dispose();

oTrn.Dispose();

oCn.Dispose();

}

oCn.Close();

return oDs;

}

图2 捕获ADO.NET中的并发性问题

在图2 的代码中,可以注意到一个示例方法,它接收一个DataSet参数,并将DataSet中的更改通过DataAdapter的Update方法提交给数据库。如果检测到并发性问题,将由一个特定的catch代码块引发和捕获DBConcurrencyException异常。此时,事务可以回滚,然后还可重新引发异常,直到最终异常被客户端应用程序捕获,从而得以通知用户并询问用户希望如何处理。这里同样要仔细地考虑catch代码块的顺序。例如,如果首先出现一般性的catch代码块,那么它已经捕获了并发性问题。异常处理的关键在于,要确保针对更特定异常的catch代码块在其他catch代码块之前出现,从而可由特定catch代码块首先捕获特定异常。

批量更新

ADO Recordset对象经常会用到的一种情形是进行批量更新。例如,在一个N层应用程序中,ADO Recordset可检索一个行集,将其从数据源中断开连接,并将Recordset发送到客户端层。在客户端应用程序中,对几行的更改将在断开连接的ADO Recordset中进行。然后Recordset可被发回给中间层,通过ADO Connection对象与数据库重新连接,其更改可通过UpdateBatch方法应用于数据库。UpdateBatch方法将接受Recordset中的所有被更改的行,并将更改应用于Recordset源查询中指示的源表。ADO.NET也有内置的处理批量更新的功能。DataSet是一个自成一体的、总是断开连接的行集集合,它可以存储行集中行与列的原始值和当前值。DataSet可以被发送给客户端应用程序,由后者进行更改操作。DataSet然后被发送到中间层,其更改将通过DataAdapter对象应用于数据库。图2说明了ADO.NET DataAdapter如何使用其不同的Command对象执行SELECT、INSERT、UPDATE和DELETE SQL命令:

//-- Setting the ADO.NET DataAdapter's command objects

oDa.InsertCommand = oInsCmd;

oDa.UpdateCommand = oUpdCmd;

oDa.DeleteCommand = oDelCmd;

oDa.Update(oDs.Tables["Orders"]);

ADO.NET技术在对数据库应用更新的方式上比传统的ADO批量更新技术更灵活。传统ADO是通过查看ADO Recordset的Source属性来确定保存更改的位置。例如,假定Recordset的源是:

SELECT OrderID, CustomerID, OrderDate, ShipCity, ShipCountry FROM Orders

这种情况下,在对Recordset已经进行批量更改而且调用了Recordset的UpdateBatch方法之后,Recordset必须指向发送更改的位置。它查看源的SQL语句,并确定Orders表的主键(OrderID)在语句中而且只用到了一个表。因此,它可以使用SQL语句创建UPDATE、INSERT和DELETE语句对Orders表进行处理。为了能够派生操作查询,Recordset必须知道唯一行标识符。同样,存储过程不能用于这种技术更新数据库,因为语句是隐式创建的。以下ADO 2.x代码说明了包含Orders的Recordset如何在客户端应用程序中进行更新:

'-- Using traditional ADO,

'-- Updating two rows in the client tier

oRs.Find "CustomerID = 'VINET'"

oRs.Fields("ShipCity") = "Somewhere"

oRs.Update

oRs.Find "CustomerID = 'CHOPS'"

oRs.Fields("ShipCity") = "Elsewhere"

oRs.Update

之后,一旦Recordset传回给中间层,将调用以下代码从而将已完成的更改应用于数据库:

'-- Using traditional ADO,

'-- Sending both updated rows to the database, in the business tier

oRs.UpdateBatch

ADO.NET CommandBuilder的工作方式与此类似,因为它可以自动地为DataAdapter生成UpdateCommand、InsertCommand和DeleteCommand对象。自动生成命令的过程也会带来系统开销。显式地指定要使用的INSERT、UPDATE和DELETE语句效率更高。与显式指定命令相比,使用ADO.NET CommandBuilder意味着性能更低下,在对数据源应用更改方式上的控制也更差。准确的SELECT、INSERT、UPDATE和DELETE语句通常在设计时是已知的,可以用来生成ADO.NET DataAdapter的四个不同Command属性,它们甚至可以用存储过程的名称表示。ADO.NET的这一功能是传统的ADO所缺乏的一个关键部分,这可以大大提高传统ADO的批量更新技术的灵活性。当然,这需要在设计时进行更多的编码工作,但为了获得它所提供的更大灵活性(可使您非常具体地指定SQL语句或更常见地用于为每个操作指定存储过程),付出这些努力也是值得的。

断开连接的行集

通过适当设置一些属性,ADO Recordset对象可以从其数据源中断开连接。通过断开连接,它能够在内存中存储整个行集,并在使用传统ADO的应用程序之间传递。以下代码示例将断开Recordset的连接:

'-- Disconnecting an ADO Recordset

oRs.CursorLocation = adUseClient

oRs.CursorType = adOpenStatic

oRs.LockType = adLockBatchOptimistic

oRs.Open

Set oRs.ActiveConnection = Nothing

断开Recordset的连接涉及的关键属性是CursorType和CursorLocation。CursorLocation必须设置为adUseClient,表示行集应该存储在Recordset的内存中。CursorType应该设置为adOpenStatic,这将允许行集的游标能够在任何方向上移动,但是不允许行集自动对基础数据库的更改处于敏感状态。通过结合使用这两个设置,Recordset可以断开连接,但是,全部步骤还没有完。一旦打开Recordset,因为它真的断开连接了,那么它的ActiveConnection属性应该被设置为关键字Nothing,就像上一个代码示例中看到的那样。构建ADO Recordset是为了在断开连接和已连接这两种模式下工作。但是在ADO.NET中,当DataSet完全断开连接时,DataReader要维持连接。DataSet是断开连接的ADO Recordset的继承者,因为它实现了一个客户端游标,可以向任何方向滚动,而且ADO.NET DataSet还支持许多其他功能。传统的ADO Recordset对象提供的断开连接功能非常有限,而ADO.NET DataSet对象是专门为断开连接而设计的。与Recordset不同,DataSet可以在DataTable对象内存储和表示多个行集,甚至还可以使用DataRelation对象将它们互相联系起来。下面是使用SqlDataAdapter的Fill方法创建和填充DataSet的一种常见方式:

//-- Creating and filling an ADO.NET DataSet

DataSet oDs = new DataSet("MyDataSet");

oDa.Fill(oDs);

DataSet可以施加关系、主键约束和外键约束,甚至实现列表达式。ADO Recordset还能够使用它的数据构形功能返回层次化的结果。虽然有这些层次化功能,但是它们需要使用我个人认为非常笨拙的数据构形语法。就此方面而言,ADO.NET DataSet更加高效也更加直观,因为它就是为使用其DataRelation和DataTable对象处理关系数据结构而构建的。

在之前,我讨论了几个传统的ADO 2.x开发中常用的技术,以及如何将它们转换到ADO.NET。ADO.NET是ADO 2.x发展的下一阶段,许多传统ADO中的功能都将在ADO.NET中进行全面改造。例如,ADO 2.x中添加了Open和Save方法的功能以支持XML,而XML支持从一开始就已经被纳入到ADO.NET的设计蓝图中了。ADO的一些功能发生了很大变化,变化之一就是Recordset的许多游标类型被分为多个不同的各有侧重的对象,例如DataReader、DataSet和DataAdapter。其他功能(例如Connection和Command类)的改变则不是那么明显。但是,说到底,从ADO到ADO.NET的迁移还是相对比较易懂的,这是因为ADO.NET的设计构筑于传统ADO的使用体验之上。

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
推荐阅读
 
 
 
>>返回首頁<<
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有