一、泛化数据访问的设计思路 数据访问是应用程序设计的一个重要功能模块,各种开发平台都提供了强大的数据访问组件。ADO.NET对SqlServer以及OleDb、ODBC等采用不同的类进行访问,这些类分布于不同的命名空间,包括System.Data.SqlClient、System.Data.OleDb、System.Data.Odbc等。这些类种类繁多,但是对开发者来说,如果不用到某种数据库的特性,其大体使用方法是一致的,都使用Connection 对象提供与数据源的连接,使用Command执行返回数据、修改数据、运行存储过程等操作命令,使用DataReader从数据源中提供高性能的数据流,使用DataAdapter 提供连接 DataSet 对象和数据源的桥梁。在编写应用系统的时候,希望能不因这么多类的不同而受到影响,能够尽量做到数据库无关,当后台数据库发生变更的时候,不需要更改客户端的代码,即实现对数据库访问的泛化。同时,为了性能和其他原因,也希望提供对数据库访问的缓存,特别是数据库连接的缓存。虽然微软给内置了数据库连接池,但是,自己控制缓存,无疑可以提供更大的灵活性和效率。 这就需要我们在实际开发过程中将这些数据库访问类再作一次封装。本文将详细阐述如何实现数据库的泛化访问,并建立自己的连接池,管理数据库连接。 本文的实现分为三个部分。首先,使用Façade模式(即外观模式)封装数据库访问的细节,用一个类TDBAccess来封装操作数据库的代码,对客户程序(所谓客户程序,指应用程序中,需要使用到这个数据访问模块的其他程序)开放这个类。客户程序不需要直接与操作数据库的实际对象打交道,无需考虑数据库类型,只要把需要执行的SQL语句作为参数传入TDBAccess,即可以执行数据访问操作。其次,在TDBAccess内部,使用类TDBToolFactory来提供真正执行数据访问所需要的数据访问对象,使用Simple Factory模式(即简单工厂模式)实现数据库访问的泛化。第三,使用Singleton模式(即单态模式)管理数据库连接池,以获得更大的灵活性和效率。这三个部分中,Facade模式对外提供接口,其实现过程需要使用到简单工厂模式产生的对象,其中,连接对象用 Singleton模式来进行管理。三个主要的类关系如图1所示。

图1 泛化数据访问的主要类的关系 以上三个类在设计时处于同一个自定义的命名空间DataAccess。在设计中,为了便于用户表示层和业务逻辑层处理数据访问时发生的异常,本文定义了一个枚举类型EnumErrorCode,用以描述当前对数据库操作的状态。 二、使用Façade模式封装数据访问 Facade 模式,是一种结构型的设计模式,它定义了一个将子系统的一组接口集成在一起的高层接口,以提供一个一致的界面。通过这个界面,其他系统能够方便地调用子系统中的功能,而不用关心子系统的细节。 在利用ADO.NET进行数据访问时,业务逻辑层以及用户表示层需要不断的与数据库进行交互。完成一次与数据库的交互需要使用到Connection、Command和DataAdapter等类,涉及到初始化数据库、打开数据库连接、执行SQL命令、返回查询结果等操作。可以使用Facade模式对上述数据访问过程进行封装,对客户端仅提供一个TDBAccess类,这个类封装了数据访问中的上述一些重复的过程,只对外提供几个简单的方法,用以执行SQL命令并返回结果。该类的结构如图2所示。

