Label
by Dan Fox
涉及技术:Visaul Studio .NET, ADO.NET, C#, VB
随着Visual Studio .NET(VS.NET)和.NET Framework的发布,许多企业都已经开始构建并实现.NET应用程序了。这些应用程序中有许多,如果不是大部分的话,都使用并提供关系数据库管理系统中的数据,如Microsoft SQL Server和Oracle。这就意味着,这些应用程序将依赖于ActiveX Data Objects .NET(ADO.NET),它是.NET Framework中的一组类,提供编程模式来处理数据。
在本文中,我将讲述几个ADO.NET技术,你可以把这些技术用于设计中需要作出决定的两个主要方面,这两方面也是架构师和开发人员在将ADO.NET用于一个多层结构时会遇到的。实现这些技术可以使社团开发组织和独立软件开发商(ISVs)提高他们代码的可重用性和可移植性,同时降低开发成本。
我将讲述的技术假定你们企业运用了一个基本的应用程序结构,其中显示服务是从商业和数据存取服务中提取的(见图1)。
图1. 总览
你将注意到,在这个合理的结构中,数据服务层包含了类(以C#、Visual Basic或任何.NET语言编写的),它们提供了方法和属性来处理存储在一个或多个数据库中的数据。我把这些类称作数据存取类(data-access classes)。将数据存取逻辑提取到一组独立的服务(或层)中就提高了代码本身和它访问的数据的可重用性、可维护性和安全性。这是因为数据存取类与应用程序的其它部分是松散藕合的,所以可以更容易地从应用程序中提取出来,重新运用。也可以对它们进行修改,而不会影响应用程序的其它部分,可以通过.NET Framework 中的基于角色的安全性(RBS: role-based security)独立地保护它们的安全。
在建立了这种结构后,在实现数据服务层时,你只需要对有关设计的两个方面作出决定:如何提供数据服务的外部接口,以及如何实现其内部接口。
完全呈现你的接口
当商业或服务层的代码处理数据时,如传递数据,或从数据存取类返回它,代码必须以某种形式表现数据。当开发人员运用ADO.NET时,他们可以选用三个主要的方法:DataSet对象、data readers或自定义的类(见图2)。
图2. 研究外部接口
第一个选项,也许是最显而易见的一个,就是运用DataSet对象来处理数据。一个DataSet对象是断开的、存储在缓存中、由ADO.NET实现的数据。DataSet可以包含多个相关的表,很像一个关系数据库,用XML紧密结合在一起。实际上,开发人员可以用XML Schema Definition(XSD)创建一个DataSet对象的schema。这就是说,DataSet可以通过.NET Framework串行到XML,在你的应用程序的各个层之间传递,即使当各个层是物理分布的也是如此(如当商业和数据服务层放置在与显示服务分离的一个服务器集群时)。DataSet对象对于通过XML Web services返回数据也是很理想的,因为当它们被串行到XML时,它们是包含在一个SOAP响应消息中的。DataSet也可以跟踪对它所做的更改,然后运用一个ADO.NET数据适配器(data adapter)对象使这些变化与数据库同步(关于一个典型的编程模式,参见列表1)。
运用一个DataSet
列表1.
这些方法为在数据存取类中运用DataSet对象实现了一个典型的模式(C#)。
public virtual DataSet ReturnSomeData(int id)
{
// Validate the parameter passed in
// Create the connection object
// Create the data adapter (da)
// Associate the commands with the data
// adapter
DataSet ds = new DataSet();
try
{
// Call the Fill method
da.Fill(ds);
return ds;
}
catch (Exception e)
{
// An exception occurred
// Wrap and throw a specific exception
}
}
public virtual DataSet SaveMyData(DataSet ds)
{
//Make sure the DataSet has some changed data
if (ds == null || ds.HasChanges() == false)
{
// Can simply return null
return null;
}
// Create the connection object
// Create the data adapter (da)
// Create the data adapter commands and
// configure
// their parameters making sure to map their
// SourceColumns and SourceVersions
// Associate the commands with the data
// adapter
// Set the ContineUpdateonError property
// to true
try
{
// Call the Update method
da.Update(ds);
// Check for errors and return error rows
if (ds.HasErrors)
{
DataSet dsErrors;
dsErrors =
ds.GetChanges(DataRowState.Modified ||
DataRowState.Deleted);
return dsErrors;
}
return null;
}
catch (Exception e)
{
// An exception occurred
// Wrap and throw a specific exception
}
}
DataSet对象编程模式的简单性使它很适合离线处理数据;例如,一个销售流程自动化应用程序需要下载、持续数据,然后使数据与一个服务器同步,也许运用XML Web services。在一个基于Web的应用程序中使用DataSet对象的唯一的缺点就是当它们被缓存时,通常使用中间层服务器上的资源。这会降低应用程序的可扩展性。
用来返回数据的第二个方法是data readers。Data reader对象在ADO.NET中实现了IdataReader接口,给数据提供了只向前的、只读的、指针类型的存取。通过在返回一个data reader的数据存取类中创建方法,开发人员就可以使方法的调用者在data reader中循环,存储它以便今后使用,或者只是将它输出到用户接口。不同于DataSet对象,data readers是连接的对象,所以当应用程序访问data reader时,一个数据库连接必须是激活的。一旦data reader被完全访问后,就可以将它关闭,释放连接,例如,可以将它放回一个连接池。虽然data readers不能像DataSet一样被更新,但它们是轻量级的,因为它们不消耗中间层服务器上的资源。这就使它们很适合为基于Web的应用程序构建用户接口。然而,data readers不能被串行,所以你不能在进程或机器间传递它们。因为它们位于创建它们的进程中,远程访问它们就需要往返。因此,当你知道创建data reader的代码——即数据存取类——与运用data reader的代码在同一个进程中运行时,data reader是最有效的。
第三种方法,也是最后一种方法,就是运用自定义的类。在这种方法中,开发人员创建包含属性的类来代表应用程序运用的实体。例如,一个为在线书商工作的开发人员将创建一个Book类,提供诸如ISBN、Title、Author和UnitCost的属性。然后,数据存取类中的方法将接受Book类型的参数,在存取数据时返回Book对象。另外,你可以创建一个包含多个Book实例的派生的集合类,这样你的数据存取类就可以返回不只一个Book(见列表2)。
运用自定义的类
列表2.
该列表显示了运用自定义的.NET类来表示数据(VB)。虽然你并没有预计到,但自定义的类(如这里显示的bookCollection类)也可以像DataSet和data reader一样运用数据绑定,将数据绑定到如ASP.NET DataGrid、DataList、Repeater和DropDownList的控件。
_
Public Class Book
Private _isbn As String
Private _title As String
Private _author As String
Private _unitCode As Decimal
Public Property ISBN() As String
Get
Return _isbn
End Get
Set(ByVal value As String)
_isbn = value
End Set
End Property
Public Property Title() As String
Get
Return _title
End Get
Set(ByVal value As String)
_title = value
End Set
End Property
Public Property Author() As String
Get
Return _author
End Get
Set(ByVal value As String)
_author = value
End Set
End Property
Public Property UnitCost() As Decimal
Get
Return _unitCost
End Get
Set(ByVal value As Decimal)
_unitCost = value
End Set
End Property
End Class
Public NotInheritable Class BookCollection
Inherits ArrayList
Friend Sub New()
' So that it is not publicly creatable
End Sub
Default Public Shadows Property Item( _
ByVal isbn As String) As Book
Get
Return Me(IndexOf(isbn))
End Get
Set(ByVal value As Book)
Me(IndexOf(isbn)) = value
End Set
End Property
Public Overloads Function Contains( _
ByVal isbn As String) As Boolean
Return (-1 <> IndexOf(isbn))
End Function
Public Overloads Function IndexOf( _
ByVal isbn As String) As Integer
Dim index As Integer = 0
Dim item As Book
For Each item In Me
If item.ISBN = isbn Then
Return index
End If
index = index + 1
Next
Return -1
End Function
Public Overloads Sub RemoveAt( _
ByVal isbn As String)
RemoveAt(IndexOf(isbn))
End Sub
Public Shadows Function Add(ByVal value _
As Book) As Integer
Return MyBase.Add(value)
End Function
End Class
自定义的类可以使开发人员运用数据服务层通过一个明确的对象来操作数据,而不是通过提取一个DataSet或data reader。实际上,这种方法确保了客户端不需要引用或了解ADO.NET,因为它只操作明确的对象。另外,开发人员用Serializable属性来标记自定义的类,这样类就可以在物理分布的应用程序中的各个层之间传递了。
运用自定义的类的缺点是开发人员必须写填充类的实例的代码,并使它与数据库同步。要实现这一点,首先要用data readers来填充一个自定义类的属性,然后传递参数到一个ADO.NET的command对象来修改数据。
用于你的数据服务层的最好的外部接口取决于应用程序的功能需求、部署模式和你希望编程模式对客户端的明确程度。例如,在一个物理分布的结构中,如一个带有远程客户端的应用程序,我建议你用DataSet对象,因为它们可以是断开的、持续的、并可以在稍后的时间同一个数据库同步。然而,当数据存取代码同显示服务运行在同一个服务器上时,data readers是最理想的,因为它们对用来在ASP.NET中构建Web页面的只读数据提供快速的访问。在这两种情况下,自定义的对象都为运用数据服务层的开发人员提供了一个自然的、明确的编程模式。
查看内部情况
一旦你决定了如何在外部显示数据后,你就可以关注一下如何在内部运用用来访问你的数据存取类中数据的ADO.NET对象了。你可以采用三个基本的方法:直接的方法、provider factory方法和data factory方法(见图3)。
图3. 构建内部接口
如果你不熟悉ADO.NET的结构,你可能不明白你为什么需要做这个决定。在ADO.NET中,你可以创建称为.NET Data Providers的类,它们对数据库提供访问。这些providers可以很小,只允许访问单个的数据库,或者也可以很大,允许访问多个数据库。.NET Framework的RTM(请阅读使用手册)版本有一个小的provider来访问SQL Server 7.0和2000,还有一个大的provider来访问任何与VS.NET兼容的OLE DB数据库。运用这种结构,小的providers通过与一个数据库直接通讯就提高了性能;它们不用通过ODB和OLE DB就可以程序化地访问数据库的特殊性能。然而,当架构师和开发人员需要选择在代码中运用哪种Data Provider(以及哪组对象)时,他们就会负担过重。
运用直接的方法就意味着,你可以根据由一个特殊的.NET Data Provider提供的具体的类来编程。换句话说,数据存取类的方法实例化SqlConnection、SqlCommand和SqlDataAdapter对象来运用SQL Server,并实例化OleDbConnection、OleDbCommand和OleDbDataAdapter对象来运用一个OLE DB provider。这种方法很简单,但它锁定数据存取类来运用一个特殊的.NET Data Provider。虽然它对只运用OLE DB providers的应用程序很适合,但当应用程序需要运用SQL Server和Oracle时(如当独立软件开发商编写软件包时),这种方法对于应用程序的性能和功能并不是最佳的。为了解决这个问题,你可以用provider factory或者data factory方法。
运用provider factory方法依赖于一个称为Abstract Factory Pattern的公共设计模式。你的数据存取类可以运用这个模式来推迟特殊的provider对象到一个factory类的形成,如connection、command和data adapter。Factory类提供了方法,根据传递到类的一个实例来创建适当的类型(关于一个简单的provider factory,见列表3)。
运用一个 Provider Factory
列表3.
这个类可以用来动态地创建特殊的.NET Data Provider对象(VB)。你会注意到,provider factory类为每个特殊的.NET Data Provider对象提供了“Create”方法。例如,为了创建一个SqlConnection对象的一个实例,数据存取方法只需要参照provider factory的一个私有的实例(_pf)并调用它的CreateConnection方法(C#)。在这个例子中,参照connection对象的变量被声明为IdbConnection,它是所有connection对象继承的接口(在VB中实现)。
Public Class ProviderFactory
Sub New(ByVal provider As ProviderType)
_pType = provider
_initClass()
End Sub
Sub New()
_initClass()
End Sub
Private _pType As ProviderType = _
ProviderType.SqlClient
Private _pTypeSet As Boolean = False
Private _conType(1), _comType(1), _
_parmType(1), _daType(1) As Type
Private Sub _initClass()
_conType(ProviderType.SqlClient) = _
GetType(SqlConnection)
_conType(ProviderType.OLEDB) = _
GetType(OleDbConnection)
_comType(ProviderType.SqlClient) = _
GetType(SqlCommand)
_comType(ProviderType.OLEDB) = _
GetType(OleDbCommand)
_parmType(ProviderType.SqlClient) = _
GetType(SqlParameter)
_parmType(ProviderType.OLEDB) = _
GetType(OleDbParameter)
_daType(ProviderType.SqlClient) = _
GetType(SqlDataAdapter)
_daType(ProviderType.OLEDB) = _
GetType(OleDbDataAdapter)
End Sub
Public Property Provider() As ProviderType
Get
Return _pType
End Get
Set(ByVal value As ProviderType)
If _pTypeSet Then
Throw New ReadOnlyException( _
"Provider already set to " & _
_pType.ToString)
Else
_pType = value
_pTypeSet = True
End If
End Set
End Property
Public Function CreateDataAdapter( _
ByVal commandText As String, _
ByVal connection As IDbConnection) _
As IDataAdapter
Dim d As IDataAdapter
Dim da As IDbDataAdapter
d = CType(Activator.CreateInstance( _
_daType(_pType), False), IDataAdapter)
da = CType(d, IDbDataAdapter)
da.SelectCommand = _
Me.CreateCommand(commandText, _
connection)
Return d
End Function
Public Overloads Function CreateParameter( _
ByVal paramName As String, _
ByVal paramType As DbType) As _
IDataParameter
Dim p As IDataParameter
p = CType(Activator.CreateInstance( _
_parmType(_pType), False), _
IDataParameter)
p.ParameterName = paramName
p.DbType = paramType
Return p
End Function
Public Function CreateConnection( _
ByVal connect As String) As IDbConnection
Dim c As IDbConnection
c = CType(Activator.CreateInstance( _
_conType(_pType), False), _
IDbConnection)
c.ConnectionString = connect
Return c
End Function
Public Function CreateCommand( _
ByVal cmdText As String, _
ByVal connection As IDbConnection) _
As IDbCommand
Dim c As IDbCommand
c = CType(Activator.CreateInstance( _
_comType(_pType), False), IDbCommand)
c.CommandText = cmdText
c.Connection = connection
Return c
End Function
End Class
然后,provider factory的方法在运行时创建适当的对象,并运用一个基于接口的和多态的方法通过ADO.NET中的公共接口返回一个参照。当开发人员运用一个provider factory,并根据各种对象的接口编程时,他们就可以编写仍然灵活的数据存取类。
运用data factory是在你的数据存取类中内部访问ADO.NET的最后的一种方法。在运用这种方法时,你提取了用来执行公共任务的provider和代码——如实例化connection对象、填充参数对象、以及执行command对象。一个data factory类提供方法,如GetDataSet、ExecuteDataReader和SyncDataSet,通过将开发人员在每个方法和类中编写的公共代码提取到一个单独的经过了良好测试的单元,使数据存取类中的代码编写起来更简单(见列表4,其中显示了一个先用直接的方法编写的方法,然后运用一个data factory重新编码)。
运用一个 Data Factory
列表4.
该代码显示了运用直接的方法与运用一个data factory的不同(VB)。它显示了,运用直接的方法来调用存储过程usp_GetTitles。然而,在运用data factory的方法中,语句GetTitles被传递到GetDataSet方法中。然后,data factory可以从一个XML文件提取特殊的数据库语法(根据所运用的.NET Data Provider),并用它来执行命令。结果就是,通过改变一组配置文件,数据库后端就可以很容易地被改变。
Public Function GetTitles(ByVal author As String, _
ByVal title As String) As DataSet
Dim cn As New SqlConnection(_connect)
Dim daSql As SqlDataAdapter
Dim parmSql As SqlParameter
Dim ds As New DataSet()
Try
daSql = New SqlDataAdapter("usp_GetTitle", cn)
daSql.SelectCommand.CommandType = _
CommandType.StoredProcedure
parmSql = _
daSql.SelectCommand.Parameters.Add( _
New SqlParameter("@author", _
SqlDbType.varchar, 50))
parmSql.value = author
parmSql = _
daSql.SelectCommand.Parameters.Add( _
New SqlParameter("@title", _
SqlDbType.varchar, 200))
parmSql.value = title
daSql.Fill(ds)
Return ds
Catch e As SqlException
' Handle Error
End Try
End Function
Public Function GetTitles(ByVal author As String, _
ByVal title As String) As DataSet
Dim parms As New HybridDictionary()
Dim ds As New DataSet()
Try
parms.Add("author", author)
parms.add("title", title)
ds = _df.GetDataSet("GetTitles",parms)
Return ds
Catch e As DataFactoryException
' Handle Error
End Try
End Function
Data factory使一个方法中的代码量减少了35%到40%,这就使开发人员的开发效率更高了,使代码更容易维护。
除了减少了代码的复杂性以外,data factory类还有另外的好处,如缓存完全填充的command对象和提取特殊的数据库语法。例如,因为data factory类的方法通常创建根据一个数据库执行的command对象,所以data factory可以把这些command对象缓存到一个Hashtable中,然后当第二次需要这个command时,就可以重用它们。首先,这么做因为消除了需要用来重建命令和参数的费用,因而提高了性能。第二,因为data factory给数据存取类提供了一个provider和与数据库无关的接口,所以它可以提取用来执行一个特殊命令的实际的SQL语法(例如,通过将它放在一个特殊的数据库XML文件中)。
同外部接口一样,你用来内部查看数据存取类的方法取决于provider和数据库的独立性,以及你需要的功能。例如,需要编写一个独立的数据库应用程序的一个独立软件开发商(ISV)可以用data factory方法来开发一个单独的代码库,而一个需要在一个SQL Server上开发一个简单的、短期的报告解决方案的ISV就可以用直接的方法。
Microsoft知道架构师和开发人员正面临我在本文中提出的这些问题。作为回应,它预展了.NET Framework下个版本的部分技术。简单地说,该技术使数据以某种方式自动地“客户化”,使其独立于provider,并以自定义的类所运用的同样的方式通过强类型的对象来访问它们。另外,你可能会看到内置的对象,它们有助于创建独立的provider对象,类似于provider factory所做的那样。例如,在将来,ADO.NET可能包含一个ConnectionFactory类,以与provider factory方法同样的方式来创建特殊的provider connection对象。然而,如果你现在就开始运用我所讲述的技术,那么你今天就可以在.NET中设计并实现灵活的、可维护的数据存取应用程序了。
关于作者:
Dan Fox是Quilogy的一位技术主管。他著有Building Distributed Applications with Visual Basic .NET(Sams, 2002)、Pure Visual Basic(Sams, 1999)、以及即将出版的Teach Yourself ADO.NET in 21 Days(Sams, 2002)。他为各种杂志投稿,并在诸如Tech-Ed的大会上发言。联系方式dfox@quilogy.com。 本文作者:佚名 来源:本站原创
CIO之家 www.ciozj.com 微信公众号:imciow
免责声明:本站转载此文章旨在分享信息,不代表对其内容的完全认同。文章来源已尽可能注明,若涉及版权问题,请及时与我们联系,我们将积极配合处理。同时,我们无法对文章内容的真实性、准确性及完整性进行完全保证,对于因文章内容而产生的任何后果,本账号不承担法律责任。转载仅出于传播目的,读者应自行对内容进行核实与判断。请谨慎参考文章信息,一切责任由读者自行承担。
延伸阅读