c#

位置:IT落伍者 >> c# >> 浏览文章

设计.NET应用程序数据访问层五大原则


发布日期:2021年09月13日
 
设计.NET应用程序数据访问层五大原则

摘要大多数使用NET框架组件工作的开发人员的一个核心工作是实现数据访问功能他们建立的数据访问层(data access layer)是应用程序的精华部分本文概述了使用Visual Studio NET和NET框架组件建立数据访问层需要考虑的五个想法这些技巧包括通过使用基类(base class)利用面相对象技术和NET框架组件基础结构使类容易继承在决定显示方法和外部界面前仔细地检验需求

如果你正在建立以数据为中心(datacentric)的NET框架组件应用程序你最终必须建立数据访问层也许你知道在NET框架组件中建立自己的代码有很多好处因为它支持实现和接口(interface)继承你的代码更容易重复使用特别是被使用不同的框架组件兼容(Frameworkcompliant)语言的开发人员使用本文我将概述为基于NET框架组件的应用程序建立数据访问层的五条规则

开始前我必须提醒你建立的任何基于本文讨论的规则的数据访问层必须与传统Windows平台上开发人员喜欢的多层或者n层应用程序兼容在这种结构中表现层包含Web窗体Windows窗体调用与数据访问层的工作相应的事务层的XML服务代码该层由多个数据访问类(data access classe)组成换句话说在事务处理协调不是必要的情况下表现层将直接调用数据访问层这种结构是传统的模型视列表控制程序(ModelViewControllerMVC)模式的变体在多种情况下被Visual Studio NET和它暴露的控件采用

规则使用面向对象特性

最基本的面向对象事务是建立一个使用实现继承的抽象类这个基类可以包括你的所有数据访问类通过继承能够使用的服务如果那些服务足够了它们就能通过在整个组织的基类分布实现重复使用例如最简单的情况是基类能够为衍生类处理连接的建立过程如列表所示

Imports SystemDataSqlClient

Namespace ACMEData

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 IDisposableDispose

_connectionDispose()

End Sub

End Class

End Namespace

列表简单基类

