在基于数据库应用的开发过程中,我们可能会遇到如下情况:

  •  需要实现业务代码与具体数据库操作代码的分离。
  •  项目可能会更换不同类型的数据库。
  •  项目中可能会使用多于一种的数据库。

如果你只是在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语句来完成。