使用DataReader可以在你的应用程序中做以下事情:
I.不需要缓存数据;
II.处理太大而不能存储的数据;
III.需要以只进、只读和快速方式一次性访问数据的。
G.使用一个自定义的强有力的DataSet类型的好处
通过创建一个继承于DataSet的子对象,你可以在运行期间执行类型检查和声明。当你有了一个确定的计划或者为你的DataSet有相关的结构,你就可以创建一个用行和列表述一个对象的DataSet。比如,你表露一个消费者对象的名字属性来取代表露消费者表的一行中的名字列。有关此节详细信息,请参考微软站点上的文章:Working with a Typed DataSet
H.在自定义的DataSet中处理无效数据
通过XSD语言检查你的DataSet确保你的DataSet适当地处理无效引用。nullValue注释使你把BBNull替换成别的字符,String.Empty;或者保留无效引用,抛出错误提示,提示将取决于你应用程序的上下文,默认情况是引用了无效字符。
I.在DataSet中刷新数据
如果你要从数据库刷新你的DataSet,使用DataAdapter.Fill,如果你的DataTable拥有主键,DataAdapter.Fill将根据主键匹配新的行,同时从数据库取值运用到已存在的行。除非已刷新行在再次刷新前被修改,否者它的RowState将会被设置为UnChanged。注意的是如果DataTable没有设置主键,你的DataSet有可能出现重复的值。如果你想从数据库刷新一个表并保留任何表中行的更改,那么你就要首先填充一个新表,然后利用preserveChanges等于true来合并那个DataTable到你的DataSet中去。
J.在DataSet中搜索数据
当你在一个DataSet中查询特殊标准的行时,利用索引查询将会增加你的查询性能。当你给一个DataTable设计主键时,索引同时也创建了。当你为一个DataTable创建DataView时,索引也同时创建了。以下是使用索引查询的一些情况:
I.如果查询与DataTable中标识主键的列顺序相反,使用DataTable.Rows.Find代替DataTable.Select;
II.如果查询包括无主键的列,你可以使用DataView为数据的多重查询改善性能。当你在DataView中使用排序时,查询的同时就会创建一个索引。DataView使用Find和FindRows方法查询DataTable中的数据;
IV.假如你不需要表的排序视图,你也可以利用DataView为DataTable创建一个索引查询。注意的是这仅仅在你执行多重查询时才有优势,如果你只是执行一个简单查询,使用此方法将会降低你的查询效率。
K.DataView的结构
前面也讲过,在给DataTable创建DataView和Sort、RowFilter或者RowStateFilter属性发生更改的同时潜在的也给DataTable创建了索引。创建DataView对象时,如果Sort、RowFilter和RowStateFilter属性也同时指定,那么索引将只创建一次;如果创建一个空的DataView,那么索引至少被创建两次。
L.页面调度
ADO.NET使你可以很清楚地控制从你的数据库返回什么样的数据和有多少数据存储到一个DataSet。以下没有单一的介绍调度一个查询结果,但是当你设计你的应用程序时应该考虑到以下情况:
I.避免在使用DataAdapter.Fill时,在startRecord和maxRecords值上溢出。
II.解决这类问题的办法是使用WHERE语句、ORDER BY语句和TOP断言。
III.还有一种解决办法是使用TOP断言和嵌套的SELECT声明。比如如下代码:
SELECT TOP 10 * FROM
(SELECT TOP 30 * FROM Customers ORDER BY Id ASC) AS Table1 ORDER BY Id DESC
IV.如果你的日期不是经常改变,你可以使用DataSet的存储功能改善执行性能,比如你可以存储相当10页的数据到你的DataSet,然后当用户访问超过在存储区的FirstPage和LastPage时才查询数据库以获得新的数据。
M.有计划地填充DataSet
当使用数据填充DataSet时,DataAdapter.Fill方法使用DataSet已有的计划和SelectCommand返回的数据对DataSet进行填充。如果DataSet中没有与之对应的表将会失败,Fill创建一个表,默认情况下,Fill仅仅定义列和列的类型。你可以通过设置DataAdapter的MissingSchemaAction属性重载默认的Fill方法。举例,要使Fill方法创建表时总是包含主键信息、唯一约束、列属性、是否允许空值、列的最大长度、只读列和自动增量列,指定DataAdapter.MissingSchemaAction为MissingSchemaAction.AddWithKey。作为选择,你也可以在调用DataAdapter.Fill之前调用DataAdpater.FillSchema来保证填充DataSet时计划到位。调用FillSchema会给数据库增加额外的负担来输出Schema信息,所以最好的建议是指定DataSet的计划,或者在调用Fill之前设置DataAdapter的MissingSchemaAction。
N.使用CommandBuilder
CommandBuilder自动地生成基于DataAdapter的SelectCommand的DataAdapter的InsertCommand、UpdateCommand和DeleteCommand属性。提供SelectCommand执行一个简单的SELECT,以下信息介绍使用CommandBuilder的最佳处理。
I.在设计阶段不要使用CommandBuilder,否者产生DataAdapter Command属性的进程将会受到干扰。如果你预先知道你的UPDATE、INSERT和DELETE声明的内容,你应该清楚地指定。一个最好的设计方案是为你的UPDATE、INSERT和DELETE创建存储过程,并在DataAdapter的Command属性中设置和使用它们。
II.CommandBuilder使用SelectCommand决定其他Command属性的值。如果DataAdapter的SelectCommand本身发生变化,应该使用RefreshSchema去刷新Command的属性。
III.只要DataAdapter的Command属性为空,CommandBuilder就仅仅创建一个Command,即使你明确地指定Command的属性值,CommandBuilder也不会重写,所以如果你想创建一个Command并保留以前的属性设置,那么就把Command的属性设置为null。
O.SQL的批声明和处理
很多的数据库都支持在一条命令中使用综合查询或批处理或多条子命令。比如SQL Server中使用“;”。在一条命令中使用综合的多重命令可以有效地减少与数据库之间交互的次数并在你的应用程序中提高效率。比如在你的应用程序中使用批处理完成所有的删除(delete)任务等等。
使用批处理确实提高了效率,但同时也在你的应用程序中管理更新DataSet数据时增加了复杂性。要使得复杂性变简单化,你就要在你的DataSet中为每个DataTable创建一个DataAdapter。
P.使用多个表填充一个DataSet
如果你是用批处理从多个表返回数据并把这些数据填充到一个DataSet,fill方法将会使用第一个表的表名命名第一个表,以后的表命名将会采用在第一个表的表名基础上加上一个递增的数字。举例,下面的代码将逐步说明fill方法的工作原理:
‘Visual Basic
Dim da As SqlDataAdapter = New SqlDataAdapter(“select * from customers;select * from orders;”,myConnection)
Dim ds As DataSet = New DataSet()
da.fill(ds,”customers”)
‘C#
SqlDataAdapter da = new SqlDataAdapter(“select * from customers;select * from orders;”,myConnection);
DataSet ds = new DataSet();
da.fill(ds,”customers”);
如上面代码所示,customers表数据将会存放在一个命名为customers的DataTable中,而orders表数据将会放在一个命名为customers1的DataTable中。当然你也可以在数据填充结束后很容易地修改customers1表属性(TableName)为orders。然而,在以后的数据填充时,只会影响customers表中数据,而orders表将会忽略并同时创建一个新的命名为customers1的表。要解决这个问题,你就要在customers1和orders之间建立一个DataTableMapping映射。其他表也如此。举例说明:
‘Visual Basic
Dim da As SqlDataAdapter = New SqlDataAdapter("SELECT * FROM Customers; SELECT * FROM Orders;", myConnection)
da.TableMappings.Add("Customers1", "Orders")
Dim ds As DataSet = New DataSet()
da.Fill(ds, "Customers")
‘C#
SqlDataAdapter da = new SqlDataAdapter("SELECT * FROM Customers; SELECT * FROM Orders;", myConnection);
da.TableMappings.Add("Customers1", "Orders");
DataSet ds = new DataSet();
da.Fill(ds, "Customers");
Q.使用DataReader
下面是使用DataReader的一些技巧和一些问题的回答:
I.在使用相关Command访问任何输出参数之前必须关闭DataReader;
II.在读取数据结束后应当关闭DataReader。如果你的Connection仅仅是用来返回DataReader,那么在关闭DataReader之后你也应该立即关闭它。另外一个关闭Connection的方法是传递CommandBehavior.CloseConnection给ExecuteReader方法。此方法在你从一个方法中返回DataReader并且对这个DataReader没有关闭控制权或者关联的连接时使用时非常有用的。
III.DataReader是为已连接的数据存取设计;
IV.使用GetString、GetInt32等返回特殊数据类型数据;
V.一个连接只允许使用一个DataReader。在ADO中,如果你只创建一个连接并使用两个recordsets,一个只读游标和一个只进游标,但实际上,ADO已经为你创建了一个隐式的连接,并在不用的时候隐式地关闭它。ADO.NET中是不行的,你必须为每个DataReader创建一个Connection,这也是为了让你在使用Connection时给予更多的控制信息。
VI.默认下,DataReader每次读取时把行中所有的数据装载到内存中。并允许你随机存取当前行中数据。如果随即存取没有必要(没有必要把所有数据都装载到内存),并想提高执行效率,给ExecuteReader方法传递CommandBehavior.SequentialAccess,这样就会改变DataReader的默认动作为仅仅装载请求的数据到内存。注意的是这种方法要求你顺序地存取行中列数据,一旦你略过某一列,以后你将再不会读取到该列的数据。
VII.如果当你在完成从一个DataReader读取数据后,仍然还有大量未读不需要的数据,这就要在调用DataReader的Close命令之前调用Cancel命令。调用DataReader的Close命令会导致把不需要的数据装载进来并在关闭游标之前清空数据。而调用Cancel命令就会丢弃这部分数据,从而DataReader在关闭之前就不会读取它们。如果你正在从你的命令返回输出参数,调用Cancel命令同样会丢弃它们。如果你需要读取任何输出参数,你就不要使用Cancel,而直接使用Close。
R.BLOBs对象
当你使用DataReader读取二进制数据时,你应该传递CommandBehavior.SequentialAccess给ExecuteReader方法调用。因为DataReader的默认情况是每次读取数据时把每行中所有的数据都存储到内存,而二进制数据又非常的大,结果就会使大量的内存空间被一个单一的BLOB占用。SequentialAccess使得你的DataReader默认行为为仅读取需要的数据。然后你就可以使用GetBytes或者GetChars决定一次读取多少数据。
记住的是使用SequentialAccess后,你不能次序颠倒地访问DataReader中不同的字段。就是说,如果你的查询返回三列,其中第三列是BLOB数据类型,如果你想访问第三列数据,那么你就必须先访问第一列,然后是第二列,再然后才是第三列的BLOB数据。这是因为此时返回的数据是有序的,而且一旦你跳过某一列,再回过头来读取这一列是不行的。
S.使用命令
ADO.NET提供了执行命令的几种不同方法,同时也提供了优化执行命令的几种不同参数。下面将要介绍的是选择最佳执行命令的技巧和改善一个可执行命令的性能。
I.OleDbCommand最佳实践
.NET 框架中各种数据提供者之间的执行命令标准几乎是一样的。但是也有不同,下面是执行OleDbCommand的一些技巧:
①使用CommandType.Text调用存储过程,使用CommandType.StoredProcedure生成;
②确定设置OleDbParameter的类型、大小(如果要求)和精度(如果是数字或者小数),注意的是,如果你不明确设置OleDbParameter,OleDbCommand将会为你的执行命令重新生成OleDbParameter。
II.SqlCommand最佳实践
使用SqlCommand快速执行存储过程:如果你要调用一个存储过程,指定SqlCommand的CommandType为存储过程的CommandType。这样在执行命令时,就会提交此命令是调用存储过程,从而达到快速执行。
III.使用已准备的方法
Command.Prepare方法优化你的参数化执行命令。Prepare结构为多重调用最优化指定命令。要使用Prepare,你首先得理解你的数据库是怎样相应Prepare调用。SQL Server 2000中,Command已经被隐式优化和Prepare不是必须的;在SQL Server7.0或其它数据库中使用Prepare是有效的。
IV.明确地指定计划和元数据
ADO.NET中的很多对象都要推断元数据信息,只要用户不指定它,举例如下:
①如果在DataSet中不存在,DataAdapter.Fill方法就会创建表和列信息;
②CommandBuilder为独立表的Select命令生成DataAdpater命令参数;
③CommandBuilder.DeriveParameters组装一个命令对象的参数信息;
如果什么时候都使用上面讲的方法,可能会降低执行性能。推荐在设计阶段和广告段应用程序中使用。可能的情况下,一般都要指定计划和元数据。这些包括指定DataSet的表和列、指定DataAdapter的Command属性和指定Command的参数信息。
V.ExecuteScalar和ExecuteNonQuery
如果你想只返回一个简单值,比如Count(*)、Sum(Price)或者Avg(Quantity),你可以使用ExecuteScalar,它帮助你一步到位得到你想要的值,从而避免使用DataReader的两步计算(ExecuteReader+GetValue);
当你不想返回行信息,比如修改数据(INSERT、UPDATE、DELETE)或者仅需要输出参数或者返回值,使用ExecuteNonQuery,它去掉不必要的处理创建一个空的DataReader。
VI.空值检查
如果在你的表中某列允许空值,你可以使用Where语句进行空值检查,下面举例说明:
select * from customers where ((LastName=@LastName) or (LastName IS NULL and @LastName IS NULL))
上面语句检查了列是否为空和参数是否为空。
VII.传递null参数值
当你在命令中传递null参数值给数据库时,你不能使用null(Nothing在vb中),应该使用DBNull.Value。举例:
‘vb
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;
VIII.使用事务处理
ADO.NET中的事务处理模型已经改变,在ADO中,一旦StartTransaction被调用,任何事务下的更新都被认为是事务的一部分。然而,在ADO.NET中,当Connection.BeginTransaction被调用,返回一个命令关联的事务对象(事务属性是由命令的事务属性指定的)。这样保证让你在一个Connection中执行多个事务。如果命令的Command.Transaction属性与开始的事务不一致,命令就不会完成并抛出错误。
IX.使用Connections
高效率的应用程序应该使用最少的时间与数据库建立连接,比如使用Connection Pooling等。下面将介绍如何使用ADO.NET建立高效率应用的一些数据库方面的技巧。
①Connection Pooling
在SQL Server、OLE DB和.NET框架结构中的Data Provider中,都提供了隐式的连接池连接支持。你可以在ConnectionString中指定不同的参数值控制连接池的行为。比如下面的例子使OLE DB的连接池无效并自动地进行事务处理:
Provider=SQLOLEDB;OLE DB Services=-4;Data Source=localhost;Integrated Security=SSPI;
在SQL Server.NET Data Provider中提供了以下参数设置控制连接池的行为:Connection Lifttime、Connection Reset、Enlist、Max Pool Size、Min Pool Size和Pooling。
②使用DataAdapter最优化连接
使用DataAdpater的Fill和Update方法时会自动地打开相应的连接。如果Fill或者Update打开一个连接,在它操作完成后它会关闭此连接。最好的执行方式是在你需要的时候才建立连接。同时减少多个操作时打开和关闭连接的次数。
推荐你在仅仅执行一个Fill或者Update时,允许Fill或者Update方法隐式地打开和关闭连接;如果你要执行多个Fill或者Update,建议你显式地建立连接、执行Fill或者Update操作然后显式地关闭连接。
额外地,在我们执行事务处理时,在开始事务之前应该显式地建立连接,并在事务结束后显式地关闭连接。举例:
‘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();
}
X.总是关闭Connections和DataReaders
在你使用完Connection或者DataReader对象后,你应该明确地关闭它们。系统中的碎片整理程序只是在最后需要的时候才进行整理,而一些很耗资源的连接还得由你自己来释放。同时如果你不明确地关闭连接,此连接就有可能不返回连接池,除非连接池的Max Pool Size已经达到并且此连接还仍然有效。
注意:在你的类的Finalize方法中不要使用Close或者Dispose运用到一个Connection或者一个DataReader或者任何被管理对象上。在一个Finalizer中,仅仅释放你的类直接拥有的无法管理的资源。如果你的类不拥有任何无法管理的资源,就不要在你的类使用Finalize方法。
XI.在C#中使用Using声明
在C#中,一个非常便利的保证关闭你使用过的Connection和DataReader对象的方法是使用Using声明。当对象超出了它的使用范围,Using声明就会自动地释放该对象。举例:
‘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));
}
}
Using声明在Visual Basic.Net中不可用。
XII.避免访问OleDbConnection.State属性
如果你需要经常检查State属性,最好在OleDbConnection上监听StateChange事件。下面的代码演示当OleDbConnection.State发生变化时使用StateChange向控制台发送一条消息:
‘Visual Basic
AddHandler nwindConn.StateChange, New StateChangeEventHandler(AddressOf OnStateChange)
Protected Shared Sub OnStateChange(sender As Object, args As StateChangeEventArgs)
Console.WriteLine("The current Connection state has changed from {0} to {1}.", _
args.OriginalState, args.CurrentState)
End Sub
‘C#
nwindConn.StateChange += new StateChangeEventHandler(OnStateChange);
protected static void OnStateChange(object sender, StateChangeEventArgs args)
{
Console.WriteLine("The current Connection state has changed from {0} to {1}.",
args.OriginalState, args.CurrentState);
}
ADO.NET最佳实践(下)