zoukankan      html  css  js  c++  java
  • 学习实践:使用模式,原则实现一个C++数据库访问类

    一、概述
    在我参与的多个项目中,大家使用libMySQL操作MySQL数据库,而且是源码即复用,在多个项目中有多套相同或相似的源码,这样的复用方式给开发带来了不变,而且libMySQL的使用比较麻烦,要应对很多的细节,很容易出错。
    我要写一个动态链接库,将对libMySQL的操作封装起来,以二进制复用代替源码级复用;要提供线程安全的接口,用户无需关系是否加锁这样细节性的问题,减少出错及死锁的机会,当然也要允许用户自己选择是否线程安全的访问数据库;要简化访问数据库的流程,接口越简单越好。
     
    我从2011年开始写这个库,我给它起名字叫HiDB。HiDB从2011年到现在,经历了一个由简到繁又有繁到简的过程,中间也包含了自己对程序理解的变化。
    很多的考虑看起来很细碎,例如代码的注释风格(选择我们一贯的注释风格还是doxygen的注释风格,代码大全中也讲到了注释风格。是否需要借鉴);代码的命名规范(匈牙利命名法还是Java,C#的命名法,还有很多开源项目的命名规范);C++设计新思维中讲到Policy classes,是否可以应用到这个库中;Effective C++中讲的接口与实现分离。
    这些年自己考虑过的一些东西包括注释风格的选择(我们一贯的注释,doxygen的注释风格,代码大全中也讲到了注释风格),出错时是使用错误码报出错误还是使用异常报出错误?C++设计新思维中讲的Policy classes,Effective C++中讲的接口与实现分析(Impl)
    二、接口
    (一)接口概述
    首先确定一下HiDB的接口。该库对外显示为一个HiDB类,该类应该包含的接口包括:
    1: 用户选择线程安全还是非线程安全
    2: 打开数据库连接
    3: 关闭数据库连接
    4: 执行insert,update等非查询操作的接口(ExecuteNoQuery)
    5: 获得查询结果第一行第一列的接口(ExecuteScaler)
    6: 获得一次查询的所有记录(多行多列)的接口(ExecuteQuery)
    7: 执行事务的接口(OnTransaction)
     
    所有的接口都在产生异常时抛出,需要对异常做一个封装,这个异常中最好能标识出异常产生的位置,异常的编号,异常的描述,引起异常的SQL语句。我设计的异常如下:
     
    /** @brief 数据库操作异常 */
    class HI_DB_EXPORT HiDBException
    {
    public:
    HiDBException();
    public:
    std::string ToSrting();
    public:
    std::string m_sql; /**< 本次操作的SQL语句 */
    std::string m_descript; /**< 异常描述 */
    std::string m_position; /**< 异常位置 */
    long m_errorId; /**< 异常编号 */
    HiDBType m_dbTyp; /**< 数据库类型 */
    };
     
     
    为了方便的抛出异常,我提供了一个宏(这个宏只有HiDB库使用)
    /** @brief 异常语句宏 */
    #define HiDBHelperOnError(ps, script,sql, id) 
    HiDBException exception;
    exception.m_position = ps;
    exception.m_descript = script;
    exception.m_sql = sql;
    exception.m_errorId = id;
    throw exception;
    //return false;
     
     
    提供该宏,除了简化用户输入外,还有一个想法就是如果用户不想使用异常,可以修改该宏,例如返回false。
     
    熟悉ADO.NET的朋友应该可以看出这些接口是借鉴了ADO.NET的。
    (二)具体接口
    <1> 构造函数
    本来在《C++设计新思维》中,有个Policy classes,适合这种根据用户需要提供安全或非安全接口的需求,但是Policy classes适合模板,不适合非模板,所以在这儿就没有使用,只是在构造函数中添加了一个布尔参数isUsingLock,如果为true则提供线程安全接口,否则接口是非线程安全的。
    HiDB打算在将来除支持MySQL外,还支持其他数据库,所以在构造函数中,除isUsingLock外,还有一个选择数据库类型的接口。
    将数据库类型写成枚举,则枚举为:
     
    /** @brief 数据库类型 */
    enum HiDBType
    {
    HiDBType_Invail, /**< 无效类型 */
    HiDBType_MySQL, /**< MySQL */
    };
     
     
    构造函数的声明就明确下来了:
     
    /**
    * @brief 构造函数
    * @param[in] type 数据库类型
    * @param[in] isUsingLock 是否需要使用互斥锁
    */
    HiDB(HiDBType type = HiDBType_MySQL, bool isUsingLock = false);
     
    <2>打开数据库连接
    打开数据库连接比较简单:
     
    /**
    * @brief 打开数据库连接
    * @param[in] conn 数据库连接字符串
    * @retval true:成功,false;失败
    * @par 实例:
    * @code
    * HiDB db;
    * if (db.Open("host=127.0.0.1;port=3306;dbname=test;user=root;pwd=root;charset=gbk;"))
    * {
    * // 打开成功
    * }
    * else
    * {
    * // 打开失败
    * }
    * @endcode
    */
    bool Open(const char* conn) throw (HiDBException);
     
     
    该接口的conn参数是一个字符串,这样字符串就具有扩展性,可以针对不同的数据库,进行不同的处理(这一点感觉不是很好,但是能提供接口的稳定性)。不同数据库,需要满足特定的格式,在MySQL中,要使用类似于“host=127.0.0.1;port=3306;dbname=test;user=root;pwd=root;charset=gbk;”的格式。
    <3> 关闭数据库连接
     
    /**
    * @brief 关闭据库连接
    */
    void Close(void);
     
    <4> IsOpen接口
    这个接口是延伸出来的,既然有open,close,提供一个IsOpen好像也是应该的,让用户了解当前的打开状态。
     
    /**
    * @brief 数据库连接是否打开
    */
    bool IsOpen();
     
     
    <5> 执行非查询语句接口
    执行SQL语句的接口,应该可以接收可变参数,除可变参数外,还应该有一个包含sql语句的字符串参数,所以接口定义如下:
     
    /**
    * @brief 执行SQL语句,并不返回结果
    * @param[in] conn SQL语句
    * @retval true:成功,false;失败
    * @par 实例:
    * @code
    * HiDB db;
    * if (db.ExecuteNoQuery("UPDATE table SET Paramer1='%s'
    * and Paramer2='%s' OR Paramer3=%d", "test1", "test2", 3))
    * {
    * // 执行成功
    * }
    * else
    * {
    * // 执行失败
    * }
    * @endcode
    */
    bool ExecuteNoQuery(const char* sql, ...) throw (HiDBException);
     
    <6> 获得查询结果第一行第一列的接口
    该接口的参数与执行非查询语句的接口一致,但是返回值应该为字符串,如果执行失败,则应该返回空字符串。触发异常时,抛出HiDBException异常。
     
     
    /**
    * @brief 执行SQL语句,返回一个结果
    * @param[in] sql SQL语句
    * @retval 获得的数据,如果为空,则失败
    */
    std::string ExecuteScalar(const char* sql, ...) throw (HiDBException);
     
    <7> 获得一次查询的所有记录(多行多列)的接口
    该接口的参数与执行非查询语句的接口一致。
    返回结果应该是包含多行多列的一个数据集,在ADO.NET中有DataTable,在这儿,我们可以用stl中的vector存储多行,map存储每行数据的多列。
    多以需要定义一个数据集:
    #ifndef HiDBTable
     
    typedef std::map<std::string, std::string> HiDBMap;
     
    /** @brief 查询结果 */
    typedef std::vector<HiDBMap> HiDBTable; /**< 查询结果 */
    #endif
     
     
    因为HiDBTable中包含多个map,所以最好避免拷贝,使用stl的shared_ptr来避免多次拷贝:
     
    /**
    * @brief 执行SQL语句,返回一个结果集合
    * @param[in] sql SQL语句
    * @retval 存储查询记录的智能指针
    */
    std::shared_ptr<HiDBTable> ExecuteQuery(const char* sql, ...) throw (HiDBException);
     
     
    <8> 执行事务的接口
    执行事务接口是一个Command模式的接口,参数应该是一个函数对象。该对象为无参无返回值的函数对象即可。stl中提供了function对象。(在最初的版本中是自己实现函数对象的)
     
     
    /**
    * @brief 在事务中执行处理
    * @param[in] fun 处理函数
    */
    void OnTransaction(const std::function<void()>& fun) throw (HiDBException);
     
    (三) 接口的使用案例
    HiDB m_DB = new HiDB(HiDBType_MySQL, true);
    try
    {
    bool ret = m_DB->Open(
    "host=127.0.0.1;port=3306;dbname=test;user=root;pwd=root;charset=gbk;"
    );
    m_DB->ExecuteNoQuery("drop table if exists table1;");
    string val = m_DB->ExecuteScalar(
    "SELECT column4 FROM table1 WHERE column1='%s' AND column3=%d",
    &val, "hitest", 59);
    shared_ptr<HiDBTable> table = this->m_DB->ExecuteQuery(
    "SELECT * FROM table1 WHERE column1='%s' OR column1='%s'",
    "hitest", "mytest");
    }
    catch(HiDBException& e)
    {
    // ...
    }
     
    (四) 其他
    其实,我以前提供的接口比现在要复杂很多,首先我模仿ADO.NET,对SQL参数进行了封装,封装了SQL参数的名称,类型,是否为空,值等信息,对获得的数据也进行了类似的封装。 ,还针对SQL参数提供了Create和Delete函数。
    有一个同时看了我的接口后说,我的接口太复杂了,分层不是明确。我接受了他的建议,就将接口修改为现在的接口了。
    另外,执行事务接口,最开始我是自己创建函数对象的,这也增加了复杂度,后来使用了stl的function对象,辅以lambda表达式,则使用起来简单多了。
    (五) 完整的接口:
    <1>HiDBCommon.h 提供接口应用到的相关枚举和结构体
    #pragma once
     
    /**
    * @defgroup 数据库模块
    * @{
    */
    #include "HiDBExport.h"
     
    #include <string>
    #include <vector>
    #include <map>
    #include <sstream>
     
    /** @brief 数据库类型 */
    enum HiDBType
    {
    HiDBType_Invail, /**< 无效类型 */
    HiDBType_MySQL, /**< MySQL */
    };
     
    #ifndef HiDBTable
     
    typedef std::map<std::string, std::string> HiDBMap;
     
    /** @brief 查询结果 */
    typedef std::vector<HiDBMap> HiDBTable; /**< 查询结果 */
    #endif
     
    /** @brief 数据库操作异常 */
    class HI_DB_EXPORT HiDBException
    {
    public:
    HiDBException();
    public:
    std::string ToSrting();
    public:
    std::string m_sql; /**< 本次操作的SQL语句 */
    std::string m_descript; /**< 异常描述 */
    std::string m_position; /**< 异常位置 */
    long m_errorId; /**< 异常编号 */
    HiDBType m_dbTyp; /**< 数据库类型 */
    };
     
    /** @brief 异常语句宏 */
    #define HiDBHelperOnError(ps, script,sql, id) 
    HiDBException exception;
    exception.m_position = ps;
    exception.m_descript = script;
    exception.m_sql = sql;
    exception.m_errorId = id;
    throw exception;
    //return false;
     
    /**//** @}*/ // 数据库模块
     
    <2> HiDB.h 主要的接口:
    #pragma once
     
    /**
    * @defgroup 数据库模块
    * @{
    */
     
    #include <memory>
    #include <functional>
     
    #include "HiDBCommon.h"
     
    class HiDBImpl;
     
    #pragma warning (disable: 4290)
     
    /**
    * @brief 数据库操作类,封装数据库的通用操作,本类使用策略模式实现
    * @author 徐敏荣
    * @date 2012-06-14
    *
    * @par 修订历史
    * @version v0.5 
    
    * @author 徐敏荣
    * @date 2012-06-14
    * @li 初始版本
    * @version v0.6 
    
    * @author 徐敏荣
    * @date 2014-08-04
    * @li 简化程序
    *
    */
    class HI_DB_EXPORT HiDB
    {
    public:
     
    /**
    * @brief 构造函数
    * @param[in] type 数据库类型
    * @param[in] isUsingLock 是否需要使用互斥锁
    */
    HiDB(HiDBType type = HiDBType_MySQL, bool isUsingLock = false);
     
    /**
    * @brief 析构函数
    */
    ~HiDB();
     
    public:
     
    /**
    * @brief 打开数据库连接
    * @param[in] conn 数据库连接字符串
    * @retval true:成功,false;失败
    * @par 实例:
    * @code
    * HiDB db;
    * if (db.Open("host=127.0.0.1;port=3306;dbname=test;user=root;pwd=root;charset=gbk;"))
    * {
    * // 打开成功
    * }
    * else
    * {
    * // 打开失败
    * }
    * @endcode
    */
    bool Open(const char* conn) throw (HiDBException);
     
    /**
    * @brief 关闭据库连接
    */
    void Close(void);
     
    /**
    * @brief 数据库连接是否打开
    */
    bool IsOpen();
     
    public:
     
    /**
    * @brief 执行SQL语句,并不返回结果
    * @param[in] conn SQL语句
    * @retval true:成功,false;失败
    * @par 实例:
    * @code
    * HiDB db;
    * if (db.ExecuteNoQuery("UPDATE table SET Paramer1='%s'
    * and Paramer2='%s' OR Paramer3=%d", "test1", "test2", 3))
    * {
    * // 执行成功
    * }
    * else
    * {
    * // 执行失败
    * }
    * @endcode
    */
    bool ExecuteNoQuery(const char* sql, ...) throw (HiDBException);
     
    public:
     
    /**
    * @brief 执行SQL语句,返回一个结果
    * @param[in] sql SQL语句
    * @retval 获得的数据,如果为空,则失败
    */
    std::string ExecuteScalar(const char* sql, ...) throw (HiDBException);
     
    public:
     
    /**
    * @brief 执行SQL语句,返回一个结果集合
    * @param[in] sql SQL语句
    * @retval 存储查询记录的智能指针
    */
    std::shared_ptr<HiDBTable> ExecuteQuery(const char* sql, ...) throw (HiDBException);
     
    public:
     
    /**
    * @brief 在事务中执行处理
    * @param[in] fun 处理函数
    */
    void OnTransaction(const std::function<void()>& fun) throw (HiDBException);
     
    private:
    /**
    * @brief 数据库操作实现指针
    */
    HiDBImpl* m_Impl; /**< 数据库操作实现指针 */
    };
     
    /**//** @}*/ // 数据库模块
     
    三 实现
    实现采用了从《Effective C++》中学习到的实现与接口相分析的原则,在HiDB中使用HiDBImpl实现访问数据库的逻辑。
    (一) 可变参数的处理
    当然,在HiDB中是需要解决根据sql参数和可变参数拼装成一个完整SQL语句的问题。
    该问题使用一个宏来实现:
    #if !defined(HISDB_ON_VARLIST)
    #define HISDB_ON_VARLIST(x, y) 
    char chArr[2048] = {0};
    char* pchar = &chArr[0];
    va_list pArgList;
    va_start(pArgList, y);
    ::_vsnprintf(chArr, 2047, x, pArgList); 
    va_end(pArgList) ;
    #endif
     
    (二) 互斥锁的实现
    自己根据临界区,实现了一个互斥锁,互斥锁接口如下:
    1: 构造函数: 实现临界区的初始化
    2: 析构函数: 实现临界区的删除
    3: 进入临界区
    4: 离开临界区
    实现函数如下:
     
    /**
    * @brief 临界区访问类,主要封装windows临界区的访问,该类主要在栈中使用,利用局部变量的构造和析构函数出入临界区
    * @author 徐敏荣
    * @date 2012-06-14
    *
    * @par 修订历史
    * @version v0.5 
    
    * @author 徐敏荣
    * @date 2012-06-14
    * @li 初始版本
    *
    */
    class HiCritical
    {
    public:
     
    /**
    * @brief 构造函数
    */
    HiCritical()
    {
    ::InitializeCriticalSection(&cs);
    }
     
    /**
    * @brief 析构函数
    */
    ~HiCritical()
    {
    ::DeleteCriticalSection(&cs);
    }
     
    /**
    * @brief 进入临界区
    */
    void Enter()
    {
    ::EnterCriticalSection(&cs);
    }
     
    /**
    * @brief 离开临界区
    */
    void Leave()
    {
    ::LeaveCriticalSection(&cs);
    }
     
    CRITICAL_SECTION* GetSection()
    {
    return &cs;
    }
    private:
     
    /**
    * @brief 临界区对象
    */
    CRITICAL_SECTION cs; /**< 临界区对象 */
    };
     
     
    另外还提供一个临界区管理类(HiCriticalMng),在构造该类时,进入临界区,析构该类时离开临界区。如果构造函数中传入的是NULL,则不进行任何互斥处理。
     
    /**
    * @brief 临界区访问管理类,利用构造函数进入临界区,利用西沟函数离开临界区
    * 如果向构造函数提供NULL参数,则不使用临界区。
    *
    */
    class HiCriticalMng
    {
    public:
    HiCriticalMng(HiCritical& crl): cl(&crl)
    {
    cl->Enter();
    }
     
    HiCriticalMng(HiCritical* crl): cl(crl)
    {
    if (cl)
    {
    cl->Enter();
    }
    }
     
    ~HiCriticalMng()
    {
    if (cl)
    {
    cl->Leave();
    }
    }
     
    private:
    HiCritical* cl;
    };
     
    (三) HiDBImpl的接口
    作为数据库访问类,HiDBImpl实现HiDB需要的所有接口,所以HiDBImpl与HiDB接口类似,但是HiDBImpl接口接收的参数为完整的SQL语句(因为带可变参数的SQL语句已经被HiDB处理了)。
    HiDBImpl不但要支持MySQL,以后还要支持其他数据库,所以不能有LibMySQL相关的东西,HiDBImpl应该是一个基类,可以被派生,例如派生出支持LibMySQL的子类。
    HiDBImpl要线程安全的,所以要包含互斥锁HiCritical,又要可以非线程安全(HiCriticalMng支持NULL参数),所以HiCritical需要时这指针,这样,HiDBImpl的接口就出来了。
    HiDBImpl接口如下:
     
    #pragma once
    /**
    * @defgroup 数据库操作实现类接口类
    * @brief 数据库操作实现类接口类,声明数据库操作实现类的接口。
    * @author 徐敏荣
    * @date 2012-06-14
    *
    * @par 修订历史
    * @version v0.5 
    
    * @author 徐敏荣
    * @date 2012-06-14
    * @li 初始版本
    * @{
    */
     
    #include "DB/HiDB.h"
    class HiCritical;
    /**
    * @brief 数据库操作实现类接口类,声明数据库操作实现类的接口
    *
    */
    class HiDBImpl
    {
    public:
     
    /**
    * @brief 构造函数
    * @param[in] isUsingLock 是否需要使用互斥锁
    */
    HiDBImpl(bool isUsingLock);
     
    /**
    * @brief 析构函数
    */
    virtual ~HiDBImpl();
     
    public:
     
    /**
    * @brief 打开数据库连接
    * @param[in] conn 数据库连接字符串
    * @retval true:成功,false;失败
    */
    virtual bool Open(const char* conn) = 0;
     
    /**
    * @brief 关闭据库连接
    */
    virtual void Close(void) = 0;
     
    public:
     
    /**
    * @brief 执行SQL语句,并不返回结果
    * @param[in] conn SQL语句
    * @retval true:成功,false;失败
    */
    virtual bool ExecuteNoQuery(const char* sql) = 0;
     
    public:
     
    /**
    * @brief 执行SQL语句,返回一个结果
    * @param[in] sql SQL语句
    * @param[out] value 取得的结果
    * @retval true:成功,false;失败
    */
    virtual std::string ExecuteScalar(const char* sql) = 0;
    public:
     
    /**
    * @brief 执行SQL语句,返回一个结果集合
    * @param[in] sql SQL语句
    * @param[out] table 取得的结果集合
    * @retval true:成功,false;失败
    */
    virtual std::shared_ptr<HiDBTable> ExecuteQuery(const char* sql) = 0;
     
    public:
     
    /**
    * @brief 事物处理
    * @retval true:成功,false;失败
    */
    virtual void OnTransaction(const std::function<void()>& fun) = 0;
     
     
    protected:
     
    /**
    * @brief 临界区对象,为空表示不需要考虑资源并发访问
    */
    HiCritical* m_pCritical;
    };
     
    /**//** @}*/ // 数据库操作实现类接口类
     
    (四)HiDB的实现:
    由HiDB负责实现可变参数转换为完整SQL语句,HiDBImpl负责实现所有数据库访问逻辑,并要为以后添加其他数据库支持这些需求可以推到出HiDB的实现代码:
    #include <stdarg.h>
    #include "DB/HiDB.h"
    #include "HiDBMySQL.h"
     
    using namespace std;
     
    #if !defined(HISDB_ON_VARLIST)
    #define HISDB_ON_VARLIST(x, y) 
    char chArr[2048] = {0};
    char* pchar = &chArr[0];
    va_list pArgList;
    va_start(pArgList, y);
    ::_vsnprintf(chArr, 2047, x, pArgList); 
    va_end(pArgList) ;
    #endif
     
     
    static bool IsImplOK(HiDBImpl* db)
    {
    if (!db)
    {
    return false;
    }
    /*
    if (!db->IsOpen())
    {
    return false;
    }*/
    return true;
    }
     
    // 构造函数
    HiDB::HiDB(HiDBType type, bool isUsingLock):m_Impl(NULL)
    {
    if (type == HiDBType_MySQL)
    {
    this->m_Impl = new HiDBMySQL(isUsingLock);
    }
    }
     
    // 析构函数
    HiDB::~HiDB()
    {
    if (this->m_Impl)
    {
    delete this->m_Impl;
    this->m_Impl = NULL;
    }
    }
     
    // 打开数据库连接
    bool HiDB::Open(const char* conn)
    {
    if (!this->m_Impl)
    {
    return false;
    }
     
    return this->m_Impl->Open(conn);
    }
     
    bool HiDB::IsOpen()
    {
    if (!this->m_Impl)
    {
    return false;
    }
     
    return true;//this->m_Impl->IsOpen();
    }
     
    void HiDB::Close(void)
    {
    if (!IsImplOK(this->m_Impl))
    {
    return;
    }
     
    return this->m_Impl->Close();
    }
     
    bool HiDB::ExecuteNoQuery(const char* sql, ...)
    {
    if (!IsImplOK(this->m_Impl))
    {
    return false;
    }
     
    HISDB_ON_VARLIST(sql, sql);
     
    return this->m_Impl->ExecuteNoQuery(chArr);
    }
     
    string HiDB::ExecuteScalar(const char* sql, ...)
    {
    if (!IsImplOK(this->m_Impl))
    {
    return "";
    }
     
    HISDB_ON_VARLIST(sql, sql);
     
    return this->m_Impl->ExecuteScalar(chArr);
    }
     
    std::shared_ptr<HiDBTable> HiDB::ExecuteQuery(const char* sql, ...)
    {
    if (!IsImplOK(this->m_Impl))
    {
    return NULL;
    }
    HISDB_ON_VARLIST(sql, sql);
     
    return this->m_Impl->ExecuteQuery(chArr);
    }
     
    void HiDB::OnTransaction(const std::function<void()>& fun)
    {
    if (!IsImplOK(this->m_Impl))
    {
    HiDBHelperOnError("HiDB::OnTransaction",
    "HiDB is not impl", "", 0);
    }
    return this->m_Impl->OnTransaction(fun);
    }
     
    四 后记
    至此,HiDB所有主要的接口和主要的实现就介绍的差不多,其他更多的实现,可以参照源码自己实现。类图将在本文后面提供。
    后续的工作包括对HiDB进行测试,我期望能进行自动化测试,类似于Junit。但是我并不知道C++有哪些自动化测试工具,没办法,只有自己写C++自动化测试工具来测试HiDB了,后面的文章我打算介绍一下我写的一个C++自动化测试工具。
    HiDB类图:

     
    源代码:http://download.csdn.net/detail/xumingxsh/7778417
     
    本文为我从自己的csdn博客转过来的文章,应该算是原创吧
  • 相关阅读:
    topcoder srm 320 div1
    topcoder srm 325 div1
    topcoder srm 330 div1
    topcoder srm 335 div1
    topcoder srm 340 div1
    topcoder srm 300 div1
    topcoder srm 305 div1
    topcoder srm 310 div1
    topcoder srm 315 div1
    如何统计iOS产品不同渠道的下载量?
  • 原文地址:https://www.cnblogs.com/admin11/p/3926712.html
Copyright © 2011-2022 走看看