图2 应用Façade模式的数据访问类
下面分析其实现代码中的关键函数。 1.函数ExcuteReader。该函数用来执行查询操作,它有两个参数:输入参数 szSQLcmd是客户代码需要执行的SQL命令;是输出参数dr,它是一个对IDataReader对象的引用,是通过IDbCommand执行查询之后产生的结果。该函数主要用于需要处理单条记录的时候。函数的返回值是自定义的枚举值EnumErrorCode。 需要注意的是,函数内部调用其他类的函数时,都是通过接口来引用的。这种面向接口的编程方式可以使对象在实例化时拥有更大的灵活性,是利用设计模式编程的主要方法之一。 public static EnumErrorCode excuteReader(string szSQLcmd, ref System.Data.IDataReader dr) { EnumErrorCode e=EnumErrorCode.Success; /* * 此处调用了工具工厂TDBToolFactory的函数,IDbCommand是一个接口,它所定义对象cmd在 * 实例化时,由类TDBToolFactory根据数据库类型自动生成相应的类的实例。函数参见3节中的 * 说明。 */ IDbCommand cmd = TDBToolFactory.getCommandObj(); /* * 此处调用了数据库连接池中获取连接对象的函数,以获得一个可用的数据库连接。IDbConnection * 是一个接口,它所定义对象conn在实例化时,由数据库连接池TConnectionPool根据数据库类型 * 自动生成相应的实例。函数参见第4节中的说明。 */ IDbConnection conn = TConnectionPool.getInstance().getConnObj(); try { cmd.Connection=conn; if(conn.State!=ConnectionState.Open) { //重新获取一个可用连接 conn = TConnectionPool.getInstance().getConnObj(); } cmd.CommandText = szSQLcmd; dr = cmd.ExecuteReader(); if (dr != null) { e = EnumErrorCode.Success; } } catch(Exception ex) { dr = null; e = EnumErrorCode.DBFailed; } finally { //释放连接资源 TConnectionPool.getInstance().restituteConn(conn); } return e; } 2.函数ExcuteNoneQuery。该函数用来执行对数据库的更新作,szSQLcmd是需要执行的SQL命令,主要是执行对数据库的添加、删除以及更新操作。该函数的Command对象和Connection对象的获取都跟上述函数一样,此处不加以说明。 public static numErrorCode executeNoneQuery(string szSQLcmd) { EnumErrorCode e = EnumErrorCode.Success; IDbConnection conn= TConnectionPool.getInstance().getConnObj(); try { if (conn.State != ConnectionState.Open) { conn = TConnectionPool.getInstance().getConnObj(); } IDbCommand cmdObj= TDBToolFactory.getCommandObj(); cmdObj.Connection = conn; cmdObj.CommandText = szSQLcmd; cmdObj.ExecuteNonQuery(); e = EnumErrorCode.Success; } catch(Exception ex) { e = EnumErrorCode.DBFailed; } finally { TConnectionPool.getInstance().restituteConn(conn); } return e; } 3.函数ExcuteQuery。该函数同ExcuteReader的功能类似,都是用来执行查询操作。不同的是ExcuteReader函数返回一个IDataReader对象的引用,而这个函数返回一个DataTable 对象。在使用时,ExcuteReader主要使用在需要操作单条记录的地方,而这个ExcuteQuery主要进行批量数据的处理。 public static EnumErrorCode executeQuery(string szSQLcmd, ref DataTable dt) { EnumErrorCode e = EnumErrorCode.Success; DataSet ds=new DataSet(); IDbConnection conn = TConnectionPool.getInstance().getConnObj(); try { if (conn.State != ConnectionState.Open) { conn = TConnectionPool.getInstance().getConnObj(); } IDbCommand cmdObj=TDBToolFactory.getCommandObj(); cmdObj.CommandText = szSQLcmd; cmdObj.Connection =conn; IDbDataAdapter adtObj=TDBToolFactory.getAdapterObj(); adtObj.SelectCommand=cmdObj; adtObj.Fill(ds); dt = ds.Tables[0]; e= EnumErrorCode.Success; } catch(Exception ex) { e = EnumErrorCode.DBFailed; } finally { TConnectionPool.getInstance().restituteConn(conn); } return e; } 三、使用Simple Factory模式泛化数据访问 Simple Factory(简单工厂模式)是对工厂方法的简化,它是一种参数化的工厂方法。该模式对客户隐藏对象实例化的过程,使客户不需要了解使用的对象属于哪个具体的子类。 简单工厂模式使得应用程序对数据库的泛化访问变得容易实现。设计了TDBToolFactory类,该类对外提供两个函数:getCommandObj和getAdapterObj,这两个函数对外生成Command对象和DataAdapter对象。在实现时,只需要在应用程序的配置文件中加入一个标示数据库类型的字段,TDBToolFactory在实例化数据库操作对象时,首先从配置文件中读取该字段,同时读取相应的数据库连接字符串,实例化相应的类。类结构如图3所示。
 图3 应用Simple Factory模式TDBToolFactory类
该类主要的两个函数说明如下: 1.函数getAdapterObj。该函数根据配置文件中数据库类型的标示,返回相应的数据适配器IDbDataAdapter对象。 public static IDbDataAdapter getAdapterObj() { try { //读取配置文件中的数据库类型字段 string szType=System.Configuration.ConfigurationSettings.AppSettings["DBType"]; //根据数据库类型,返回相应的数据适配器IDbDataAdapter对象 if(szType=="SQLServer") { return new System.Data.SqlClient.SqlDataAdapter(); } else if(szType=="Access") { return new System.Data.OleDb.OleDbDataAdapter(); } else if(szType=="FoxPro") { return new System.Data.OleDb.OleDbDataAdapter(); } else if(szType=="ODBC") { return new System.Data.Odbc.OdbcDataAdapter(); } else { return null; } } catch (Exception ex) { return null; } } 2.函数getCommandObj。该函数根据配置文件中数据库类型的标示,返回相应的IDbCommand 对象。 public static IDbCommand getCommandObj() { try { //读取配置文件中的数据库类型字段 string szType =System.Configuration.ConfigurationSettings.AppSettings["DBType"]; //根据数据库类型,返回相应的数据适配器IDbCommand 对象 if(szType=="SQLServer") { return new System.Data.SqlClient.SqlCommand(); } else if(szType=="Access") { return new System.Data.OleDb.OleDbCommand(); } else if(szType=="FoxPro") { return new System.Data.OleDb.OleDbCommand(); } else if(szType=="ODBC") { return new System.Data.Odbc.OdbcCommand(); } else { return null; } } catch(Exception ex) { return null; } } 程序的配置文件如下,各个配置的功能参见配置文件中的注释,本处不再一一列举。 <?xml version="1.0"?> <configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0"> <!-- 自定义参数模块 --> <appSettings> <!-- DBType标示数据库类型 如果使用SQLServer2000, 则value值为"SQLServer"; 如果使用FoxPro,则Value的值为"FoxPro"; 如果使用Access,则值为"Access"; 如果使用ODBC,则值为"ODBC"; --> <add key="DBType" value="SQLServer"></add> <!-- 使用SQLServer时使用的连接字符串 --> <add key="SQLConnstring" value="server=.;dataBase=OA;uid=sa;pwd=;"></add> <!-- 使用Access数据库时使用的连接字符串 --> <add key="AccessConnstring" value="DataBase/DataBase.mdb"></add> <!-- 使用FoxPro时使用的连接字符串 --> <add key="FoxProConnstring" value="DataBase"></add> <!-- 使用ODBC时使用的连接字符串 --> <add key="ODBC" value="MES"></add> <!-- 自定义数据库连接池的大小 --> <add key="connPoolSize" value="100"></add> </appSettings> </configuration> 四、 用Singleton模式管理数据库连接池 先看Singleton模式的经典含义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。Singleton模式是一种细粒度的模式。它的结构非常简单,包括防止其他对象创建实例的私有构造函数、保存唯一实例的私有静态成员变量,以及全局访问接口等。现在,需要构建一个缓冲池,用以保存数据库对象,并供整个应用程序来调用,正好适用Singleton模式。 Singleton模式的数据库连接池类TConnectionPool的结构如图4所示。
图4 Singleton模式的数据库连接池 该类的构造函数是私有的;同时,该类拥有一个私有静态成员变量TConnectionPool,用以保存类的唯一实例。该类的一些主要属性和函数的说明如下。 1.m_ConnContainer。这是一个ArrayList类型的成员变量,用以存放数据库连接对象。 2.m_poolObj。这是一个静态的TConnectionPool类型的成员变量,是全局连接池实例。 3.构造函数TConnectionPool。这是一个私有的构造函数。私有的目的是为了防止其它类通过new的方法来产生该类的对象,从而保证整个应用中只有该类一个实例存在。 private TConnectionPool() { //读取根据配置文件中自定义的连接池大小 int iSize = Convert.ToInt32(System.Configuration.ConfigurationSettings.AppSettings["connPoolSize"]); m_ConnContainer=new ArrayList(iSize); } 4.函数getInstance。该函数是数据库连接池的全局访问点,由它向外提供对数据库连接池的访问入口。 public static TConnectionPool getInstance() { if (m_poolObj == null) { m_poolObj = new TConnectionPool(); } return m_poolObj; } 5.函数getNewConnObj。该函数根据配置文件中数据库的类型以及连接字符串,生成数据库连接对象。这个函数是一个私有函数,用于在连接池内部建立对数据库的可用连接。 private System.Data.IDbConnection getNewConnObj() { string szConnstring; IDbConnection conn; //读取根据配置文件中数据库类型 string szType = System.Configuration.ConfigurationSettings.AppSettings["DBType"]; if (szType == "SQLServer") { szConnstring = System.Configuration.ConfigurationSettings.AppSettings["SQLConnstring"]; conn = new SqlConnection(szConnstring); } else if (szType == "FoxPro") { string szPath = AppDomain.CurrentDomain.BaseDirectory.ToString(); szPath = szPath + "DataBase"; szConnstring = "Provider=Microsoft.Jet.OLEDB.4.0;Extended Properties=dBASE IV;User ID=Admin;Password=;Data Source=" + szPath; conn = new System.Data.OleDb.OleDbConnection(szConnstring); } else if (szType == "Access") { string szPath = AppDomain.CurrentDomain.BaseDirectory.ToString(); szPath = szPath + System.Configuration.ConfigurationSettings.AppSettings["AccessConnstring"]; szConnstring = "Provider=Microsoft.Jet.OLEDB.4.0;Password=;Data Source=" + szPath; conn = new OleDbConnection(szConnstring); } else if (szType == "ODBC") { szConnstring = "DSN="+System.Configuration.ConfigurationSettings.AppSettings["ODBC"]+";"; conn = new System.Data.Odbc.OdbcConnection(szConnstring); } else { return null; } try { conn.Open(); } catch (Exception ex) { return null; } return conn; } 6.函数getConnObj。这是一个public类型的函数,对外提供可用的数据库连接对象。 public IDbConnection getConnObj() { IDbConnection conn = null; //如果池中有可用连接,则获取当前该连接 if (m_ConnContainer.Count > 0) { conn = (IDbConnection)m_ConnContainer[0]; m_ConnContainer.RemoveAt(0); if (conn.State == ConnectionState.Closed) { getConnObj(); } } else { conn = getNewConnObj(); } return conn; } 7.函数restituteConn。这也是一个public类型的函数,数据操作类TDBAccess在使用完访问数据的操作后,需要归还这个可用的连接到数据库连接池中,供其他数据访问操作使用该连接。 public void restituteConn(IDbConnection connObj) { m_ConnContainer.Add(connObj); } 8.函数freeAllConn。这也是一个public类型的函数,应用程序在运行结束后,需要调用这个函数来断开所有与数据库的连接。 public void freeAllConn() { for (int i = 0; i < m_ConnContainer.Count - 1; i++) { if (((IDbConnection)m_ConnContainer[i]).State != ConnectionState.Closed) { ((IDbConnection)m_ConnContainer[i]).Close(); ((IDbConnection)m_ConnContainer[i]).Dispose(); } } } 五、调用示例 我们只需要在应用程序中加入包含上述三个类的文件,并引入命名空间DataAccess,就可以使用这几个类了。客户程序在使用这个功能模块时,只需要了解TDBAccess这一个类的结构。下面举例说明,该例子来源于笔者编写的OA程序的一个内部BBS模块,该函数属于业务逻辑层的,功能是获取当前所有正常开放的内部BBS论坛,并将该信息存于一个DataTable中,供用户表示层来使用,调用的代码如下: /* * 函数名:getAllInuseBBS * 功 能:获取所有的正常开放的论坛信息 * 参 数:[Out ] dt 用以返回信息的DataTable */ public static EnumErrorCode getAllInuseBBS(ref DataTable dt) { string szSQLcmd = null; if (dt == null) { return EnumErrorCode.InputContainerIsNull; } try { szSQLcmd = "select bName,Subject,WelcomeWord,Manager from BBSInfo where status='开放'"; //客户端程序只需要使用TDBAccess提供的方法即可,不需要关心访问数据库的细节 return TDBAccess.executeQuery(szSQLcmd, ref dt); } catch (Exception ex) { return EnumErrorCode.DBFailed; } } 该DataTable即可交给用户表示层使用,部分实例代码如下: DataTable dt = new DataTable(); BusinessRules.TBBSRules.getAllInuseBBS(ref dt); gvAllBBS.DataSource = dt; gvAllBBS.DataBind(); 其中,gvAllBBS是一个用以显示所有论坛信息的GridView。 六、结语 以上介绍了在ADO.NET下利用设计模式实现数据访问泛化的一种方法。笔者在实际项目中,在数据库处理层时,多次采用了上面的方法,取得了很好的效果。从这个例子也可以看出,设计模式的应用,极大地提高了面向对象软件设计的抽象层次(更多针对接口的抽象),为软件设计更高层次更大规模的复用提供了可能,大大减小了设计的复杂度,同时也提高了软件设计的质量,更有利于软件的维护。
|