1 引言
在近期使用.NET开发项目时,发现这样一些问题,影响着程序的复用性和软件的灵活性:
(1)数据库类型是变动的
原因有两个:一是因为不同的客户,对同一个应用程序的性能要求或费用开销不同,要求使用的数据库类型不同;二是我们在开发程序时,在不同的使用环境下,需要使用不同类型的数据库。
比如,如果使用ACCESS数据库,那么在程序中会使用System.Data.OleDb命名空间下的类,如OleDbConnection,OleDbParameter,OleDbDataAdapter等等。如果现在要求支持SQL Server数据库该怎么办呢?那么就需要修改现有的大量的代码,将System.Data.OleDb下的类全部替换为System.Data.SqlClient命名空间下的类,即使是使用“查找/替换”进行批量的修改,也是非常繁琐,也很容易出错。当然还会存在一些错误,比如,OleDb是使用“?”来传递参数的,而SQL Server中使用的是命名参数(如,“@para”)。因此还需要修改大量的SQL语句,麻烦啊!
(2)主键生成的规则不同
不同应用环境下,可能使用不同的主键生成策略,有的需要使用数据库唯一键,有的使用表唯一键,有的需要加入时间或其他标记,有的只使用简单的计数器就可以,有的使用GUID…… 如果程序中写死了,下次环境变了,需要使用新的规则时,就又得改代码喽。
(3)数据库连接管理
控制连接的数目,有时时单个连接,有时是若干个连接,有时使用连接池……
(4)SQL语句安全性检查
为了防止恶意攻击,需要对SQL进行检查。有很多种检查方法,究竟使用哪个呢?
(5)程序中SQL语句到处飞
如果使用关系数据库,SQL语句是少不了的。但是SQL语句在应用层出现太多,一旦数据库发生变化,那么上层需要修改的SQL语句就太多了,而且在编译时发现不了SQL语句的错误,在运行时才能找到。另外大量的SQL语句,还严重影响了程序的美观。
对于上面的问题,最好的解决方法就是建立一个数据库访问中间层,来屏蔽这些问题。(当然有一些现成的框架,如NHibernate,可以使用,不需要我们自己动手。)本文后续部分将讨论如何解决这些问题,主要在如何设计,并给出部分实现。
2 约定
(1)本文中谈及的数据库仅限于关系数据库。数据库类型指不同的关系数据库系统,如Oracle,SQL Server,Sybase等等。
(2)数据库对象指ADO.NET中访问数据库的对象:Connection对象,Command对象,Adapter对象、Parameter对象。
3 应用程序的一般结构
通常,数据库相关的应用程序应该具有图1所示的结构。至于为什么,就不用多说了。从图中可以看到数据库访问层所处的位置,及其应该具有的功能。
图1 应用程序一般结构
4 适应不同类型的数据库
如第1节所述,在ADO.NET中,使用不同类型的数据库,主要就是这些数据库对象的变化:Connection,Command,Adapter,Parameter。在程序中我们需要根据不同的数据库创建合适的对象。比如,使用SQL Server数据库,就需要引用System.Data.SqlClient命名空间,并使用SqlConnection, SqlCommand, SqlAdapter对象;如果使用ORACLE数据库,则要引用System.Data.OracleClient命名空间,并使用OracleConnection,OralceCommand等等。
通常的简单做法是使用一个数据库对象工厂,在工厂中通过条件判断,是何种类型的数据库,创建相应的数据库对象,代码如下:
[代码1] 简单,但缺乏弹性和复用性的方法
上面的代码中都是条件判断,而且CreateConnection函数和CreateParameter函数代码几乎一样,DataAdapter和Command的创建也是如此。这样的做法缺点主要有两点:
(1)代码重复,可维护性差;
(2)新增加一种数据库类型时,需要修改现有的代码。
其实我们仔细分析以下数据库类型和对象之间的关系,可以发现图2所示的规律:
图2 不同数据库和对象的关系
很明显,采用抽象工厂模式可以很好的处理不同数据库和不同数据库对象之间的组和系列的关系。如图3:
图3 数据库对象工厂设计
根据上面的类图,生成C#代码如下:
[代码2: DbObjectFactory.cs] (有删节)
[代码3: OleDbObjectFactory.cs] (有删节)
[代码4: OleDbObjectFactory.cs] (有删节)
使用时,根据应用环境的数据库类型,创建相应的具体工厂对象即可。这样就避免了使用生硬的条件判断了。新增加一种类型的数据库时,只要添加一个类,从DbObjectFactory继承即可,而现有的代码不需要做任何的修改。(第8节描述了应用程序如何使用这些类)
5 主键的生成
不同的业务需要,生成主键的方式不同。(BTW,主键最好是无意义的)。一种简单的做法和第4节的[代码1]类似,缺点也很明显,不再赘述。
所有的主键生成器使用一个接口,不同类型的生成器实现改接口即可,然后由具体的应用程序来选择使用不同的主键生成器。(第8节描述了应用程序如何使用)
图4 关键字生成器类图
根据上面的类图,生成代码如下:
[代码5: IdGenerator.cs]
[代码6: CounterIdGenerator.cs]
[代码7:KeyTableIdGenerator.cs]
如果有新的ID生成规则,则从IdGenerator继承并应用即可。
6、数据库连接管理
方法和第5节的ID生成器一样。描述 从略。
图5 数据库连接管理类图
7、SQL语句安全性检查
方法和第5节的ID生成器一样。描述从略。
图6 SQL语句检查器类图
8、组合起来(如何使用上面的东东)
上面谈到了如何适应不同的数据库,不同的主键生成,不同的数据库连接管理和SQL语句安全性检查,那么在具体某一中类型的应用程序中,如何使用他们呢?
不同的主键生成器,不同的数据库连接器,实现了不同的行为或算法,而不同应用程序程序就是不同的使用环境,因此采用策略模式是很自然的了。
策略模式设计到三个角色类:
环境角色:应用程序充当使用的环境
抽象策略角色:抽象类DatabaseConfig
具体策略:由应用程序实现的一个具体类,从DatabaseConfig类继承。
三个角色类如图7所示,Application是应用程序类,环境角色;DatabaseConfig是抽象策略类;XxxAppDbConfig是具体策略类,由该应用程序自己创建并实现。
图7 策略模式的使用
[代码8:DatabaseConfig.cs]
[代码9:XxxAppDbConfig.cs]
9、表模型
至此,基本的结构已经搭建好了,但是距离数据库访问层还差一步,就是实现数据的访问模式,实现对物理数据库中的表、视图等的访问。
应用程序对数据库的访问有好几种方式:
(1)事务脚本(存储过程)。一种面向过程的方法。
(2)ORM(对象-关系映射)。一种面向对象的方法。
(3)表模型。以物理数据表为基本单位进行访问,类似 .NET中的DataTable。
我觉得在 .NET中还是第三种方式更容易实现一点。
因为表和视图有很多相似点,不同的是视图是只读的,表是可读写的。因此建立一个基类DataModel(抽象类),提供表和视图相同的操作,然后为表和视图各建立一个类TableModel和ViewModel,从DataModel继承,并添加各自不同的操作。如图8:
图8 表模型类图
DataModel中的GetView方法作为查询,TableModel中的Delete方法是删除,XxxModel中的Insert和Update方法分别是添加和修改。XxxModel是由实际的应用程序实现的,针对表xxx的数据模型。这样我们可以为每一个表和视图都建立一个Model,来实现对数据库表和视图的操作。这是数据访问层的核心。(注:DataModel中的TableName,KeyName是抽象属性,由基类(如XxxModel)实现)
[代码10:DataModel.cs]
[代码11:TableModel.cs]
[代码12:XxxModel.cs]
从上面的代码可以看出,所有的SQL语句都到了Model类中,业务逻辑层和应用程序层都不需要书写SQL语句,减轻了数据库变化带来的影响。
10、结束语
吁~,终于写完了。以上是我的设计,很多都没有考虑到,比如性能啊等等,因为水平有限,其中有很多问题和错误,请大家的评点和指教,谢谢!