[导读]实现数据访问功能是大多数使用.NET Framework的开发人员的核心工作,并且由他们生成的数据访问层是其应用程序必不可少的组成部分。本文概述了五个建议,希望您在使用Visual Studio .NET和.NET Framework生成数据访问层时予以考虑。这些技巧包括通过使用基类来利用面向对象的技术和.NET Framework基础结构,通过遵循某些准则使类变得易于继承,以及在就表示方法和外部接口进行决策之前仔细分析自己的需要。
如果您要针对Microsoft .NET Framework来开发以数据为中心的应用程序,那么您终将需要创建数据访问层(DAL)。您可能知道在.NET Framework中生成代码带来的一些好处。因为它同时支持实现和接口继承,所以您的代码可以具有更高的可重用性,尤其是可供您的组织中那些使用与Framework兼容的其他编程语言的开发人员重用。在本文中,我将介绍为了针对基于.NET Framework的应用程序而开发DAL的五个规则。在开始之前,我要说明的是,基于本文中讨论的规则生成的任何DAL都将与Windows平台上的开发人员所喜爱的传统的多层或N层应用程序兼容。在该体系结构中,表示层由对协调数据访问层工作的业务层进行调用的Web窗体、Windows窗体或XML Web Service代码组成。该层包含多个数据访问类。另外,在不需要进行业务处理协调的情况下,表示层可能会直接对DAL进行调用。该体系结构是传统的模型-视图-控制器(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 |
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 |
<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 |
Public Overloads Function GetOrders() As DataSet Public Overloads Function GetOrders(ByVal customerId As Integer) As DataSet |
Public Sub New(ByVal connect As String) _connection = New SqlConnection(connect) _dalSwitch = New BooleanSwitch("DAL", "Data Access Code") End Sub |
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 |
<?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> |
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 ' Add custom members here Public ConnectString As String End Class |
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语句轻松地捕获来自DAL的所有异常。有关信息,请参阅MSDN上发布的 《Exception Management Application Block Overview》一文(http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnbda/html/emab-rm.asp)。该框架通过一组对象将异常的发布与应用程序日志耦合在一起。实际上,您可以通过从.NET Framework提供的BaseApplicationException类继承自己的自定义异常类,来将它们插入到该框架中。
规则4:仔细选择外部接口
当您设计数据访问类的方法时,需要考虑它们将如何接收和返回数据。对于大多数开发人员而言,有三种主要选择:直接使用ADO.NET对象、使用XML和使用自定义类。如果您要直接公开ADO.NET对象,则可以利用两个编程模型中的一个。第一个模型包含DataSet和DataTable对象,它们对于断开连接的数据访问很有用。关于DataSet及其关联的DataTable,已在许多文章中进行过介绍,但是在需要处理已经与基础数据存储区断开连接的数据时,它们非常有用。换句话说,DataSet可以在应用层之间传递,即使这些层分布在不同的物理位置也可以进行传递,如在业务层和数据服务层被放在与表示服务不同的服务器群集上的情况下。
另外,对于通过基于XML的Web Service返回数据的情况来说,DataSet对象也是理想的选择,这是因为它们可以序列化,因而可以在SOAP响应消息中返回。这有别于使用实现IDataReader接口的类(例如,SqlDataReader和OleDbDataReader)访问数据。这些数据读取器用于以只进、只读方式访问数据。这两者之间的巨大差异在于:DataSet和DataTable对象可以按值在应用程序域之间(因而也可以在相同或不同计算机上的进程之间)传递,而数据读取器则可以到处传递,并且总是按引用传递。请参见图5,其中Read和GetValues是在服务器进程中执行的,并且它们的返回值被复制到客户端。
图5 远程处理数据读取器 |
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 |
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]; // Initialize the types for the providers _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; } } |
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) |