• 学习实践:使用模式,原则实现一个C++数据库訪问类


    一、概述

    在我參与的多个项目中。大家使用libMySQL操作MySQL数据库,并且是源代码级复用,在多个项目中同样或相似的源代码。这种复用方式给开发带来了不便。
    libMySQL的使用比較麻烦。非常easy出错。
    基于以上原因。我写了一个动态链接库,将对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
  • 相关阅读:
    洛谷 P2713:「罗马游戏」
    洛谷 P4014:「分配问题」
    「洛谷P1433」吃奶酪
    信号的频谱分析,加噪降噪处理
    javascript学习笔记
    IDA学习笔记
    inline内联函数
    api hook学习笔记
    java反射学习笔记
    android基于MBR的bootkit病毒学习笔记
  • 原文地址:https://www.cnblogs.com/cynchanpin/p/6768785.html
Copyright © 2020-2023  润新知