摘要:大多数使用.NET框架组件工作的开发人员的一个核心工作是实现数据访问功能,他们建立的数据访问层(data aclearcase/" target="_blank" >ccess layer)是应用程序的精华部分。本文概述了使用Visual Studio .NET和.NET框架组件建立数据访问层需要考虑的五个想法。这些技巧包括通过使用基类(base class)利用面相对象技术和.NET框架组件基础结构,使类容易继承,在决定显示方法和外部界面前仔细地检验需求。
如果你正在建立以数据为中心(data-centric)的.NET框架组件应用程序,你最终必须建立数据访问层。也许你知道在.NET框架组件中建立自己的代码有很多好处。因为它支持实现和接口(interface)继承,你的代码更容易重复使用,特别是被使用不同的框架组件兼容(Framework-compliant)语言的开发人员使用。本文我将概述为基于.NET框架组件的应用程序建立数据访问层的五条规则。
开始前,我必须提醒你建立的任何基于本文讨论的规则的数据访问层必须与传统Windows平台上开发人员喜欢的多层或者n层应用程序兼容。在这种结构中,表现层包含Web窗体、Windows窗体、调用与数据访问层的工作相应的事务层的XML服务代码。该层由多个数据访问类(data access classe)组成。换句话说,在事务处理协调不是必要的情况下,表现层将直接调用数据访问层。这种结构是传统的模型-视列表-控制程序(Model-View-Controller,MVC)模式的变体,在多种情况下被Visual Studio .NET和它暴露的控件采用。
规则1:使用面向对象特性
最基本的面向对象事务是建立一个使用实现继承的抽象类。这个基类可以包括你的所有数据访问类通过继承能够使用的服务。如果那些服务足够了,它们就能通过在整个组织的基类分布实现重复使用。例如最简单的情况是基类能够为衍生类处理连接的建立过程,如列表1所示。
Imports System.Data.SqlClient Namespace ACME.Data Protected Sub New(ByVal connect As String) Protected ReadOnly Property Connection() As SqlConnection Public Sub Dispose() Implements IDisposable.Dispose End Class |
在列表中可以看到,对DALBase类作了MustInherit标记(C#中的抽象),以确保它在继承关系中使用。接着该类在公共构造函数中包括了一个实例化的私有SqlConnection对象,它接收连接字符串作为一个参数。当来自IDisposable接口的Dispose方法确保连接对象已经被配置了的时候,受保护的(protected)Connection属性允许衍生类访问该连接对象。
即使在下面简化的例子中你也能开始看到抽象基类的用处:
Public Class WebData : Inherits DALBase Public Function GetOrders() As DataSet da.Fill(ds) |
在这种情况下,WebData类继承自DALBase,结果就是不必担心实例化SqlConnection对象,而是通过MyBase关键字(或者C#中的基关键字)简单地把连接字符串传递给基类。WebData类的GetOrders方法能使用Me.Connection(在C#中是this.Connection)访问受保护的属性。虽然这个例子相对简单,但是你将在规则2和3中看到基类也提供了其它的服务。
当数据访问层必须在COM+环境中运行时抽象的基类很有用。在这种情况下,因为允许组件使用COM+的必要代码复杂得多,所以更好的方式是建立一个如列表2所示的服务组件(serviced component)基类。
<ConstructionEnabled(True), _ Private _connection As SqlConnection Protected Overrides Sub Construct(ByVal s As String) Protected ReadOnly Property Connection() As SqlConnection |
在这段代码中,DALServicedBase类包含的基本功能与列表1中的相同,但是加上了从System.EnterpriseServices名字空间的ServicedComponent的继承,并且包括了一些属性,指明组件支持对象构造、事务和静态跟踪。接着该基类仔细地捕捉组件服务管理器(Component Services Manager)中的构造字符串并且再次建立和暴露SqlConnection对象。我们要注意的是当一个类继承自DALServicedBase时,它也继承了属性的设置。换句话说,一个衍生类的事务选项也设置为Supported。如果衍生类想重载这种行为,它能在类的层次重新定义该属性。
此外,衍生类在适当情况下应该有利于自身重载和共享方法。使用重载的方法(一个方法有多个调用信号)在本质上有两种情况。首先,它们在一个方法需要接受多种类型的参数时使用。框架组件中的典型例子是System.Convert类的方法。例如ToString方法包含18个接受一个参数的重载方法,每个重载方法的类型不同。其次,重载的方法用于暴露参数数量不断增长的信号,而不是不同类型的必要参数。在数据访问层中这类重载变得效率很高,因为它能用于为数据检索和修改暴露交替的信号。例如GetOrders方法可以重载,这样一个信号不接受参数并返回所有订单,但是附加的信号接受参数以表明调用程序希望检索特定的顾客订单,代码如下:
Public Overloads Function GetOrders() As DataSet Public Overloads Function GetOrders(ByVal customerId As Integer) As DataSet |
这种情况下的一个好的实现技巧是抽象GetOrders方法的功能到一个能被每个重载信号调用的私有的或者受保护的方法中。
共享方法(C#中的静态方法)也能用于暴露数据访问类的所有实例能够访问的字段、属性和方法。尽管共享成员不能与使用组件服务(Component Services)的类一起使用,但是对于在数据访问类的共享构造函数中检索并被所有实例读取的只读数据是有用的。使用共享成员读/写数据时要小心,因为为了访问该共享数据,执行的多个线程可能会竞争。
规则2:坚持设计指导
随Visual Studio .NET一起发布的在线文档中有一个叫"类库开发人员的设计指导(Design Guidelines for Class Library Developers)"的主题,它覆盖了类、属性和方法的名字转换,是重载的成员、构造函数和事件的补充模式。你必须遵循名字转换的主要原因之一是.NET框架组件提供的跨语言(cross-language)继承。如果你在Visual Basic .NET中建立一个数据访问层基类,你想确保使用.NET框架组件兼容的其它语言的开发人员能继承它并容易理解它怎样工作。通过坚持我概述的指导方针,你的名字转换和构造就不会是语言特定的(language specific)。例如,你可能注意到在本文例子的代码中第一个词小写,并加上intercaps是用于方法的参数的,每个词大写是用于方法的,基类使用Base标志来标识它是一个抽象类。
可以推测.NET框架组件设计指导都是普通设计模式,像Gang of Four (Addison-Wesley, 1995)写的Design Patterns记载的一样。例如.NET框架组件使用了Observer模式的一个变体,叫做Event模式,在类中暴露事件时你必须遵循它。
规则3:利用基础结构(Infrastructure)
.NET框架组件包括一些类和构造,它们能辅助处理通常的与基础结构相关的事务,例如装置和异常处理。通过基类把这些概念与继承组合起来将非常强大。例如,你能考虑一下System.Diagnostics名字空间中暴露的跟踪功能。除了提供Trace和Debug类外,该名字空间还包括衍生自Switch和TraceListener的类。Switch类的BooleanSwitch和TraceSwitch能被配置用于打开和关闭应用程序和配置文件,在TraceSwitch中可以暴露多层次跟踪。TraceListener类的TextWriterTraceListener和EventLogTraceListener分别将Trace和Debug方法的输入定位到文本文件和事件日志。
这样作的结果是给基类添加了跟踪功能,使衍生类记录消息日志更简单。接着应用程序能使用配置文件控制是否允许跟踪。你能包括一个BooleanSwitch类型的私有变量并在构造函数中实例化它来给列表1中的DALBase添加这个功能:
Public Sub New(ByVal connect As String) |
传递给BooleanSwitch的参数包括名字和描述。接着你能添加一个受保护的属性打开和关闭开关,也能添加一个属性使用Trace对象的WriteLineIf方法格式化并写入跟踪消息:
Protected Property TracingEnabled() As Boolean Protected Sub WriteTrace(ByVal message As String) |
通过这种途径,衍生类自己并不知道开关(switch)和监听(listener)类,当数据访问类产生一个有意义的信号时能够简单地调用WriteTrace方法。
<?xml version="1.0" encoding="utf-8" ?> <configuration> <system.diagnostics> <switches> <add name="DAL" value="1" /> </switches> <trace autoflush="true" indentsize="4"> <listeners> <add name="myListener" type="System.Diagnostics.TextWriterTraceListener" initializeData="DALLog.txt" /> </listeners> </trace> </system.diagnostics> </configuration> 列表3.跟踪的配置文件 |
为了建立一个监听器并打开它,需要使用应用程序配置文件。列表3显示了一个简单的配置文件,它能够打开刚才显示的数据访问类开关,并通过myListener调用TextWriterTraceListener把输出定位到文件DALLog.txt中。当然,你能通过从TraceListener类衍生程序化地建立监听器并把该监听器直接包含在数据访问类中。
Public Class DALException : Inherits ApplicationException Public Sub New(ByVal message As String) Public Sub New(ByVal message As String, ByVal innerException As |
你从中收益的第二个基础结构是结构化异常处理(SEH)。在最基本的层次,数据访问类能够暴露它的衍生自System.ApplicationException 的Exception(异常)对象并能进一步暴露自定义成员。例如,列表4中显示的DALException对象能用于包装数据访问类中的代码产生的异常。接着基类能暴露一个受保护的方法包装该异常,组装自定义成员,并把它发回给调用程序,如下所示:
Protected Sub ThrowDALException(ByVal message As String, _ newMine.ConnectString = Me.Connection.ConnectionString |
使用这种方法,衍生类能简单地调用受保护的方法,传递进去一个特定的数据异常(典型的有SqlException或者 OleDbException),该异常被截取并添加了从属于特定数据域的消息。基类在DALException中包装该异常并把它发回到调用程序。这就允许调用程序用一个Catch语句轻易地捕捉所有来自数据访问类的异常。
作为选择之一,你可以看一看MSDN上发布的"Exception Management Application Block Overview"。该框架组件通过一系列对象结合了异常和应用程序日志记录。实际上,通过从.NET 框架组件提供的BaseApplicationException类衍生的自定义异常类能够简单地插入该框架组件。
规则4:仔细选择外部界面
在你设计数据访问类的方法时,需要考虑它们怎样接受和返回数据。对大多数开发人员来说,主要有三个选择:直接使用ADO.NET对象、使用XML、使用自定义类。
如果直接暴露ADO.NET对象,你能使用一到两个编程模型。第一个包括数据集和数据表对象,它们对不连接数据访问很有用。有很多关于数据集和与它关联的数据表的文章,但是当你必须使用从下层数据存储断开的数据时它才最有用处。换句话说,数据集能在应用程序各层之间传递,即使那些层在物理上是分布式的,当业务和数据服务层放置在同一群服务器上并且与表现服务分开时也能使用。此外,数据集对象是通过基于XML的Web服务返回数据的理想方法,因为它们是可串行化的,因此能在SOAP回应消息中返回。
这与使用实现IDataReader接口的类(例如SqlDataReader 和OleDbDataReader)访问数据不同。数据阅读器(data reader)用只向前的,只读的方式访问数据。两者之间最大的不同是数据集和数据表对象能在应用程序域之间传递,通过传递值(by value)实现,然而数据阅读器能在各处传递,但是一般通过引用(by reference)实现。在列表5中,Read和GetValues在服务器过程中执行并且它们的返回值复制到客户端。
图1.远程数据阅读器
该图显示了数据阅读器怎样存活在应用程序域中,它在那儿它被建立,并且对它的所有访问结果都在客户端和服务器应用程序域之间的循环之中。这意味着当数据访问方法在相同的应用程序域运行时,应该返回数据阅读器作为调用者。
使用数据阅读器时有两个问题需要考虑。首先,当你从数据访问类的一个方法返回数据阅读器时,你必须考虑与数据阅读器关联的连接对象的生存期。默认情况是当调用程序通过数据阅读器重复时连接仍然是忙的,不幸的是当调用程序结束后,连接仍然打开,因此它不返回到连接池(如果允许连接池)。但是,当通过传递CommandBehavior.CloseConnection 枚举给command对象的ExecuteReader方法,连接的Close方法被调用时,你能命令数据阅读器关闭它的连接。
其次,为了把表现层从特定的框架组件数据提供程序(例如SqlClient或者OleDb)中分离出来,调用代码应该使用IDataReader接口(例如SqlDataReader)而不是具体类型来引用返回值。通过这种方法,如果应用程序后端从Oracle移植到 SQL Server,或者数据访问类的一个方法的返回类型改变了,表现层也不需要更改。
如果你希望数据访问类返回XML,你可以从System.Xml名字空间中的XmlDocument和XmlReader中选择一个,它与数据集和IDataReader类似。换句话说,当数据从数据源断开时你的方法应该返回一个XmlDocument(或者XmlDataDocument),然而XmlReader可用于访问XML数据的流。
最后,你也能决定与公共属性一起返回自定义类。这些类可以使用Serialization(串行化)属性标记,这样它们就能跨越应用程序域复制。另外,如果你从方法中返回多个对象,就需要强化类型(strongly typed)的集合类。
Imports System.Xml.Serialization <Serializable()> _ Public Function CompareTo(ByVal o As Object) As Integer _ Public NotInheritable Class BookCollection : Inherits ArrayList Public Overloads Function Contains(ByVal productId As Integer) As _ Public Overloads Function IndexOf(ByVal productId As Integer) As _ For Each item In Me Public Overloads Sub RemoveAt(ByVal productId As Integer) Public Shadows Function Add(ByVal value As Book) As Integer |
上列表(列表6)包含了一个简单的Book类和与它关联的集合类的例子。你能注意到Book类用Serializable做了标记,使它跨越应用程序域能使用"by value"语法。该类实现了IComparable接口,因此当它包含在一个集合类中的时候,默认情况下它将按Title排序。BookCollection类从System.Collections名字空间的ArrayList衍生,并且为了将该集合限制到Book对象而隐藏了Item属性和ADD方法。
通过使用自定义类你完全地控制了数据的表现、开发人员的效率并且没有依赖ADO.NET的调用。但是这种途径需要更多的代码,因为.NET框架组件没有包含任何与对象相关的技术映射。在这种情况下,你应该在数据访问类中建立一个数据读取器并使用它来组合自定义类。
规则5:抽象.NET框架组件数据提供程序
最后一条规则说明了为什么和怎样抽象数据访问类内部使用的.NET框架组件数据提供程序(data provider)。先前我说过ADO.NET编程模型暴露了特定的.NET框架组件数据提供程序,包括SqlClient、OleDb和其它MSDN Online Web站点上可用的。但是这种设计的结果是提高性能,为数据提供程序暴露特定数据源功能的能力,它强迫你决定使用那种数据提供程序编码。换句话说,开发人员典型地会选择使用SqlClient或OleDb,接着在各自的名字空间直接对它们的类进行编程。
如果你想改变.NET框架组件数据提供程序,你必须重新编写数据访问方法。为了避免这种情况发生,你可以使用Abstract Factory设计模式。使用这种模式,你能建立一个简单的类,它暴露方法来建立主要的.NET框架组件数据提供程序对象(command、connection、data adapter和parameter),而那些对象基于传递给构造函数的.NET框架组件数据提供程序的信息。列表7中的代码就是这样一个简单的类。
public enum ProviderType :int {SqlClient = 0, OLEDB = 1} public class ProviderFactory { public ProviderFactory() {
// 为提供程序初始化类型 public ProviderType Provider { |
为了使用该类,数据访问类的代码必须对多个.NET框架组件数据提供程序实现的接口(包括IDbCommand、IDbConnection、IDataAdapter和IDataParameter)进行编程。例如,为了使用一个参数化存储过程的返回值来填充数据集,必须在数据访问类的某个方法中有下面的代码:
Dim _pf As New ProviderFactory(ProviderType.SqlClient) Dim db As IDbDataAdapter = CType(da, IDbDataAdapter) Dim ds As New DataSet("Books") |
典型的情况是你在类的层次声明ProviderFactory变量并在数据访问类的构造函数中实例化它。另外,它的构造函数与从配置文件中读取的提供程序一起组装,而不应该是硬代码。你可以想象,ProviderFactory是数据访问类的一个重大的补充,并且能被包括进部件,分发给其它的开发人员。
结论
在Web服务时代将建立越来越多的应用程序操作来自独立的应用程序层的数据。如果你遵循一些基本规则并形成习惯,编写数据访问代码将更快、更容易,并且更能重新使用,把你的错误保存到服务器,允许你保持数据独立。