三、数据库连接
上一章讲的是本系统的核心部分,Meta-Data驱动方式,本章来讲一下数据库的连接。
本系统的设计目标是支持多种数据库,所以在设计的时候就要在这上面多考虑一下。
说到多数据库支持,有些地方也叫做“数据库无关”,就是不管客户选择什么样的数据库,都可以支持。其实我这个系统目前也只能做到支持MSSQL及Oracle,因为这两个数据库属于企业级数据库,并且在国内的用户量应该是最大的,Access因为它太过弱小,所以没有对它做太多的处理,对于其它的数据库,如果想增加支持,只要在这个模块中做一点处理就可以。
为了实现多数据库的支持,目前有两种比较流行的做法:
u 使用标准的ODBC或OleDB
u 针对不同的数据库写不同的SQL语句
第一种方式,在数据库扩展上能力相当强,因为它相当于使用的基本全是标准的SQL语法,尽量避开每个数据库特有的东西;而第二种方式则不管这一套,可以尽情的使用每个数据库的特色。第一种方式在灵活的同时也在不同程度上损失了数据库的性能,而第二种方式则在这方面有所增强,但是扩展数据库显得有些麻烦。我的系统采用的是第二种方式。
尽管对每个数据库要写不同的组件,但是在其它模块调用的时候,不应该再去考虑数据库属于何种类型,起码大部分情况不需要知道,这地方就是我要说的一个技巧,使用接口技术来实现。
interface IData
这个接口定义几个方法:
int ExecuteNonQuery(string commandText);
int ExecuteNonQuery(string[] sqls);
DataSet ExecuteReader(string sql);
string ExecuteScalar(string commandText);
然后再建立两个类来实现这个接口,分别叫做OracleData 和SqlData,把上述的几个方法全部重新实现,在这两个类中,会用到OracleClient和SqlClient,这就摆明了可以允许利用各自数据库的特色进行开发。
不管怎么做“与数据库无关”,但是系统最终还是必须知道到底用的是什么数据库,这些信息我全部用一个专门的配置文件来存储,而没有使用web.config,这样保证我这个类可以用于各种场景,而不会局限于BS结构。这个文件的命名为 config.xml,其内容如下:
<?xml version="1.0" encoding="utf-8" ?>
<config>
<connection>
<connection_name>test</connection_name>
<type>ORACLE</type>
<server></server>
<database></database>
<user>test</user>
<password>weJhxRH71Ds=</password>
<minpoolsize>10</minpoolsize>
</connection>
</config>
这个文件中有以下几个部分的内容我来说明一下
connection_name |
连接的名字,只是一个标识,不会被系统使用 |
type |
用于区分Oracle或MSSQL |
server |
指明数据库的来源,oracle使用TNS名字,为空表示本机,MSSQL使用连接名,如(local)等 |
database |
只对MSSQL有效,指明操作的数据库 |
user |
连接用户名 |
password |
加密形式存在,保证文件不会被轻易破解,系统会把这个密码反向解开得到真正的密码来使用 |
minpoolsize |
用于配置连接池,标准的asp.net的功能 |
程序中有一个专门对外的类,叫做DataAccessor,负责外界对数据库的调用,这个类首先会去查找config.xml这个文件,检查其中的数据库类型,根据不同的类型,会把连接指引到OracleData或SqlData中去。DataAccessor同样封装了和IData 相同的几个函数,在外部调用的时候很方便,而且对config.xml的寻找完全在内部来实现,不需要外部的干预。为了系统的性能,在首次读入配置文件后,配置文件的内容会被写入到内存中,保证下次再读的时候不会产生IO操作。这样使用的负面效果就是当改动配置文件后,必须重新起动IIS才可以生效。
上面提到的密码的加密存储,采用的是DES标准加密,而不是MD5的单向加密算法,保证系统能反向求出原密码,而DES加密后是二进制的形式,所以我又做了一个Base64的转换,保证可以存储在XML文件中。
每种数据库都有自己的特别的语法,举个最简单的例子,判断一个函数是否为空,在MSSQL中使用的是 isnull 函数,而在oracle中使用的是 nvl 函数,这样在外部引用的时候,首先会判断数据源为何种数据库,再写不同的SQL语句来实现。这样看来,这种做法肯定是不能接受,它太麻烦了,所以我做了一个专门的类,叫做DBConverter,它有几个静态函数,专门负责不同数据库之间的兼容处理。对上面的是否为空的函数,就有一个专门的静态函数如下:
public static string NVL(string serverType)
{
switch(serverType.ToUpper())
{
case "ORACLE":
{
return "nvl";
}
case "MSSQL":
{
return "isnull";
}
default:
{
return "nvl";
}
}
}
这样,在外部调用的时候,只要把DataAccessor的数据库类型作为参数传进去,就可以得到正确的表达方式,而不需要写不同的SQL来处理,这对多数据库支持起到了非常大的作用。以此类推,还可以把top 的语法写成函数来处理。当这种方式不能涵盖所有的数据库的时候,就有必要写单独的SQL来实现特定的功能了。
由上可以看出,如果需要新的数据库支持,主要的修改在于去实现一个新的I Data接口,并对DataAccessor类进行扩展即可。
再来说一下性能问题
int ExecuteNonQuery(string commandText) 的内部,最后都有一个提交的动作,这样当然是为了保证数据的完整性,这种做法很正确,但是当有大量的DML操作的时候,会反复的调用此函数,会导致性能急剧下降,而int ExecuteNonQuery(string[] sqls)这个函数正是为了解决这一问题而存在的,在这个函数的内部,首先会开始一个事物处理,然后依次去运行sqls中指定的SQL语句,最后统一做一个提交的动作,这样会使效率提高很多。
尽管上述方式能提高很多效率,但是说到底,每次SQL的提交都会有一个客户端到服务器的提交动作,即便IIS和oracle在同一台服务器,这个动作的负担也是相当重的,所以最好的方式是多做一些存储过程或package(oracle专享),尽量减少数据库与外界的交互,这样才可以达到真正的高效,针对这个技巧,我会在其它文章中做专门讨论。
这里还有一个性能问题,DataSet ExecuteReader(string sql)是以结果集的形式返回,DataSet的特点是把返回结果集全部一起取出放在内存中使用,对于这种情况,可以尽可能的维持数据的完整性,是它的优势,但是如果表很大,就会花费很长的时间来装入内存,并且占用的内存量比原始数据表还要大很多。所以在使用的时候,尽可能的减少返回的记录,比如限制返回行数等,大量的返回结果对前端展示并没有太大的意义,在我的前端展示控件中,有一个专门的筛选器,可以很方便的进行记录的查找。这样可以从本质上提高性能。
到此为止,数据库的连接部分基本搞定,下面来介绍多语言的实现方式。