摘要:大多数使用.NET框架组件工作的开发人员的一个核心工作是实现数据访问功能,他们建立的数据访问层(data access 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
Public MustInherit Class DALBase : Implements IDisposable
PRivate _connection As SqlConnection
Protected Sub New(ByVal connect As String)
_connection = New SqlConnection(connect)
End Sub
Protected ReadOnly Property Connection() As SqlConnection
Get
Return _connection
End Get
End Property
Public Sub Dispose() Implements IDisposable.Dispose
_connection.Dispose()
End Sub
End Class
End Namespace
列表1.简单基类
在列表中可以看到,对DALBase类作了MustInherit标记(C#中的抽象),以确保它在继续关系中使用。接着该类在公共构造函数中包括了一个实例化的私有SqlConnection对象,它接收连接字符串作为一个参数。当来自IDisposable接口的Dispose方法确保连接对象已经被配置了的时候,受保护的(protected)Connection属性答应衍生类访问该连接对象。
即使在下面简化的例子中你也能开始看到抽象基类的用处:
Public Class WebData : Inherits DALBase
Public Sub New()
MyBase.New(ConfigurationSettings.AppSettings("ConnectString"))
End Sub
Public Function GetOrders() As DataSet
Dim da As New SqlDataAdapter("usp_GetOrders", Me.Connection)
da.SelectCommand.CommandType = CommandType.StoredProcedure
Dim ds As New DataSet()
da.Fill(ds)
Return ds
End Function
End Class
在这种情况下,WebData类继续自DALBase,结果就是不必担心实例化SqlConnection对象,而是通过MyBase要害字(或者C#中的基要害字)简单地把连接字符串传递给基类。WebData类的GetOrders方法能使用Me.Connection(在C#中是this.Connection)访问受保护的属性。虽然这个例子相对简单,但是你将在规则2和3中看到基类也提供了其它的服务。
当数据访问层必须在COM+环境中运行时抽象的基类很有用。在这种情况下,因为答应组件使用COM+的必要代码复杂得多,所以更好的方式是建立一个如列表2所示的服务组件(serviced component)基类。
<ConstrUCtionEnabled(True), _
Transaction(TransactionOption.Supported), _
EventTrackingEnabled(True)> _
Public MustInherit Class DALServicedBase : Inherits ServicedComponent
Private _connection As SqlConnection
Protected Overrides Sub Construct(ByVal s As String)
_connection = New SqlConnection(s)
End Sub
Protected ReadOnly Property Connection() As SqlConnection
Get
Return _connection
End Get
End Property
End Class
列表2.服务组件基类
在这段代码中,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)
_connection = New SqlConnection(connect)
_dalSwitch = New BooleanSwitch("DAL", "Data Access Code")
End Sub
传递给BooleanSwitch的参数包括名字和描述。接着你能添加一个受保护的属性打开和关闭开关,也能添加一个属性使用Trace对象的WriteLineIf方法格式化并写入跟踪消息:
Protected Property TracingEnabled() As Boolean
Get
Return _dalSwitch.Enabled
End Get
Set(ByVal Value As Boolean)
_dalSwitch.Enabled = Value
End Set
End Property
Protected Sub WriteTrace(ByVal message As String)
Trace.WriteLineIf(Me.TracingEnabled, Now & ": " & message)
End Sub
通过这种途径,衍生类自己并不知道开关(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()
MyBase.New()
End Sub
Public Sub New(ByVal message As String)
MyBase.New(message)
End Sub
Public Sub New(ByVal message As String, ByVal innerException As
Exception)
MyBase.New(message, innerException)
End Sub
'在这儿添加自定义成员
Public ConnectString As String
End Class
列表4.自定义异常类
你从中收益的第二个基础结构是结构化异常处理(SEH)。在最基本的层次,数据访问类能够暴露它的衍生自System.ApplicationException 的Exception(异常)对象并能进一步暴露自定义成员。例如,列表4中显示的DALException对象能用于包装数据访问类中的代码产生的异常。接着基类能暴露一个受保护的方法包装该异常,组装自定义成员,并把它发回给调用程序,如下所示:
Protected Sub ThrowDALException(ByVal message As String, _
ByVal innerException As Exception)
Dim newMine As New DALException(message, innerException)
newMine.ConnectString = Me.Connection.ConnectionString
Me.WriteTrace(message & "{" & innerException.Message & "}")
Throw newMine
End Sub
使用这种方法,衍生类能简单地调用受保护的方法,传递进去一个特定的数据异常(典型的有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 Class Book : Implements IComparable
<XmlAttributeAttribute()> Public ProductID As Integer
Public ISBN As String
Public Title As String
Public Author As String
Public UnitCost As Decimal
Public Description As String
Public PubDate As Date
Public Function CompareTo(ByVal o As Object) As Integer _
Implements IComparable.CompareTo
Dim b As Book = CType(o, Book)
Return Me.Title.CompareTo(b.Title)
End Function
End Class
Public NotInheritable Class BookCollection : Inherits ArrayList
Default Public Shadows Property Item(ByVal productId As Integer) _
As Book
Get
Return Me(IndexOf(productId))
End Get
Set(ByVal Value As Book)
Me(IndexOf(productId)) = Value
End Set
End Property
Public Overloads Function Contains(ByVal productId As Integer) As _
Boolean
Return (-1 <> IndexOf(productId))
End Function
Public Overloads Function IndexOf(ByVal productId As Integer) As _
Integer
Dim index As Integer = 0
Dim item As Book
For Each item In Me
If item.ProductID = productId Then
Return index
End If
index = index + 1
Next
Return -1
End Function
Public Overloads Sub RemoveAt(ByVal productId As Integer)
RemoveAt(IndexOf(productId))
End Sub
Public Shadows Function Add(ByVal value As Book) As Integer
Return MyBase.Add(value)
End Function
End Class
列表6.使用自定义类
上列表(列表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(ProviderType provider) {
_pType = provider;
_initClass();
}
public ProviderFactory() {
_initClass();
}
private ProviderType _pType = ProviderType.SqlClient;
private bool _pTypeSet = false;
private Type[] _conType, _comType, _parmType, _daType;
private void _initClass() {
_conType = new Type[2];
_comType = new Type[2];
_parmType = new Type[2];
_daType = new Type[2];
// 为提供程序初始化类型
_conType[(int)ProviderType.SqlClient] = typeof(SqlConnection);
_conType[(int)ProviderType.OLEDB] = typeof(OleDbConnection);
_comType[(int)ProviderType.SqlClient] = typeof(SqlCommand);
_comType[(int)ProviderType.OLEDB] = typeof(OleDbCommand);
_parmType[(int)ProviderType.SqlClient] = typeof(SqlParameter);
_parmType[(int)ProviderType.OLEDB] = typeof(OleDbParameter);
_daType[(int)ProviderType.SqlClient] = typeof(SqlDataAdapter);
_daType[(int)ProviderType.OLEDB] = typeof(OleDbDataAdapter);
}
public ProviderType Provider {
get {
return _pType;
}
set {
if (_pTypeSet) {
throw new ReadOnlyException("Provider already set to "
+ _pType.ToString());
}
else {
_pType = value;
_pTypeSet = true;
}
}
}
public IDataAdapter CreateDataAdapter(string commandText,IDbConnection
connection) {
IDataAdapter d;
IDbDataAdapter da;
d = (IDataAdapter)Activator.CreateInstance(_daType[(int)_pType],
false);
da = (IDbDataAdapter)d;
da.SelectCommand = this.CreateCommand(commandText, connection);
return d; }
public IDataParameter CreateParameter(string paramName, DBType
paramType) {
IDataParameter p;
p = (IDataParameter)Activator.CreateInstance(_parmType[(int)_pType],
false);
p.ParameterName = paramName;
p.DbType = paramType;
return p;
}
public IDataParameter CreateParameter(string paramName, DbType
paramType, Object value) {
IDataParameter p;
p = (IDataParameter)Activator.CreateInstance(_parmType[(int)_pType],
false);
p.ParameterName = paramName;
p.DbType = paramType;
p.Value = value;
return p;
}
public IDbConnection CreateConnection(string connect) {
IDbConnection c;
c = (IDbConnection)Activator.CreateInstance(_conType[(int)_pType],
false);
c.ConnectionString = connect;
return c;
}
public IDbCommand CreateCommand(string cmdText, IDbConnection
connection) {
IDbCommand c;
c = (IDbCommand)Activator.CreateInstance(_comType[(int)_pType],
false);
c.CommandText = cmdText;
c.Connection = connection;
return c;
}
}
列表7. ProviderFactory
为了使用该类,数据访问类的代码必须对多个.NET框架组件数据提供程序实现的接口(包括IDbCommand、IDbConnection、IDataAdapter和IDataParameter)进行编程。例如,为了使用一个参数化存储过程的返回值来填充数据集,必须在数据访问类的某个方法中有下面的代码:
Dim _pf As New ProviderFactory(ProviderType.SqlClient)
Dim cn As IDbConnection = _pf.CreateConnection(_connect)
Dim da As IDataAdapter = _pf.CreateDataAdapter("usp_GetBook", cn)
Dim db As IDbDataAdapter = CType(da, IDbDataAdapter)
db.SelectCommand.CommandType = CommandType.StoredProcedure
db.SelectCommand.Parameters.Add(_pf.CreateParameter("@productId",DbType.Int32, id))
Dim ds As New DataSet("Books")
da.Fill(ds)
典型的情况是你在类的层次声明ProviderFactory变量并在数据访问类的构造函数中实例化它。另外,它的构造函数与从配置文件中读取的提供程序一起组装,而不应该是硬代码。你可以想象,ProviderFactory是数据访问类的一个重大的补充,并且能被包括进部件,分发给其它的开发人员。
结论
在Web服务时代将建立越来越多的应用程序操作来自独立的应用程序层的数据。假如你遵循一些基本规则并形成习惯,编写数据访问代码将更快、更轻易,并且更能重新使用,把你的错误保存到服务器,答应你保持数据独立。