经过几天断断续续的努力,这篇文章终于翻译结束,文章主要讲了DNN的数据访问策略,对于了解系统整体上是如何工作的有一定的帮助,希望能给dnn的初学者一些有用的信息。由于翻译的匆忙+水平有限,错误或不当之处在所难免,欢迎大家讨论、指正。
原作者:
Shaun Walker – Perpetual Motion Interactive Systems Inc.
http://www.perpetualmotion.ca
目录
简介... 2
策略... 2
需求... 3
配置... 4
数据访问层 ( DAL ). 8
数据库脚本... 11
数据库对象命名... 12
应用程序块... 12
数据传输... 12
业务逻辑层 ( BLL ). 13
自定义业务对象助手 ( CBO ). 14
空处理机制... 16
实现细节... 19
缓存... 25
性能... 26
开发... 26
自定义模块... 27
改进核心模块... 29
sql命令发生器... 30
参考... 30
简介
DotNetNuke(以下简称DNN)的最终目的是创建一个门户的框架平台,这个平台可以为开发者增添模块搭建应用程序提供坚实的可靠的支持。应用程序的一个关键的功能就是数据存取。.NET Framework提供了多种数据存取的方法,从架构的角度来看从这么多方法中选出适合自己的需求的最佳的解决方案很难。本白皮书将尝试着在DNN应用程序的实现中提供最合适的数据存取策略。
策略
在很多资料中有各种关于.NET Framework数据存取方法的介绍,然而他们大多脱离了现实的实际应用。虽然大家常常讨论的是这些方法的优点和缺点,但是现在仍有很多开发者不知道如何选择自己的最佳策略。事实上每种方法都有适合它的不同的用例,理论上这是对的,这也是难以选择的原因,然而在实践中,每一个开发者都在寻找一个适合所有企业应用的存取策略。
一致的数据存取策略有许多好处,有了统一定义好的数据存取策略,开发者就无需浪费时间来为每个任务选择数据存取方法,这种模式提高了代码的维护性,在所有的应用程序范围内实现了一致性。通过数据存取组件的集中处理使得数据存取策略风险降低了,也使得代码的完整性增强了。
一致的兼容的数据存取策略的概念的确跟每个需求应用其最佳的数据存取策略相悖。为每个应用程序选择相应的最佳的数据存取策略能够获得最好的性能(假定你能够从所有的用例中筛选出最适合的方案)。可是这样可能导致团队在开发实践中难以协调的合作。
DotNetNuke抛弃了众所周知的传统的80/20原则,它把精力集中在提供一致的兼容的数据存取策略,这个策略理想的目标是把80%的精力放在应用程序用例上,剩下的20%用来考虑跟用其他的数据存取方法相比是否性能要求是必须的,同时也采用了上面所述的策略。
需求
DNN的一个重要的需求就是要提供一个能够支持多种数据存储应用程序的实现方法。
由于对外部数据存储通信的灵活性和性能的要求,我们选择放弃一般的数据存取方法而打造一个新的应用,这个新的应用主要利用了数据库本地化特征集(也就是用.NET管理提供者、所有的SQL语言、存储过程等等)。在选择特殊的数据库访问类时我们做了权衡,我们需要为我们想要支持的每个数据库平台写一个特殊的数据访问层,因此应用程序也就包含了更多的代码。数据访问层共享了大量共同的代码,每一个访问层都明确的处理了特殊数据库应用。
为了简便的应用数据库访问类我们选择了提供者模式(也就是GOF描述的工厂设计模式),这种模式是通过反射的在应用程序运行时动态的加载正确的(适合的)数据访问对象。工厂是这样实现的:先创建一个抽象类,这个类声明了一个方法,这个方法是每一个数据访问类都必须继承实现的。对每一个我们支持的数据库,我们创建了一个具体的实现类,这个类实现了抽象类或“协议”中定义的各种数据库操作的代码。为了支持在运行时动态加载具体的操作类,我们在工厂里实现了一个Instance()方法,这个方法依赖于提供者类从配置文件读取并反射过来的值来决定需要加载哪个程序集。由于反射在应用程序性能方面非常耗费资源,我们把数据库提供者类的构造器储存到缓存里。
那么为什么用抽象类而不用接口呢?这是因为接口是(不可变的)静态的,而且因此接口也不能将其自身复制(翻译)。由于接口不支持实现方法继续继承,所以这些类的模式不采用接口。为接口增加一个方法跟为基类增加一个抽象方式是等效的;任何类实现了接口它也就终止了,因为这些类不能再实现新的方法(确切的说应该是接口定义以外的方法接口无法使用)。
下面的图表展示了业务逻辑、工厂以及数据库存取类是如何相互联系的。这个解决方案的关键优势是只要数据访问类实现了DataProvider抽象类的方法,数据库访问类就能够在业务逻辑类之后编译。这就意味着在我们想要创建另一个数据库的实现方法时我们无需改变业务逻辑层(或用户界面层),创建另一个实现方法的步骤是:
1、 为新的数据库创建数据库访问类,这些类实现了DataProvider的抽象类。
2、 将这些类类编译成一个程序集。
3、 测试并配置这个新的数据访问程序集到正在运行的服务器。
4、 修改配置文件,来指定新的数据库访问类。
5、 无需对业务逻辑组件进行任何改变也无需重新编译它。
配置
Web.config文件包含了许多配置节来使DataProvider模式起作用。第一个配置节注册了这些提供者(Providers)和他们相应的配置节处理方法(ConfigurationSectionHandlers)。尽管在这个例子中我们仅仅展示了DotNetNuke配置节组中的一个,我们可以通过类似的方法配置其它的提供者(也就是抽象验证提供者等等)。但有一点是必须保证的,那就是web.config文件中必须实现这些配置节。
<configSections>
<sectionGroup name="dotnetnuke">
<section name="data" type="DotNetNuke.ProviderConfigurationHandler, DotNetNuke" />
</sectionGroup>
</configSections>
下面的配置节是为老式的数据访问方法的模块而保留的:
<appSettings>
<add key="connectionString" value="Server=localhost;Database=DotNetNuke;uid=sa;pwd=;" />
</appSettings>
如上最终实现了大量的提供者模块。<dotnetnuke>配置组中的<data>配置节名称需要有一个默认的提供者(defaultProvider)属性,这个属性依赖于下面的<providers>集合中的特定的实例。在从一个provider转变为另一个provider的时候defaultProvider被用来作为单一的转换器。如果没有指定默认的provider,这个配置集合中的第一项就被认为是默认的。
<data>配置节也包含了一个<providers>集合说明,确定了所有的<data>实现都是唯一的。每一个provider都必须至少包含name、type和providerPath属性(name不是特定的但是通常相应的类名,type指定了provider相关的强类名,providerPath指定了provider的特殊资源例如脚本在哪里)。每一个提供者同样可以有多个自定义属性。
<dotnetnuke>
<data defaultProvider="SqlDataProvider" >
<providers>
<clear/>
<add name = "SqlDataProvider"
type = "DotNetNuke.Data.SqlDataProvider, DotNetNuke.SqlDataProvider"
connectionString = "Server=localhost;Database=DotNetNuke;uid=sa;pwd=;"
providerPath = "~\database\SqlDataProvider\"
objectQualifier = "DotNetNuke"
databaseOwner = "dbo"
/>
<add name = "AccessDataProvider"
type = "DotNetNuke.Data.AccessDataProvider, DotNetNuke.AccessDataProvider"
connectionString = "PROVIDER=Microsoft.Jet.OLEDB.4.0;"
providerPath = "~\database\AccessDataProvider\"
objectQualifier = "DotNetNuke"
databaseFilename = "DotNetNuke.mdb"
/>
</providers>
</data>
</dotnetnuke>
下面是对providers集合中的节点作用的详细说明。
<providers>配置节包含了一个或多个<add>、<remove>、<clear>元素。下面的规则应用在处理这些元素的时候:
1、 声明一个空的<providers />不是错误。
2、 Providers继承了父配置中<add>声明的项。
3、 如果某项已经存在了或被继承了再用<add>重新定义那么这是错误的。
4、 <remove>一个不存在的项是错误的。
5、 如果一个项被<add>后又被<remove>了,然后再<add>这个完全相同的项是可以的(不是错误)。
6、 如果一个项<add>, <clear>,然后再<add>是可以的(不是错误的)。
7、 <clear>会清除所有在先前定义的和继承的项。例如:先用<add>声明再用<clear>清除那么项就不存在了,而在<clear>后再<add>声明的项是不会被清除的。
<add> |
|
描述 |
增加一个数据提供者(data provider)。 |
属性 |
Name——provider的友好的名称。 Type——一个实现了provider接口的类。这个值是一个程序集的完整的关联。 providerPath——查找provider的特殊资源(如脚本)的路径。 其它name/value对——也许还有一些附加的名称/值对,所有的名称/值对都是provider能够理解的(处理的)。 |
<remove> |
|
描述 |
清除一个指定的数据提供者 |
属性 |
Name——要清除的provider的友好名称。 |
<clear> |
|
描述 |
清除所有的继承的提供者。 |
\Components\Provider.vb
Provider.vb类提供了所有的实现细节,这些实现包括从web.config文件加载provider的信息以及应用<add>, <remove>, <clear>处理的规则(标准)。这是一个通用的(generic)类,它不仅仅适用于数据访问。
\Components\DataProvider.vb
DataProvider.vb是一个抽象类,这个类包含了DNN的所有的数据访问方法。它包含了一个工厂本身的实例方法(Instance()),它负责在运行时动态加载web.config中描述的合适的程序集。
' unique provider name used for caching
Private Const ProviderType As String = "data"
Private Const ProviderName As String = ""
Public Shared Function Instance() As DataProvider
' Use the cache because the reflection used later is expensive
Dim cache As System.Web.Caching.Cache = System.Web.HttpContext.Current.Cache
If cache(ProviderName & ProviderType & "provider") Is Nothing Then
' Get the name of the provider
Dim objProviderConfiguration As ProviderConfiguration = ProviderConfiguration.GetProviderConfiguration(ProviderType)
' The assembly should be in \bin or GAC, so we simply need to get an instance of the type
Try
' Get the typename of the DataProvider ( ie. DotNetNuke.Data.SqlDataProvider, DotNetNuke.SqlDataProvider )
Dim strTypeName As String = CType(objProviderConfiguration.Providers(objProviderConfiguration.DefaultProvider), Provider).Type
' Override the typename if a ProviderName is specified ( this allows the application to load a different DataProvider assembly for custom modules )
strTypeName.Replace(objProviderConfiguration.DefaultProvider, ProviderName & objProviderConfiguration.DefaultProvider)
' Use reflection to store the constructor of the class that implements DataProvider
Dim t As Type = Type.GetType(strTypeName, True)
' Insert the type into the cache
cache.Insert(ProviderName & ProviderType & "provider", t.GetConstructor(System.Type.EmptyTypes))
Catch e As Exception
End Try
End If
Return CType(CType(cache(ProviderName & ProviderType & "provider"), ConstructorInfo).Invoke(Nothing), DataProvider)
End Function
所有的数据访问方法都被定义成必须重写的(MustOverride)。也就是说所有的从词类派生的数据提供者类都必须提供这些方法的实现。这些定义了业务逻辑层和数据存取层之间联系的抽象类协议。
' links module
Public MustOverride Function GetLinks(ByVal ModuleId As Integer) As IDataReader
Public MustOverride Function GetLink(ByVal ItemID As Integer, ByVal ModuleId As Integer) As IDataReader
Public MustOverride Sub DeleteLink(ByVal ItemID As Integer)
Public MustOverride Sub AddLink(ByVal ModuleId As Integer, ByVal UserName As String, ByVal Title As String, ByVal Url As String, ByVal MobileUrl As String, ByVal ViewOrder As String, ByVal Description As String, ByVal NewWindow As Boolean)
Public MustOverride Sub UpdateLink(ByVal ItemId As Integer, ByVal UserName As String, ByVal Title As String, ByVal Url As String, ByVal MobileUrl As String, ByVal ViewOrder As String, ByVal Description As String, ByVal NewWindow As Boolean)
数据访问层( DAL )
数据访问层(DAL)必须实现数据提供者抽象类中声明的方法。然而,每一个DAL提供者在实现这些方法时也许很不相同。这种处理允许提供者灵活的选择他们自己的数据库访问协议(也就是说:.NET管理的OleDB, ODBC等)。同样也允许提供者处理数据库平台之间的所有不同之处(例如:存储过程,sql语言语法,@@IDENTITY)。
每一个数据提供者必须为其在web.config中的自定义属性指定一个实现方法。
Imports System
Imports System.Data
Imports System.Data.SqlClient
Imports Microsoft.ApplicationBlocks.Data
Imports System.IO
Imports System.Web
Imports DotNetNuke
Namespace DotNetNuke.Data
Public Class SqlDataProvider
Inherits DataProvider
Private Const ProviderType As String = "data"
Private _providerConfiguration As ProviderConfiguration = ProviderConfiguration.GetProviderConfiguration(ProviderType)
Private _connectionString As String
Private _providerPath As String
Private _objectQualifier As String
Private _databaseOwner As String
Public Sub New()
' Read the configuration specific information for this provider
Dim objProvider As Provider = CType(_providerConfiguration.Providers(_providerConfiguration.DefaultProvider), Provider)
' Read the attributes for this provider
_connectionString = objProvider.Attributes("connectionString")
_providerPath = objProvider.Attributes("providerPath")
_objectQualifier = objProvider.Attributes("objectQualifier")
If _objectQualifier <> "" And _objectQualifier.EndsWith("_") = False Then
_objectQualifier += "_"
End If
_databaseOwner = objProvider.Attributes("databaseOwner")
If _databaseOwner <> "" And _databaseOwner.EndsWith(".") = False Then
_databaseOwner += "."
End If
End Sub
Public ReadOnly Property ConnectionString() As String
Get
Return _connectionString
End Get
End Property
Public ReadOnly Property ProviderPath() As String
Get
Return _providerPath
End Get
End Property
Public ReadOnly Property ObjectQualifier() As String
Get
Return _objectQualifier
End Get
End Property
Public ReadOnly Property DatabaseOwner() As String
Get
Return _databaseOwner
End Get
End Property
数据访问方法必须涉及成简单的查询(例如 单一的select,insert,update,delete),以便于在所有的数据库平台上他们都能被实现。业务逻辑(例如条件分支,计算或局部变量)应该在业务逻辑层实现,这样才能从数据库抽象出来并集中到一个应用程序(模块)里处理。如果你经常用那些使你可以在数据库层实现程序逻辑的富sql语言变量工作的话,这种数据库访问是相当简单的。
DNN中的Sql server/msde 数据提供者用了存储过程作为最好的数据访问技术。
' links module
Public Overrides Function GetLinks(ByVal ModuleId As Integer) As IDataReader
Return CType(SqlHelper.ExecuteReader(ConnectionString, DatabaseOwner & ObjectQualifier & "GetLinks", ModuleId), IDataReader)
End Function
Public Overrides Function GetLink(ByVal ItemId As Integer, ByVal ModuleId As Integer) As IDataReader
Return CType(SqlHelper.ExecuteReader(ConnectionString, DatabaseOwner & ObjectQualifier & "GetLink", ItemId, ModuleId), IDataReader)
End Function
Public Overrides Sub DeleteLink(ByVal ItemId As Integer)
SqlHelper.ExecuteNonQuery(ConnectionString, DatabaseOwner & ObjectQualifier & "DeleteLink", ItemId)
End Sub
Public Overrides Sub AddLink(ByVal ModuleId As Integer, ByVal UserName As String, ByVal Title As String, ByVal Url As String, ByVal MobileUrl As String, ByVal ViewOrder As String, ByVal Description As String, ByVal NewWindow As Boolean)
SqlHelper.ExecuteNonQuery(ConnectionString, DatabaseOwner & ObjectQualifier & "AddLink", ModuleId, UserName, Title, Url, MobileUrl, IIf(ViewOrder <> "", ViewOrder, DBNull.Value), Description, NewWindow)
End Sub
Public Overrides Sub UpdateLink(ByVal ItemId As Integer, ByVal UserName As String, ByVal Title As String, ByVal Url As String, ByVal MobileUrl As String, ByVal ViewOrder As String, ByVal Description As String, ByVal NewWindow As Boolean)
SqlHelper.ExecuteNonQuery(ConnectionString, DatabaseOwner & ObjectQualifier & "UpdateLink", ItemId, UserName, Title, Url, MobileUrl, IIf(ViewOrder <> "", ViewOrder, DBNull.Value), Description, NewWindow)
End Sub
DNN用到存储过程(存储过程查询)但是没有用参数自动查找的特性( CommandBuilder.DeriveParameters ),因此参数必须明确的定义。
' links module
Public Overrides Function GetLinks(ByVal ModuleId As Integer) As IDataReader
Return CType(OleDBHelper.ExecuteReader(ConnectionString, CommandType.StoredProcedure, ObjectQualifier & "GetLinks", _
New OleDbParameter("@ModuleId", ModuleId)), IDataReader)
End Function
Public Overrides Function GetLink(ByVal ItemId As Integer, ByVal ModuleId As Integer) As IDataReader
Return CType(OleDBHelper.ExecuteReader(ConnectionString, CommandType.StoredProcedure, ObjectQualifier & "GetLink", _
New OleDbParameter("@ItemId", ItemId), _
New OleDbParameter("@ModuleId", ModuleId)), IDataReader)
End Function
Public Overrides Sub DeleteLink(ByVal ItemId As Integer)
OleDBHelper.ExecuteNonQuery(ConnectionString, CommandType.StoredProcedure, ObjectQualifier & "DeleteLink", _
New OleDbParameter("@ItemId", ItemId))
End Sub
Public Overrides Sub AddLink(ByVal ModuleId As Integer, ByVal UserName As String, ByVal Title As String, ByVal Url As String, ByVal MobileUrl As String, ByVal ViewOrder As String, ByVal Description As String, ByVal NewWindow As Boolean)
OleDBHelper.ExecuteNonQuery(ConnectionString, CommandType.StoredProcedure, ObjectQualifier & "AddLink", _
New OleDbParameter("@ModuleId", ModuleId), _
New OleDbParameter("@UserName", UserName), _
New OleDbParameter("@Title", Title), _
New OleDbParameter("@Url", Url), _
New OleDbParameter("@MobileUrl", MobileUrl), _
New OleDbParameter("@ViewOrder", IIf(ViewOrder <> "", ViewOrder, DBNull.Value)), _
New OleDbParameter("@Description", Description), _
New OleDbParameter("@NewWindow", NewWindow))
End Sub
Public Overrides Sub UpdateLink(ByVal ItemId As Integer, ByVal UserName As String, ByVal Title As String, ByVal Url As String, ByVal MobileUrl As String, ByVal ViewOrder As String, ByVal Description As String, ByVal NewWindow As Boolean)
OleDBHelper.ExecuteNonQuery(ConnectionString, CommandType.StoredProcedure, ObjectQualifier & "UpdateLink", _
New OleDbParameter("@ItemId", ItemId), _
New OleDbParameter("@UserName", UserName), _
New OleDbParameter("@Title", Title), _
New OleDbParameter("@Url", Url), _
New OleDbParameter("@MobileUrl", MobileUrl), _
New OleDbParameter("@ViewOrder", IIf(ViewOrder <> "", ViewOrder, DBNull.Value)), _
New OleDbParameter("@Description", Description), _
New OleDbParameter("@NewWindow", NewWindow))
End Sub
数据库脚本
DNN包含了一个自动升级的特性,也就是当有了一个新的应用程序版本发布后,应用程序能够自动更新数据库。不过脚本必须根据版本号和数据提供者命名(例如02.00.00.SqlDataProvider),并且必须放在web.config文件中providerPath属性所指定的目录下。动态升级的实现需要脚本中重写provider所实现的ExecuteScript的方法。这对安全规范和对象命名很有用。
create procedure {databaseOwner}{objectQualifier}GetLinks
@ModuleId int
as
select ItemId,
CreatedByUser,
CreatedDate,
Title,
Url,
ViewOrder,
Description,
NewWindow
from {objectQualifier}Links
where ModuleId = @ModuleId
order by ViewOrder, Title
GO
数据库对象命名
web.config文件包含了一个名字叫objectQualifer的属性,它允许你为数据库对象指定一个前缀(例如DNN_)。Web主机往往只提供一个数据库服务器,因此必须跟其它web应用程序共享一个账号。如果你没有指定前缀也许会跟已经存在的其它应用程序的数据库对象名称冲突。指定前缀的另一个好处就是这些数据库对象在SQL Server企业管理器等管理工具里当按字母排序的时候他们会分组显示,这就方便了管理。
如果你升级的是DotNetNuke2.0以前版本的数据库,你需要设置objectQualifier为“”。这是因为你也许用了第三方模块,而这些模块不用新的DAL体系架构而是用特定的对象名称。升级设置objectQualifier时会将你所有的核心的数据库对象重命名,这可能会使你的自定义的模块出错。
应用程序块
微软数据访问应用程序块(MSDAAB)是一个.NET组件,它包含了最优化的数据访问代码,它能帮助你依靠SQL Server数据库调用存储过程、发布sql文本命令。我们借用这些方法作为dnn的一个建造模块,来减少创建、测试和维护大量自定义的数据访问方法的代码。我们也为基于MSDAAB代码的微软Access数据提供者创建了一个OleDB.ApplicationBlocks.Data程序集。
比起在我们的DAL实现方法里包含真正的资源代码来说,我们选择用MSDAAB作为一个黑箱组件的实现方法能够帮助我们防止修改MSDAAB代码,能够使我们完美的升级组件,这个新特征是可以实现的。
数据传输
DotNetNuke用DataReader把从数据访问层(DAL)那里读取的数据集合传递给业务逻辑层(BLL)。选用DataReader是因为它是ADO.NET提供的租块的数据传输机制(一个只向前的只读的数据流)。IDataReader是所有.NET兼容的数据读取器(DataReaders)的基本接口。这个抽象的IDataReader接口使我们能够在各层之间传输数据,而无需考虑数据访问协议在实际的数据提供者实现中可不可用(例如SqlClient, OleDB, ODBC等)。
业务逻辑层 ( BLL )
好的面向对象设计推荐我们将数据存储从应用程序中提取(抽象)出来。通过提取可以在应用程序上建立一个独立的业务逻辑接口集;因此减少了对下面的数据库物理实现的依赖。
DNN的业务逻辑层被有效的定义在\Components的子文件夹下。业务逻辑层包含了表述层调用的各种应用程序服务的抽象类。在数据访问方面,业务逻辑层向前调用应用程序接口(API)到适当的数据提供者,这个过程就文档前面介绍的数据提供者工厂机制。
自定义业务对象是一个用自定义的数据结构封装数据的面向对象的技术。自定义业务对象需要一些自定义的代码,这就在类型安全设计模型、分离数据存储和序列化方面增加了开销。自定义业务对象提供了最大程度的灵活性,他们使应用程序可以在它的抽象范围内定义自己的数据结构;也消除了对所有数据容器的依赖(例如RecordSet, DataSet )。
什么是类型安全的设计模型呢?考虑一下下面的数据访问代码例子:变量= DataReader(“字段名”)。这里将数据库字段的值赋给了一个变量,这段代码的问题在于无法保证数据库字段的数据类型和变量的数据类型相匹配;并且这个过程中的任何错误都将提交给运行时(run-time)来处理。采用自定义业务对象那么代码将是这样:变量=对象.属性(variable = Object.Property)。这样的话编译器就会在数据不匹配的时候迅速的告知我们。类型安全的设计还可以智能感知和改进代码的易读性。
一组对象我们成为一个集合。在DNN里我们用了标准的动态数组(ArrayList)来描述一组自定义业务对象。ArrayLists是ASP.NET内部对象,它包含了基本集合所需要的所有特征(add,remove,find,iterate)。在ArrayList的特征里我们最重要的特征是它实现了IEnumerable接口,它可以被数据绑定(databound)给ASP.NET web控件(web control)。
DNN的数据访问层是以DataReader的形式传递信息给业务逻辑层的。关于这种实现的一个问题就是为什么DNN用DataReader作为数据传输容器(container)来传输数据而不直接从DAL层传递数据给自定义业务对象。这是因为虽然这两种方法都是可行的,但我们相信使DAL层从BLL层完全独立出来是由一些优点的。例如:我们要为自定义业务对象增加一个附加属性,这种情况下这个属性仅仅是表述层用到而数据库中根本不需要,用DNN的方法,DAL层实现方法不需要做任何改动,因为他们对上面的BLL层没有任何依赖;但是如果DAL直接提供数据给自定义业务对象,所有的DAL层实现都需要重新编译来符合BLL层结构的需要。
自定义业务对象助手 ( CBO )
为了最小化移植从数据层传输来到自定义业务逻辑对象信息的代码工作量,创建了一个通用的utility类。这个类包含了两个公共的方法(函数)——一个是返回一个单独对象的实例,一个是返回一个集合对象(arraylist)。一般来说这个类里定义的每个属性在Datareader里都有相应的字段对应。这些影射的信息的名称和数据类型都必须是唯一的。下面的代码展示了如何用反射将datareader里的数据填充给自定义业务对象然后再关闭datareader。
Public Class CBO
Private Shared Function GetPropertyInfo(ByVal objType As Type) As ArrayList
' Use the cache because the reflection used later is expensive
Dim objCache As System.Web.Caching.Cache = System.Web.HttpContext.Current.Cache
If objCache(objType.Name) Is Nothing Then
Dim objProperties As New ArrayList()
Dim objProperty As PropertyInfo
For Each objProperty In objType.GetProperties()
objProperties.Add(objProperty)
Next
objCache.Insert(objType.Name, objProperties)
End If
Return CType(objCache(objType.Name), ArrayList)
End Function
Private Shared Function GetOrdinals(ByVal objProperties As ArrayList, ByVal dr As IDataReader) As Integer()
Dim arrOrdinals(objProperties.Count) As Integer
Dim intProperty As Integer
If Not dr Is Nothing Then
For intProperty = 0 To objProperties.Count - 1
arrOrdinals(intProperty) = -1
If CType(objProperties(intProperty), PropertyInfo).CanWrite Then
Try
arrOrdinals(intProperty) = dr.GetOrdinal(objProperties(intProperty).Name)
Catch
' property does not exist in datareader
End Try
End If
Next intProperty
End If
Return arrOrdinals
End Function
Private Shared Function CreateObject(ByVal objType As Type, ByVal dr As IDataReader, ByVal objProperties As ArrayList, ByVal arrOrdinals As Integer()) As Object
Dim objObject As Object = Activator.CreateInstance(objType)
Dim intProperty As Integer
' fill object with values from datareader
For intProperty = 0 To objProperties.Count - 1
If arrOrdinals(intProperty) <> -1 Then
If IsDBNull(dr.GetValue(arrOrdinals(intProperty))) Then
' translate Null value
objProperties(intProperty).SetValue(objObject, Null.SetNull(CType(objProperties(intProperty), PropertyInfo)), Nothing)
Else
Try
' try implicit conversion first
objProperties(intProperty).SetValue(objObject, dr.GetValue(arrOrdinals(intProperty)), Nothing)
Catch ' data types do not match
Try
' try explicit conversion
objProperties(intProperty).SetValue(objObject, Convert.ChangeType(dr.GetValue(arrOrdinals(intProperty)), CType(objProperties(intProperty), PropertyInfo).PropertyType), Nothing)
Catch
' error assigning a datareader value to a property
End Try
End Try
End If
End If
Next intProperty
Return objObject
End Function
Public Shared Function FillObject(ByVal dr As IDataReader, ByVal objType As Type) As Object
Dim objFillObject As Object
Dim intProperty As Integer
' get properties for type
Dim objProperties As ArrayList = GetPropertyInfo(objType)
' get ordinal positions in datareader
Dim arrOrdinals As Integer() = GetOrdinals(objProperties, dr)
' read datareader
If dr.Read Then
' fill business object
objFillObject = CreateObject(objType, dr, objProperties, arrOrdinals)
Else
objFillObject = Nothing
End If
' close datareader
If Not dr Is Nothing Then
dr.Close()
End If
Return objFillObject
End Function
Public Shared Function FillCollection(ByVal dr As IDataReader, ByVal objType As Type) As ArrayList
Dim objFillCollection As New ArrayList()
Dim objFillObject As Object
Dim intProperty As Integer
' get properties for type
Dim objProperties As ArrayList = GetPropertyInfo(objType)
' get ordinal positions in datareader
Dim arrOrdinals As Integer() = GetOrdinals(objProperties, dr)
' iterate datareader
While dr.Read
' fill business object
objFillObject = CreateObject(objType, dr, objProperties, arrOrdinals)
' add to collection
objFillCollection.Add(objFillObject)
End While
' close datareader
If Not dr Is Nothing Then
dr.Close()
End If
Return objFillCollection
End Function
End Class
空处理
每一个数据存取系统都有一个特殊的构造来处理那些没有明确指定的字段值。
在大多数关系数据库管理系统中,这个构造就是众所周知的null值。从应用程序的角度看,在表述层和数据存取层传递null值是一个架构上的挑战。这是因为表述层必须从数据库的特定信息抽象出来;而且,当一个属性值没有明确指定的时候表述层也必须能够表达说明。事实上这相当复杂,.NET Framework的本身的数据类型不能自动的转换从数据库返回的null值(如果你试图直接那样赋值的话将会抛出一个异常)。另外,每一个数据存储都有它自己的属性来实现null。唯一合理的解决方案就是创建一个抽象的传输服务,来编码/解码应用程序各层之间的null值。
乍一看,你也许会想到用vb.net中的“nothing”关键字可以很好的担负起这个传输服务的任务。不幸的是,调查显示,.NET Framework本身的数据类型处理“nothing”的时候没有预想的那么好。尽管分配为nothing的属性不会抛出异常,实际上这个属性的值将非常依赖于它的数据类型(String = Nothing, Date = Date.MinValue, Integer = 0, Boolean = False, 等等)并且自带的IsNothing()函数的结果还不是一致的(兼容的)结果。
在DNN里,我们创建了一个通用的类来处理null的问题,它统一管理应用程序各层的null问题。在应用程序中用一个常量来描述每种数据类型的null情况,再把这个常量转化成各种数据存储实现里的实际的null值。这个类包含的各种方法将null转换服务的物理细节从应用程序中抽象出来了。
* 记住,这个类仅仅用在数据库字段允许有null值的情况下。还要记住,这个类要求DAL和BLL层之间的数据类型一致(例如:一个BLL信息类里的属性字段的数据类型必须跟DAL 数据提供者传递过来的参数的数据类型一致)。
Public Class Null
' define application encoded null values
Public Shared ReadOnly Property NullInteger() As Integer
Get
Return -1
End Get
End Property
Public Shared ReadOnly Property NullDate() As Date
Get
Return Date.MinValue
End Get
End Property
Public Shared ReadOnly Property NullString() As String
Get
Return ""
End Get
End Property
Public Shared ReadOnly Property NullBoolean() As Boolean
Get
Return False
End Get
End Property
' sets a field to an application encoded null value ( used in Presentation layer )
Public Shared Function SetNull(ByVal objField As Object) As Object
If TypeOf objField Is Integer Then
SetNull = NullInteger
ElseIf TypeOf objField Is Date Then
SetNull = NullDate
ElseIf TypeOf objField Is String Then
SetNull = NullString
ElseIf TypeOf objField Is Boolean Then
SetNull = NullBoolean
Else
Throw New NullReferenceException()
End If
End Function
' sets a field to an application encoded null value ( used in BLL layer )
Public Shared Function SetNull(ByVal objPropertyInfo As PropertyInfo) As Object
Select Case objPropertyInfo.PropertyType.ToString
Case "System.Int16", "System.Int32", "System.Int64", "System.Single", "System.Double", "System.Decimal"
SetNull = NullInteger
Case "System.DateTime"
SetNull = NullDate
Case "System.String", "System.Char"
SetNull = NullString
Case "System.Boolean"
SetNull = NullBoolean
Case Else
Throw New NullReferenceException()
End Select
End Function
' convert an application encoded null value to a database null value ( used in DAL )
Public Shared Function GetNull(ByVal objField As Object, ByVal objDBNull As Object) As Object
GetNull = objField
If TypeOf objField Is Integer Then
If objField = NullInteger Then
GetNull = objDBNull
End If
ElseIf TypeOf objField Is Date Then
If objField = NullDate Then
GetNull = objDBNull
End If
ElseIf TypeOf objField Is String Then
If objField = NullString Then
GetNull = objDBNull
End If
ElseIf TypeOf objField Is Boolean Then
If objField = NullBoolean Then
GetNull = objDBNull
End If
Else
Throw New NullReferenceException()
End If
End Function
' checks if a field contains an application encoded null value
Public Shared Function IsNull(ByVal objField As Object) As Boolean
If objField = SetNull(objField) Then
IsNull = True
Else
IsNull = False
End If
End Function
End Class
实现细节
下面的一段代码例子示范了应用程序的各层之间是如何完成数据访问的。
表述层( UI )
表述层依赖于它上面的业务逻辑层。自定义业务逻辑对象的属性和方法建立了这两个曾之间的基础接口(表述层不要直接调用数据访问层的方法)。
获取
' create a Controller object
Dim objAnnouncements As New AnnouncementsController
' get the collection
lstAnnouncements.DataSource = objAnnouncements.GetAnnouncements(ModuleId)
lstAnnouncements.DataBind()
增加/更新
...
Private itemId As Integer
If Not (Request.Params("ItemId") Is Nothing) Then
itemId = Int32.Parse(Request.Params("ItemId"))
Else
itemId = Null.SetNull(itemId)
End If
...
' create an Info object
Dim objAnnouncement As New AnnouncementInfo
' set the properties
objAnnouncement.ItemId = itemId
objAnnouncement.ModuleId = ModuleId
objAnnouncement.CreatedByUser = Context.User.Identity.Name
objAnnouncement.Title = txtTitle.Text
objAnnouncement.Description = txtDescription.Text
objAnnouncement.Url = txtExternal.Text
objAnnouncement.Syndicate = chkSyndicate.Checked
If txtViewOrder.Text <> "" Then
objAnnouncement.ViewOrder = txtViewOrder.Text
Else
objAnnouncement.ViewOrder = Null.SetNull(objAnnouncement.ViewOrder)
End If
If txtExpires.Text <> "" Then
objAnnouncement.ExpireDate = txtExpires.Text
Else
objAnnouncement.ExpireDate = Null.SetNull(objAnnouncement.ExpireDate)
End If
' create a Controller object
Dim objAnnouncements As New AnnouncementsController
If Null.IsNull(itemId) Then
' add
objAnnouncements.AddAnnouncement(objAnnouncement)
Else
' update
objAnnouncements.UpdateAnnouncement(objAnnouncement)
End If
** Notice the use of the Null.SetNull() and Null.IsNull() helper methods
删除
' create a Controller object
Dim objAnnouncements As New AnnouncementsController
' delete the record
objAnnouncements.DeleteAnnouncement(itemId)
业务逻辑层( BLL )
每一个应用程序业务方法都有跟它相对应的多关系业务对象组成的物理文件。每一个业务对象定义都有一个定义它的属性的信息类和定义它的方法的控制类。
Public Class AnnouncementInfo
' local property declarations
Private _ItemId As Integer
Private _ModuleId As Integer
Private _UserName As String
Private _Title As String
Private _Url As String
Private _Syndicate As Boolean
Private _ExpireDate As Date
Private _Description As String
Private _ViewOrder As Integer
Private _CreatedByUser As String
Private _CreatedDate As Date
Private _Clicks As Integer
' constructor
Public Sub New()
' custom initialization logic
End Sub
' public properties
Public Property ItemId() As Integer
Get
Return _ItemId
End Get
Set(ByVal Value As Integer)
_ItemId = Value
End Set
End Property
Public Property ModuleId() As Integer
Get
Return _ModuleId
End Get
Set(ByVal Value As Integer)
_ModuleId = 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 Url() As String
Get
Return _Url
End Get
Set(ByVal Value As String)
_Url = Value
End Set
End Property
Public Property Syndicate() As Boolean
Get
Return _Syndicate
End Get
Set(ByVal Value As Boolean)
_Syndicate = Value
End Set
End Property
Public Property ViewOrder() As Integer
Get
Return _ViewOrder
End Get
Set(ByVal Value As Integer)
_ViewOrder = Value
End Set
End Property
Public Property Description() As String
Get
Return _Description
End Get
Set(ByVal Value As String)
_Description = Value
End Set
End Property
Public Property ExpireDate() As Date
Get
Return _ExpireDate
End Get
Set(ByVal Value As Date)
_ExpireDate = Value
End Set
End Property
Public Property CreatedByUser() As String
Get
Return _CreatedByUser
End Get
Set(ByVal Value As String)
_CreatedByUser = Value
End Set
End Property
Public Property CreatedDate() As Date
Get
Return _CreatedDate
End Get
Set(ByVal Value As Date)
_CreatedDate = Value
End Set
End Property
Public Property Clicks() As Integer
Get
Return _Clicks
End Get
Set(ByVal Value As Integer)
_Clicks = Value
End Set
End Property
End Class
每一个数据库的字段在信息类里都有相对应的属性。为了使通用的自定义业务对象助手(CBO Helper)类能自动的把从IDataReader接口获取的数据转换成自定义的业务对象,数据库字段和与它直接关联的属性在名称和数据类型方面都必须是唯一的。
Public Class AnnouncementsController
Public Function GetAnnouncements(ByVal ModuleId As Integer) As ArrayList
Return CBO.FillCollection(DataProvider.Instance().GetAnnouncements(ModuleId), GetType(AnnouncementInfo))
End Function
Public Function GetAnnouncement(ByVal ItemId As Integer, ByVal ModuleId As Integer) As AnnouncementInfo
Return CType(CBO.FillObject(DataProvider.Instance().GetAnnouncement(ItemId, ModuleId), GetType(AnnouncementInfo)), AnnouncementInfo)
End Function
Public Sub DeleteAnnouncement(ByVal ItemID As Integer)
DataProvider.Instance().DeleteAnnouncement(ItemID)
End Sub
Public Sub AddAnnouncement(ByVal objAnnouncement As AnnouncementInfo)
DataProvider.Instance().AddAnnouncement(objAnnouncement.ModuleId, objAnnouncement.CreatedByUser, objAnnouncement.Title, objAnnouncement.Url, objAnnouncement.Syndicate, objAnnouncement.ExpireDate, objAnnouncement.Description, objAnnouncement.ViewOrder)
End Sub
Public Sub UpdateAnnouncement(ByVal objAnnouncement As AnnouncementInfo)
DataProvider.Instance().UpdateAnnouncement(objAnnouncement.ItemId, objAnnouncement.CreatedByUser, objAnnouncement.Title, objAnnouncement.Url, objAnnouncement.Syndicate, objAnnouncement.ExpireDate, objAnnouncement.Description, objAnnouncement.ViewOrder)
End Sub
End Class
你可能注意到了传递信息到数据库的控制方法(例如:增加和更新)是传递一个自定义业务对象实例作为一个参数的。这样做的优点是:对象定义从BLL层独立出来,这样就减少了类定义改变后相应的修改。个别的对象属性被提取出来作为数量值传递给数据访问层(这是因为DAL不关心BLL对象的结构)。
数据访问层 ( DAL )
采用本文上述介绍的提供者技术DNN可以支持多种数据存储。实际上它包含一个基类,这个基类在运行时决定哪个具体的数据访问类适合当前的数据访问请求。
DataProvider ( 基类 )
' announcements module
Public MustOverride Function GetAnnouncements(ByVal ModuleId As Integer) As IDataReader
Public MustOverride Function GetAnnouncement(ByVal ItemId As Integer, ByVal ModuleId As Integer) As IDataReader
Public MustOverride Sub DeleteAnnouncement(ByVal ItemID As Integer)
Public MustOverride Sub AddAnnouncement(ByVal ModuleId As Integer, ByVal UserName As String, ByVal Title As String, ByVal URL As String, ByVal Syndicate As Boolean, ByVal ExpireDate As Date, ByVal Description As String, ByVal ViewOrder As Integer)
Public MustOverride Sub UpdateAnnouncement(ByVal ItemId As Integer, ByVal UserName As String, ByVal Title As String, ByVal URL As String, ByVal Syndicate As Boolean, ByVal ExpireDate As Date, ByVal Description As String, ByVal ViewOrder As Integer)
SqlDataProvider (具体实现类)
在具体类中包含了下面的帮助方法,这个方法用来独立数据库null的实现(这个例子中DBNull.Value 是针对SQL Server而言的)并且提供一个简单的接口。
' general
Private Function GetNull(ByVal Field As Object) As Object
Return Null.GetNull(Field, DBNull.Value)
End Function
每一个在基类里表明必须继承的方法在具体类里都必须实现。注意上面的add/update方法里描述的GetNull()函数的使用。
' announcements module
Public Overrides Function GetAnnouncements(ByVal ModuleId As Integer) As IDataReader
Return CType(SqlHelper.ExecuteReader(ConnectionString, DatabaseOwner & ObjectQualifier & "GetAnnouncements", ModuleId), IDataReader)
End Function
Public Overrides Function GetAnnouncement(ByVal ItemId As Integer, ByVal ModuleId As Integer) As IDataReader
Return CType(SqlHelper.ExecuteReader(ConnectionString, DatabaseOwner & ObjectQualifier & "GetAnnouncement", ItemId, ModuleId), IDataReader)
End Function
Public Overrides Sub DeleteAnnouncement(ByVal ItemId As Integer)
SqlHelper.ExecuteNonQuery(ConnectionString, DatabaseOwner & ObjectQualifier & "DeleteAnnouncement", ItemId)
End Sub
Public Overrides Sub AddAnnouncement(ByVal ModuleId As Integer, ByVal UserName As String, ByVal Title As String, ByVal URL As String, ByVal Syndicate As Boolean, ByVal ExpireDate As Date, ByVal Description As String, ByVal ViewOrder As Integer)
SqlHelper.ExecuteNonQuery(ConnectionString, DatabaseOwner & ObjectQualifier & "AddAnnouncement", ModuleId, UserName, Title, URL, Syndicate, GetNull(ExpireDate), Description, GetNull(ViewOrder))
End Sub
Public Overrides Sub UpdateAnnouncement(ByVal ItemId As Integer, ByVal UserName As String, ByVal Title As String, ByVal URL As String, ByVal Syndicate As Boolean, ByVal ExpireDate As Date, ByVal Description As String, ByVal ViewOrder As Integer)
SqlHelper.ExecuteNonQuery(ConnectionString, DatabaseOwner & ObjectQualifier & "UpdateAnnouncement", ItemId, UserName, Title, URL, Syndicate, GetNull(ExpireDate), Description, GetNull(ViewOrder))
End Sub
缓存
很多数据访问频繁的方法采用了web缓存技术通过减少后台数据库请求次数的方法来提高性能。System.Web.Caching.Cache命名空间提供一些往缓存中增加内容或从缓存中重新获取内容的工具。它包含一个字典接口,凭借一个字符串关键字来查找对应的对象。这个对象将持续在应用程序的整个生命周期。当应用程序重起后这个缓存才被重新创建。注意,只有序列化的对象才能被加入到缓存里。
Dim objCache As System.Web.Caching.Cache = System.Web.HttpContext.Current.Cache
If objCache("GetHostSettings") Is Nothing Then
objCache.Insert("GetHostSettings", GetHostSettings())
End If
Me.HostSettings = objCache("GetHostSettings")
System.Web.Caching.Cache对象还支持缓存管理的一些特征。为防止缓存因为大量的内容项而臃肿不堪,DNN采用一个动态调整的方法来删除那些超过60秒没有被访问的对象。这个特征主要用在有缓存设置的地方。
If objCache("GetPortalTabModules" & intTabId.ToString) Is Nothing Then
dr = DataProvider.Instance().GetPortalTabModules(Me.PortalId, Me.ActiveTab.TabId)
objCache.Insert("GetPortalTabModules" & intTabId.ToString, ConvertDataReaderToDataSet(dr), Nothing, DateTime.MaxValue, TimeSpan.FromSeconds(60))
End If
ds = objCache("GetPortalTabModules" & intTabId.ToString)
当应用程序的操作会影响缓存内容时,每一个受影响的缓存内容项都会被删除,这样新的内容项才能加进来。
If Not objCache("GetHostSettings") Is Nothing Then
objCache.Remove("GetHostSettings")
End If
性能
为了评测系统的性能我们用了微软应用程序评测中心(Microsoft Application Center Test)工具,这个工具可以模拟大量的用户打开很多服务器连接并且快速的提交http请求。为了对比,我们分析了DotNetNuke1.0.10( 采用SqlCommandGenerator )跟DotNetNuke 2.0 ( 采用新的抽象DAL )。下面是测试结果(特别感谢Kenny Rice提供测试援助)。
DotNetNuke 2.0 ( DAL enhancement )
Total number of requests: 93,254
Total number of connections: 93,253
Average requests per second: 310.85
Average time to first byte (msecs): 2.37
Average time to last byte (msecs): 2.46
Average time to last byte per iteration (msecs): 29.58
Number of unique requests made in test: 12
Number of unique response codes: 1
DotNetNuke 1.0.10 ( SqlCommandGenerator )
Total number of requests: 42,350
Total number of connections: 42,350
Average requests per second: 141.17
Average time to first byte (msecs): 6.02
Average time to last byte (msecs): 6.15
Average time to last byte per iteration (msecs): 116.94
Number of unique requests made in test: 17
Number of unique response codes: 2
开发
DNN提供了灵活的门户软件架构。这个应用程序核心提供了大量的服务作为通用的方法,例如会员,角色安全,个性化,管理,站点log,导航和数据存取(访问)。它还提供了灵活的扩展应用程序增加特殊业务功能的能力。大多数情况下建议将特殊的业务功能从框架核心抽象出来并用自定义模块实现。这保持了核心的完成性并且为将来升级提供了最好的选择。但是,如果你绝对必须修改核心实体的话,你也不会受到你的需求变化的限制。
自定义模块
DNN允许将自定义模块打包成私有的程序集发布到门户安装。只要较小的修改,自定义模块在数据存取方面可以采用相同的技术作为核心。这种方式的另一个优点是能够为每一个支持的数据库平台提供自定义模块的不同版本。
在你采用下面论述的数据访问技术之前,你需要先考虑一下你的组件实际上是否需要支持多种数据库。DNN不强制你采用提供者模式创建自定义模块。事实上,如果你清楚你的组件仅仅用在单一的数据库平台,那么不需要增加额外的开发努力。开发者作这些决定(判断)是一种基本的职责(原则)。
\PrivateAssemblies\Survey
概观自定义模块,它的架构其实跟DNN核心的架构是同样的方法。它包含了一个叫做SurveyDB.vb的业务逻辑层的类,这个类包含了业务逻辑层的方法。它还包含了它自己的SurveyDataProvider.vb的类(文件名/类名很重要,因为你不想它跟DNN里的DataProvider类冲突)。这种情况下ProviderName常量被设置成上述的类名+ DataProvider字符串值的形式。这对于工厂方法里使用通过相同的配置集中得到的基于相同的数据库的数据提供者很重要(例如:SurveySqlDataProvider 将会跟SqlDataProvider 使用相同的web.config配置集)。
Imports System
Imports System.Web.Caching
Imports System.Reflection
Namespace DotNetNuke
Public MustInherit Class SurveyDataProvider
' unique provider name used for caching
Private Const ProviderType As String = "data"
Private Const ProviderName As String = "Survey"
而且象DNN里的DataProvider类一样,它包含了必须的数据存取方法。
Public MustOverride Function GetSurveys(ByVal ModuleId As Integer) As IDataReader
Public MustOverride Function GetSurvey(ByVal SurveyID As Integer, ByVal ModuleId As Integer) As IDataReader
Public MustOverride Sub AddSurvey(ByVal ModuleId As Integer, ByVal Question As String, ByVal ViewOrder As String, ByVal OptionType As String, ByVal UserName As String)
Public MustOverride Sub UpdateSurvey(ByVal SurveyId As Integer, ByVal Question As String, ByVal ViewOrder As String, ByVal OptionType As String, ByVal UserName As String)
Public MustOverride Sub DeleteSurvey(ByVal SurveyID As Integer)
Public MustOverride Function GetSurveyOptions(ByVal SurveyId As Integer) As IDataReader
Public MustOverride Sub AddSurveyOption(ByVal SurveyId As Integer, ByVal OptionName As String, ByVal ViewOrder As String)
Public MustOverride Sub UpdateSurveyOption(ByVal SurveyOptionId As Integer, ByVal OptionName As String, ByVal ViewOrder As String)
Public MustOverride Sub DeleteSurveyOption(ByVal SurveyOptionID As Integer)
Public MustOverride Sub AddSurveyResult(ByVal SurveyOptionId As Integer)
\database
自定义模块包含数据提供者的实现,我们必须再一次在web.config文件里指定一个自定义属性定义的实现。
Imports System
Imports System.Data
Imports System.Data.SqlClient
Imports Microsoft.ApplicationBlocks.Data
Namespace DotNetNuke.Data
Public Class SurveySqlDataProvider
Inherits SurveyDataProvider
Private Const ProviderType As String = "data"
Private _providerConfiguration As ProviderConfiguration = ProviderConfiguration.GetProviderConfiguration(ProviderType)
Private _connectionString As String
Private _providerPath As String
Private _objectQualifier As String
Private _databaseOwner As String
Survey.dnn ( deployment )
为了发布私有的自定义模块,DNN用到了一个清单文件。增加多个提供者的时候这个文件的结构有些微的变化。
<module>
<folder>DesktopModules/Survey</folder>
<friendlyname>Survey</friendlyname>
<desktopsrc>Survey.ascx</desktopsrc>
<mobilesrc></mobilesrc>
<editsrc>EditSurvey.ascx</editsrc>
<description>Survey allows you to create custom surveys to obtain public feedback</description>
<editmoduleicon>icon_survey_32px.gif</editmoduleicon>
<uninstall></uninstall>
<files>
<file>
<name>Survey.ascx</name>
</file>
<file>
<name>EditSurvey.ascx</name>
</file>
<file>
<name>Survey.dll</name>
</file>
<file>
<name>DotNetNuke.SurveySqlDataProvider.dll</name>
</file>
<file>
<name>Survey01.00.00.SqlDataProvider</name>
</file>
<file>
<name>Survey Uninstall.SqlDataProvider</name>
</file>
<file>
<name>DotNetNuke.SurveyAccessDataProvider.dll</name>
</file>
<file>
<name>Survey01.00.00.AccessDataProvider</name>
</file>
<file>
<name>Survey Uninstall.AccessDataProvider</name>
</file>
</files>
</module>
改进核心模块
自定义模块是为门户架构增加附加功能的首选方法。当然,有时候修改核心功能来满足你特定的需求也是必需的。为了能修改核心模块的数据访问方法,你必须对控制管理提供者模型的面向对象原则有最基本的了解。
理论上,这个提供者模型采用一个基类派生出实例化的子类的工厂设计模式。事实上,DataProvider类就起这个基类的作用,它定义了所有应用程序的核心数据存取方法。所有的方法被定义成公共的必须继承的,也就是说它们在基类中只是实简单的定义而且都没有实现。
DataProvider类起到一个契约的作用,这个契约要求它的子类都必须完全实现,否则对象的实例化将会失败。这个意思也就是说如果一个基类要求必须被继承的方法的参数列表或返回值被修改,那么所有子类的实现也都必须作相应的修改,否则它们不会被正确的上载。不能正确的上载不仅意味着某个特殊方法的调用会失败,实际上,子类的正确实例化也将彻底失败。这个契约机制虽然给应用程序带来一定的弱点,但是它保证了每个子类实现有了最小的标准。
下面的例子示范了有关扩展核心的步骤,假定我们增加一个新的字段到核心表中:
1、 如果需要,修改表述层来显示和修改新的字段。
2、 修改相关联的业务逻辑层的类,在相关方法中添加这个字段(例如AddTable, UpdateTable)。
3、 修改DataProvider基类因步骤2中变化而带来的必须的变化,并重新编译应用程序。
4、 为每一个DataProvider的子类实现(例如:SqlDataProvider,AccessDataProvider)做必要的修改,重新编译会显示基类跟其实现类之间的差异。需要修改的实现类的数量依赖于你的应用支持的不同数据库的数量。
5、用特殊的数据库更新命令(如:ALTER TABLE)修改每一个DataProvider子类实现的脚本。如果数据库提供者用了存储过程,那么同样必须写新版本的存储过程(用相关的DROP 和CREATE 命令)。
SQL命令发生器(SqlCommandGenerator)
早期版本的DNN包含一个叫做SqlCommandGenerator的类,这个类能够简单的调用SQL Server / MSDE数据库。然而它是反射每一个数据库调用的,这带来很严重的性能影响。这个类虽然仍然还保留着,但很明显的我们鼓励开发者采用DataProvider的模式。
参考
微软ASP.NET 组的Rob Howard提供了大量的指导和DataProvider模型实现的示例代码,.NET Pet Shop 3.0也提供了很多优秀的参考资料。Microsoft Data Access Application Block一个快速开发SQL Server Provider实现的一个有效的工具。