一、引言
ADO.NET为应用程序开发人员提供了一种全新的数据库访问机制,它使得数据库编程变得相当容易。然而,在运用ADO.NET进行数据库编程时,开发人员往往会因为不注意某些细节问题而使得应用程序的可扩展性很差,也即某个数据库应用程序只能应用于某个特定类型的数据库,而不能和更多的其他类型的数据库进行交互或是移植到其它数据库平台下。本文将深入研究如何运用ADO.NET中的接口技术来实现通用数据库编程技术并构建通用数据库访问层。
二、ADO.NET体系结构
ADO.NET是由一系列的数据库相关类和接口组成的,它的基石是XML技术,所以通过运用ADO.NET技术应用程序不仅能访问关系型数据库中的数据,而且还能访问层次化的XML数据。ADO.NET为应用程序提供了两种数据访问的模式:连接模式(Connected Mode)和非连接模式(Disconnected Mode)。运用过ADO技术的开发人员对前一种模式应该是非常熟悉的,而后一种模式则是ADO.NET才具有的。相比于传统的数据库访问模式,非连接的模式为应用程序提供了更大的可升级性和灵活性。在该模式下,一旦应用程序从数据源中获得所需的数据,它就断开与原数据源的连接,并将获得的数据以XML的形式存放在主存中。在应用程序处理完数据后,它再取得与原数据源的连接并完成数据的更新工作。
ADO.NET中的DataSet类是非连接模式的核心,数据集对象(DataSet)是以XML的形式存放数据。应用程序既可以从一个数据库中获取一个数据集对象,也可以从一个XML数据流中获取一个数据集对象。而从用户的角度来看,数据源在哪里并不重要,也是无需关心的。这样一个统一的编程模型就可被运用于任何使用了数据集对象的应用程序。
ADO.NET体系结构中还有一个非常重要的部分就是数据提供者对象(Data Provider),它是访问数据库的必备条件。通过它应用程序可以产生相应的数据集对象;同时它还提供了连接模式下的数据库访问支持。图1描述了ADO.NET总体的体系结构。
图1 ADO.NET的体系结构
三、数据提供者对象
本文研究的是通用数据库编程并如何运用该技术实现通用数据库访问层,因此要从ADO.NET体系结构中的数据提供者对象入手,使得一个应用程序具有访问多个不同类型数据库的能力。ADO.NET中的数据提供者对象包括数据库连接接口(IDbConnection)、数据库命令接口(IDbCommand)、数据读取器接口(IDataReader)和数据适配器接口(IDbDataAdapter)等不同种类的接口。通过这些接口,应用程序可以访问数据库、执行相关的命令操作并获取相应结果,获取的结果可以是以XML数据的形式存放在数据集对象中,也可以是直接被应用程序所使用。目前,微软的.NET Framework已经是1.1版本了,所以其中的ADO.NET支持了更广泛的数据提供者对象:一种为SQL Server数据提供者对象,它是专门应用于MS SQL Server数据库的,所以性能得到了优化;一种为OleDb数据提供者对象,它可以通过COM层和OLE DB进行交互;一种为ODBC数据提供者对象,它可以直接跟ODBC数据源进行交互;最后一种Oracle数据提供者对象,它是专门针对Oracle数据库的,所以性能上也得到了不少优化。与这四种数据提供者对象相关联的类的前缀分别为:Sql、OleDb、Odbc以及Oracle,而与其相关联的命名空间则分别为:System.Data.SqlClient、System.Data.OleDb、System.Data.Odbc以及System.Data.OracleClient。在实际的开发中,开发人员可以根据需要选择相应类型的数据提供者对象。不过为了使应用程序具有通用性,开发人员应通过使用数据提供者对象的接口而并非某个特定类型的数据提供者对象来实现数据库的访问操作,这样就可以实现通用数据库编程并构建通用数据库访问层了。
下面的表格列举了各种接口的名称以及相应的描述:
接口名称 | 描述 |
IDbConnection | 数据库连接接口,代表了到数据源的一个连接。 |
IDbCommand | 数据库命令接口,代表了对数据源进行操作的一系列SQL语句或命令对象。 |
IDataReader | 数据读取器接口,在连接的模式下访问数据源,以向前只读的方式获取数据。 |
IDbDataAdatpter | 数据适配器接口,在非连接的模式下工作,作为数据源和数据集对象之间的桥梁。 |
IDbTransaction | 数据事务处理接口,能实现对数据源的事务处理操作。 |
IDataParameter | 数据参数接口,代表了对应于数据库命令对象的一系列参数对象。 |
表1 各种接口名称以及其相应描述
下面的表格列举了四种不同类型的数据提供者对象中的各个相互并行的类:
SQL Server | OleDb | ODBC | Oracle |
SqlConnection | OleDbConnection | OdbcConnection | OracleConnection |
SqlCommand | OleDbCommand | OdbcCommand | OracleCommand |
SqlDataReader | OleDbDataReader | OdbcDataReader | OracleDataReader |
SqlDataAdapter | OleDbDataAdapter | OdbcDataAdapter | OracleDataAdapter |
SqlTransaction | OleDbTransaction | OdbcTransaction | OracleTransaction |
SqlDataParameter | OleDbDataParameter | OdbcDataParameter | OracleDataParameter |
表2 四种类型的数据提供者对象中的类
四、构建通用数据库访问层
数据提供者对象构成了ADO.NET的基础,通过实现其中的各种不同的接口,构建通用数据库访问层并非难事。一般来说,运用数据提供者对象访问并更新数据的操作会包含以下几个步骤:
1. 运用数据库连接对象建立和数据源的连接。
2. 根据上面建立的数据库连接对象创建一个数据库命令对象以执行特定的操作。
3. 执行数据库命令对象或创建并执行数据适配器对象,以返回数据读取器对象(连接模式)或填充数据集对象(非连接模式)或取得其他相应结果。
4. 数据处理完毕后,通过数据库命令对象或数据适配器对象将处理结果更新到数据源。
5. 最后释放各种数据提供者对象资源。
基于以上考虑,构建通用数据库访问层主要得实现对数据源访问的底层操作的封装,而仅仅暴露出数据读取器对象或是数据集对象等以供商业逻辑层调用。因此,本文介绍的通用数据库访问层主要实现了以下一些方法:
1. ExecuteNonQuery:执行INSERT、DELETE、UPDATE等SQL语句并返回受影响的行的数目。
2. ExecuteReader:执行SELECT操作并返回一个数据读取器对象。
3. ExecuteScalar:执行SQL操作并返回单值对象,即结果集中第一行的第一条数据。
4. PopulateDataSet:执行SQL操作,通过数据适配器对象将从数据源获取的数据填充到数据集对象中并返回之。
下面便是本文介绍的通用数据库访问层的具体实现,其命名空间和类名均为UniversalDAL,其中还运用到了一个枚举类型-DBType,用于枚举各种不同类型的数据库。UniversalDAL类主要提供了上面介绍的四个基本方法,同时还提供了DatabaseType和ConnectionString这两个基本属性。考虑到具体实现上的性能要求,UniversalDAL类还对构造函数和某些方法进行了重载操作。其中的GetConnection、GetCommand以及GetDataAdapter等函数是UniversalDAL类的核心部分,正是它们实现了通用数据库的访问操作。
using System;
using System.Data;
using System.Data.SqlClient;
using System.Data.OleDb;
using System.Data.Odbc;
using System.Data.OracleClient;
namespace UniversalDAL
{
/// <summary>
/// 该枚举类型用于枚举数据库的类型
/// </summary>
public enum DBType
{
SQLServer,
OleDb,
ODBC,
Oracle
}
/// <summary>
/// 通用数据库访问类-UniversalDAL类,
/// 支持SQL Server、OleDb、ODBC、Oracle等
/// 不同类型的数据源
/// </summary>
public class UniversalDAL
{
private DBType _DatabaseType = DBType.SQLServer;
/// <summary>
/// 数据库类型属性
/// </summary>
public DBType DatabaseType
{
get { return _DatabaseType; }
set { _DatabaseType = value; }
}
private string _ConnectionString = "";
/// <summary>
/// 数据库连接字符串属性
/// </summary>
public string ConnectionString
{
get { return _ConnectionString; }
set { _ConnectionString = value; }
}
/// <summary>
/// 构造函数
/// </summary>
public UniversalDAL()
{
}
/// <summary>
/// 构造函数
/// </summary>
/// <param name="DbType">访问的数据库类型</param>
public UniversalDAL(DBType DbType)
{
this._DatabaseType = DbType;
}
/// <summary>
/// 构造函数
/// </summary>
/// <param name="DbType">访问的数据库类型</param>
/// <param name="ConString">数据库连接字符串</param>
public UniversalDAL(DBType DbType, string ConString)
{
this._DatabaseType = DbType;
this._ConnectionString = ConString;
}
/// <summary>
/// 根据数据库类型获取数据库连接接口
/// </summary>
/// <returns>数据库连接接口</returns>
private IDbConnection GetConnection()
{
IDbConnection con = null;
switch (this.DatabaseType)
{
case DBType.SQLServer: con = new
SqlConnection(this.ConnectionString); break;
case DBType.OleDb: con = new
OleDbConnection(this.ConnectionString); break;
case DBType.ODBC: con = new
OdbcConnection(this.ConnectionString); break;
case DBType.Oracle: con = new
OracleConnection(this.ConnectionString); break;
default: con = new
SqlConnection(this.ConnectionString); break;
}
return con;
}
/// <summary>
/// 根据数据库类型获取数据库命令接口
/// </summary>
/// <param name="cmdText">数据库命令字符串</param>
/// <param name="con">数据库连接接口</param>
/// <returns>数据库命令接口</returns>
private IDbCommand GetCommand(string cmdText, IDbConnection con)
{
IDbCommand cmd = null;
switch (this.DatabaseType)
{
case DBType.SQLServer: cmd = new SqlCommand(cmdText,
(SqlConnection)con); break;
case DBType.OleDb: cmd = new OleDbCommand(cmdText,
(OleDbConnection)con); break;
case DBType.ODBC: cmd = new
OdbcCommand(cmdText,(OdbcConnection)con); break;
case DBType.Oracle: cmd = new OracleCommand(cmdText, (OracleConnection)con); break;
default: cmd = new SqlCommand(cmdText,
(SqlConnection)con); break;
}
return cmd;
}
/// <summary>
/// 根据数据库类型获取数据适配器接口
/// </summary>
/// <param name="cmdText">数据库命令字符串</param>
/// <param name="conString">数据库连接字符串</param>
/// <returns>数据适配器接口</returns>
private IDataAdapter GetDataAdapter(string cmdText, string conString)
{
IDataAdapter da = null;
switch (this._DatabaseType)
{
case DBType.SQLServer: da = new SqlDataAdapter(cmdText, conString); break;
case DBType.OleDb: da = new OleDbDataAdapter(cmdText, conString); break;
case DBType.ODBC: da = new OdbcDataAdapter(cmdText, conString); break;
case DBType.Oracle: da = new OracleDataAdapter(cmdText, conString); break;
default: da = new SqlDataAdapter(cmdText, conString); break;
}
return da;
}
/// <summary>
/// 根据数据库类型获取数据适配器接口
/// </summary>
/// <param name="cmd">数据库命令接口</param>
/// <returns>数据适配器接口</returns>
private IDataAdapter GetDataAdapter(IDbCommand cmd)
{
IDataAdapter da = null;
switch (this.DatabaseType)
{
case DBType.SQLServer: da = new
SqlDataAdapter((SqlCommand)cmd); break;
case DBType.OleDb: da = new
OleDbDataAdapter((OleDbCommand)cmd); break;
case DBType.ODBC: da = new
OdbcDataAdapter((OdbcCommand)cmd); break;
case DBType.Oracle: da = new
OracleDataAdapter((OracleCommand)cmd); break;
default: da = new
SqlDataAdapter((SqlCommand)cmd); break;
}
return da;
}
/// <summary>
/// 执行SQL语句并返回受影响的行的数目
/// </summary>
/// <param name="cmdText">数据库命令字符串</param>
/// <returns>受影响的行的数目</returns>
public int ExecuteNonQuery(string cmdText)
{
IDbConnection con = null;
IDbCommand cmd = null;
try
{
con = this.GetConnection();
cmd = this.GetCommand(cmdText, con);
con.Open();
return cmd.ExecuteNonQuery();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine(ex.Message);
return 0;
}
finally
{
if (cmd != null)
cmd.Dispose();
if (con != null)
con.Dispose();
}
}
/// <summary> /// 执行SQL语句并返回数据行 /// </summary> /// <param name="cmdText">数据库命令字符串</param> /// <returns>数据读取器接口</returns> public IDataReader ExecuteReader(string cmdText) { IDbConnection con = null; IDbCommand cmd = null; try { con = this.GetConnection(); cmd = this.GetCommand(cmdText, con); con.Open(); return cmd.ExecuteReader(CommandBehavior.CloseConnection); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex.Message); return null; } } /// <summary> /// 执行SQL语句并返回单值对象 /// 即结果集中第一行的第一条数据 /// </summary> /// <param name="cmdText">数据库命令字符串</param> /// <returns>单值对象-结果集中第一行的第一条数据</returns> public object ExecuteScalar(string cmdText) { IDbConnection con = null; IDbCommand cmd = null; try { con = this.GetConnection(); cmd = this.GetCommand(cmdText, con); con.Open(); return cmd.ExecuteScalar(); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex.Message); return null; } finally { if (cmd != null) cmd.Dispose(); if (con != null) con.Dispose(); } } /// <summary> /// 填充一个数据集对象并返回之 /// </summary> /// <param name="cmdText">数据库命令字符串</param> /// <param name="conString">数据库连接字符串</param> /// <returns>数据集对象</returns> public DataSet PopulateDataSet(string cmdText, string conString) { IDataAdapter da = null; DataSet ds = null; try { da = this.GetDataAdapter(cmdText, conString); ds = new DataSet(); da.Fill(ds); return ds; } catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex.Message); return null; } }
/// <summary> /// 填充一个数据集对象并返回之 /// </summary> /// <param name="cmdText">数据库命令字符串</param> /// <returns>数据集对象</returns> public DataSet PopulateDataSet(string cmdText) { IDbConnection con = null; IDbCommand cmd = null; IDataAdapter da = null; DataSet ds = null; try { con = this.GetConnection(); cmd = this.GetCommand(cmdText, con); da = this.GetDataAdapter(cmd); ds = new DataSet(); da.Fill(ds); return ds; } catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex.Message); return null; } finally { if (cmd != null) cmd.Dispose(); if (con != null) con.Dispose(); } } } } 五、构建测试项目 创建完通用数据库访问层,我们来构建一个测试项目。该项目能通过通用数据库访问层同时访问SQL Server 2000和Access 2000中的Northwind数据库。考虑到实际项目中的需求,在开发阶段我们可能仅仅需要Access数据库作为开发平台,而到了实际应用阶段,后台的数据库则须更改为SQL Server或是其他的诸如Oracle、DB2之类的数据库。那么在这种情况下,通用数据库访问层的作用就十分明显了,我们要做的仅仅是更改项目中的数据库类型以及数据库连接字符串,而其他的商业逻辑则可以原封不动。这样带来的益处和效率也是显而易见的。 本测试项目运用了Northwind数据库,其中SQL Server 2000中的是英文版的,而Access 2000中的则为中文版。同时,本测试项目还用到了应用程序配置文件,该文件是一个XML文件,它存储了数据库连接字符串。这样的好处是一旦数据库连接发生变化,我们仅仅需要更改该文件中的相应属性,而不必去更改源代码并重新编译建立项目。该应用程序配置文件如下: <?xml version="1.0" encoding="utf-8" ?> <configuration> <appSettings> <add key="NorthwindProConString" value="server=localhost;database=Northwind;integrated security=SSPI;"/> <add key="NorthwindDevConString" value="Provider=Microsoft.Jet.OLEDB.4.0;Data Source=C:/Northwind.mdb"/> </appSettings> </configuration> 本测试项目主要运用了Windows Froms中的DataGrid控件,它能良好地显示数据源中的数据,项目的主窗体如图2所示。
图2 测试项目的主窗体界面
考虑到DataGrid控件不能直接将数据读取器对象作为数据源,本测试项目还引入了一个功能函数-GetDataTable,它能将一个数据读取器接口中的数据填充到一个数据表对象中并返回该数据表对象,其实现方法如下所示: /// <summary> /// 该函数将一个数据读取器接口中的数据 /// 填充到一个数据表对象中并返回数据表对象 /// </summary> /// <param name="_reader">数据读取器接口</param> /// <returns>数据表对象</returns> private System.Data.DataTable GetDataTable(System.Data.IDataReader _reader) { System.Data.DataTable _table = _reader.GetSchemaTable(); System.Data.DataTable _dt = new System.Data.DataTable(); System.Data.DataColumn _dc; System.Data.DataRow _row; System.Collections.ArrayList _al = new System.Collections.ArrayList(); for (int i=0; i<_table.Rows.Count; i++) { _dc = new System.Data.DataColumn(); if (! _dt.Columns.Contains(_table.Rows[i]["ColumnName"].ToString())) { _dc.ColumnName = _table.Rows[i]["ColumnName"].ToString(); _dc.Unique = Convert.ToBoolean(_table.Rows[i]["IsUnique"]); _dc.AllowDBNull = Convert.ToBoolean(_table.Rows[i]["AllowDBNull"]); _dc.ReadOnly = Convert.ToBoolean(_table.Rows[i]["IsReadOnly"]); _al.Add(_dc.ColumnName); _dt.Columns.Add(_dc); } } while (_reader.Read()) { _row = _dt.NewRow(); for (int i=0; i<_al.Count; i++) { _row[((System.String) _al[i])] = _reader[(System.String) _al[i]]; } _dt.Rows.Add(_row); } return _dt; } 这样,本测试项目就既能以DataReader的方式又能以DataSet的方式来实现数据的显示了,主界面中的四个按钮的消息响应函数分别如下所示: private void btnDRSQLServer_Click(object sender, System.EventArgs e) { try { string sqlConStr = ConfigurationSettings.AppSettings["NorthwindProConString"]; string sqlStr = "SELECT TOP 5 * FROM Customers";
UniversalDAL uDAL = new UniversalDAL(DBType.SQLServer, sqlConStr); System.Data.IDataReader dr = uDAL.ExecuteReader(sqlStr); dataGridSQL.DataSource = this.GetDataTable(dr); } catch (Exception) {} } private void btnDRAccess_Click(object sender, System.EventArgs e) { try { string oledbConStr = ConfigurationSettings.AppSettings["NorthwindDevConString"]; string sqlStr = "SELECT TOP 5 * FROM 客户"; UniversalDAL uDAL = new UniversalDAL(DBType.OleDb, oledbConStr); System.Data.IDataReader dr = uDAL.ExecuteReader(sqlStr); dataGridOleDb.DataSource = this.GetDataTable(dr); } catch (Exception) {} } private void btnDASQLServer_Click(object sender, System.EventArgs e) { try { string sqlConStr = ConfigurationSettings.AppSettings["NorthwindProConString"]; string sqlStr = "SELECT TOP 5 * FROM Customers";
UniversalDAL uDAL = new UniversalDAL(DBType.SQLServer, sqlConStr); DataSet ds = uDAL.PopulateDataSet(sqlStr); dataGridSQL.DataSource = ds.Tables[0].DefaultView; } catch (Exception) {} } private void btnDAAccess_Click(object sender, System.EventArgs e) { try { string oledbConStr = ConfigurationSettings.AppSettings["NorthwindDevConString"]; string sqlStr = "SELECT TOP 5 * FROM 客户"; UniversalDAL uDAL = new UniversalDAL(DBType.OleDb); DataSet ds = uDAL.PopulateDataSet(sqlStr, oledbConStr); dataGridOleDb.DataSource = ds.Tables[0].DefaultView; } catch (Exception) {} } 最后本测试项目运行的效果如图3、图4所示。
图3 本测试项目在DataReader方式下的运行效果
图4 本测试项目在DataSet方式下的运行效果
六、总结 本文研究了运用ADO.NET中数据提供者对象的接口技术实现通用数据库访问层的方法,并给出了一个具体的测试项目以显示通用数据库访问层给实际项目带来的潜在益处和效率。ADO.NET中数据提供者对象的接口技术是非常有用的,而平常开发人员却很容易将其忽视掉,那样的结果就是实际项目的可扩展性、灵活性和通用性大打折扣。而通过运用这项技术,实际项目的通用性,灵活性和可扩展性都获得了大大的提高,这为项目以后可能的升级或移植作好了前期的准备,从而可以减小项目的相关风险系数。最后,笔者希望本文能对广大读者有不少帮助。 |