ADO.NET 2.0 基础

译自Apress.Pro.ASP.NET.2.0.in.C#.2005

7 ADO.NET基础

相当多的计算机应用,包括桌面和Web应用,都是数据驱动的(data-driven)。这些应用特别关心获取、显示和修改数据。

获取和处理数据看起来是很直截了当的工作,但在过去十年里,应用使用数据的方法反复改变了多次。开发人员从简单的使用本地数据的客户端应用转移到了基于专业集中化的数据库的分布是系统上。同时,数据访问技术得到了发展。如果你有使用微软语言的经验,你可能已经听过(甚至用过)数据访问技术的一些名词,包括ODBC,DAO,RDO,RDSADO

.NET框架有它自己的数据访问技术---ADO.NETADO.NET包含了可管理的类。这些类允许.NET应用连接到数据源(通常是关系数据库),执行命令,并且管理未连接的数据。ADO.NET的一点神奇之处是它允许你为Web应用写的数据访问代码与C/S桌面应用的代码差不多的方式,甚至与0连接到本地数据库的单用户应用一样的。

本章描述了ADO.NET的结构和ADO.NET数据提供者。你会学习到ADO.NET的基础,如打开连接,执行SQL语句或存储过程,获得查询的结果。你也会学习如何防止SQL注入攻击和使用事务。

 

ADO.NET.NET2.0里的变化

如果你已经习惯了.NET1.1编程,你会想知道在最新的ADO.NET中有哪些亲折内容。ASP.NET应用最大的变化是数据绑定模型(在第9章和第10章描述). 数据绑定使你能够在编写数据显示程序时, 减少大量代码, 你甚至可以不写一行代码.(如果你愿意面对使用页面进行支付时具有很少的灵活性, 优化相当困难的局面的话).

尽管有了新的数据绑定模型, ADO.NET的底层变化并不大。多数变化是内部的(如DataSet串行格式更加浓缩,占用更小的内存),或者对那些在未来的Web应用中用处不大的装饰性内容进行了修改(如用于在两个数据库服务器间快速传整整个表的新的SQL批量拷贝特征)。在.NET2.0 Beta版中,一些特征被去掉了(如为了获得查询的部分结果的内建页支持,为关系映射而开发的ObjectSpaces系统,使Dataset-to-XML会话功能更强的xmlAdapter,等等)。这些功能可以在分离的工具或者后续版本的.net中找到,但现在的程序员还得不得到这便利。

那么,那到底给我们带来了什么呢?这里列举了一些真正吸引人的变化:

提供者工厂(Provider factories:通用数据访问代码(也就是写一次就能应用于多个不同数据库的代码)之梦在.NET2.0中获得了巨大的飞跃。这个飞跃来自于提供者工厂。提供者工厂是一个新的组件,它能够快速地创建强类型的连接(connection,命令(command)和数据适配器(DataAdapter)对象。在这一章中你会了解到它。

改变通知(Change notification):为了创建确实可扩展的Web应用,你需要缓存从数据库获得的数据,使它能够重用,而不需要每次都连接到数据源。但是,缓存的引入可能会导致过期信息(out-of-date information),ADO.NET包含了新的改变通知特征,以便于数据库的记录更新后可以自动移除缓存的数据。这个内容将在第11章中介绍。

连接统计(Connection statistics):这是一个小小的点缀,但是SqlConnection对象的新的连接跟踪特征将帮助你描述不同数据访问策略。本章中将对此进行介绍。

SQL Server2005SQL Server2005引入了一整套新特征,ASP.NET2.0对这些新特征提供了无缝支持。这些特征包括基于.NET类的用户自定义数据类型,也可用在.NET语言编写存储过程语句。关于这些特征的更多信息可以参考专门的SQL Server2005书籍,比如《A First Look at Microsoft SQL Server 2005 for Developers(Addison-Wesley, 2004) or Pro SQL Server 2005 Assemblies (Apress, 2005).

 

ADO.NET结构

ADO.NET使用了多层结构,这个结构围绕几个关键概念进行,比如Connection, Command, DataSet对象。但是,AD.NET结构与经典的ADO还是有些区别的。

ADOADO.NET的一个关键区别是怎样面对不同数据源的挑战。在ADO中,程序员通常使一个通用的对象集,而不需要下层的数据源是什么。例如,如果你想从Oracle数据库中获取一条记录,你使用与在SQL Server中完成相同任务的同样的Connection类。这种情况在ADO.NET中不会发生,因为后者使用了数据提供者(data provider)模型。

 

ADO.NET数据提供者

数据提供者(data provider)是一个ADO.NET类集,它允许你访问特定的数据库,执行SQL命令,获取数据。本质上,数据提供者是应用和数据源的桥梁。

这些类包装数据提供者包含如下内容:

Connection: 你使用这个对象去建立到数据源的连接。

Command: 你使用这个对象执行SQL命令和存储过程。

DataReader: 这个对象提供从查询中快速地、前向地(forward-only)、只读地(read-only)访问数据。

DataAdapter: 这个对象执行两项任务。第一项,你可以使用它来将从数据源获取的特定信息填充DataSetDataSet是一个非连接的表和关系的集合)。第二项,你可以使用它将修改后的内容应用到数据源。

ADO.NET并没有包含通用的的数据提供对象。相反,它为不同类型的数据源提供了不同的数据提供者。每个数据提供者都有为优化特定的关系数据库管理系统(RDBMS)的Connection, Command, DataReaderDataAdapter类的特定的实现。例如,如果你想创建到SQL Server数据库的连接,你需要使用SqlConnection连接类。

ADO.NET提供者模型的一个基本的思想是可扩展性。换句说话,程序员能够为私有的数据源创建自己的提供者。事实上,可以找到大量的概念示例来帮助你了解如何简便地创建自定义的ADO.NET提供者,以便于包装非关系的数据存储,如文件系统或者目录服务。一些第三方开发者有自定义的.NET提供者出售。

.NET框架由下面四个极小的提供者集组合而成。

SQL Server provider:提供对SQL Server数据库的优化访问(不低于7.0版)。

OLE DB provider:提供具有OLE DB驱动的任何数据源的访问,包括SQL Server 7.0以前的版本。

Oracel provider:提供对Oracle数据库的优化访问(不低于8i版本)。

ODBC provider:提供对具有ODBC驱动的任何数据源的访问。

7-1显示了ADO.NET提供者模型的各个层次

7-1 ADO.NET结构

选择提供者时,你应该首先选择本地的.NET提供者来为你的数据源进行自定义。如果找不到本地的提供者,你可以使用OLD DB提供者,只要具备为了访问数据源的OLD DB驱动。OLE DB技术在很多年前就已经成为ADO的一部分了,因此大多数数据源提供OLE DB驱动(包括SQL Server, Access, MySQL等等)。在很少的情冲下,你找不到专门的.NET提供者和OLE DB驱动器,那么你可以退回去使用ODBC提供者,它连接到ODBC驱动器进行工作。

提示:在同时能够使用OLE DB提供者和为某种数据源专门提供的提供者时,你应该选择后者,因为后者是为其专门进行了优化的,执行得最好。

 

ADO.NET标准化

粗看起来,ADO.NET提供的模型是零散的,因为它没有包含一个通过的对象集,来处理多种不同类型的数据库。因此,如果你从使用的RDBMS改变成其它数据库,你需要使用不同的类集,修改数据访问代码。

尽管不同的.NET数据提供者使用不同的类,但所有提供者都是采用同一方法进行标准化的。说得更明确一点,就是每个提供者都是基于相同的基类和接口集。例如,每个连接对象都执行IDbConnection接口,这些接口定义了核心的方法,如Open()Close()。这个标准化保证每个连接类以同样的方式工作,并且向用户提供的核心属性和方法集是一样的。

在可视的界面之后,不同的提供者使用完全不同的底层调用和API。举个例来说,SQL Server提供者使用私有的TDSTabular Data Stream)协议来与服务器通信。这个模型的好处并不是显而易见的,但它们很重要:

因为每个提供者使用相同的接口和基类,你可以编写通用的数据访问代码(只需一点点的努力),而不是需要针对具体的提供者类编写代码。你将在“Provider-Agnostic Code”一节中看到这个技术所起的作用。

由于每个提供者的实现是完全分离的,它可以使用私有的优化措施(这与ADO模型不同。在ADO中,每个数据库调用需要在到达底层数据库驱动前要使用通过的层来进行过滤)。另外,自定义提供者能够添加非标准的功能,扩展在其它提供者中的不足(如SQL Server能够执行XML查询)。

ADO.NET也有另一个标准的层:DataSetDataSet是一个能满足所有要求的数据容器,这些数据来自于一个数据源的一个或多个表。DataSet非常通用,换句话说,自定义提供者不需要定义DataSet类的自定义版本。无论你使用哪种数据提供者,你都能够以同样的方法提取数据和将数据放入未连接的DataSet。这样就使得数据获取代码和数据处理代码分离开来。如果你改变了底层的数据库,你只需要改变数据获取代码,但如果你使用的DataSet和你的信息具有相同的结构,你就不需要改变数据处理的方法。

提示:下一章将对DataSet进行详细介绍,你将学到如何使用ADO.NET执行直接的、基于连接的访问的基本原理。

 

