初探資料庫通用程式碼庫的封裝(C#版)

曹化宇發表於2014-08-12

在基於資料庫應用的開發過程中,我們可能會遇到如下情況:

  •  需要實現業務程式碼與具體資料庫操作程式碼的分離。
  •  專案可能會更換不同型別的資料庫。
  •  專案中可能會使用多於一種的資料庫。

如果你只是在Windows窗體或Web窗體中使用ADO.NET元件來運算元據庫,那麼,當你遇到以上兩種情況時,會感到非常的麻煩;此時,我們應該做的就是將資料庫的操作從整個專案的程式碼結構中提煉出來,並進行相應的封裝;本文將從六個過程來討論這些程式碼的封裝與應用問題:

  •  資料庫操作的抽象。
  •  使用非連線資料元件。
  •  合理的程式碼結構。
  •  實現不同型別資料庫的具體操作。
  •  必要時使用包裝類。
  •  程式碼庫應用。

請注意,本文中的程式碼進行了簡化,你可以根據實際需要修改或擴充套件程式碼功能。

1. 資料庫操作的抽象

資料庫操作的抽象工作,也就是要找到對於資料庫的共性操作。如果你有一定的開發資料應用開發經驗,可能會對資料庫的基本操作有一些認識,但是,在這裡我們不會假設讀者是一個有經驗的開發者。 那麼,我們如何完成資料庫操作的抽象工作呢?

其實,我們還有一個方法,那就是檢視ADO.NET元件的定義。在MSDN Library或Help Library中,我們可以找到一系列資料庫操作元件的定義和說明。

接下來,我們就從簡單的功能開始討論,如資料連線和命令執行功能。用於資料連線操作的相關型別有IDbConnection介面、DbConnection類、SqlConnection類和OleDbConnection類等。用於命令執行的相關型別有IDbCommand介面、DbCommand類、SqlCommand類和OleDbCommand類等。

如果我們觀察這些型別的定義,就可以找到一些共性的東西,特別是具體的資料庫操作型別,如SqlConnection和OleDbConnection、SqlCommand和OleDbCommand等。

對於資料庫的連線,我們可以看到如下一些共性的內容:

  •  ConnectionString屬性,設定資料庫連線字串。
  •  Open()方法,開啟資料庫連線。
  •  Close()方法,關閉資料庫連線。

而對於命令執行操作,我們可以看到如下的共性操作:

  •  ExecuteNonQuery()方法,執行命令,如果是insert、update或delete語句,返回影響的記錄數;否則返回-1。
  •  ExecuteScalar()方法,執行命令,返回查詢結果中的第一條記錄第一個欄位的值,如果沒有查詢結果則返回null值。
  •  ExecuteReader()方法,執行命令,返回查詢結果。

實際上,通過這些內容,我們就已經找到了資料庫的一些共性操作,但是,我們知道,單純的資料庫連線操作實際上並沒有太多的功能,如果我們將資料庫的連線操作和命令執行操作進行組合,就可以在程式碼中就能夠進一步地簡化資料庫操作程式碼;而對於這樣的組合,我們稱為資料庫引擎元件,它的功能包括:

  •  設定資料庫連線字串,然後在需要時自動開啟資料庫連線。
  •  執行命令,並返回影響的行數,如封裝為ExecuteNonQuery()方法。
  •  執行命令,並返回查詢結果中第一行第一個欄位的值,如封裝為ExecuteScalar()方法。
  •  執行命令,返回查詢結果。此時,而為了更自由地使用查詢結果,我們並不使用ExecuteReader()方法,而是將返回的查詢結果設定為DataSet型別,可以命名為ExecuteDataSet()方法。

以上是關於資料庫引擎元件功能的總結,主要包含了資料庫連線、命令執行和資料讀取操作;在實際開發中,我們還需要對資料表進行更多的操作,如讀取一條或多條記錄、在資料表中插入新資料或更新現有資料等。

使用SQL語句,我們可以很方便對資料進行批量地操作,但出於資料同步和效能等原因,除非有需要,我們還是應該一次儘可能少的運算元據,比如,我們可以一次對一條記錄進行操作,完成這一操作,我們可以封裝為資料記錄操作元件,其成員包括:

  •  資料庫引擎,用於指定資料來源。
  •  資料表名稱。
  •  資料表中的主鍵名稱,為了簡化功能,我們只使用單主鍵;此外,為了更方便地利用SQL語法,主鍵應儘可能地使用整數型別的Identity欄位(在資料庫中使用整數作為主鍵型別會有更高的效能)。
  •  讀取一條記錄,如Load()方法。
  •  將一條記錄儲存到資料表,如Save()方法。
  •  判斷主鍵是否存在,如Exists()方法。

本部分,我們對資料庫的常用操作進行了抽象分析,分討論了需要封裝的兩個元件:資料庫引擎元件和資料記錄操作元件。接下來,我們就討論如何實現這些元件。

2. 使用非連線資料元件

如果要達到資料庫通用操作的效果,我們封裝後的程式碼“介面”部分就不應該有具體資料庫型別的操作元件,也就是說,在元件的“介面”部分不應該使用ADO.NET中的連線類資料元件。這裡有兩點需要注意:

  •  “介面”是指元件中提供給其他程式碼呼叫的方式、方法,如介面、類、列舉型別等,而不只是介面型別。
  •  只是在呼叫介面中不使用連線類資料元件,而在後臺,我們並沒有辦法在不使用連線元件的情況下對資料庫進行操作,稍後討論。

在呼叫介面中使用非連線資料元件時,我們可以使用的型別包括:介面型別、非連線資料元件(如DataSet、DataTable等),以及自定義元件。

介面部分 對於資料庫引擎和資料記錄操作,我們可以分別定義為兩個介面型別。根據前面抽象分析結果,我們可以定義為:

public enum EDbEngineType 
{
    Unknow = 0,
    SqlServer = 1,
    OleDb = 2
}

public interface IDbEngine
{
    string CnnStr {get; set;}
    EDbEngineType EngineType {get;}
    int ExecuteNonQuery(string sql);
    object ExecuteScalar(string sql);
    DataSet ExecuteDataSet(string sql);
}

public interface IDbRecord
{
    IDbEngine DbEngine {get;}
    string TableName {get;}
    string PkName {get;}
    CDataCollection Load(string sql);
    object Save(CDataCollection dataColl);
    bool Exists(object pkValue);
}

程式碼中請注意,EDbEngineType列舉型別用於定義資料庫型別,這裡我們定義了常用的SQL Server資料庫和OLEDB資料來源。另一個需要注意的型別是CDataCollection,這是一個自定義型別,稍後討論。

此外,在這些介面中定義的方法,我們只支援了SQL語句的執行,如果在SQL語句執行中需要傳遞引數,我們可以定義帶有CDataCollection物件引數的方法,如:

int ExecuteNonQuery(string sql, CDataCollection dataColl);

為了簡化示例,我們就不具體實現了。

自定義元件

在這裡,我們需要定義兩個基本的元件,它們是:

  • CDataItem類,表示一個資料項,主要有Name和Value屬性,分別表示資料名稱和資料值。在實際應用中,可以在此類中新增更多的型別轉換方法,以便快速獲取相應型別的資料,可參考《淺談C#中的資料型別轉換與物件複製》一文。
  • CDataCollection類,表示一個資料集合,其包含了一系列的CDataItem物件。在其內部,我們使用ArrayList型別來儲存資料集合,這樣可以有效保證資料項的順序,這一點在向一些資料庫傳遞引數時非常重要(如Access資料庫)。我們可以定義一些方法來新增或移除資料項,如Add()、Append()和Remove()方法,此外,我們還可以定義兩個索引,分別使用整數索引或資料名索引來獲取資料項。

下面的程式碼,我們給出了CDataItem和CDataCollection類的簡單定義,大家可以根據需要擴充套件它們的功能。

public class CDataItem
{
    private string myName;
    private object myValue;
    public CDataItem(string sName, object oValue)
    {
        myName = sName;
        myValue = oValue;
    }
    public CDataItem() : this("", null) { }

    public string Name
    {
        get { return myName; }
        set { myName = value; }
    }

    public object Value
    {
        get { return myValue; }
        set { myValue = value; }
    }
}

// CDataCollection類
public class CDataCollection
{
    private ArrayList myData;
    public CDataCollection()
    {
        myData = new ArrayList();
    }

    public int Find(string sName)
    {
        for(int i=0; i<myData.Count; i++)
        {
            if ( String.Compare(sName, 
                    (myData[i] as CDataItem).Name, false)==0)
                return i;
        }
        return -1;
    }

   // 你可以充分利用Find()方法實現其他成員
   // public CDataItem this[int index] {get;set;}
   // public CDataItem this[string sName] {get;set;}
   // public void Add(CDataItem dItem); // 資料項存在則替換,否則追加
   // public void Append(CDataItem dItem); // 直接追加,效能更高
   // public void Remove(int index);
   // public void Remove(string sName);
}

在我們討論的程式碼庫中,這兩個自定義元件非常重要,它們沒有具體的資料型別(但可隨時給出所需要的資料型別),可以在介面、業務程式碼和資料庫之間進行資料的傳遞,靈活性很高;此外,它們定義為引用型別,傳遞的效率也可以保證。

3. 合理的程式碼結構

基本的操作介面和型別已經定義完成,但它們還不能進行真正的工作,因為還沒有真正的資料庫操作程式碼來實現它們。接下來,我們的工作就是合理的組織這一系列的程式碼,為了便於管理和使用,在我的程式碼庫中,用於資料庫操作的程式碼都定義在了chyx.datax命令空間。

現在,我們需要停下Coding的步伐,思考一下程式碼的結構問題。

實際上,我們可以在ADO.NET元件中學習到程式碼結構的組織方法,以命令執行元件為例,如下圖:

enter image description here

瞭解設計模式的朋友可以在這一結構中找到模板方法模式(Template Method Pattern)的影子;而我們的資料庫引擎和資料記錄元件也可以參考此方法來組織。

設計模式是通過實踐和總結,整理出的與某一問題相對應的解決方案,而設計模式的應用是一個很複雜的問題,我們應該在充分掌握設計模式特點的情況下來合理、正確地應用,只有這樣才能使用程式碼結構更靈活、更有彈性;否則,只能帶來不必要的麻煩。

回到我們的程式碼庫,為了簡化一些程式碼,我們可以定義一個資料庫引擎基類和一個資料記錄操作基類。

資料庫引擎基類如下:

public abstract class CDbEngineBase : IDbEngine
{
    private string myCnnStr;
    // 建構函式
    public CDbEngine(string cnnStr) { myCnnStr = cnnStr; }
    public CDbEngine() { myCnnStr = ""; }
    //
    public string CnnStr 
    {
        get { return myCnnStr; }
        set { myCnnStr = value; }
    }
    public abstract EDbEngineType EngineType {get;}
    public abstract int ExecuteNonQuery(string sql);
    public abstract object ExecuteScalar(string sql);
    public abstract DataSet ExecuteDataSet(string sql);
}

資料記錄操作基類如下:

public abstract class CDbRecordBase : IDbRecord
{
    protected IDbEngine myDbEngine;
    protected string myTableName;
    protected string myPkName;
    //
    public IDbEngine DbEngine { get{ return myDbEngine; } }
    public string TableName { get{ return myTableName; } }
    public string PkName { get{ return myPkName; } }
    //
    public abstract CDataCollection Load(string sql);
    public abstract object Save(CDataCollection dataColl);
    public abstract bool Exists(object pkValue);
}

4. 實現不同型別資料庫的具體操作

最終,我們還是要使用具體的資料庫連線類元件來完成真正的工作。本部分,我們將以SQL Server 2005資料庫為例,來編寫SQL Server資料庫相應的元件,如CSqlEngine類和CSqlRecord類。大家可以編寫其他型別的資料操作類,在這一過程中,大家應該注意一些問題SQL語法上的差異,每一種資料庫對於SQL語法的實現,都存在不同程度上的區別,它們並不都是完全按照ANSI SQL標準來實現的。比如:

  •  特殊功能的應用。比如,我們在程式碼中可以使用SQL Server 2005中的inserted表返回新插入記錄的主鍵值;而在Access資料庫中,我們可以使用“select @@IDENTITY;”語句返回新插入的Identity欄位值(這也是我們為什麼說主鍵要儘量使用整數Identity欄位的原因,此外,這條語句在很多資料庫中都是有效的。)。
  •  具體語法上的區別。在Access和SQL Server 2005中,只讀取將幾條記錄,可以使用top子句,如“select top 1 field_name from table_name;”,而在MySQL資料庫,此功能應該使用limit子句,如“select field_name from table_name limit 1;”;
  •  ……

如果我們需要使用哪一種資料庫,就必須對它的SQL進行深入的學習和理解,以便達到資料庫操作的最佳效果。

接下來,我們就來看一看CSqlEngine類的定義,它的功能是對SQL Server 2005資料庫操作。

public class CSqlEngine : CDbEngineBase
{
    // 建構函式
    public CSqlEngine(string cnnStr) : base(cnnStr) {}
    public CSqlEngine() {}
    // 
    public override EDbEngineType 
    { get { return EDbEngineType.SqlServer; } }
    //
    public override int ExecuteNonQuery(string sql)
    {
        try
        {
            using(SqlConnection cnn = new SqlConnection(this.CnnStr))
            {
                cnn.Open();
                SqlCommand cmd = cnn.CreateCommand();
                cmd.CommandText = sql;
                return cmd.ExecuteNonQuery();
            }
        }
        catch { return -1; }
    }
    //
    public override object ExecuteScalar(string sql)
    {
        try
        {
            using(SqlConnection cnn = new SqlConnection(this.CnnStr))
            {
                cnn.Open();
                SqlCommand cmd = cnn.CreateCommand();
                cmd.CommandText = sql;
                return cmd.ExecuteScalar();
            }
        }
        catch { return null; }
    }
    //
    public override DataSet ExecuteDataSet(string sql)
    {
        try
        {
            using(SqlConnection cnn = new SqlConnection(this.CnnStr))
            {
                cnn.Open();
                SqlCommand cmd = cnn.CreateCommand();
                cmd.CommandText = sql;
                using(SqlDataAdapter ada = new SqlDataAdapter(cmd))
                {
                    DataSet ds = new DataSet();
                    ada.Fill(ds, "table1");
                    return ds;
                }
            }
        }
        catch { return null; }
    }
}

接下來是CSqlRecord類的定義:

public class CSqlRecord : CDbRecordBase
{
    // 建構函式
    public CSqlRecord(IDbEngine dbe, string tableName,string pkName)
    {
        if (dbe.EngineType == EDbEngineType.SqlServer)
        {
            myDbEngine = dbe;
            myTableName = tableName;
            myPkName = pkName;
        }
    }
    //
    public IDbEngine DbEngine { get{ return myDbEngine; } }
    public string TableName { get{ return myTableName; } }
    public string PkName { get{ return myPkName; } }
    //
public override CDataCollection Load(string sql)
{
    try
{
    using(SqlConnection cnn = 
new SqlConnection(this.DbEngine.CnnStr))
{
    cnn.Open();
    SqlCommand cmd = cnn.CreateCommand();
    cmd.CommandText = sql;
    using(SqlDataReader dr = cmd.ExecuteReader())
{
    if(dr.Read())
    {
        CDataCollection dc = new CDataCollection();
        for(int i=0;i < dr.FieldCount;i++)
        {
            dc.Append(dr.GetName(i),dr[i]);
        }
        return dc;
    }
    else
    {
        return null;
    }
    }
}
}
catch { return null; }
}
    // 儲存記錄時應注意dataColl中是否包含主鍵值,決定更新或插入
    // public override object Save(CDataCollection dataColl);
    // public override bool Exists(object pkValue);
}

5. 必要時使用包裝類

在一個專案中使用資料庫引擎元件時,如果只有一個資料庫,我們可以定義一個資料庫引擎,比如在CCApp類中定義一個Db物件作為專案的主資料庫。

public static class CCApp
{
private static string DbCnnStr = @””;
public static IDbEngine Db = new CDbEngine(DbCnnStr);
// public static IDbEngine Db = new COleDbEngine(DbCnnStr);
}

我們可以在程式碼中只使用CCApp.Db物件來運算元據庫,而且可以使用統一的方式,即IDbEngine介面型別中定義的成員來運算元據庫。

使用資料記錄操作元件時,可能會有些不一樣。

首先,在專案中,可能需要很多業務類都會讀/寫資料表中的記錄,此時,我們可以封裝一個通用類作為這些業務元件的基類,而這個類將是各種資料庫型別資料記錄操作的包裝類。如:

public class CDbRecord : IDbRecord
{
    private IDbRecord myRec;
    // 建構函式
    public CDbRecord(IDbEngine dbe, string tableName, string pkName)
    {
        switch(dbe.EngineType)
        {
        case EDbEngineType.SqlServer:
        {
             myRec = new CSqlRecord(dbe,tableName,pkName);
        }break;
        case EDbEngineType.OleDb:
         {
             myRec = new COleDbRecord(dbe,tableName,pkName);
         }break;
         default:
        {
            myRec = null;
        }break;
        }
    }
    // 
    public IDbEngine DbEngine 
    { 
        get
        { 
            if (myRec == null) return null;
            return myRec.DbEngine;
        } 
    }
    // public string TableName { get;}
    // public string PkName { get; }
    //
    // public CDataCollection Load(string sql);
    // public object Save(CDataCollection dataColl);
    // public bool Exists(object pkValue);
}

在這個包裝類中,實現了IDbRecrod介面的成員,都是具體型別的資料記錄元件來完成的;在業務程式碼中建立一個基於資料記錄操作類的子類時,可很方便的完成,如:

public class CUser : CDbRecord
{
    public CUser() : base(CCApp.Db, “UserMain”, “UserId”)
    { }
    // 其他功能
}

在CUser類中,已經實現了UserMain表的基本操作,我們只需要新增相應的擴充套件功能就可以了。並且,我們可以直接使用this.DbEngine來做很多工作,比如查詢資料或執行特定的SQL語句。但在執行SQL時應注意不同資料庫SQL語法的區別,我們還是應該更多地使用IDbEngine介面或IDbRecord介面中的成員來完成資料操作任務。

6. 程式碼庫應用

現在,我們已有了一個比較完整的資料庫操作程式碼庫,其結構示意圖如下:

enter image description here

從本圖中,我們可以看到程式碼結構主要的組成部分:

通過統一的資料庫訪問介面IDbEngine,我們可以使用相同的方法訪問各種型別的資料庫;這是通過實現此介面的各種資料庫引擎類具體實現的,如CSqlEngine、COleDbEngine等。

介面IDbRecord定義了對於單條資料記錄的操作方法,不同型別的資料庫使用相應用的型別來實現,如CSqlRecord、COleDbRecord等。

CDbRecord類是一個包裝類,其目的是為了方便在在業務程式碼中使用。可以使用一個業務類直接繼承此類,從而實現基本的資料表讀/寫操作;然後,在業務類中可以擴充套件相應的功能。通過更改資料引擎(IDbEngine型別),可以方便地切換資料來源,而不需要過多的考慮資料庫的型別。

使用非連線資料型別或自定義型別(如CDataItem、CDataCollection)可以在資料庫、資料操作程式碼、業務程式碼、介面之間進行資料的傳遞,而與資料庫型別無關。

在使用程式碼庫時,應注意:

擴充套件資料庫型別支援。需要定義相應的資料庫引擎類、資料記錄操作類,需要重寫其中一些成員;還需要在CDbRecord包裝類的建構函式中新增相應的支援。

功能擴充套件。對於常用的資料庫操作,我們可以進一步擴充套件介面成員,儘量使用標準的方法對資料庫進行操作,而不是在業務程式碼或介面中直接寫SQL語句來完成。

相關文章