探索设计模式开篇
——探索设计模式系列之一
Terrylee,2005年12月06日
前言
加入Design & Pattern团队有几个月的时间了,惭愧的是从没有写过关于设计模式的随笔,得到wayfarer的同意,把企业库系列的随笔放在了团队的首页上。不是不想去写这样的随笔,也不是没有时间,自己初学设计模式,去写设计模式的文章,有点班门弄斧的味道。园子里吕震宇 老师的《设计模式系列》和wayfarer的《设计之道》堪称设计模式里的经典之作。可是正如wafarer所说的那样,受到发表欲的蛊惑,本着交流就是进步的想法,思考再三,还是决定写这样的随笔,来对设计模式做一些探索和总结,起名曰“探索设计模式”,有些言过其实,就当是记录自己学习设计模式的历程吧,不过还是希望能得到各位前辈的指点!
设计模式
设计模式是规则吗?
地上本没有路,走得人多了也就成了路。设计模式如同此理,它是经验的传承,并非体系;是被前人发现,经过总结形成了一套某一类问题的一般性解决方案,而不是被设计出来的定性规则;它不像算法那样可以照搬照用。
设计模式是架构吗?
架构和模式应该是一个属于相互涵盖的过程,但是总体来说架构更加关注的是所谓的High-Level Design,而模式关注的重点在于通过经验提取的“准则或指导方案”在设计中的应用,因此在不同层面考虑问题的时候就形成了不同问题域上的模式。模式的目标是,把共通问题中的不变部分和变化部分分离出来。不变的部分,就构成了模式,因此,模式是一个经验提取的“准则”,并且在一次一次的实践中得到验证,在不同的层次有不同的模式,小到语言实现,大到架构。在不同的层面上,模式提供不同层面的指导。
设计模式,软件的永恒之道?
这个问题没有答案,有的只是讨论,看一下一位前辈结合建筑学得出的几点心得吧:
和建筑结构一样,软件中亦有诸多的“内力”。和建筑设计一样,软件设计也应该努力疏解系统中的内力,使系统趋于稳定、有生气。一切的软件设计都应该由此出发。
任何系统都需要有变化,任何系统都会走向死亡。作为设计者,应该拥抱变化、利用变化,而不是逃避变化。
好的软件只能“产生”而不能“创造”,我们所能做的只是用一个相对好的过程,尽量使软件朝向好的方向发展。
需要设计模式吗?
答案是肯定的,但你需要确定的是模式的应用是否过度?我得承认,世界上有很多天才的程序员,他可以在一段代码中包含6 种设计模式,也可以不用模式而把设计做得很好。但我们的目标是追求有效的设计,而设计模式可以为这个目标提供某种参考模型、设计方法。
我们不需要奉GOF的设计模式为圭臬,但合理的运用设计模式,才是正确的抉择。很多人看过GOF的《Design Patterns》,对这23 种模式也背得滚瓜烂熟。但重要的不是你熟记了多少个模式的名称,关键还在于付诸实践的运用。为了有效地设计,而去熟悉某种模式所花费的代价是值得的,因为很快你会在设计中发现这种模式真的很好,很多时候它令得你的设计更加简单了。
其实在软件设计人员中,唾弃设计模式的可能很少,盲目夸大设计模式功用的反而更多。言必谈“模式”,并不能使你成为优秀的架构师。真正出色的设计师,懂得判断运用模式的时机。还有一个问题是,很多才踏入软件设计领域的人员,往往对设计模式很困惑。对于他们来说,由于没有项目的实际经验,OO 的思想也还未曾建立,设计模式未免过于高深了。其实,即使是非常有经验的程序员,也不敢夸口对各种模式都能合理应用。[--摘自wayfare的设计之道]
后记
关于设计模式的理论性的文章,已经写了很多了,我不想再继续重复抄写下去,仅记录下上面几段话,用它来作探索设计模式系列的一个开篇吧。
简介
本文为您提供了在 Microsoft ADO.NET 应用程序中实现和获得最佳性能、可伸缩性以及功能的最佳解决方案;同时也讲述了使用 ADO.NET 中可用对象的最佳实践;并提出一些有助于优化 ADO.NET 应用程序设计的建议。
本文包含:
• | 有关 .NET 框架包含的 .NET 框架数据提供程序的信息。 |
• | DataSet 和 DataReader 之间的比较,以及这些对象中每个对象最佳用法的解释。 |
• | 解释如何使用 DataSet、Commands 和 Connections。 |
• | 有关与 XML 集成的信息。 |
• | 通用的技巧和问题。 |
使用 DataReader、DataSet、DataAdapter 和 DataView
ADO.NET 提供以下两个对象,用于检索关系数据并将其存储在内存中:DataSet 和 DataReader。DataSet 提供一个内存中数据的关系表示形式,一整套包括一些表在内的数据(这些表包含数据、对数据进行排序并约束数据),以及表之间的关系。DataReader 提供一个来自数据库的快速、只进、只读数据流。
当使用 DataSet 时,经常会利用 DataAdapter(也可能是 CommandBuilder)与数据源进行交互。当使用 DataSet 时,也可以利用 DataView 对 DataSet 中的数据应用排序和筛选。也可以从 DataSet 继承,创建强类型 DataSet,用于将表、行和列作为强类型对象属性公开。
下列主题包括的信息涉及:使用 DataSet 或 DataReader 的最佳时机、如何优化访问它们所包含数据、以及如何优化使用 DataAdapter(包括 CommandBuilder)和 DataView 的技巧。
DataSet 与 DataReader
当设计应用程序时,要考虑应用程序所需功能的等级,以确定使用 DataSet 或者是 DataReader。
要通过应用程序执行以下操作,就要使用 DataSet:
• | 在结果的多个离散表之间进行导航。 |
• | 操作来自多个数据源(例如,来自多个数据库、一个 XML 文件和一个电子表格的混合数据)的数据。 |
• | 在各层之间交换数据或使用 XML Web 服务。与 DataReader 不同的是,DataSet 能传递给远程客户端。 |
• | 重用同样的行组,以便通过缓存获得性能改善(例如排序、搜索或筛选数据)。 |
• | 每行执行大量处理。对使用 DataReader 返回的每一行进行扩展处理会延长服务于 DataReader 的连接的必要时间,这影响了性能。 |
• | 使用 XML 操作对数据进行操作,例如可扩展样式表语言转换(XSLT 转换)或 XPath 查询。 |
对于下列情况,要在应用程序中使用 DataReader:
• | 不需要缓存数据。 |
• | 要处理的结果集太大,内存中放不下。 |
• | 一旦需要以只进、只读方式快速访问数据。 |
注填充 DataSet 时,DataAdapter 使用 DataReader。因此,使用 DataAdapter 取代 DataSet 提升的性能表现为节省了 DataSet 占用内存和填充 DataSet 需要的循环。一般来说,此性能提升只是象征性的,因此,设计决策应以所需功能为基础。
使用强类型 DataSet 的好处
DataSet 的另一个好处是可被继承以创建一个强类型 DataSet。强类型 DataSet 的好处包括设计时类型检查,以及 Microsoft Visual Studio .NET 用于强类型 DataSet 语句结束所带来的好处。修改了 DataSet 的架构或关系结构后,就可以创建一个强类型 DataSet,把行和列作为对象的属性公开,而不是作为集合中的项公开。例如,不公开客户表中行的姓名列,而公开 Customer 对象的 Name 属性。类型化 DataSet 从 DataSet 类派生,因此不会牺牲 DataSet 的任何功能。也就是说,类型化 DataSet 仍能远程访问,并作为数据绑定控件(例如 DataGrid)的数据源提供。如果架构事先不可知,仍能受益于通用 DataSet 的功能,但却不能受益于强类型 DataSet 的附加功能。
处理强类型 DataSet 中的空引用
使用强类型 DataSet 时,可以批注 DataSet 的 XML 架构定义语言 (XSD) 架构,以确保强类型 DataSet 正确处理空引用。nullValue 批注使您可用一个指定的值 String.Empty 代替 DBNull、保留空引用或引发异常。选择哪个选项取决于应用程序的上下文。默认情况下,如果遇到空引用,就会引发异常。
有关更多信息,请参阅 Working with a Typed DataSet。
刷新 DataSet 中的数据
如果想用服务器上的更新值刷新 DataSet 中的值,就使用 DataAdapter.Fill。如果有在 DataTable 上定义的主键,DataAdapter.Fill 会根据主键进行新行匹配,并且当更改到现有行时应用服务器上的值。即使刷新之前修改了它们,刷新行的 RowState 仍被设置为 Unchanged。注意,如果没有为 DataTable 定义主键,DataAdapter.Fill 就用可能重复的主键值添加新行。
如果想用来自服务器的当前值刷新表,并同时保留对表中的行所做的任何更改,必须首先用 DataAdapter.Fill 填充表,并填充一个新的 DataTable,然后用 preserveChanges 值 true 把 DataTableMerge 到 DataSet 中。
在 DataSet 中搜索数据
在 DataSet 中查询与特定条件相匹配的行时,可以利用基于索引的查找提高搜索性能。当把 PrimaryKey 值赋给 DataTable 时,会创建一个索引。当给 DataTable 创建 DataView 时,也会创建一个索引。下面是一些利用基于索引进行查找的技巧。
• | 如果对组成 DataTable 的 PrimaryKey的列进行查询,要使用 DataTable.Rows.Find 而不是 DataTable.Select。 |
• | 对于涉及到非主键列的查询,可以使用 DataView 为数据的多个查询提高性能。当把排序顺序应用到 DataView 时,就会建立一个搜索时使用的索引。DataView 公开 Find 和 FindRows 方法,以便查询基础 DataTable 中的数据。 |
• | 如果不需要表的排序视图,仍可以通过为 DataTable 创建 DataView 来利用基于索引的查找。注意,只有对数据执行多个查询操作时,这样才会带来好处。如果只执行单一查询,创建索引所需要的处理就会降低使用索引所带来的性能提升。 |
DataView 构造
如果创建了 DataView,并且修改了 Sort、RowFilter 或 RowStateFilter 属性,DataView 就会为基础 DataTable 中的数据建立索引。创建 DataView 对象时,要使用 DataView 构造函数,它用 Sort、RowFilter 和 RowStateFilter 值作为构造函数参数(与基础 DataTable 一起)。结果是创建了一次索引。创建一个“空”DataView 并随后设置 Sort、RowFilter 或 RowStateFilter 属性,会导致索引至少创建两次。
分页
ADO.NET 可以显式控制从数据源中返回什么样的数据,以及在 DataSet 中本地缓存多少数据。对查询结果的分页没有唯一的答案,但下面有一些设计应用程序时应该考虑的技巧。
• | 避免使用带有 startRecord 和 maxRecords 值的 DataAdapter.Fill 重载。当以这种方式填充 DataSet 时,只有 maxRecords 参数(从 startRecord 参数标识的记录开始)指定的记录数量用于填充 DataSet,但无论如何总是返回完整的查询。这就会引起不必要的处理,用于读取“不需要的”记录;而且为了返回附加记录,会耗尽不必要的服务器资源。 |
• | 用于每次只返回一页记录的技术是创建 SQL 语句,把 WHERE 子句以及 ORDER BY 子句和 TOP 谓词组合起来。此技术取决于存在一种可唯一标识每一行的办法。当浏览下一页记录时,修改 WHERE 子句使之包含所有唯一标识符大于当前页最后一个唯一标识符的记录。当浏览上一页记录时,修改 WHERE 子句使之返回所有唯一标识符小于当前页第一个唯一标识符的记录。两种查询都只返回记录的 TOP 页。当浏览上一页时,需要以降序为结果排序。这将有效地返回查询的最后一页(如果需要,显示之前也许要重新排序结果)。有关这个技术的一个示例,请参阅 Paging Through a Query Result。 |
• | 另一项每次只返回一页记录的技术是创建 SQL 语句,把 TOP 谓词和嵌入式 SELECT 语句的使用结合在一起。此技术并不依赖于存在一种可唯一标识每一行的办法。使用这项技术的第一步是把所需页的数量与页大小相乘。然后将结果传递给 SQL Query 的 TOP 谓词,该查询以升序排列。再把此查询嵌入到另一个查询中,后者从降序排列的嵌入式查询结果中选择 TOP 页大小。实质上,返回的是嵌入式查询的最后一页。例如,要返回查询结果的第三页(页大小是 10),应该书写如下所示的命令: SELECT TOP 10 * FROM (SELECT TOP 30 * FROM Customers ORDER BY Id ASC) AS Table1ORDER BY Id DESC 注意,从查询中返回的结果页以降序显示。如果需要,应该重新排序。 |
• | 如果数据不经常变动,可以在 DataSet 中本地维护一个记录缓存,以此提高性能。例如,可以在本地 DataSet 中存储 10 页有用的数据,并且只有当用户浏览超出缓存第一页和最后一页时,才从数据源中查询新数据。 |
有关更多信息,请参阅 .NET Data Access Architecture Guide。
用架构填充 DataSet
当用数据填充 DataSet 时,DataAdapter.Fill 方法使用 DataSet 的现有架构,并使用从 SelectCommand 返回的数据填充它。如果在 DataSet 中没有表名与要被填充的表名相匹配,Fill 方法就会创建一个表。默认情况下,Fill 仅定义列和列类型。
通过设置 DataAdapter 的 MissingSchemaAction 属性,可以重写 Fill 的默认行为。例如,要让 Fill 创建一个表架构,并且还包括主键信息、唯一约束、列属性、是否允许为空、最大列长度、只读列和自动增量的列,就要把 DataAdapter.MissingSchemaAction 指定为 MissingSchemaAction.AddWithKey。或者,在调用 DataAdapter.Fill 前,可以调用 DataAdapter.FillSchema 来确保当填充 DataSet 时架构已到位。
对 FillSchema 的调用会产生一个到服务器的额外行程,用于检索附加架构信息。为了获得最佳性能,需要在调用 Fill 之前指定 DataSet 的架构,或者设置 DataAdapter 的 MissingSchemaAction。
使用 CommandBuilder 的最佳实践
假设 SelectCommand 执行单一表 SELECT,CommandBuilder 就会以 DataAdapter 的 SelectCommand 属性为基础自动生成 DataAdapter 的 InsertCommand、UpdateCommand、和 DeleteCommand 属性。下面是为获得最佳性能而使用 CommandBuilder 的一些技巧。
• | CommandBuilder 的使用应该限制在设计时或即席方案中。生成 DataAdapter 命令属性所必需的处理会影响性能。如果预先知道 INSERT/UPDATE/DELETE 语句的内容,就显式设置它们。一个比较好的设计技巧是,为 INSERT/UPDATE/DELETE 命令创建存储过程并显式配置 DataAdapter 命令属性以使用它们。 |
• | CommandBuilder 使用 DataAdapter 的 SelectCommand 属性确定其他命令属性的值。如果 DataAdapter 的 SelectCommand 本身曾经更改过,确保调用 RefreshSchema 以更新命令属性。 |
• | 如果 DataAdapter 命令属性为空(命令属性默认情况下为空),CommandBuilder 仅仅为它生成一条命令。如果显式设置了命令属性,CommandBuilder 不会重写它。如果希望 CommandBuilder 为以前已经设置过的命令属性生成命令,就把命令属性设置为空。 |
批处理 SQL 语句
很多数据库支持把多条命令合并或批处理成一条单一命令执行。例如,SQL Server 使您可以用分号 (;) 分隔命令。把多条命令合并成单一命令,能减少到服务器的行程数,并提高应用程序的性能。例如,可以把所有预定的删除在应用程序中本地存储起来,然后再发出一条批处理命令调用,从数据源删除它们。
虽然这样做确实能提高性能,但是,当对 DataSet 中的数据更新进行管理时,可能会增加应用程序的复杂性。要保持简单,可能要在 DataSet 中为每个 DataTable 创建一个 DataAdapter。
用多个表填充 DataSet
如果使用批处理 SQL 语句检索多个表并填充 DataSet,第一个表用指定给 Fill 方法的表名命名。后面的表用指定给 Fill 方法的表名加上一个从 1 开始并且增量为 1 的数字命名。例如,如果运行下面的代码:
'Visual BasicDim 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 中。
填充完 DataSet 之后,可以很容易地把 "Customers1" 表的 TableName 属性改为 "Orders"。但是,后面的填充会导致 "Customers" 表被重新填充,而 "Orders" 表会被忽略,并创建另外一个 "Customers1" 表。为了对这种情况作出补救,创建一个 DataTableMapping,把 "Customers1" 映射到 "Orders",并为其他后面的表创建其他的表映射。例如:
'Visual BasicDim 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");
使用 DataReader
下面是一些使用 DataReader 获得最佳性能的技巧,同时还回答了一些关于使用 DataReader 的常见问题。
• | 在访问相关 Command 的任何输出参数之前,必须关闭 DataReader。 |
• | 完成读数据之后总是要关闭 DataReader。如果使用 Connection 只是用于返回 DataReader,那么关闭 DataReader 之后立刻关闭它。 另外一个显式关闭 Connection 的方法是把 CommandBehavior.CloseConnection 传递给 ExecuteReader 方法,以确保相关的连接在关闭 DataReader 时被关闭。如果从一个方法返回 DataReader,而且不能控制 DataReader 或相关连接的关闭,则这样做特别有用。 |
• | 不能在层之间远程访问 DataReader。DataReader 是为已连接好的数据访问设计的。 |
• | 当访问列数据时,使用类型化访问器,例如,GetString、GetInt32 等。这使您不用进行将 GetValue 返回的 Object 强制转换成特定类型所需的处理。 |
• | 一个单一连接每次只能打开一个 DataReader。在 ADO 中,如果打开一个单一连接,并且请求两个使用只进、只读游标的记录集,那么 ADO 会在游标生存期内隐式打开第二个、未池化的到数据存储区的连接,然后再隐式关闭该连接。对于 ADO.NET,“秘密”完成的动作很少。如果想在相同的数据存储区上同时打开两个 DataReaders,就必须显式创建两个连接,每个 DataReader 一个。这是 ADO.NET 为池化连接的使用提供更多控制的一种方法。 |
• | 默认情况下,DataReader 每次 Read 时都要把整行加载到内存。这允许在当前行内随机访问列。如果不需要这种随机访问,为了提高性能,就把 CommandBehavior.SequentialAccess 传递给 ExecuteReader 调用。这将 DataReader 的默认行为更改为仅在请求时将数据加载到内存。注意,CommandBehavior.SequentialAccess 要求顺序访问返回的列。也就是说,一旦读过返回的列,就不能再读它的值了。 |
• | 如果已经完成读取来自 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 中访问 BLOB 的详细描述,请参阅 Obtaining BLOB Values from a Database。
使用命令
ADO.NET 提供了几种命令执行的不同方法以及优化命令执行的不同选项。下面包括一些技巧,它们是关于选择最佳命令执行以及如何提高执行命令的性能。
使用 OleDbCommand 的最佳实践
不同 .NET 框架数据提供程序之间的命令执行被尽可能标准化了。但是,数据提供程序之间仍然存在差异。下面给出一些技巧,可微调用于 OLE DB 的 .NET 框架数据提供程序的命令执行。
• | 按照 ODBC CALL 语法使用 CommandType.Text 调用存储过程。使用 CommandType.StoredProcedure 只是秘密地生成 ODBC CALL 语法。 |
• | 一定要设置 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 的许多对象就会推断元数据信息。下面是一些示例:
• | DataAdapter.Fill 方法,如果 DataSet 中没有表和列,DataAdapter.Fill 方法会在 DataSet 中创建表和列。 |
• | CommandBuilder,它会为单表 SELECT 命令生成 DataAdapter 命令属性。 |
• | 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 的任何不必要处理。
有关更多信息,请参阅 Executing a Command。
测试 Null
如果表(在数据库中)中的列允许为空,就不能测试参数值是否“等于”空。相反,需要写一个 WHERE 子句,测试列和参数是否都为空。下面的 SQL 语句返回一些行,它们的 LastName 列等于赋给 @LastName 参数的值,或者 LastName 列和 @LastName 参数都为空。
SELECT * FROM CustomersWHERE ((LastName = @LastName) OR (LastName IS NULL AND @LastName IS NULL))
把 Null 作为参数值传递
对数据库的命令中,当把空值作为参数值发送时,不能使用 null(Visual Basic庐 .NET 中为 Nothing)。而需要使用 DBNull.Value。例如:
'Visual BasicDim 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;
执行事务
ADO.NET 的事务模型已经更改。在 ADO 中,当调用 StartTransaction 时,调用之后的任何更新操作都被视为是事务的一部分。但是,在 ADO.NET 中,当调用 Connection.BeginTransaction 时,会返回一个 Transaction 对象,需要把它与 Command 的 Transaction 属性联系起来。这种设计可以在一个单一连接上执行多个根事务。如果未将 Command.Transaction 属性设置为一个针对相关的 Connection 而启动的 Transaction,那么 Command 就会失败并引发异常。
即将发布的 .NET 框架将使您可以在现有的分布式事务中手动登记。这对于对象池方案来说很理想;在该方案中,一个池对象打开一次连接,但是在多个独立的事务中都涉及到该对象。.NET 框架 1.0 发行版中这一功能并不可用。
有关事务的更多信息,请参阅 Performing Transactions 以及 .NET Data Access Architecture Guide。
使用连接
高性能应用程序与使用中的数据源保持最短时间的连接,并且利用性能增强技术,例如连接池。下面的主题提供一些技巧,有助于在使用 ADO.NET 连接到数据源时获得更好的性能。
连接池
用于 ODBC 的 SQL Server、OLE DB 和 .NET 框架数据提供程序隐式缓冲连接。通过在连接字符串中指定不同的属性值,可以控制连接池的行为。有关如何控制连接池的行为的详细信息,请参阅 Connection Pooling for the SQL Server .NET Data Provider 和 Connection Pooling for the OLE DB .NET Data Provider。
用 DataAdapter 优化连接
DataAdapter 的 Fill 和 Update 方法在连接关闭的情况下自动打开为相关命令属性指定的连接。如果 Fill 或 Update 方法打开了连接,Fill 或 Update 将在操作完成的时候关闭它。为了获得最佳性能,仅在需要时将与数据库的连接保持为打开。同时,减少打开和关闭多操作连接的次数。
如果只执行单个的 Fill 或 Update 方法调用,建议允许 Fill 或 Update 方法隐式打开和关闭连接。如果对 Fill 和/或 Update 调用有很多,建议显式打开连接,调用 Fill 和/或 Update,然后显式关闭连接。
另外,当执行事务时,显式地在开始事务之前打开连接,并在提交之后关闭连接。例如:
'Visual BasicPublic 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();}
始终关闭 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)); }}
Using 语句不能用于 Microsoft庐 Visual Basic庐 .NET。
避免访问 OleDbConnection.State 属性
如果连接已经打开,OleDbConnection.State 属性会对 DBPROP_CONNECTIONSTATUS 属性的 DATASOURCEINFO 属性集执行本地 OLE DB 调用 IDBProperties.GetProperties,这可能会导致对数据源的往返行程。也就是说,检查 State 属性的代价可能很高。所以仅在需要时检查 State 属性。如果需要经常检查该属性,监听 OleDbConnection 的 StateChange 事件可能会使应用程序的性能好一些。有关 StateChange 事件的详细信息,请参阅 Working with Connection Events。
与 XML 集成
ADO.NET 在 DataSet 中提供了广泛的 XML 集成,并公开了 SQL Server 2000 及其更高版本提供的部分 XML 功能。还可以使用 SQLXML 3.0 广泛地访问 SQL Server 2000 及其更高版本中的 XML 功能。下面是使用 XML 和 ADO.NET 的技巧和信息。
DataSet 和 XML
DataSet 与 XML 紧密集成,并提供如下功能:
• | 从 XSD 架构中加载 DataSet 的架构或关系型结构。 |
• | 从 XML 加载 DataSet 的内容。 |
• | 如果没有提供架构,可以从 XML 文档的内容推断出 DataSet 的架构。 |
• | 把 DataSet 的架构写成 XSD 架构。 |
• | 把 DataSet 的内容写成 XML。 |
• | 同步访问使用 DataSet 的数据的关系表示,以及使用 XmlDataDocument 的数据的层次表示。 |
注 可以使用这种同步把 XML 功能(例如,XPath 查询和 XSLT 转换)应用到 DataSet 中的数据,或者在保留原始 XML 保真度的前提下为 XML 文档中数据的全部或其中一个子集提供关系视图。
关于 DataSet 提供的 XML 功能的详细信息,请参阅 XML and the DataSet。
架构推断
从 XML 文件加载 DataSet 时,可以从 XSD 架构加载 DataSet 架构,或者在加载数据前预定义表和列。如果没有可用的 XSD 架构,而且不知道为 XML 文件的内容定义哪些表和列,就可以在 XML 文档结构的基础上对架构进行推断。
架构推断作为迁移工具很有用,但应只限于设计阶段应用程序,这是由于推断处理有如下限制。
• | 对架构的推断会引入影响应用程序性能的附加处理。 |
• | 所有推断列的类型都是字符串。 |
• | 推断处理不具有确定性。也就是说,它是基于 XML 文件内容的,而不是预定的架构。因此,对于两个预定架构相同的 XML 文件,由于它们的内容不同,结果得到两个完全不同的推断架构。 |
有关更多信息,请参阅 Inferring DataSet Relational Structure from XML。
用于 XML 查询的 SQL Server
如果正从 SQL Server 2000 FOR XML 返回查询结果,可以让用于 SQL Server 的 .NET 框架数据提供程序使用 SqlCommand.ExecuteXmlReader 方法直接创建一个 XmlReader。
SQLXML 托管类
.NET 框架中有一些类,公开用于 SQL Server 2000 的 XML 的功能。这些类可在 Microsoft.Data.SqlXml 命名空间中找到,它们添加了执行 XPath 查询和 XML 模板文件以及把 XSLT 转换应用到数据的能力。
SQLXML 托管类包含在用于 Microsoft SQL Server 2000 的 XML (SQLXML 2.0) 发行版中,可从 XML for Microsoft SQL Server 2000 Web Release 2 (SQLXML 2.0) ??μ?。
更多有用的技巧
下面是一些编写 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 代替自动增量的列。它们是唯一的,并且避免了使用自动增量的列所必需的额外工作。
有关从数据源检索自动增量的列值的示例,请参阅 Retrieving Identity or AutoNumber Values。
检查开放式并发冲突
按照设计,由于 DataSet 是与数据源断开的,所以,当多个客户端在数据源上按照开放式并发模型更新数据时,需要确保应用程序避免冲突。
在测试开放式并发冲突时有几项技术。一项技术涉及在表中包含时间戳列。另外一项技术是,验证一行中所有列的原始值是否仍然与通过在 SQL 语句中使用 WHERE 子句进行测试时在数据库中找到的值相匹配。
有关包含代码示例的该主题的详细讨论,请参阅 Optimistic Concurrency。
多线程编程
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 是否满足设计需求。
摘自【李天平】
你不必严格遵守这些原则,违背它们也不会被处以宗教刑罚。但你应当把这些原则看成警铃,若违背了其中的一条,那么警铃就会响起 ----- Arthur J.Riel
(1)所有数据都应该隐藏在所在的类的内部。
(2)类的使用者必须依赖类的共有接口,但类不能依赖它的使用者。
(3)尽量减少类的协议中的消息。
(4)实现所有类都理解的最基本公有接口[例如,拷贝操作(深拷贝和浅拷贝)、相等性判断、正确输出内容、从ASCII描述解析等等]。
(5)不要把实现细节(例如放置共用代码的私有函数)放到类的公有接口中。
如果类的两个方法有一段公共代码,那么就可以创建一个防止这些公共代码的私有函数。
(6)不要以用户无法使用或不感兴趣的东西扰乱类的公有接口。
(7)类之间应该零耦合,或者只有导出耦合关系。也即,一个类要么同另一个类毫无关系,要么只使用另一个类的公有接口中的操作。
(8)类应该只表示一个关键抽象。
包中的所有类对于同一类性质的变化应该是共同封闭的。一个变化若对一个包影响,则将对包中的所有类产生影响,而对其他的包不造成任何影响 .
(9)把相关的数据和行为集中放置。
设计者应当留意那些通过get之类操作从别的对象中获取数据的对象。这种类型的行为暗示着这条经验原则被违反了。
(10)把不相关的信息放在另一个类中(也即:互不沟通的行为)。
朝着稳定的方向进行依赖.
(11)确保你为之建模的抽象概念是类,而不只是对象扮演的角色。
(12)在水平方向上尽可能统一地分布系统功能,也即:按照设计,顶层类应当统一地共享工作。
(13)在你的系统中不要创建全能类/对象。对名字包含Driver、Manager、System、Susystem的类要特别多加小心。
规划一个接口而不是实现一个接口。
(14)对公共接口中定义了大量访问方法的类多加小心。大量访问方法意味着相关数据和行为没有集中存放。
(15)对包含太多互不沟通的行为的类多加小心。
这个问题的另一表现是在你的应用程序中的类的公有接口中创建了很多的get和set函数。
(16)在由同用户界面交互的面向对象模型构成的应用程序中,模型不应该依赖于界面,界面则应当依赖于模型。
(17)尽可能地按照现实世界建模(我们常常为了遵守系统功能分布原则、避免全能类原则以及集中放置相关数据和行为的原则而违背这条原则) 。
(18)从你的设计中去除不需要的类。
一般来说,我们会把这个类降级成一个属性。
(19)去除系统外的类。
系统外的类的特点是,抽象地看它们只往系统领域发送消息但并不接受系统领域内其他类发出的消息。
(20)不要把操作变成类。质疑任何名字是动词或者派生自动词的类,特别是只有一个有意义行为的类。考虑一下那个有意义的行为是否应当迁移到已经存在或者尚未发现的某个类中。
(21)我们在创建应用程序的分析模型时常常引入代理类。在设计阶段,我们常会发现很多代理没有用的,应当去除。
(22)尽量减少类的协作者的数量。
一个类用到的其他类的数目应当尽量少。
(23)尽量减少类和协作者之间传递的消息的数量。
(24)尽量减少类和协作者之间的协作量,也即:减少类和协作者之间传递的不同消息的数量。
(25)尽量减少类的扇出,也即:减少类定义的消息数和发送的消息数的乘积。
(26)如果类包含另一个类的对象,那么包含类应当给被包含的对象发送消息。也即:包含关系总是意味着使用关系。
(27)类中定义的大多数方法都应当在大多数时间里使用大多数数据成员。
(28)类包含的对象数目不应当超过开发者短期记忆的容量。这个数目常常是6。
当类包含多于6个数据成员时,可以把逻辑相关的数据成员划分为一组,然后用一个新的包含类去包含这一组成员。
(29)让系统功能在窄而深的继承体系中垂直分布。
(30)在实现语义约束时,最好根据类定义来实现。这常常会导致类泛滥成灾,在这种情况下,约束应当在类的行为中实现,通常是在构造函数中实现,但不是必须如此。
(31)在类的构造函数中实现语义约束时,把约束测试放在构造函数领域所允许的尽量深的包含层次中。
(32)约束所依赖的语义信息如果经常改变,那么最好放在一个集中式的第3方对象中。
(33)约束所依赖的语义信息如果很少改变,那么最好分布在约束所涉及的各个类中。
(34)类必须知道它包含什么,但是不能知道谁包含它。
(35)共享字面范围(也就是被同一个类所包含)的对象相互之间不应当有使用关系。
(36)继承只应被用来为特化层次结构建模。
(37)派生类必须知道基类,基类不应该知道关于它们的派生类的任何信息。
(38)基类中的所有数据都应当是私有的,不要使用保护数据。
类的设计者永远都不应该把类的使用者不需要的东西放在公有接口中。
(39)在理论上,继承层次体系应当深一点,越深越好。
(40)在实践中,继承层次体系的深度不应当超出一个普通人的短期记忆能力。一个广为接受的深度值是6。
(41)所有的抽象类都应当是基类。
(42)所有的基类都应当是抽象类。
(43)把数据、行为和/或接口的共性尽可能地放到继承层次体系的高端。
(44)如果两个或更多个类共享公共数据(但没有公共行为),那么应当把公共数据放在一个类中,每个共享这个数据的类都包含这个类。
(45)如果两个或更多个类有共同的数据和行为(就是方法),那么这些类的每一个都应当从一个表示了这些数据和方法的公共基类继承。
(46)如果两个或更多个类共享公共接口(指的是消息,而不是方法),那么只有他们需要被多态地使用时,他们才应当从一个公共基类继承。
(47)对对象类型的显示的分情况分析一般是错误的。在大多数这样的情况下,设计者应当使用多态。
(48)对属性值的显示的分情况分析常常是错误的。类应当解耦合成一个继承层次结构,每个属性值都被变换成一个派生类。
(49)不要通过继承关系来为类的动态语义建模。试图用静态语义关系来为动态语义建模会导致在运行时切换类型。
(50)不要把类的对象变成派生类。对任何只有一个实例的派生类都要多加小心。
(51)如果你觉得需要在运行时刻创建新的类,那么退后一步以认清你要创建的是对象。现在,把这些对象概括成一个类。
(52)在派生类中用空方法(也就是什么也不做的方法)来覆写基类中的方法应当是非法的。
(53)不要把可选包含同对继承的需要相混淆。把可选包含建模成继承会带来泛滥成灾的类。
(54)在创建继承层次时,试着创建可复用的框架,而不是可复用的组件。
(55)如果你在设计中使用了多重继承,先假设你犯了错误。如果没犯错误,你需要设法证明。
(56)只要在面向对象设计中用到了继承,问自己两个问题:(1)派生类是否是它继承的那个东西的一个特殊类型?(2)基类是不是派生类的一部分?
(57)如果你在一个面向对象设计中发现了多重继承关系,确保没有哪个基类实际上是另一个基类的派生类。
(58)在面向对象设计中如果你需要在包含关系和关联关系间作出选择,请选择包含关系。
(59)不要把全局数据或全局函数用于类的对象的薄记工作。应当使用类变量或类方法。
(60)面向对象设计者不应当让物理设计准则来破坏他们的逻辑设计。但是,在对逻辑设计作出决策的过程中我们经常用到物理设计准则。
(61)不要绕开公共接口去修改对象的状态。
摘自:《OOD 启思录》 Arthur J.Riel