SQL Server 2005

ADO.NET提供少量支持SQL Server 2005的功能,这些功能包括:

多活动结果集(MARSmultiple active result sets):这允许你同时有多个活动的查询。例如,你可以查询顾客列表,在不关闭查询的同时查询订单列表。这个技术有时很有用,但它要求你能够避免额外的过载。

用户自定义数据类型(User-defined data types):使用.NET编码,你可以自定义类,并且将类的实例直接存储在数据库的一列里。这会使检查一行里的内个字段,并且手工创建应用中要使用的相应的数据对象的工作大为减少。

被管理的存储过程:SQL Server 2005能够以CLR为宿主,这样就可以使用纯C#代码在数据库中编写存储过程。

SQL通知(SQL nitificatons):当数据库中指定的改变发生时,使用通知可以编写相应的响应代码。在ASP.NET中,这个功能通常用于数据库中的一条或多条记录更新时,使缓存的数据失效。这仅仅是SQL Server 2005的功能,但同时也支持SQL Server 7 SQL Server 2000,当然,这需要使用不同的机制。

事务隔离快照(Snapshop transaction isolation):这是一个新的事务层次,它可以使你增强并发水平。它允许事务查看稍有点早的数据版本,尽管这些数据已经被别的事务更新了。

本书的大部分内容都集中关注与关系数据库相关的程序设计技术。当然,由于SQL通知在ASP.NET中的应用很广,在第11章也对SQL通知进行了详细讲述。在更早的SQL Server版本中,它们通过别的技术,也提供了支持。本章简要地涉及了隔离快照。针对SQL Server 2005的其它更多功能,你可以查阅 A First Look at Microsoft SQL Server 2005 for Developers(Addison–Wesley,2004) Pro SQL Server 2005 Assemblies(Apress, 2005).

 

ADO.NET基础类

ADO.NET有两种对象:基于连接的(connection-based)和基于内容的(content-based)。

基于连接的对象Connection-based objects):有一些数据提供者,如Connection, Command, DataAdapterDataReader。它们执行SQL语句,连接到数据库,或者填充DataSet。基于连接的对象是针对特定的数据源开发的。

基于内容的对象Content-based objects):这些对象是数据包。它们包含DataSetDataColumn, DataRow, DataRelation,以及其它的几个对象。它们与数据源的类型相独立,在System.Data命名空间定义。

本章的余下部分里,你将学习到ADO.NET的第一个层次,也就是基于连接的对象,包括Connection, Command, DataReader。你根本不需要了解更高层次的DataAdapter,因为DataAdapter被设计来用于DataSet,这将在第8章讨论(本质上,DataAdapter是一组相关的通用对象,这些对象使DataSet和数据源进行同步)。

.NET类被组合到几个命名空间中。每个提供者有它们自己的命名空间,通用的类,如DataSet存储在System.Data命名空间中,表7-1描述了命名空间:

命名空间

描述

System.Data

包含关键数据容器类,它们规范了列(Columns),关系(relations),表(tables),数据集(dataset),行(rows),视图(views)和约束(constraints)。另外, 包含了基于连接的数据对象的实现的关键接口。

System.Data.Common

包含基础的,绝大多数是抽象的类。实现了来自于System.Data的一些接口,并且定义了ADO.NET的核心功能。数据提供者从这些类继续并且创建它们自己特定的版本。

System.Data.OleDb

包含了常用于连接到OLE DB的提供者,包括OleDbConmmand, OleDbConnection, OleDbDataAdapter。这些类支持大多数的OLE DB提供者,要求OLE DB 2.5版本接口的除外。

System.Data.SqlClient

包含了连接到SQL Server数据库的类,包括SqlDbCommand, SqlDbConnectionSqlDBDataAdpater。这些类优化了连接到SQL ServerTDS接口。

System.Data.OracleClient

包含了连接到Oracle数据库的类(不低于8.1.7版本),包括:OracleCommand, OracleConnection, OracleDataAdapter。这些类使用优化的Oracle调用接口(OCI, Call Interface)。

System.Data.Odbc

包含了要求连接到多数ODBC驱动的类。这些类包括OdbcCommand, OdbcConnectionOdbcDataAdapterODBC驱动适用于所有的数据源,通过控制面版中的数据源图标进行配置。

System.Data.SqlTypes

包含了匹配SQL Server数据库的本地数据的结构。这些类并不是必需的,但是提供了使用标准.NET数据类型的一种方法,以便于自动转换。

7-1  ADO.NET命名空间

 

Connection

Connection类允许你建立到数据源的连接,以便于与之进行交互。在你做任何事情之前(包括获取、删除、插入、更新数据),你需要建立连接。

Connection的核心属性和方法由IdbConnection接口接定,这些接口由所有Connection类实现。

 

