使用DataReader
下面是一些使用DataReader获得最佳性能的技巧,同时还回答了一些关于使用DataReader的常见问题。
1) 在访问相关Command的任何输出参数之前,必须关闭DataReader。
2) 完成读数据之后总是要关闭DataReader。如果使用Connection只是用于返回DataReader,那么关闭DataReader之后立刻关闭它。
另外一个显式关闭Connection的方法是将CommandBehavior.CloseConnection传递给ExecuteReader方法,以确保相关的连接在关闭DataReader时被关闭。如果从一个方法返回DataReader,而且不能控制DataReader或相关连接的关闭,则这样做特别有用。
1) 不能在层之间远程访问DataReader。DataReader是为已连接好的数据访问设计的。
2) 当访问列数据时,使用类型化访问器,例如,GetString、GetInt32等。这使您不用进行将GetValue返回的Object强制转换成特定类型所需的处理。
3) 一个单一连接每次只能打开一个DataReader。在ADO中,如果打开一个单一连接,并且请求两个使用只进、只读游标的记录集,那么ADO会在游标生存期内隐式打开第二个、未池化的到数据存储区的连接,然后再隐式关闭该连接。对于ADO.NET,“秘密”完成的动作很少。如果想在相同的数据存储区上同时打开两个DataReaders,就必须显式创建两个连接,每个DataReader一个。这是ADO.NET为池化连接的使用提供更多控制的一种方法。
4) 默认情况下,DataReader每次Read时都要将整行加载到内存。这允许在当前行内随机访问列。如果不需要这种随机访问,为了提高性能,就将CommandBehavior.SequentialAccess传递给ExecuteReader调用。这将DataReader的默认行为更改为仅在请求时将数据加载到内存。注意,CommandBehavior.SequentialAccess要求顺序访问返回的列。也就是说,一旦读过返回的列,就不能再读它的值了。
5) 如果已经完成读取来自DataReader的数据,但仍然有大量挂起的未读结果,就在调用DataReader的Close之前先调用Command的Cancel。调用DataReader的Close会导致在关闭游标之前检索挂起的结果并清空流。调用Command的Cancel会放弃服务器上的结果,这样,DataReader在关闭的时候就不必读这些结果。如果要从Command返回输出参数,还要调用Cancel放弃它们。如果需要读取任何输出参数,不要调用Command的Cancel,只要调用DataReader的Close即可。
二进制大对象(BLOB)
用DataReader检索二进制大对象(BLOB)时,应该将CommandBehavior.SequentialAccess传递给ExecuteReader方法调用。因为DataReader的默认行为是每次Read都将整行加载到内存,又因为BLOB值可能非常大,所以结果可能由于单个BLOB而使大量内存被用光。SequentialAccess将DataReader的行为设置为只加载请求的数据。然后还可以使用GetBytes或GetChars控制每次加载多少数据。
记住,使用SequentialAccess时,不能不按顺序访问DataReader返回的不同字段。也就是说,如果查询返回三列,其中第三列是BLOB,并且想访问前两列中的数据,就必须在访问BLOB数据之前先访问第一列的值,然后访问第二列的值。这是因为现在数据是顺序返回的,并且DataReader一旦读过该数据,该数据就不再可用。
使用命令
ADO.NET提供了几种命令执行的不同方法以及优化命令执行的不同选项。下面包括一些技巧,它们是关于选择最佳命令执行以及如何提高执行命令的性能。
使用OleDbCommand的最佳实践
不同.NET框架数据提供程序之间的命令执行被尽可能标准化了。但是,数据提供程序之间仍然存在差异。下面给出一些技巧,可微调用于OLE DB的.NET框架数据提供程序的命令执行。
1) 按照ODBC CALL语法使用CommandType.Text调用存储过程。使用CommandType.StoredProcedure只是秘密地生成ODBC CALL语法。
2) 一定要设置OleDbParameter的类型、大小(如果适用)、以及精度和范围(如果参数类型是numeric或decimal)。注意,如果不显式提供参数信息,OleDbCommand会为每个执行命令重新创建OLE DB参数访问器。
使用SqlCommand的最佳实践
使用SqlCommand执行存储过程的快速提示:如果调用存储过程,将SqlCommand的CommandType属性指定为StoredProcedure的CommandType。这样通过将该命令显式标识为存储过程,就不需要在执行之前分析命令。
使用Prepare方法
对于重复作用于数据源的参数化命令,Command.Prepare方法能提高性能。Prepare指示数据源为多次调用优化指定的命令。要想有效利用Prepare,需要彻底理解数据源是如何响应Prepare调用的。对于一些数据源(例如SQL Server 2000),命令是隐式优化的,不必调用Prepare。对于其他(例如SQL Server 7.0)数据源,Prepare会比较有效。
显式指定架构和元数据
只要用户没有指定元数据信息,ADO.NET的许多对象就会推断元数据信息。下面是一些示例:
1) DataAdapter.Fill方法,如果DataSet中没有表和列,DataAdapter.Fill方法会在DataSet中创建表和列。
2) CommandBuilder,它会为单表SELECT命令生成DataAdapter命令属性。
3) CommandBuilder.DeriveParameters,它会填充Command对象的Parameters集合。
但是,每次用到这些特性,都会有性能损失。建议将这些特性主要用于设计时和即席应用程序中。在可能的情况下,显式指定架构和元数据。其中包括在DataSet中定义表和列、定义DataAdapter的Command属性、以及为Command定义Parameter信息。
ExecuteScalar和ExecuteNonQuery
如果想返回像Count(*)、Sum(Price)或Avg(Quantity)的结果那样的单值,可以使用Command.ExecuteScalar。ExecuteScalar返回第一行第一列的值,将结果集作为标量值返回。因为单独一步就能完成,所以ExecuteScalar不仅简化了代码,还提高了性能;要是使用DataReader就需要两步才能完成(即,ExecuteReader+取值)。
使用不返回行的SQL语句时,例如修改数据(例如INSERT、UPDATE或DELETE)或仅返回输出参数或返回值,请使用ExecuteNonQuery。这避免了用于创建空DataReader的任何不必要处理。
测试Null
如果表(在数据库中)中的列允许为空,就不能测试参数值是否“等于”空。相反,需要写一个WHERE子句,测试列和参数是否都为空。下面的SQL语句返回一些行,它们的LastName列等于赋给@LastName参数的值,或者LastName列和@LastName参数都为空。
SELECT * FROM Customers WHERE ((LastName = @LastName) OR (LastName IS NULL AND @LastName IS NULL)) |
'Visual Basic Dim param As SqlParameter = New SqlParameter("@Name", SqlDbType.NVarChar, 20) param.Value = DBNull.Value //C# SqlParameter param = new SqlParameter("@Name", SqlDbType.NVarChar, 20); param.Value = DBNull.Value; |
'Visual Basic Public Sub RunSqlTransaction(da As SqlDataAdapter, myConnection As SqlConnection, ds As DataSet) myConnection.Open() Dim myTrans As SqlTransaction = myConnection.BeginTransaction() myCommand.Transaction = myTrans Try da.Update(ds) myTrans.Commit() Console.WriteLine("Update successful.") Catch e As Exception Try myTrans.Rollback() Catch ex As SqlException If Not myTrans.Connection Is Nothing Then Console.WriteLine("An exception of type " & ex.GetType().ToString() & " was encountered while attempting to roll back the transaction.") End If End Try Console.WriteLine("An exception of type " & e.GetType().ToString() & " was encountered.") Console.WriteLine("Update failed.") End Try myConnection.Close() End Sub //C# public void RunSqlTransaction(SqlDataAdapter da, SqlConnection myConnection, DataSet ds) { myConnection.Open(); SqlTransaction myTrans = myConnection.BeginTransaction(); myCommand.Transaction = myTrans; try { da.Update(ds); myCommand.Transaction.Commit(); Console.WriteLine("Update successful."); } catch(Exception e) { try { myTrans.Rollback(); } catch (SqlException ex) { if (myTrans.Connection != null) { Console.WriteLine("An exception of type " + ex.GetType() +" was encountered while attempting to roll back the transaction."); } } Console.WriteLine(e.ToString()); Console.WriteLine("Update failed."); } myConnection.Close(); } |
精品教程尽在w ww.xvna.com
始终关闭Connection和DataReader
完成对Connection或DataReader对象的使用后,总是显式地关闭它们。尽管垃圾回收最终会清除对象并因此释放连接和其他托管资源,但垃圾回收仅在需要时执行。因此,确保任何宝贵的资源被显式释放仍然是您的责任。并且,没有显式关闭的Connections可能不会返回到池中。例如,一个超出作用范围却没有显式关闭的连接,只有当连接池大小达到最大并且连接仍然有效时,才会被返回到连接池中。
注不要在类的Finalize方法中对Connection、DataReader或任何其他托管对象调用Close或Dispose。最后完成的时候,仅释放类自己直接拥有的非托管资源。如果类没有任何非托管资源,就不要在类定义中包含Finalize方法。
在C#中使用“Using”语句
对于C#程序员来说,确保始终关闭Connection和DataReader对象的一个方便的方法就是使用using语句。using语句在离开自己的作用范围时,会自动调用被“使用”的对象的Dispose。例如:
//C# string connString = "Data Source=localhost;Integrated Security=SSPI;Initial Catalog=Northwind;"; using (SqlConnection conn = new SqlConnection(connString)) { SqlCommand cmd = conn.CreateCommand(); cmd.CommandText = "SELECT CustomerId, CompanyName FROM Customers"; conn.Open(); using (SqlDataReader dr = cmd.ExecuteReader()) { while (dr.Read()) Console.WriteLine("{0}\t{1}", dr.GetString(0), dr.GetString(1)); } } |
更多有用的技巧
下面是一些编写ADO.NET代码时的通用技巧。
避免自动增量值冲突
就像大多数数据源一样,DataSet使您可标识那些添加新行时自动对其值进行递增的列。在DataSet中使用自动增量的列时,如果自动增量的列来自数据源,可避免添加到DataSet的行和添加到数据源的行之间本地编号冲突。
例如,考虑一个表,它的主键列CustomerID是自动增量的。两个新的客户信息行添加到表中,并接收到自动增量的CustomerID值1和2。然后,只有第二个客户行被传递给DataAdapter的方法Update,新添加的行在数据源接收到一个自动增量的CustomerID值1,与DataSet中的值2不匹配。当DataAdapter用返回值填充表中第二行时,就会出现约束冲突,因为第一个客户行已经使用了CustomerID值1。
要避免这种情况,建议在使用数据源上自动增量的列以及DataSet上自动增量的列时,将DataSet中的列创建为AutoIncrementStep值等于-1并且AutoIncrementSeed值等于0,另外,还要确保数据源生成的自动增量标识值从1开始,并且以正阶值递增。因此,DataSet为自动增量值生成负数,与数据源生成的正自动增量值不冲突。另外一个选择是使用GUID类型的列,而不是自动增量的列。生成GUID值的算法应该永远不会使数据源中生成的GUID值与DataSet中生成的GUID值一样。
如果自动增量的列只是用作唯一值,而且没有任何意义,就考虑使用GUID代替自动增量的列。它们是唯一的,并且避免了使用自动增量的列所必需的额外工作。
检查开放式并发冲突
按照设计,由于DataSet是与数据源断开的,所以,当多个客户端在数据源上按照开放式并发模型更新数据时,需要确保应用程序避免冲突。
在测试开放式并发冲突时有几项技术。一项技术涉及在表中包含时间戳列。另外一项技术是,验证一行中所有列的原始值是否仍然与通过在SQL语句中使用WHERE子句进行测试时在数据库中找到的值相匹配。
多线程编程
ADO.NET对性能、吞吐量和可伸缩性进行优化。因此,ADO.NET对象不锁定资源,并且必须只用于单线程。一个例外是DataSet,它对多个阅读器是线程安全的。但是,在写的时候需要将DataSet锁定。
仅在需要的时候才用COM Interop访问ADO
ADO.NET的设计目的是成为许多应用程序的最佳解决方案。但是,有些应用程序需要只有使用ADO对象才有的功能,例如,ADO多维(ADOMD)。在这些情况下,应用程序可以用COM Interop访问ADO。注意使用COM Interop访问具有ADO的数据会导致性能降低。在设计应用程序时,首先在实现用COM Interop访问ADO的设计之前,先确定ADO.NET是否满足设计需求。