在列表中可以看到对DALBase类作了MustInherit标记(C#中的抽象)以确保它在继承关系中使用接着该类在公共构造函数中包括了一个实例化的私有SqlConnection对象它接收连接字符串作为一个参数当来自IDisposable接口的Dispose方法确保连接对象已经被配置了的时候受保护的(protected)Connection属性允许衍生类访问该连接对象

即使在下面简化的例子中你也能开始看到抽象基类的用处

Public Class WebData : Inherits DALBase

Public Sub New()

MyBaseNew(ConfigurationSettingsAppSettings(ConnectString))

End Sub

Public Function GetOrders() As DataSet

Dim da As New SqlDataAdapter(usp_GetOrders MeConnection)

daSelectCommandCommandType = CommandTypeStoredProcedure

Dim ds As New DataSet()

daFill(ds)

Return ds

End Function

End Class

在这种情况下WebData类继承自DALBase结果就是不必担心实例化SqlConnection对象而是通过MyBase关键字(或者C#中的基关键字)简单地把连接字符串传递给基类WebData类的GetOrders方法能使用MeConnection(在C#中是thisConnection)访问受保护的属性虽然这个例子相对简单但是你将在规则中看到基类也提供了其它的服务

当数据访问层必须在COM+环境中运行时抽象的基类很有用在这种情况下因为允许组件使用COM+的必要代码复杂得多所以更好的方式是建立一个如列表所示的服务组件(serviced component)基类

Transaction(TransactionOptionSupported) _

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

列表服务组件基类

在这段代码中DALServicedBase类包含的基本功能与列表中的相同但是加上了从SystemEnterpriseServices名字空间的ServicedComponent的继承并且包括了一些属性指明组件支持对象构造事务和静态跟蹤接着该基类仔细地捕捉组件服务管理器(Component Services Manager)中的构造字符串并且再次建立和暴露SqlConnection对象我们要注意的是当一个类继承自DALServicedBase时它也继承了属性的设置换句话说一个衍生类的事务选项也设置为Supported如果衍生类想重载这种行为它能在类的层次重新定义该属性

此外衍生类在适当情况下应该有利于自身重载和共享方法使用重载的方法(一个方法有多个调用信号)在本质上有两种情况首先它们在一个方法需要接受多种类型的参数时使用框架组件中的典型例子是SystemConvert类的方法例如ToString方法包含个接受一个参数的重载方法每个重载方法的类型不同其次重载的方法用于暴露参数数量不断增长的信号而不是不同类型的必要参数在数据访问层中这类重载变得效率很高因为它能用于为数据检索和修改暴露交替的信号例如GetOrders方法可以重载这样一个信号不接受参数并返回所有订单但是附加的信号接受参数以表明调用程序希望检索特定的顾客订单代码如下

Public Overloads Function GetOrders() As DataSet

Public Overloads Function GetOrders(ByVal customerId As Integer) As DataSet

这种情况下的一个好的实现技巧是抽象GetOrders方法的功能到一个能被每个重载信号调用的私有的或者受保护的方法中

共享方法(C#中的静态方法)也能用于暴露数据访问类的所有实例能够访问的字段属性和方法尽管共享成员不能与使用组件服务(Component Services)的类一起使用但是对于在数据访问类的共享构造函数中检索并被所有实例读取的只读数据是有用的使用共享成员读/写数据时要小心因为为了访问该共享数据执行的多个线程可能会竞争

规则坚持设计指导

随Visual Studio NET一起发布的在线文档中有一个叫类库开发人员的设计指导(Design Guidelines for Class Library Developers)的主题它覆盖了类属性和方法的名字转换是重载的成员构造函数和事件的补充模式你必须遵循名字转换的主要原因之一是NET框架组件提供的跨语言(crosslanguage)继承如果你在Visual Basic NET中建立一个数据访问层基类你想确保使用NET框架组件兼容的其它语言的开发人员能继承它并容易理解它怎样工作通过坚持我概述的指导方针你的名字转换和构造就不会是语言特定的(language specific)例如你可能注意到在本文例子的代码中第一个词小写并加上intercaps是用于方法的参数的每个词大写是用于方法的基类使用Base标志来标识它是一个抽象类

可以推测NET框架组件设计指导都是普通设计模式像Gang of Four (AddisonWesley )写的Design Patterns记载的一样例如NET框架组件使用了Observer模式的一个变体叫做Event模式在类中暴露事件时你必须遵循它

规则利用基础结构(Infrastructure)

NET框架组件包括一些类和构造它们能辅助处理通常的与基础结构相关的事务例如装置和异常处理通过基类把这些概念与继承组合起来将非常强大例如你能考虑一下SystemDiagnostics名字空间中暴露的跟蹤功能除了提供Trace和Debug类外该名字空间还包括衍生自Switch和TraceListener的类Switch类的BooleanSwitch和TraceSwitch能被配置用于打开和关闭应用程序和配置文件在TraceSwitch中可以暴露多层次跟蹤TraceListener类的TextWriterTraceListener和EventLogTraceListener分别将Trace和Debug方法的输入定位到文本文件和事件日志

这样作的结果是给基类添加了跟蹤功能使衍生类记录消息日志更简单接着应用程序能使用配置文件控制是否允许跟蹤你能包括一个BooleanSwitch类型的私有变量并在构造函数中实例化它来给列表中的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 _dalSwitchEnabled

End Get

Set(ByVal Value As Boolean)

_dalSwitchEnabled = Value

End Set

End Property

Protected Sub WriteTrace(ByVal message As String)

TraceWriteLineIf(MeTracingEnabled Now & : & message)

End Sub

通过这种途径衍生类自己并不知道开关(switch)和监听(listener)类当数据访问类产生一个有意义的信号时能够简单地调用WriteTrace方法

type=SystemDiagnosticsTextWriterTraceListener

initializeData=DALLogtxt />

列表跟蹤的配置文件

为了建立一个监听器并打开它需要使用应用程序配置文件列表显示了一个简单的配置文件它能够打开刚才显示的数据访问类开关并通过myListener调用TextWriterTraceListener把输出定位到文件DALLogtxt中当然你能通过从TraceListener类衍生程序化地建立监听器并把该监听器直接包含在数据访问类中

Public Class DALException : Inherits ApplicationException

Public Sub New()

MyBaseNew()

End Sub

Public Sub New(ByVal message As String)

MyBaseNew(message)

End Sub

Public Sub New(ByVal message As String ByVal innerException As

Exception)

MyBaseNew(message innerException)

End Sub

在这儿添加自定义成员

Public ConnectString As String

End Class

列表自定义异常类

你从中收益的第二个基础结构是结构化异常处理(SEH)在最基本的层次数据访问类能够暴露它的衍生自SystemApplicationException 的Exception(异常)对象并能进一步暴露自定义成员例如列表中显示的DALException对象能用于包装数据访问类中的代码产生的异常接着基类能暴露一个受保护的方法包装该异常组装自定义成员并把它发回给调用程序如下所示

Protected Sub ThrowDALException(ByVal message As String _

ByVal innerException As Exception)

Dim newMine As New DALException(message innerException)

newMineConnectString = MeConnectionConnectionString

MeWriteTrace(message & { & innerExceptionMessage & })

Throw newMine

End Sub

使用这种方法衍生类能简单地调用受保护的方法传递进去一个特定的数据异常(典型的有SqlException或者 OleDbException)该异常被截取并添加了从属于特定数据域的消息基类在DALException中包装该异常并把它发回到调用程序这就允许调用程序用一个Catch语句轻易地捕捉所有来自数据访问类的异常

作为选择之一你可以看一看MSDN上发布的Exception Management Application Block Overview该框架组件通过一系列对象结合了异常和应用程序日志记录实际上通过从NET 框架组件提供的BaseApplicationException类衍生的自定义异常类能够简单地插入该框架组件

规则仔细选择外部界面

在你设计数据访问类的方法时需要考虑它们怎样接受和返回数据对大多数开发人员来说主要有三个选择直接使用ADONET对象使用XML使用自定义类

如果直接暴露ADONET对象你能使用一到两个编程模型第一个包括数据集和数据表对象它们对不连接数据访问很有用有很多关于数据集和与它关联的数据表的文章但是当你必须使用从下层数据存储断开的数据时它才最有用处换句话说数据集能在应用程序各层之间传递即使那些层在物理上是分布式的当业务和数据服务层放置在同一群服务器上并且与表现服务分开时也能使用此外数据集对象是通过基于XML的Web服务返回数据的理想方法因为它们是可串行化的因此能在SOAP回应消息中返回

这与使用实现IDataReader接口的类(例如SqlDataReader 和OleDbDataReader)访问数据不同数据阅读器(data reader)用只向前的只读的方式访问数据两者之间最大的不同是数据集和数据表对象能在应用程序域之间传递通过传递值(by value)实现然而数据阅读器能在各处传递但是一般通过引用(by reference)实现在列表Read和GetValues在服务器过程中执行并且它们的返回值复制到客户端

该图显示了数据阅读器怎样存活在应用程序域中它在那儿它被建立并且对它的所有访问结果都在客户端和服务器应用程序域之间的循环之中这意味着当数据访问方法在相同的应用程序域运行时应该返回数据阅读器作为调用者

使用数据阅读器时有两个问题需要考虑首先当你从数据访问类的一个方法返回数据阅读器时你必须考虑与数据阅读器关联的连接对象的生存期默认情况是当调用程序通过数据阅读器重复时连接仍然是忙的不幸的是当调用程序结束后连接仍然打开因此它不返回到连接池(如果允许连接池)但是当通过传递CommandBehaviorCloseConnection 枚举给command对象的ExecuteReader方法连接的Close方法被调用时你能命令数据阅读器关闭它的连接

其次为了把表现层从特定的框架组件数据提供程序(例如SqlClient或者OleDb)中分离出来调用代码应该使用IDataReader接口(例如SqlDataReader)而不是具体类型来引用返回值通过这种方法如果应用程序后端从Oracle移植到 SQL Server或者数据访问类的一个方法的返回类型改变了表现层也不需要更改

如果你希望数据访问类返回XML你可以从SystemXml名字空间中的XmlDocument和XmlReader中选择一个它与数据集和IDataReader类似换句话说当数据从数据源断开时你的方法应该返回一个XmlDocument(或者XmlDataDocument)然而XmlReader可用于访问XML数据的流

最后你也能决定与公共属性一起返回自定义类这些类可以使用Serialization(串行化)属性标记这样它们就能跨越应用程序域复制另外如果你从方法中返回多个对象就需要强化类型(strongly typed)的集合类

Imports SystemXmlSerialization

_

Public Class Book : Implements IComparable

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 IComparableCompareTo

Dim b As Book = CType(o Book)

Return MeTitleCompareTo(bTitle)

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 ( <> IndexOf(productId))

End Function

Public Overloads Function IndexOf(ByVal productId As Integer) As _

Integer

Dim index As Integer =

Dim item As Book

For Each item In Me

If itemProductID = productId Then

Return index

End If

index = index +

Next

Return

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 MyBaseAdd(value)

End Function

End Class

列表使用自定义类

上列表(列表)包含了一个简单的Book类和与它关联的集合类的例子你能注意到Book类用Serializable做了标记使它跨越应用程序域能使用by value语法该类实现了IComparable接口因此当它包含在一个集合类中的时候默认情况下它将按Title排序BookCollection类从SystemCollections名字空间的ArrayList衍生并且为了将该集合限制到Book对象而隐藏了Item属性和ADD方法

通过使用自定义类你完全地控制了数据的表现开发人员的效率并且没有依赖ADONET的调用但是这种途径需要更多的代码因为NET框架组件没有包含任何与对象相关的技术映射在这种情况下你应该在数据访问类中建立一个数据读取器并使用它来组合自定义类

规则抽象NET框架组件数据提供程序

最后一条规则说明了为什么和怎样抽象数据访问类内部使用的NET框架组件数据提供程序(data provider)先前我说过ADONET编程模型暴露了特定的NET框架组件数据提供程序包括SqlClientOleDb和其它MSDN Online Web站点上可用的但是这种设计的结果是提高性能为数据提供程序暴露特定数据源功能的能力它强迫你决定使用那种数据提供程序编码换句话说开发人员典型地会选择使用SqlClient或OleDb接着在各自的名字空间直接对它们的类进行编程

如果你想改变NET框架组件数据提供程序你必须重新编写数据访问方法为了避免这种情况发生你可以使用Abstract Factory设计模式使用这种模式你能建立一个简单的类它暴露方法来建立主要的NET框架组件数据提供程序对象(commandconnectiondata adapter和parameter)而那些对象基于传递给构造函数的NET框架组件数据提供程序的信息列表中的代码就是这样一个简单的类

public enum ProviderType :int {SqlClient = OLEDB = }

public class ProviderFactory {

public ProviderFactory(ProviderType provider) {

_pType = provider;

_initClass();

}

public ProviderFactory() {

_initClass();

}

private ProviderType _pType = ProviderTypeSqlClient;

private bool _pTypeSet = false;

private Type[] _conType _comType _parmType _daType;

private void _initClass() {

_conType = new Type[];

_comType = new Type[];

_parmType = new Type[];

_daType = new Type[];

// 为提供程序初始化类型

_conType[(int)ProviderTypeSqlClient] = typeof(SqlConnection);

_conType[(int)ProviderTypeOLEDB] = typeof(OleDbConnection);

_comType[(int)ProviderTypeSqlClient] = typeof(SqlCommand);

_comType[(int)ProviderTypeOLEDB] = typeof(OleDbCommand);

_parmType[(int)ProviderTypeSqlClient] = typeof(SqlParameter);

_parmType[(int)ProviderTypeOLEDB] = typeof(OleDbParameter);

_daType[(int)ProviderTypeSqlClient] = typeof(SqlDataAdapter);

_daType[(int)ProviderTypeOLEDB] = typeof(OleDbDataAdapter);

}

public ProviderType Provider {

get {

return _pType;

}

set {

if (_pTypeSet) {

throw new ReadOnlyException(Provider already set to

+ _pTypeToString());

}

else {

_pType = value;

_pTypeSet = true;

}

}

}

public IDataAdapter CreateDataAdapter(string commandTextIDbConnection

connection) {

IDataAdapter d;

IDbDataAdapter da;

d = (IDataAdapter)ActivatorCreateInstance(_daType[(int)_pType]

false);

da = (IDbDataAdapter)d;

daSelectCommand = thisCreateCommand(commandText connection);

return d; }

public IDataParameter CreateParameter(string paramName DbType

paramType) {

IDataParameter p;

p = (IDataParameter)ActivatorCreateInstance(_parmType[(int)_pType]

false);

pParameterName = paramName;

pDbType = paramType;

return p;

}

public IDataParameter CreateParameter(string paramName DbType

paramType Object value) {

IDataParameter p;

p = (IDataParameter)ActivatorCreateInstance(_parmType[(int)_pType]

false);

pParameterName = paramName;

pDbType = paramType;

pValue = value;

return p;

}

public IDbConnection CreateConnection(string connect) {

IDbConnection c;

c = (IDbConnection)ActivatorCreateInstance(_conType[(int)_pType]

false);

cConnectionString = connect;

return c;

}

public IDbCommand CreateCommand(string cmdText IDbConnection

connection) {

IDbCommand c;

c = (IDbCommand)ActivatorCreateInstance(_comType[(int)_pType]

false);

cCommandText = cmdText;

cConnection = connection;

return c;

}

}

列表 ProviderFactory

为了使用该类数据访问类的代码必须对多个NET框架组件数据提供程序实现的接口(包括IDbCommandIDbConnectionIDataAdapter和IDataParameter)进行编程例如为了使用一个参数化存储过程的返回值来填充数据集必须在数据访问类的某个方法中有下面的代码

Dim _pf As New ProviderFactory(ProviderTypeSqlClient)

Dim cn As IDbConnection = _pfCreateConnection(_connect)

Dim da As IDataAdapter = _pfCreateDataAdapter(usp_GetBook cn)

Dim db As IDbDataAdapter = CType(da IDbDataAdapter)

dbSelectCommandCommandType = CommandTypeStoredProcedure

dbSelectCommandParametersAdd(_pfCreateParameter(@productIdDbTypeInt id))

Dim ds As New DataSet(Books)

daFill(ds)

典型的情况是你在类的层次声明ProviderFactory变量并在数据访问类的构造函数中实例化它另外它的构造函数与从配置文件中读取的提供程序一起组装而不应该是硬代码你可以想象ProviderFactory是数据访问类的一个重大的补充并且能被包括进部件分发给其它的开发人员

结论

在Web服务时代将建立越来越多的应用程序操作来自独立的应用程序层的数据如果你遵循一些基本规则并形成习惯编写数据访问代码将更快更容易并且更能重新使用把你的错误保存到服务器允许你保持数据独立

               

上一篇:C#获取WAVE文件文件头信息

下一篇:C#开发智能手机游戏推箱子软件