连接串(Connection Strings

当你创建一个Connection对象时,你需要提供一个Connection串。连接串是分号分隔的名字/值(name/value)设置序列。这些设置的顺序和大小写并不重要。这些设置放在一起,就构成了创建连接的基本信息。

尽管连接串是基于RDBMS(关系数据库管理系统)和使用的提供者的,其中只有一些信息是经常用到的:

数据库所在的服务器:本书的示例中,数据库服务器与ASP.NET应用基本上都处于同一台电脑上,环回别名localhost就代替了计算机名。

希望使用的数据库:本书的大多数示例使用Northwind数据库,它是大多数SQL Server默认安装的数据库。

数据库怎样进行授权:OracleSQL Server提供者使你能够选择信任机制或者作为当前用户进行登录。后者是最好的,因为这不需要你在代码中或者配置文件中放置密码信息。

举个例来说:下面是一个连接到Northwind数据库的连接字符串,数据库服务器位于本机,使用整合安全(integrated security,也就是说使用登录windows的用户访问数据库):

string connectionString = "Data Source=localhost;Initial Catalog=Northwind;" +"Integrated Security=SSPI";

如果不支持整合安全,连接必须指明一个有效的用户和密码组合。对于新安装的SQL Server数据库,sa(系统管理员)帐户通常都存在,下面的连接串使用了这个帐户:

String connectionString = “Data Source = localhost; Initial Catalog = Northwind;” + “user id = sa; password = opensesame”;

如果使用的是OLE DB提供者,连接串是相似的,只是需要额外的提供者设置来标识是OLE DB驱动器。例如,你可以使用下面的连接串,通过MSDAORA OLE DB提供者来连接到Oracle数据库。

String connectionString = “Data Source =- localhost; Initial Catalog = Sales;” + “user id = sa; password =; Provider = MSDAORA”;

连接到Access数据库文件的示例也在这里列出:

String connectionString = “ Provider = Microsoft.Jet.OLEDB.4.0;” +@”Data Source = c:/DataSources/Northwind.mdb”;

提示:如果使用的不是SQL Server,你需要查询相应的数据提供者文档,以确定支持的连接串值。例如,大多数数据库支持Connect Timeout设置,它在抛出异常前会有相应数量的时间来等待连接(SQL Server的默认值是15秒)。

你当你创建Connection对象时,可以将连接串作为构造函数的参数进行传递。同时,你可以在打开连接前,手工设置ConnectionString属性。

没有理由硬编码连接串。如第5章中讨论的那样,Web.config文件中的<connectionStrings>节很便于放置连接串。示例如下:

<configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0">

<connectionStrings>

<add name="Northwind" connectionString=

"Data Source=localhost;Initial Catalog=Northwind;Integrated Security=SSPI"/>

</connectionStrings>

...

</configuration>

你可以通过名字来获取连接串,这需要使用WebConfigurationManager.ConnectionStrings集,见下例:

String connectionString = WebConfigurationManager.ConnectionStrings[“Northwind”].ConnectionString;

接下来的例子都假设你已经将连接串添加到Web.config文件中了。

 

测试连接

一旦你选择了连接串,管理连接就非常容易了----你只需要使用Open() Close()方法就可以了。你可以Page.Load事件句柄中编写如下代码来测试连接,并且将它的状态写入到标签(图7-2所示):

// Create the Connection object.

string connectionString =

WebConfigurationManager.ConnectionStrings["Northwind"].ConnectionString;

SqlConnection con = new SqlConnection(connectionString);

try

{

// Try to open the connection.

con.Open();

lblInfo.Text = "<b>Server Version:</b> " + con.ServerVersion;

lblInfo.Text += "<br /><b>Connection Is:</b> " + con.State.ToString();

}

catch (Exception err)

{

// Handle an error by displaying the information.

lblInfo.Text = "Error reading the database. ";

lblInfo.Text += err.Message;

}

finally

{

// Either way, make sure the connection is properly closed.

// Even if the connection wasn't opened successfully,

// calling Close() won't cause an error.

con.Close();

lblInfo.Text += "<br /><b>Now Connection Is:</b> ";

lblInfo.Text += con.State.ToString();

}

7-2显示了运行结果

7-2 测试连接

 

连接限制了服务器资源。这句话的意思是要求你尽可能晚地打开连接,并且尽可能快地关闭连接。在前面的代码中,异常处理器用来保证发生没有处理的错误时,连接会在最后的代码块里关闭。如果不设计对异常的处理,直到垃圾收集器处理SqlConnection对象时,连接将保持打开状态。

另一个方法是在一个using块中对数据访问代码进行封装。Using语句声明你正在使用可以在短期内随意使用对象。只要using块结束,CLR就通过调用Dispose()方法,立即释放相应的对象。特别有意思的是,调用连接对象的Dispose()方法与调用Close()是一样的。这样你就可以将以前的代码写得更为精练,如下例所示:

string connectionString =

WebConfigurationManager.ConnectionStrings["Northwind"].ConnectionString;

SqlConnection con = new SqlConnection(connectionString);

using (con)

{

con.Open();

lblInfo.Text = "<b>Server Version:</b> " + con.ServerVersion;

lblInfo.Text += "<br /><b>Connection Is:</b> " + con.State.ToString();

}

lblInfo.Text += "<br /><b>Now Connection Is:</b> ";

lblInfo.Text += con.State.ToString();

最美妙的地方就是你无需再写finally块了,因为using声明释放了正在使用的对象,即便你退出的块是未处理异常的结果也是一样的。

 

连接池(Connection Pooling

获取连接会消耗一些时间,在Web应用中,当新的请求到达时,连接会被不断地打开和关闭,以高效地处理请求。在这种环境里,要求建立连接时负载很小变得很重要,并且成了系统扩展性的瓶颈。

一个解决办法就是连接池(Connection Pooling)。连接池就是在使用相同的数据源时,使会话共享的数据库连接保持持久的设置。这样可以避免总在创建和销毁连接。在ADO.NET中,连接池对于程序员是完全透明的,数据访问代码根本不需要修改。当客户通过调用Open()请求连接时,它直接从可获得的池中获得服务,而不是重新创建。当客户通过调Close()Dispose()释放连接时,也不需要丢弃连接,而是返回到池中,为下一个请求服务。

ADO.NET本身没有包含连接池机制。但是,多数ADO.NET提供者实现了连接池的某些形式。SQL ServerOracel 实现了它们自己的高效的连接池算法。这些算法在可管理代码中完全实现----这与某些流行的错误观念形成鲜明对比---不使用COM+企业服务。对于在SQL Server Orace中需要重用的连接来说,连接串能够精确匹配。如果稍有不同,在新的池中会创建新的连接。

提示:SQL ServerOracle池使用纯文本算法。意思就是连接串中的任何丁点的改变都会阻碍连接池,即便是简单地更改参数的顺序或者是在最后面添加一个额外的空格也不能使用连接池。它强制你在Web页中不进行硬编码连接串。相反,你应该在一个地方存放连接串(最好是在web.config)文件的<connectionStrings>节中存放)。

使用SQL ServerOracle提供者,连接池是可用的并且自动使用。然而,你也可以使用连接串参数来配置池的大小。

7-2 连接池设置

设置

描述

Max Pool Size

池中允许的最大连接数量(默认100),如果达到最大值,新的打开连接请求会放入队列,直到连接可用。(如果连接可用前,Connection.Timeout的值超时,会产生一个错误。)

Min Pool Size

池中保留的最少连接数(默认0)。当第一个连接被打开时,会创建相应数量的连接,这会导致第一个请求有一些延迟。

Pooling

设置为true时,连接会从池中提取,或者,在必要时,创建并添加到池中。

Connection Lifetime

指定几秒的时间间隔,如果连接返回到池中时超过了创建时设置的生存时间,它就会被销毁。默认值是0,也就是说禁止了这个行为。当你想立即反复使用大量的连接时,这个功能就会非常有用。

 

下面是设置了minimum pool size的连接串的示例:

string connectionString = "Data Source=localhost;Initial Catalog=Northwind;" +

"Integrated Security=SSPI;Min Pool Size=10";

SqlConnection con = new SqlConnection(connectionString);

// Get the connection from the pool (if it exists)

// or create the pool with 10 connections (if it doesn't).

con.Open();

// Return the connection to the pool.

con.Close();

有些提供者包含了清空连接池的方法。例如,使用SqlConnection,你可以调用静态的ClearPool()ClearAllPools()方法。当调用ClearPool()时,你提供一个SqlConnection,所有匹配的连接都将被移除。

ClearAllPools()在当前的应用域内,清空所有的连接池。(从技术上讲,这个方法并不关闭连接,只是将其标记为无效,这样它们就会超时,当规则的连接过会儿清除时,关闭它们)。这个功能很少使用。唯一的使用场合是,如果你知道池被无效连接充满了(如重启SQL Server时可能会造成这种情况),为了避免出错,才会用到。

提示:Sql ServerOracle连接池通常作为应用域内的全局资源的一部分被维护。因此,如果应用域重启,那么,所有的连接都将丢失(例如,由于更新或者达到了某个阀值)。与此类似,连接池不能够在同一Web服务器上互不相关的Web应用之间,或者在Web应用与其它的.NET应用之间重用连接池。

 

连接统计(Connection Statistics

如果使用SQL Server提供者,你可以使用SqlConnection.RetrieveStatistics()方法(.NET2.0以前没有这个方法)获得一些有趣的统计。RetrieveStatistics返回一个哈希表和不同的底层细节,来帮助你分析命令的性能和执行的任务的数量。连接统计在部署了的应用中并不会经常用到,但在测试和成型期间分析性能时很有用。例如,它们提供了一个工具,你可以使用它来确定不同的数据访问策略执行有何不同(其它工具包括SQL Server管理工具,如SQL ProfilerQuery Analyzer)。

默认的情况下,连接统计被禁用以提高性能。为了使用连接统计,你需要将SqlConnection.StatisticsEnabled属性设置为true。这样就告诉了SqlConnection类收集它执行的每个动作的信息。在任何断点之后,你都可以调用RetrieveStatistics()方法来检查这个信息,或者使用ResetStatistics()来清空它,然后重新开始捕捉。

下面的示例显示了开启统计功能后,从连接上接收的字节数:

Hashtable statistics = con.RetrieveStatistics();

lblBytes.Text = "Retrieved bytes: " + statistics["BytesRetrieved"].ToString();

统计在一个松散类型的名字/值集中提供。这意味着在获取统计之前必须知道它的名字。在MSDN中可以找到帮助列表,在这儿仅列出少数最有用的内容:

ServerRoundtrips:显示连接导致多少次到数据库的请求。典型地,这个值对应执行的命令的数量,诸如批处理命令之类的策略会对其造成影响。

ConnectionTime:显示连接被打开的累积数量。

BytesReceived:显示从数据库服务器获得的字节总数(执行的所有命令的累积结果)。

SumResultSets:显示执行的请求的次数。

SelectRows:记录执行每次请求获得的行的总数。

 

CommandDataReader

Command类允许你执行任何类型的SQL语句。尽管可以使用Command类执行数据定义(data-definition)任务(如创建和修改数据库、表和索引),你可能更愿意执行数据控制(data-manipulation)任务(如获取和更新表中的记录)

提供者明确的Command类执行标准的功能,就象Connection类一样。在这种情况下,IDbCommand接口定义了Command方法的核心集,用于在打开的连接上执行命令。

 

Command基础

在使用Command之前,你需要选择Command类型,设置command文本,并且将command绑定到connection,你可以通过设置对应的属性(CommandType, CommandText, Connection)来完成这项工作,或者将需要的信息作为构造器的参数进行传递。

Command文本可以是SQL语句,存储过程,或者表名。它取决于使用的Command类型。有三种command类型,已在表7-3中列出:

描述

CommandType.Text

Command将直接执行SQL语句,SQL语在CommandText属性中提供,这是默认值。

CommandType.StoreProcedure

Command将执行数据源中的存储过程,CommadnText属性提供了存储过程的名字。

CommandType.TableDirect

Command将查询表中的所有记录。CommandText是需要获取记录的表的名字。(这个选项是为了前向兼容,如OLE DB驱动。SQL SERVER数据提供者对此不支持,它也不能象明确目标的查询那样执行良好。)

7-3 CommandType枚举值

 

下面的示例就是创建一个Command对象来表示查询:

SqlCommand cmd = new SqlCommand();

cmd.Connection = con;

cmd.CommandType = CommandType.Text;

cmd.CommandText = “SELECT * FROM Emplyees”;

使用Command构造器会是效率更高的办法。注意,你不需要指定CommandType,因为CommandType.Text是默认的。

SqlCommand cmd = new SqlCommand(“SELECT * FROM Employees”, con);

除此之外,使用存储过程也是可以的,代码示例如下:

SqlCommand cmd = new SqlCommand(“GetEmployees”, con);

cmd.CommandType = CommandType.StoreProcedure;

这几个例子仅仅简单地定义了Command对象,它们并没有真正执行。Command对象提供了三种方法可用于执行Command,这取决于你是想要获得所有的结果集、单个值,或者仅仅是执行非查询请求(nonquery command)。表7-4列出了这几种方法:

方法

描述

ExecuteNonQuery()

执行non-SELECT命令,诸如insertdelete, updateSQL命令。返回值表示命令所影响的行数。

ExecuteScalar()

执行SELECT查询,并且返回从command命令产生的行集里的第一行的第一个字段。当在SELECT命令中执行使用COUNT()或者SUM()函数计算单个值的总计时,这个方法很有用。

ExecuteReader()

执行SELECT查询,并且返回DataReader对象,它包装了一个只读的、前向的游标。

 

DataReader

DataReader类可以读取通过SELECT命令返回的数据,一次一条记录,以只读、前向流的形式。有时这叫做火带游标(firehose cursor)。使用DataReader是获取数据的最简单的方法,但是它缺乏索引和未连接的DataSet的相关的能力,DataSet将在第8章介绍。尽管如此,DataReader提供了最快的无废话(no-nonsense)数据访问.

7-5列出了DataReader的核心方法。

方法

描述

Read()

在流中推进行游标到下一行。这个方法在读取第一行的数据前就要调用。(当DataReader第一次创建时,行游标被放置在第一行前)。如果还有别的行可读取,Read()方法返回true,如果游标位于最后一行,则返回false

GetValue()

在当前选择的行中,通过指定的列名或者索引,返回存储在字段中的值。返回值的类型与存储在数据源中的当地值最为匹配。如果通过索引访问字段,并且不小心传递了无效的索引,指向不存在的字段,就会抛出一个IndexOutOfRangeException异常。也可以通过名字来访问相同的值,只是这样效率会略低一点,因为DataReader要使用特定的名字来查找相应的行。

GetValues()

将当前行的值保存到一个数组中。保存的字段的数量取决于传递给这个方法的数组的大小。你可以使用DataReader.FieldCount属性来确定行中的字段的数量,然后创建相应大小的数组来保存字段。

GetInt32(),GetChar(), GetDataTime(), GetXxx()

这些方法根据索引返回当前行的指定的字段值,在方法名内指定了数据类型。注意,如果你将返回的值分配给了一个错误的变量类型,会抛出一个InvalidCastException异常。

NextResult()

如果Command产生的DataReader返回了多于一行的值,这个方法移动指针到下一个行集(第一行之前)。

Close()

关闭ReaderReader关闭后,如果发起者Command运行存储过程并且返回了一个输出值,那个值仅仅能从各自的参数中读取。

7-5 DataReader方法

 

ExecuteReader()方法和DataReader

下面的示例创建了一个简单的查询Command,从Northwind数据库的Employees表中返回所有的记录。网页加载时创建了Command

protected void Page_Load(object sender, System.EventArgs e)

{

// Create the Command and the Connection objects.

string connectionString =

WebConfigurationManager.ConnectionStrings["Northwind"].ConnectionString;

SqlConnection con = new SqlConnection(connectionString);

string sql = "SELECT * FROM Employees";

SqlCommand cmd = new SqlCommand(sql, con);

注意:SELECT查询使用了*通配符来获取所有的字段,但在实际的代码中,你可能只需要返回需要的字段,以避免浪费时间。使用where子句来限制返回的记录是一个好的想法,这样可以避免返回不需要的记录。

然后,打开连接,通过ExecuteReader()执行Command,返回一个SqlDataReader,如下所示:

...

// Open the Connection and get the DataReader.

con.Open();

SqlDataReader reader = cmd.ExecuteReader();

...

一旦有了DataReader,就可以在While循环中使用Read()方法遍历所有记录。DataReader移动行游标到下一条记录(第一次调用时,移到第一条记录)。Read()方法同时也返回布尔值来表明是否还有更多的行可读。在下面的示例中,直到Read()返回false时,循环才自然结束。

每条记录的信息随后被连接到一个大的字符串中。为了保证字符串快速执行,使用了StringBuilder(它的命名空间是System.Text),而不是普通的字符串对象。

...

// Cycle through the records, and build the HTML string.

StringBuilder htmlStr = new StringBuilder("");

while (reader.Read())

{

htmlStr.Append("<li>");

htmlStr.Append(reader["TitleOfCourtesy"]);

htmlStr.Append(" <b>");

htmlStr.Append(reader.GetString(1));

htmlStr.Append("</b>, ");

htmlStr.Append(reader.GetString(2));

htmlStr.Append(" - employee from ");

htmlStr.Append(reader.GetDateTime(6).ToString("d"));

htmlStr.Append("</li>");

}

...

这段代码使用依名字排序的项索引器(Item indexer)访问字段,获取了TitleOfCourtesy字段。由于Item属性是默认的索引器,因此在获取字段值时,不需要显性地包含Item属性名。接下来,代码使用字段索引(例中是12),通过调用GetString()读取了LastNameFirstName字段。最后,使用索引值6,通过调用GetDateTime()访问了HireDate字段。这些方法用来显示支持的变量都是相同的和内置的。

注意,在这个示例中,StringBuilder保证了性能的大幅提升。如果使用+操作符来连接字符串,这个操作每次都会销毁和创建一个新串,这个操作会慢很多,特别是大的字符串时更为明显。StringBuilder对象通过在内存中为字符为配缓冲区避免了这个问题。

最后一步是关闭readerconnection,然后将产生的文本在一个服务器控件(如标签控件HtmlContent)中显示。

如果运行这个网页,会得到图7-3所示的结果。

7-3 使用DataReaer得到的结果

 

在大多数ASP.NET网页中,你不会使用这个强实验性的方法来显示数据。相反,你会使用数据控件(在后面的章节中介绍)。尽管如此,在数据库组中编写数据访问代码时,你仍可能使用DataAdapter

 

CommandBehavior

使用CommandBehavior枚举作为参数时,ExecuteReader()方法有一个过载的版本。一个有用的值是CommandBehavior.CloseConnection。当你传递这个值到ExecuteReader()方法时,DataReader会在你关闭DataReader的同时关闭相关连接。

使用这个技术,你可以将前面的代码改写成这样:

SqlDataReader reader = cmd.ExecuteReader(CommandBehavior.CloseConnection);

// (Build the HTML string here.)

// No need to close the connection. You can simply close the reader.

reader.Close();

HtmlContent.Text = htmlStr.ToString();

如果你在一个方法中获取一个DataReader,并且需要传递给别的方法来处理时,这种行为就非常有效。如果你使用CommandBehavior.CloseConnection值,连接会随着方法中关闭reader而自动关闭。

另一个可能的值是CommandBehavior.SingleRow,它在获取单个的行时,能够提高查询执行的性能。举个例来说,如果你正在使用唯一的主关键字段(CustomID, ProductID等)来获取单条记录时,你可以使用它来进行优化。也可以使用Command.Behavior.SequentialAccess来一次读取一个二进制字段的一部分,这样可以减少大的二进制字段时的内存过载。在第10章会看到这个技术。

其它的值使用得就比较少了,在这里就不再述及。可以参考.NET文档来查看全部列表。

 

处理多个结果集

执行Command返回一个单一的结果集并不是必须的。相反,它可以使用相同的Command执行多个查询,而且返回多个结果集。这在需要获取大量相关数据时特别有用,比如产品列表和产品分类。

有两种方法可以返回多个结果集:

使用存储过程时,可以使用多个SELECT语句。

如果使用直接的文本command,可以使用分号来分隔多条Command。并不是所有的提供者都支持这个技术,但SQL Server数据库提供此支持。

下面的示例显示了包含三条SELECT语句的字符串:

string sql = "SELECT TOP 5 * FROM Employees;" +

"SELECT TOP 5 * FROM Customers;SELECT TOP 5 * FROM Suppliers";

这个字符串包含了三条查询。它们同时分别从EmployeesCustomersSupplies表中一起返回前面的5条记录。

处理这些结果是相当直截了当的。起初,DataReader提供对从Employees表中获取得的结果进行访问。一旦完成了使用Read()方法来读取所有记录,你可以调用NextResult()来获取下一个结果集。如果没有更多的结果集了,这个方法则返回false

你可以使用while循环所有可获得的结果集,尽管这种情况下你必须小心,在完成读取第一个记录集前,不要调用NextResult()

示例如下:

// Cycle through the records and all the rowsets,

// and build the HTML string.

StringBuilder htmlStr = new StringBuilder("");

int i = 0;

do

{

htmlStr.Append("<h2>Rowset: ");

htmlStr.Append(i.ToString());

htmlStr.Append("</h2>");

while (reader.Read())

{

htmlStr.Append("<li>");

// Get all the fields in this row.

for (int field = 0; field < reader.FieldCount; field++)

{

htmlStr.Append(reader.GetName(field).ToString());

htmlStr.Append(": ");

htmlStr.Append(reader.GetValue(field).ToString());

htmlStr.Append("&nbsp;&nbsp;&nbsp;");

}

htmlStr.Append("</li>");

}

htmlStr.Append("<br /><br />");

i++;

} while (reader.NextResult());

// Close the DataReader and the Connection.

reader.Close();

con.Close();

// Show the generated HTML code on the page.

HtmlContent.Text = htmlStr.ToString();

 

注意,在这种情况下,所有的字段都是使用通用的GetValue()方法访问的,它使用了字段的索引。其原因在于代码被设计为通用地读取返回结果集的所有字段,而无论你使用什么查询。然而,在现实的数据库应用中,你需要确切地知道需要哪些表,以及相应的表和字段名。

7-4 显示了页面输出。

7-4 获取多个结果集

 

你不必总是一步一步地处理每条记录。如果你想显示精确的数据,不需要额外的处理和格式化,你可以添加一个GricView控件到网页中,并且绑定DataReaderGrieView控件。见下面的代码:

// Specify the data source.

GridView1.DataSource = reader;

// Fill the GridView with all the records in the DataReader.

DataView1.DataBind();

你将会在第9章和第10章中了解到更多的数据绑定和怎样自定义它。

 

ExecuteScalar()方法

ExecuteScalar()方法返回结果集的第一行第一个字段的值,这个结果集是由SELECT查询命令产生的。这个方法通常用于只获取一个字段的值,可能由SQL统计函数,如COUNT()或者SUM()函数计算。

下面的过程显示了用这种方法如何从Employees表中获取记录的数量:

SqlConnection con = new SqlConnection(connectionString);

string sql = " SELECT COUNT(*) FROM Employees ";

SqlCommand cmd = new SqlCommand(sql, con);

// Open the Connection and get the COUNT(*) value.

con.Open();

int numEmployees = (int)cmd.ExecuteScalar();

con.Close();

// Display the information.

HtmlContent.Text += "<br />Total employees: <b>" +

numEmployees.ToString() + "</b><br />";

这段代码是显而易见的,但是它没有多少意义,你必须将它返回的类型(对象)转换为正确的数据类型。

 

ExecuteNonQuery()方法

ExecuteNonQuery()方法执行不需要返回结果的command,如INSERTDELETEUPDATE。它只返回一小段信息,即影响了多少条记录。

下面的示例通过动态地构建SQL串来使用了DELETE command

SqlConnection con = new SqlConnection(connectionString);

string sql = "DELETE FROM Employees WHERE EmployeeID = " + empID.ToString();

SqlCommand cmd = new SqlCommand(sql, con);

try

{

con.Open();

int numAff = cmd.ExecuteNonQuery();

HtmlContent.Text += string.Format("<br />Deleted <b>{0}</b> record(s)<br />",

numAff);

}

catch (SqlException exc)

{

HtmlContent.Text += string.Format("<b>Error:</b> {0}<br /><br />", exc.Message);

}

finally

{

con.Close();

}

 

SQL注入攻击(SQL Injection Attacks

到目前为止,所有的示例都使有和了硬编码值。这样可使得示例简单、显而易见、相对安全。但同时也意味着这样与实现的应用有段距离,它们没有演示对于web应用最为严重的威胁,即数据库注入攻击。

简单地说,SQL注入攻击就是将SQL代码传入应用的过程,它们并未被应用开发者预见到或是是不希望见到的。在设计较差的应用里,这是很有可能的。而且它只影响使用了SQL串构建技术来创建Command的应用,这些Command需要用户输入值。

考虑图7-5所示的例子。在这个例子中,用户输入了customerIDGrieView显示了那个客户的所有行。在更贴近现实的例子中,用户还需要提供认证信息,如密码,或者用户ID可能是基于先前的登录屏幕,文本控件应该允许用户输入额外的要求,如数据范围或者订单里的产品名字。

7-5 获取一个客户的订单

 

问题是Command如何被执行。在这个例子中,SQL语句使用串构建技术来动态创建。来自于文本控件的txtID值被粘贴在字符串中间,代码如下:

string connectionString =

WebConfigurationManager.ConnectionStrings["Northwind"].ConnectionString;

SqlConnection con = new SqlConnection(connectionString);

string sql =

"SELECT Orders.CustomerID, Orders.OrderID, COUNT(UnitPrice) AS Items, " +

"SUM(UnitPrice * Quantity) AS Total FROM Orders " +

"INNER JOIN [Order Details] " +

"ON Orders.OrderID = [Order Details].OrderID " +

"WHERE Orders.CustomerID = '" + txtID.Text + "' " +

"GROUP BY Orders.OrderID, Orders.CustomerID";

SqlCommand cmd = new SqlCommand(sql, con);

con.Open();

SqlDataReader reader = cmd.ExecuteReader();

GridView1.DataSource = reader;

GridView1.DataBind();

reader.Close();

con.Close();

在这个例子中,用户可能窜改SQL语句。通常,类似这样的攻击的首要目标是获取错误信息。如果错误没有被正确处理,将底层的信息展示给了攻击者,这些信息就会被利用来发起更为复杂的攻击。

例如,假设用户在文本控件中输入了如下信息,会发生什么情况呢?

ALFKI OR 1 = 1

现在注意刚才创建的完整的SQL语句:

SELECT Orders.CustomerID, Orders.OrderID, COUNT(UnitPrice) AS Items,

SUM(UnitPrice * Quantity) AS Total FROM Orders

INNER JOIN [Order Details]

ON Orders.OrderID = [Order Details].OrderID

WHERE Orders.CustomerID = 'ALFKI' OR '1'='1'

GROUP BY Orders.OrderID, Orders.CustomerID

这个语句返回了所有的订单记录。尽管订单并不是ALFKI创建的,对于每一行来讲,它仍然为true 。这个结果就不是只返回了当前客户的信息,所有的信息都展示给了攻击者,如图7-6所示。如果显示出来的信息是有意义的,如社会保险号、生日、信用卡信息,这将是令人恐怖的问题。事实上,象这样的简单SQL注入攻击往往是影响电子商务公司的问题之源。一般来说,漏洞并不是出现在文本控件中,而是在查询串中(它可以用来将值传递给数据库,如来自于列表页到详细页面的唯一ID。)

7-6 SQL注入攻击显示了所有的行

更复杂的攻击也是可能的。例如,恶意用户能够简单地在你的SQL语句后面增加两个连字符(--)。这种攻击对SQL Server是很有效的,在MySQL中使用#符号和在Oracle中使用分号(;)可以同样被利用。攻击者也能使用批命令来执行任意的SQL命令。使用SQL Server提供者,攻击者仅仅需要在分号后添加一个新的命令就要以实现攻击。利用这些漏洞可以使用户删除另一个表的内容,甚至可以在命令行中使用SQL Server xp_cmdshell系统存储过程来执行任意的程序。

这段代码显示了在文本控件中输入什么信息就可以进行复杂的SQL注入攻击,删除Customers表中的所有行。

ALFKI’; DELETE * FROM Cumstomers - -

那么,怎样来防范SQL注入攻击呢?你应当在大脑中记住几条指导原则:首先,使用TextBox.MaxLength属性来限制超过需要长度的输入是一个不错的主意。这可以减少粘贴大块脚本的机会。另外,你应当限制错误消息的信息。如果你捕捉到了一个数据库异常,应该只产生普通的出错误信息,如”Data Source error”,而不是显示在Exception.Mexxage属性中的信息,这些信息可能暴露系统弱点。

更为重要的是,你必须小心移除字符。例如,你能够将所有的单个引用标签转换成两个引用标签,因此,在SQL语句中必须确保使用分隔符没有造成混淆:

String ID = txtID.Text().Replace(“‘”,“‘’”)

当然,如果在值中包含省略号,将是一件令人头疼的事情。因为一些SQL注入攻击仍然可能存在,这是一个问题。替换省略号可以防止恶意用户过早地关闭串值。然而,如果你创建一个包含了数值的动态SQL语句,进行SQL注入攻击就只需要一个空格就可以了。这个漏洞经常被忽略。

一个更好的方法是使用参数化的command或者存储过程来避免受到SQL注入攻击。下一节描述了这些技术。

提示:另一个好主意是限制用于访问数据库的用户的权限,那样它就没有权限访问其它数据库或者执行额外的系统存储过程。然而,这并不能防范SQL脚本注入问题,因为连接到数据库的进程经常需要较大的特权,它的权限比分配给其它单个用户的权限要大。通过限制帐户,你可以防止诸如删除表的攻击,但你可能无法阻止盗取他人其它信息的攻击。

 

使用参数化的命令(Using Parameterized Commands

参数化的命令就是在Command中仅仅使用SQL文本的占位符(placeholder)。占位符表示动态地提供值,这些值通过command对象的参数集传递。

举个例,下面这是SQL语句:

SELECT * FROM Customers WHERE CustomerID = ‘ALFKI’

这将会使占位符随后分别地被添加,而且自动编码。

参数化的命令的语法在不同的提供者中略有不同。在SQL Server提供者中,参数化的命令使用命名的占位符(使用唯一的名字)。在OLE DB提供者中,每个硬编码的值被一个问题标签替代。每一种情况都需要你为每个参数提供参数对象,用来插入到Command.Parameters集中。使用OLE DB提供者时,你必须保证添加的参数的顺序与它们在SQL串中的顺序是一致的。SQL Server提供者并不要求这样,因为参数是根据它们的名字来匹配占位符的。

下面的例子重写了查询,除去了SQL注入攻击的可能:

string connectionString =

WebConfigurationManager.ConnectionStrings["Northwind"].ConnectionString;

SqlConnection con = new SqlConnection(connectionString);

string sql =

"SELECT Orders.CustomerID, Orders.OrderID, COUNT(UnitPrice) AS Items, " +

"SUM(UnitPrice * Quantity) AS Total FROM Orders " +

"INNER JOIN [Order Details] " +

"ON Orders.OrderID = [Order Details].OrderID " +

"WHERE Orders.CustomerID = @CustID " +

"GROUP BY Orders.OrderID, Orders.CustomerID";

SqlCommand cmd = new SqlCommand(sql, con);

cmd.Parameters.Add("@CustID", txtID.Text);

con.Open();

SqlDataReader reader = cmd.ExecuteReader();

GridView1.DataSource = reader;

GridView1.DataBind();

reader.Close();

con.Close();

如果你试图在这个修改后的页面中执行SQL注入攻击,你将得不到任何记录。因为没有订单项包含CustomerID值等于串ALFKI OR1 =1。这正是你所期望的结果。

 

调用存储过程(Calling Stored Procedures

参数化的命令相比调用强大的存储过程,只是一个小小的进步。

存储过程是存储在数据库中的一条或多条SQL语句的组合。存储过程与函数相似,它们是封闭良好的逻辑块,能够通过输入参数接收数据和通过结果集和输出参数返回数据。存储过程有许多好处:

易于维护。例如,你可以在不重新编译应用的情况下在存储过程中优化Commands

允许你实现更加安全的数据库使用。例如,你可以允许运行ASP.NET程序的Windows帐户使用存储过程,但限制了它访问底层的表。

可以提高性能。由于存储过程结合了多条语句,你可以只与数据库服务器交互一次就可以做很多工作。如果数据库在另一台计算机上,就可以在执行复杂任务时大幅地减少时间。

注意:SQL Server 7.0版本以后对所有SQL语句进行预编译,包括即时语句(off-the-cuff SQL statements)。这意味着无论你是否使用了存储过程,你都将获得编译带来的好处。当然,存储过程的目的仍是增强性能,因为它们限制了SQL语句的改变,因此,确保一个单独的编译执行计划能够被更加频繁和更加高效地重用。同时,由于数据库代码存在于数据库中,而不是客户端,对于数据库管理员来说,进行调优、锁定和配置其它的优化策略就变得更加容易。

下面的SQL代码创建了一个存储过程,存储过程将单条记录插入到Employees表中。这个存储过程最先并不在Northwind数据库中,因此在使用之前,你需要将它添加到数据库中(使用企业管理器或查询分析器之类的工具)。

CREATE PROCEDURE InsertEmployee

@TitleOfCourtesy varchar(25),

@LastName varchar(20),

@FirstName varchar(10),

@EmployeeID int OUTPUT

AS

INSERT INTO Employees

(TitleOfCourtesy, LastName, FirstName, HireDate)

VALUES (@TitleOfCourtesy, @LastName, @FirstName, GETDATE());

SET @EmployeeID = @@IDENTITY

GO

这个存储过程为employeetitle of courtesylast namefirst name指定了3个参数。INSERT语句使用@@IDENTITY函数后,通过输出参数@EmployeeID返回新记录的ID。这是使用存储过程可以简化工作的一个简单示例。如果不使用存储过程,确定刚插入的新记录的identity值(这个值是自动产生的)就变得相当困难。

接下来,你可以创建SqlCommand来包装对存储过程的调用。这个命令同样使用3个参数作为输入,使用@@IDENTITY来获得并返回新记录的ID。这是第一步,它创建了所需的对象,并且设置InsertEmployees作为command text

string connectionString =

WebConfigurationManager.ConnectionStrings["Northwind"].ConnectionString;

SqlConnection con = new SqlConnection(connectionString);

// Create the command for the InsertEmployee stored procedure.

SqlCommand cmd = new SqlCommand("InsertEmployee", con);

cmd.CommandType = CommandType.StoredProcedure;

现在,你需要添加存储过程参数给Command.Parameters集。在这过程中,你需要明确数据的类型和参数的长度来匹配数据库中的要求。

下面的代码显示了单个参数如何工作的:

cmd.Parameters.Add(new SqlParameter("@TitleOfCourtesy", SqlDbType.NVarChar, 25));

cmd.Parameters["@TitleOfCourtesy"].Value = title;

第一行创建了一个新的SqlParameter对象设置了它的名字、类型、在构造器中的大小,并将它添加到了参数集。第二行为参数指定了值,将在执行command时被发送到存储过程。

 

注意:一些提供者包含一个过载参数。 Add()方法允许你创建参数对象,而不需要指定数据类型。然而,这种方法通常要求一定程度的reflection,这意味着数据提供者必须查询数据源来查找参数的细节。最好的实现方法是指定数据类型的细节,尽管这会导致冗长的代码。

 

接下来,按相同的方法添加另两个参数:

cmd.Parameters.Add(new SqlParameter("@LastName", SqlDbType.NVarChar, 20));

cmd.Parameters["@LastName"].Value = lastName;

cmd.Parameters.Add(new SqlParameter("@FirstName", SqlDbType.NVarChar, 10));

cmd.Parameters["@FirstName"].Value = firstName;

最后一个参数是输出参数,它允许存储过程将信息返回给你的程序。尽管Parameter对象采用同样的方法创建,你仍必须通过为Output设置Direction属性来明确指定它是输出参数。你不需要提供值。

cmd.Parameters.Add(new SqlParameter("@EmployeeID", SqlDbType.Int, 4));

cmd.Parameters["@EmployeeID"].Direction = ParameterDirection.Output;

最后,你可以打开连接,然后然后使用ExecuteNonquery()执行command。当command完成后,就可以读取输出值,如下所示:

con.Open();

try

{

int numAff = cmd.ExecuteNonQuery();

HtmlContent.Text += String.Format(

"Inserted <b>{0}</b> record(s)<br />", numAff);

// Get the newly generated ID.

empID = (int)cmd.Parameters["@EmployeeID"].Value;

HtmlContent.Text += "New ID: " + empID.ToString();

}

finally

{

con.Close();

}

在下一章中,你将看到一个小巧但功能完备的数据库组件来操作存储过程。

 

事务(Transactions

事务是一个操作集,它们作为一个整体,要么完全成功,要么整体失败。事务的目的是保证数据的有效性和一致性。

举个例,考虑事务:从账户A向账户B转移1000美元。很显然,有两步操作:

从账户A减少1000美元

为账户B增加1000美元

设想一下,应用成功完成了第1步,但由于某种错误,第2步失败了。这就导致了数据不一致,因为系统的钱的总数已经不再准确了,1000美元不翼而飞了。

事务帮助避免这类型的问题。使用事务,确保提交给数据源的改变的所有步骤都成功的。因此,在这个例子中,如果第2步失败了,第1步引起的改变也不会提交给数据库。这就确保了数据库处在两种有效状态之一,即初始状态(没有转移钱)和完成状态(钱从一个账户完成了向另一个账户的转移)。

事务由四个属性(一般称为ACID属性)描述。ACID是下面四个概念的首字母的缩写:

原子的Atomic):事务中的所有步骤必须是整体成功或整体失败。任何一步不成功,都认为事务是失败的。

一致的Consistent):事务确保底层数据库从一个稳定状态变换到另一个稳定状态(没有中间状态,译注)。

独立的Isolated):每个事务都是独立的实体。一个事务不能影响同时运行的另一个事务。

持久的Durable):在宣布事务成功前,事务期间发生的改变永久地存储在某种介质上,典型的介质是硬盘。要维护日志,以便于发生硬件或网络错误时,数据库能够恢复到一个有效的状态。

注意:尽管这些是事务的理想特征,但并不能总是完全实现。一个问题是为了确保独立性,RDBMS需要锁定数据,这样就会导致在事务处理期间其它用户无法访问数据。在事务运行期间,使用锁定越频繁,锁定的块越大,用户执行其它任务的困难就更大。换句话说,通常需要在用户并发性和独立性之间进行平衡。

 

事务和ASP.NET应用

ASP.NET Web应用中,可以使用3种基本的事务类型,列举如下(按负载轻重排序):

存储过程事务(Stored procedure transactions):这些事务完全在数据库中发生。存储过程事务提供了最好的性能,因为它只需要与数据库进行一次交互。缺点是你必须使用SQL语句来编写事务逻辑(这可能比用纯C#编写要困难一点)。

客户端发起的事务(Client-initiated ADO.NET transactions):这些事务由ASP.NET Web页代码的程序进行控制。在页面之内,它们使用与存储过程事务相同的命令,但必须使用一些ADO.NET对象来包装这些细节。缺点是开启和提交事务增强了与数据库的交互次数。

COM+事务:这些事务由COM+运行时处理,是基于你添加到代码中的明确的属性来进行的。COM+事务会发生额外的负载。它们要求你创建一个分离的被服务的组件类。如果事务跨越多个事务感知的资源管理者,COM+组件是一个好的选择。因为COM+内置了对分布式事务的支持。例如,一个简单的COM+事务能够跨越在SQL ServerOracle数据库中的事务。COM+事务在本章中没有涉及,你可以在第32章中同Web服务一起简要了解它们。

注意:ASP.NET 2.0引入了一个新概念:可提升的事务(promotable stransaction)。但是,可提升事务并不是事务的新类型。它只是一种创建客户端初始化事务的方法,在需要的时候能够自动将其提升为COM+事务。一般情况下不需要可提升事务,因为使用它们之后预料性能和可扩展性变得更加困难。关于可提升事务的更多信息见Pro ADO.NET 2.0 (Apress,2005)

尽管ADO.NET对事务提供了很好的支持,你也不必总是需要使用事务。事实上,每次你使用任何一种事务,都会自动增加一些负载。同时,事务也包含了对一些表的行的锁定。因此,过度地使用事务会对应用的整体扩展性带来不利影响。

实现事务的时候,可以遵循下面的规则来获取最佳的效果:

l     保持事务尽可能地短。

l     避免在事务中使用SELECT查询返回数据。理想情况下,你应该在事务开始前返回数据。

l     如果确实需要获取记录,仅采集需要的行,以避免锁定太多资源和降低性能。

l     尽可能在存储过程中写事务,而不要使用ADO.NET事务。

l     避免事务合并多个独立的工作块。将分离的块放入独立的事务中去。

l     只要有可能,尽量减少影响的记录数。

提示:一条小规则,仅在有必要时才使用事务。例如,如果你仅需要从数据库选取记录,或者释放一条查询,你就不需要事务。另一方面,如果你插入一条订单记录的同时影响一些相关的订单项记录,那你就有可能使用事务。总体来说,对于一条单一的语句命令,不需要事务,如UPDATE, DELETE, INSERT语句。

 

存储过程事务(Stored Procedure transactions

如果可能,将事务放在存储过程的代码中是最好的办法。这样保证了服务器端的代码总是可控的,可以防止客户端意外地抓住事务时间太长而导致其它用户无法进行更新。它也保证了最佳的性能,因为所有的动作都只在数据源端完成,而不需要通过网络进行通信。一般地,事务跨度越短,数据的并发响应能力就越强,进入队列的数据库请求数就越少。

存储过程代码的改变取决于正在使用的数据库,但是大多数RDBMS支持SQL语句BEGIN TRANSACTION。一旦开始了事务,所有子语句都被看成是事务的一部分。可以使用COMMIT或者ROLLBACK语句来结束事务。如果没有使用语句,事务会自动回滚(roll back)。

下面的伪代码示例执行了账户间的资金转移。它是一个简化了的版本,允许账户被设置为负数。

CREATE Procedure TransferAmount

(

@Amount Money

@ID_A int

#ID_B int

)

AS

BEGIN TRANSACTION

UPDATE Accounts SET Balance = Balance + @Amount WHERE AccountID = @ID_A

UPDATE Accounts SET Balance = Balance - @Amount WHERE AccountID = @ID_B

IF (@@ERROR > 0)

ROLLBACK

ELSE

COMMIT

注意:在SQL Server中,存储过程也能执行分布式的事务(多个数源分别位于不同的服务器上)。默认情况下,每个事务是作为本地事务开始的。如果要访问位于另一台服务器上的数据库,事务会自动升级为分布式事务,这由Windows DTCDistributed Transaction Coordinator)服务进行控制。

 

客户端发起的ADO.NET事务(Client-Initiated ADO.NET Transaction

大多数ADO.NET数据提供者对数据库事务提供支持。通过Connection对象调用BeginTransaction()方法来开始事务。这个方法返回提供者明确的Transaction对象,用来管理事务。所有的事务类都实现IDbTransaction接口。例如SqlTransation, OleDbTransaction, OracleTransaction等。

Transaction类提供了两个核心方法:

Commit():这个方法确定事务完成并且所有未决的改变都已经永久地存储在数据源中。

Rollback():这个方法指明事务失败。未决的改变被丢弃,数据库状态保持不变(没发生改变)。

一般地,在操作的结尾使用Commit()。但是,如果在进行的过程中发生了异常,则应当调用Rollbaci()

下面的例子中,在Employees表中插入了两条记录:

string connectionString =

WebConfigurationManager.ConnectionStrings["Northwind"].ConnectionString;

SqlConnection con = new SqlConnection(connectionString);

SqlCommand cmd1 = new SqlCommand(

"INSERT INTO Employees (LastName, FirstName) VALUES ('Joe','Tester')");

SqlCommand cmd2 = new SqlCommand(

"INSERT INTO Employees (LastName, FirstName) VALUES ('Harry','Sullivan')");

SqlTransaction tran = null;

try

{

// Open the connection and create the transaction.

con.Open();

// Enlist two commands in the transaction.

cmd1.Transaction = tran;

cmd2.Transaction = tran;

// Execute both commands.

cmd1.ExecuteNonQuery();

cmd2.ExecuteNonQuery();

// Commit the transaction.

tran.Commit();

}

catch

{

// In the case of error, roll back the transaction.

tran.Rollback();

}

finally

{

con.Close();

}

注意:仅仅创建和提交事务是不充分的,还必须显性地支持每个Command对象作为事务的一部分,这通过为Transaction对象设置Command.Transaction属性来完成。如果在当前事务运行时,你试图执行并不是当前事务的一部分的command,将会返回一个错误。当然,未来的对象模型可能允许在相同的连接上允许提供者同时支持多个事务。

提示:不使用command对象,你还可以对相同的对象执行两次,仅仅需要修改其中的CommandText属性(动态SQL语句时)或者参数值(参数化的command时)。例如,如果command插入了一条新记录,你可以在同样的事务中使用这个方法插入两条记录。

为了测试事务的回滚功能,可以在前面例子中的Commit()方法前插入下面的行:

Throw new ApplicationExeception()

这会产生一个异常,触发一个回滚,保证了没有记录被提交给数据库。

尽管ADO.NET事务围绕ConnectionTransaction对象进行,底层的command与存储过程事务并没有什么不同。例如,当你使用SQL Server提供者调用BeginTransaction()时,它发送了BEGIN TRANSACTION命令给数据库。

提示:事务应该尽可能快地完成(尽可能晚地开始,尽可能早地结束)。同时,一个活动的事务对不同的资源进行了锁定,因为你应当只选取你需要的那部分记录。

 

独立层次(Isolation Levels

独立层次确定了如何感知由其它进程事务造成的事务改变。例如,在默认情况下,当两个事务独立地运行时,一个事务执行的插入记录对于另一个事务是不可见的,除第一个事务已经提交。

独立层次的概念与锁定概念密切相关,因为通过确定给定事务的独立层次,也就确定了需要哪种类型的锁定。共享锁定(Shared locks)是事务试图从数据库读取数据时放置的锁定。当一个表或者行或者范围里有共享锁定时,任何其它的事务都不能修改数据。当然,多个用户可以使用共享锁定同时读取数据。排它锁定(Exclusive locks)是一种防止其它事务同时修改数据的锁定。排它锁定用于事务需要更新数据而且这部分数据没有其它锁定时。当排它锁定起作用时,其它用户都不能读取或者修改这部分数据。

注意:SQL Server事实上还有几种类型的锁定,可以一起工作来防止死锁和其它情况。要了解更多信息,请参考SQL Server在线帮助中锁定的相关信息。

SQL Server存储过程中,你可以使用SET TRANSACTION ISOLATION LEVEL命令来设置独立层次。在ADO.NET中,你可以将值从IsolationLevel枚举传递给Connection.Beginransation()方法。表7-6列举了可能的值。

描述

ReadUncommitted

没有放置共享锁定,排它锁定也不会使用。当你试图在所有数据中匹配特定的条件时,这种类型的独立层次是适当的,这不需要考虑是否提交。这可能会出现糟糕的读取(dirty read),但提高了性能。

ReadCommitted

当事务读取数据时,放置了共享锁定。这可以避免糟糕的读取,但数据可以在事务完成前被改变。这可能导致不可重复的读取和虚构的行。这是SQL Server使用的默认独立层次。

RepeatedRead

在这种情况下,在查询中使用到的所有数据中放置了共享锁定。这可以防止其他人修改数据,也防止了不可重复的读取。当然,虚构行仍然存在。

Serializable

在使用的数据上放置的范围锁定,可以防止其它用户在那个范围中更新或者插入行。这是能够防止虚构行的唯一的独立层次。但是,它会对用户访问的并发性带来负面影响,在多用户的场合下应用较少。

Snapshot

存储事务访问的数据的拷贝。因为,事务不会看到其它事务引起的改变。这个方法减少了锁定,因为即便其它事务采用快照锁定(snapshot isolation)在数据上进行了锁定,仍可以读取这些数据的拷贝。这个选项仅在SQL Server 2005中被支持,并且需要设置database-level选项来使其可用。

7-6 IsolationLevel枚举的值

 

7-6中的独立层次按照锁定度从低到高进行了排序。默认情况下,ReadCommitted是多数事务采用的好的折衷方案。表7-7总结了不同的独立层次的锁定行为。

7-7 独立层次比较

 

保存点(Savepoints

无论何时回滚了一个事务,事务开始之后的每一个命令都将无效。那么,如果你仅想回滚运行中的事务的一部分,那又是什么样的呢? SQL Server使用了保存点功能来处理这个问题。

保存点就是象书签那样的标记。你在事务流中的某一点进行标记,然后就可以回滚到那一点。通过使用Transaction.Save()方法来设置保存点。需要注意的是,Save()方法仅仅在SqlTransaction类中可用,它并不是标准的IDbTransaction接口的一部分。

下面的例子可以对保存点的使用进行概念性的了解:

// Start the transaction.

SqlTransaction tran = con.BeginTransaction();

// (Enlist and execute some commands inside the transaction.)

// Mark a savepoint.

tran.Save("CompletedInsert");

// (Enlist and execute some more commands inside the transaction.)

// If needed, roll back to the savepoint.

tran.Rollback("CompletedInsert");

// Commit or roll back the transaction.

tran.Commit();

请注意在Rollback()方法中怎样使用保存点作为参数的。如果想回滚整个事务,只需要简单忽略这个参数就可以了。

注意:一旦回滚到一个保存点,所有在那个保存点之后的保存点都会丢失,如果还需要那些保存点,就必须重新设置。

 

嵌套事务(Nested Transactions

保存点允许保存点被组织成动作序列,以便可以单个地回滚。嵌套回滚本质上的角色与保存点是一样的:它们允许你在一个大的事务里开始一个更小的事务,可以单个地提交或回滚。为了发起嵌套事务,必须调用Transaction对象的Begin()方法,这将返回一个Transaction对象,这样就可以象普通的Transaction对象那样使用。

嵌套事务的实现取决于数据源。一些数据源,如SQL Server,并不能正确地支持嵌套事务。在SQL Server中,回滚一个嵌套事务事实上就回滚了整个事务。正因为如此,并没有为SqlTransation类提供Begin()方法,相反,你可以使用保存点来完成相似的功能。

 

提供者无关的代码(Provider-Agnostic Code

ADO.NET的大部分提供者模型是处理不同数据源的理想解决方案。它允许每个数据库卖主在增强高层的连接(high level of consistency)时开发本地的、优化的解决方案,这样可使开发人员不必去重新学习基础的知识。

然而,提供者模型并不是完美的。尽管你可以使用标准的接口与CommandConnection对象进行交互,但是当你实例化CommandConnection对象时,你仍需要知道要使用的提供者特定的、强类型的类(如SqlConnection)。这个限制使构建其它工具或者使用ADO.NET的附加项变得困难。举个例来说,在第9章中,你将考虑一个新的ASP.NET数据源控件,它允许你创建数据绑定页而不需要写任何代码。为了提供这个功能,你需要为那个数据控件创建隐藏在界面后的ADO.NET对象。在.NET1.1中如果没有强力的限制,这个功能根本就无法实现。

.NET2.0中解决了这个问题,并且为提供者无关的编码添加了增强的支持(代码可以同任何数据库一起工作),其秘密在于一个新的工厂模型(factory model)。

注意:在构建专门的组件时,提供者无关编码非常有用。在期望未来将应用移到一个不同的数据库或者不能确定未来可能使用的数据库类型时,数据库无关编码也是很有意义的。数据库无关编码不能利用一些特定提供者的功能(数据SQL Server中的XML查询),优化也比较困难。因此,在大型的专业Web应用中,应用得并不广。

 

创建工厂(Creating the Factory

工厂模型的基本思想是使用一个工厂对象来创建所需的每个提供者特定类型的对象,然后可以通过一个通用基础类集,以完全通用的方法来与提供者特定的对象进行交互。

工厂类是特定提供者自定义的。举个例来说,SQL Server提供者包含了一个名叫System.Data.SqlClient.SqlClientFactory的类。Oracle提供者使用了System.Data.OracleClient.OracleClientFactory。初看起来,这看起来可能阻止你编写数据库无关的代码。然而,这是一个完全的标准化类用于动态地发现和创建所需工厂。这个类就是System.Data.Common.DbProviderFactories。它提供了表态的GetFactory()方法来返回基于提供者名字的工厂。

例如,下面的代码使用DbProviderFactories来获取SqlClientFactory:

String factory = “System.Data.SqlClient”;

尽管DbProviderFactories类返回了强类型的SqlClientFactory对象,但你不必同样的处置。相反,你的代码应当作为DbProviderFactory实例来访问它。这是因为所有的工厂继承于DbProviderFactory。如果你仅使用DbProviderFactory的成员,你可以编写与任何工厂工作的代码。

前面的代码段中显示的弱点是你需要为DbProviderFactories.GetFactory方法传递一个标识提供者的串。你一般通过应用设置的web.config文件来读取它。那样,你就可以编写完全的数据库无关的代码,并且可以简单地通过修改单个的设置来将应用转换到另一个提供者。

提示,在实践中,你可能需要在配置文件中存储几个提供者特定的细节。不仅你需要获取提供者名字,而且也需要获得连接串。如果想要避免硬编码,你可能需要获取查询或者存储过程名字,因为它们可能发生改变。这需要你在发展复杂性和灵活性之间寻找理想的平衡。

要使DbProviderFactories类起作用,你的提供者需要在machine.config或者web.config配置文件中注意工厂。Machine.config文件注册了.NET框架中的4种提供者:

<configuration>

<system.data>

<DbProviderFactories>

<add name="Odbc Data Provider" invariant="System.Data.Odbc"

type="System.Data.Odbc.OdbcFactory, ..." />

<add name="OleDb Data Provider" invariant="System.Data.OleDb"

type="System.Data.OleDb.OleDbFactory, ..." />

<add name="OracleClient Data Provider" invariant="System.Data.OracleClient"

type="System.Data.OracleClient.OracleClientFactory, ..." />

<add name="SqlClient Data Provider" invariant="System.Data.SqlClient"

type="System.Data.SqlClient.SqlClientFactory, ..." />

</DbProviderFactories>

</system.data>

...

</configuration>

这个步骤标识了工厂类,并且为提供者指定了唯一的名字(习惯上,它们与那个提供者的命名空间是一样的)。如果想使用第三方提供者,你需要在machine.config文件(通过特定的计算机访问它)或者Web.config文件(在特定的Web应用中访问它)的<DbProvider>节中注册它。个人或公司开发提供者时,可能已经包含了安装程序来自动完成这个任务,或者显性地进行配置。

 

使用工厂创建对象(Create Objects with Factory

一旦有了工厂,就可以创建其它对象了,如使用DbProviderFactory.CreateXxx()方法创建ConnectionCommand实例。例如,CreateConnection()方法为数据提供者返回了Connection对象。再进一步讲,你并不知道你要使用哪个提供者,你能够仅仅使用标准的基础类与工厂创建的对象进行交互。

7-8给出了快速参考,显示了创建每种类型的数据访问对象需要哪些方法,哪些类可以安全地使用。

7-8 标准ADO.NET对象的接口

注意:如本章中前面解释的那样,提供者特定的对象也实现一定的接口(如IDbConnection)。然而,由于一些对象使用多个ADO.NET接口(如DataReader同时实现IDataRRecordIDataReader),基础类简化了模型。

 

带提供者无关的代码的查询

为了更好地理解这些块是如何组合在一起的,从一个简单的示例开始比较有用。在这一节中,你将看到怎样执行一个查询并且使用提供者无关的代码显示结果。事实上,这个示例是对前面看到的图7-3的一个重写。区别仅在于不再紧紧地绑定到SQL Server提供者。

第一步是在web.config文件里设置连接串,提供者名字和本例的查询:

<configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0">

<connectionStrings>

<add name="Northwind" connectionString=

"Data Source=localhost;Initial Catalog=Northwind;Integrated Security=SSPI"/>

</connectionStrings>

<appSettings>

<add key="factory" value="System.Data.SqlClient" />

<add key="employeeQuery" value="SELECT * FROM Employees" />

</appSettings>

...

</configuration>

接下来是基于工厂的代码:

// Get the factory.

string factory = WebConfigurationManager.AppSettings["factory"];

DbProviderFactory provider = DbProviderFactories.GetFactory(factory);

// Use this factory to create a connection.

DbConnection con = provider.CreateConnection();

con.ConnectionString =

WebConfigurationManager.ConnectionStrings["Northwind"].ConnectionString;

// Create the command.

DbCommand cmd = provider.CreateCommand();

cmd.CommandText = WebConfigurationManager.AppSettings["employeeQuery"];

cmd.Connection = con;

// Open the Connection and get the DataReader.

con.Open();

DbDataReader reader = cmd.ExecuteReader();

// The code for navigating through the reader and displaying the records

// is identical from this point on.

为了对这个例子进行真实的测试,在web.config文件修改为使用一个不同的提供者,例如,可以通过做如下的修改,通过OLE DB提供者来访问相同的数据库:

<configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0">

<connectionStrings>

<add name="Northwind" connectionString="Provider=SQLOLEDB;Data Source=

localhost;Initial Catalog=Northwind;Integrated Security=SSPI"/>

</connectionStrings>

...

</configuration>

现在运行页面,你会看到相同的记录列表。区别就是DbDataFactories类创建了OLE DB对象来和你的代码一起工作。

注意:提供者无关的挑战并没有完全解决。即使是使用提供者工厂,你仍然要面对一些问题。例如,并没有通用的方法来catch数据库异常对象(因为不同的提供者特定的异常对象并不是继承于通用的基础类)。同时,不同的提供者使用参数名字的规则有点不同,而且也可能支持通过通用基础类无法获得的功能。通过通用基础类,可能需要编写痛苦的条件逻辑。

 

总结

在这一章中,你了解到了使用ADO.NET访问数据库的第一个层次:连接访问。在很多情况下,使用简单的命令和快速只读游标来获取结果,这对于Web应用来说,是最简单和最高效的编写数据访问代码的方法。在这过程中,你了解了一些高级话题,包括SQL注入攻击、事务、提供者无关代码。

在下一章中,你将学习如何使用这些技术来你自己的数据访问类和怎样使用ADO.NET非连接的DataSet

 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值