一、引言
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中資料提供者對象的接口技術是非常有用的,而平常開發人員卻很容易将其忽視掉,那樣的結果就是實際項目的可擴充性、靈活性和通用性大打折扣。而通過運用這項技術,實際項目的通用性,靈活性和可擴充性都獲得了大大的提高,這為項目以後可能的更新或移植作好了前期的準備,進而可以減小項目的相關風險系數。最後,筆者希望本文能對廣大讀者有不少幫助。 |