作者 Eaton

导语
在后台开发中,我们经常需要和数据库打交道,而在 C++ 开发中,MySQL Connector/C++ 只提供了基础操作接口,复杂的业务常常需要一系列复杂的调用过程,非常容易出错,那有什么方法可以避免呢?TarsCpp 中提供了数据库操作类 TC_Mysql,使我们能够方便地进行数据库操作,提高业务开发效率。本文将对 TC_Mysql 进行介绍分析。

目录

MySQL

简介

数据库是计算机应用系统中一种专门管理数据资源的系统,以数据为中心,解决了传统文件存储数据存在的数据冗余、移植性差等问题。在后台开发中,数据库操作具有非常重要的地位,不可避免地需要和数据库打交道。现在市面上的数据库软件多种多样,最常用的便是 MySQL。它是一款安全、跨平台、高效的关系型数据库系统,由瑞典的 MySQL AB 公司开发、发布并支持。由于其体积小、速度快、总体拥有成本低,尤其是开放源码这一特点,使得很多公司都采用 MySQL 数据库以降低成本。

数据库的基础操作包括增、删、改、查四种操作,对应的在 MySQL 中为 Insert, Delete, Update, Select 等,能够方便地实现对数据库中数据的管理。

MySQL 常用 API

MySQL 支持了各种主流的程序设计语言,提供了丰富的数据库操作 API 函数。在 C++ 开发中,MySQL 官方提供了相关的数据库连接动态库 MySQL Connector/C++,接口定义在 mysql.h 中,包含 MySQL 基础操作 API,像常用的 mysql_real_connect, mysql_real_query, mysql_store_result, mysql_fetch_row 等。其中

  • mysql_real_connect 用于创建到 MySQL 数据库服务的连接;
  • mysql_real_query 用于执行 MySQL 语句;
  • mysql_store_result 用于将执行结果保存到本地;
  • mysql_fetch_row 用于获取返回结果中的行。

这四个函数一般情况下能够满足大部分需求,它们在 mysql.h 中的声明如下

MYSQL *STDCALL mysql_real_connect(MYSQL *mysql, const char *host,
                                  const char *user, const char *passwd,
                                  const char *db, unsigned int port,
                                  const char *unix_socket,
                                  unsigned long clientflag);

int STDCALL mysql_real_query(MYSQL *mysql, const char *q, unsigned long length);

MYSQL_RES *STDCALL mysql_store_result(MYSQL *mysql);

MYSQL_ROW STDCALL mysql_fetch_row(MYSQL_RES *result);

mysql_real_connect 函数有很多参数,涵盖了连接数据库所需要的基本信息如 host, user, password 等,成功创建连接会获得一个 MYSQL 对象,并将参数中的 MYSQL * 指针 mysql 指向该对象,供其它操作使用。

mysql_real_query 函数需要传入刚才提到的连接对象指针,SQL 字符串 q 和字符串长度 length,返回执行结果的行数。

mysql_store_result 函数传入 MYSQL 对象指针,返回执行结果,类型为 MYSQL_RES

mysql_fetch_row 传入获取的结果,返回每行的数据。

存在的问题

然而使用这些接口进行一次 MySQL 操作是非常麻烦的一件事,下面我们通过一个例子来看看如何通过这四个函数实现一次 MySQL 查询操作

#include "mysql.h"

using namespace std;

int main() 
{
    // 创建 MYSQL 对象
    MYSQL *mysql = mysql_init(NULL);
    
    // 创建连接
    mysql_real_connect(mysql, "127.0.0.1", "root", "123456", "db_tars", 3306, NULL, 0);
    
    // 执行sql语句
    string sql = "select * from server";
    mysql_real_query(mysql, sql.c_str(), sql.length());
    
    // 获取执行结果
    MYSQL_RES *res = mysql_store_result(mysql);
    
    // 字段名数组
    vector<string> fields;
    MYSQL_FIELD *field;
    // 通过 mysql_fetch_field 获取字段名保存在数组 fields 中
    while((field = mysql_fetch_field(res))) 
    {
        fields.push_back(field->name);
    }

    // 声明 row 用于保存每行数据
    MYSQL_ROW row;
    // 读取返回结果所有字段值
    // mysql_fetch_row 从 res 中获取一行数据
    while((row = mysql_fetch_row(res) != (MYSQL_ROW)NULL)) 
    {
        // 获取每个字段值的字符串长度,因为每个字段是一个字符数组 (C 风格字符串)
        unsigned long * lengths = mysql_fetch_lengths(res);
        for(size_t i = 0; i < fields.size(); i++) 
        {
            cout << fields[i] << ":" << string(row[i], lengths[i]) << ";";
        }
        cout << endl;
    }
    return 0;
}

上述代码在 main 函数中,用最简单的方式实现了查询操作,没有包含任何错误和返回值判断的逻辑,但是看起来已经很复杂了。而实际业务场景中通常还需要对一些错误码还有返回结果进行判断,比如连接失败或断开,返回值为空等,总的来说,存在以下几个问题

  • 需要自己构造 SQL 语句,容易出错;
  • 需要开发者自己添加错误码和异常的判断和处理逻辑;
  • 每次查询返回的结果需要调用 mysql_fetch 系列的多个函数才能完成结果读取。

可见,开发者需要关注的细节太多,会很大程度上降低了开发效率。因此,把开发者无需关注的或重复的过程和细节隐藏起来,将 MySQL 操作 API 进一步封装,显得非常必要。自己封装的话未免太过于小题大做,而且难免有疏漏,对开发者自身能力要求也比较高。常见的方式是引入完备的第三方库,TarsCpp 的工具组件中就包含数据库操作类 TC_Mysql,能够完美解决这些问题。

TC_Mysql

TC_Mysql 是 TarsCpp 中提供的 MySQL 操作类,定义在文件 tc_mysql.h 中,对 MySQL C++ 库中提供的 API 进一步地封装,屏蔽了很多与业务开发无关的细节,使用上更加方便简单。

文件 tc_mysql.h 中定义了三个类 TC_DBConf, TC_Mysql, TC_Mysql_Exception。其中 TC_Mysql 类中还定义两个子类。如图所示

tc_mysql.h
│
├── TC_DBConf           # 数据库配置接口
├── TC_Mysql            # 数据库操作类
│   ├── MysqlRecord     # 数据库记录类(单条查询记录)
│   └── MysqlData       # 数据库数据类(所有查询记录)
└── TC_Mysql_Exception  # 数据库异常类

其中异常类 TC_Mysql_Exception 和数据库配置接口 TC_DBConf 作为辅助类,主要在类 TC_Mysql 中使用。

TC_Mysql 类中的两个子类 MysqlRecordMysqlData,作为数据存储类,类似于 MySQL C++ 库中的 MYSQL_ROWMYSQL_RES,用于保存每次查询返回的结果。

下面我们就来对每个类进行分析。

数据库配置接口 TC_DBConf

在类 TC_Mysql 中,使用 TC_DBConf 类型的成员变量 _dbConf 来保存 MySQL 配置信息。TC_DBConf 用于保存数据库的连接配置如 host、user 等,提供了 loadFromMap 接口,能够从 map 类型变量中读取数据库配置。我们可以通过两种方式来加载配置

  • 直接对成员变量赋值
TC_DBConf dbConf;
dbConf._host = "127.0.0.1";
dbConf._user = "root";
...
  • 使用 loadFromMapmap<string, string> 类型参数读取配置
map<string, string> mapConf;
mapConf.insert(make_pair("dbhost", "127.0.0.1"));
mapConf.insert(make_pair("dbuser", "root"));
...

TC_DBConf dbConf;
dbConf.loadFromMap(mapConf);

TC_DBConf 的定义也非常简单,具体如下

/**
* @brief 数据库配置接口
*/
struct TC_DBConf
{
    string _host;
    string _user;
    string _password;
    string _database;
    string _charset;
    int _port;
    int _flag;

    /**
    * @brief 构造函数
    */
    TC_DBConf()
        : _port(0)
        , _flag(0)
    {}

    /**
    * @brief 读取数据库配置. 
    * 
    * @param mpParam 存放数据库配置的map 
    *        dbhost:主机地址
    *        dbuser:用户名
    *        dbpass:密码
    *        dbname:数据库名称
    *        dbport:端口
    */
    void loadFromMap(const map<string, string> &mpParam)
    {
        map<string, string> mpTmp = mpParam;

        _host        = mpTmp["dbhost"];
        _user        = mpTmp["dbuser"];
        _password    = mpTmp["dbpass"];
        _database    = mpTmp["dbname"];
        _charset     = mpTmp["charset"];
        _port        = atoi(mpTmp["dbport"].c_str());
        _flag        = 0;

        if(mpTmp["dbport"] == "")
        {
            _port = 3306;
        }
    }
};

数据库操作类 TC_Mysql

构造函数

TC_Mysql 提供了三个版本的构造函数,实现了多种初始化方式。

  1. 支持传入一个 TC_DBConf 对象来进行初始化,简化了参数,降低了出错的可能,也能提高代码可读性。
    TC_Mysql(const TC_DBConf& tcDBConf);
  1. 提供默认构造函数,不传入参数,后续使用时再进行初始化。
    TC_Mysql();
  1. mysql_real_connet 参数相似,传入数据库配置信息来初始化。
不同的是这种方式在构造对象时即完成 MySQL 连接的初始化,而且可以直接使用 string 类型字符串。mysql_real_connect 需要先通过 mysql_init 创建对象后才能调用(可见 MySQL 常用 API 部分的示例),并且只能传入 C 风格的字符串。
    TC_Mysql(const string& sHost, const string& sUser = "", const string& sPasswd = "", const string& sDatabase = "", const string &sCharSet = "", int port = 0, int iFlag = 0);

下面这个例子展示了这三种构造方式

// 声明并初始化数据库配置对象 dbConf
TC_DBConf dbConf;
dbConf._port = 3306;
dbConf._host = "127.0.0.1";
dbConf._user = "root";
dbConf._password = "12345";
dbConf._database = "db_tars";
dbConf._charset  = "utf8";

// 通过 TC_DBConf 对象构造初始化
TC_Mysql mysqlObj0(dbConf);

// 先构造对象,不初始化,后续使用init初始化
TC_Mysql mysqlObj1 = new TC_Mysql();
mysqlObj1.init(dbConf);

// 直接传入数据库配置初始化
TC_Mysql mysqlObj2("127.0.0.1", "root", "12345", "db_tars", "utf8", 3306);

MySQL 操作函数

TC_Mysql 中的包含了 Insert、Update、Replace、Delete、Query 等常用操作的函数,更加符合一般使用场景,相比 MySQL C++ 库都通过 mysql_real_query 来实现,使用上要简单得多。常用的几个操作函数声明如下

// 插入记录
size_t insertRecord(const string &sTableName, const map<string, pair<FT, string> > &mpColumns);
// 更新记录
size_t updateRecord(const string &sTableName, const map<string, pair<FT, string> > &mpColumns, const string &sCondition);
// 替换记录
size_t replaceRecord(const string &sTableName, const map<string, pair<FT, string> > &mpColumns);
// 删除记录
size_t deleteRecord(const string &sTableName, const string &sCondition = "");
// 获取记录计数
size_t getRecordCount(const string& sTableName, const string &sCondition = "");
// 字符转义
string realEscapeString(const string& sFrom);
// 查询记录
MysqlData queryRecord(const string& sSql);
更多其他操作函数参见 tc_mysql.h
  • Insert、Update、Replace、Delete

从上述定义中可以看出,增、删、改相关的操作不需要自己构建 SQL 语句,传入相关参数就能够完成操作。

实际上,构造 SQL 语句的过程封装在函数中。

其中参数 sTableName 为表名,mpColumns 为需要插入、更新或替换的数据, sCondition 为条件。下面通过一个例子来看看如何使用这些操作函数

...
    // 表名
    string tableName = "tars_table";
    
    // 构造需要增、删、改的数据
    map<string, pair<TC_Mysql::FT, string>> record0;
    record0.insert(make_pair("user_id"  , make_pair(TC_Mysql::DB_STR, "abcd")));
    record0.insert(make_pair("age"      , make_pair(TC_Mysql::DB_INT, "25")));

    // 构造用于替换的数据
    map<string, pair<TC_Mysql::FT, string>> record1;
    record1.insert(make_pair("user_id"  , make_pair(TC_Mysql::DB_STR, "abcd")));
    record1.insert(make_pair("age"      , make_pair(TC_Mysql::DB_INT, "40")));

    try 
    {
        // mysqlObj0 为前文已经初始化好的 TC_Mysql 对象指针
        // 向 tars_table 插入记录
        mysqlObj0->insertRecord(tableName, record0);
        // 获取 user_id 为 abcd 的记录计数
        int count = mysqlObj0->getRecordCount(tableName, "where `user_id`='abcd'");
        // 替换 user_id 为 abcd 的记录(不存在则为插入数据), 替换后 age 值为 40
        mysqlObj0->replaceRecord(tableName, record1);
        // 更新 user_id 为 abcd 的记录,更新后 age 值为 25
        mysqlObj0->updateRecord(tableName, record0, "where `user_id`='abcd'");
        // 删除 age 为 25 的记录
        mysqlObj0->deleteRecord(tableName, "where `age`=40");
    } 
    catch (exception &e) 
    { 
        // 异常处理
        ...
    }
...

可以看到上述示例中存在一个 TC_Mysql::FT 类型,定义如下

    /**
     * @brief 定义字段类型, 
     *  DB_INT:数字类型
     *  DB_STR:字符串类型
     */
    enum FT
    {
        DB_INT,     
        DB_STR,    
    };

它是 TC_Mysql 类中定义的枚举类型,用于定义字段的类型为字符串还是数字,判断在构建 SQL 语句时是否需要添加引号 '' 并转义。

例如上述例子中,最后实际执行的 Insert SQL 语句中,abcd 有引号,25 没有引号,如下

insert into tars_table (`user_id`, `age`) values ('abcd', 25)
  • Query

增、删、改都有了,那么查(Query)呢?就是前面定义中的 queryRecord 了。与 mysql_real_query 类似,参数传入 SQL 语句字符串。

MysqlData queryRecord(const string& sSql);

不同的是,queryRecord 传入参数类型为 string,不需要额外传入字符串长度;并且 queryRecord 直接返回查询结果记录,不需要再调用 mysql_store_result 等函数来获取(实际上这个过程封装在函数内,参见 TC_Mysql 源码)。

具体使用方式如下

    ...
    // 声明返回数据
    TC_Mysql::MysqlData res;

    try
    {
        res = mysqlObj->queryRecord("select user_id, age from tars_table");
    }
    catch (exception &e)
    {
        cout << "Error: " << e.what() << endl;
    }

    size_t resCount = res.size();
    // 输出返回数据
    for (size_t i = 0; i < resCount; ++i)
    {
        cout << "user_id: " << res[0]["user_id"] 
             << " age: "    << res[0]["age"] 
             << endl;
    }
    ...

返回数据的类型为 MysqlData,它是 TC_Mysql 的子类,用于保存数据,相当于 MySQL C++ 库中的 MYSQL_RES,我们会在下一部分详细介绍它。读取 MysqlData 类型数据的过程非常友好方便,能够直接以数组的方式遍历,并且读取字段值的类型为 string 而不是 char *,不需要额外获取字符串长度。这也使得代码变得更加简洁清晰。

数据存储类 MysqlRecord 与 MysqlData

TC_Mysql 中包含了两个子类 MysqlRecordMysqlData,用于保存查询返回的数据,方便数据的读取。MysqlRecord 相当于 map,用于保存一条记录;MysqlData 相当于 MysqlRecord 类型数组,用于保存多条记录。

MysqlRecord

MysqlRecord 类用于记录一条 mysql 的记录,相当于 MYSQL_ROW,在 MysqlData 中被使用。MysqlRecord 类型的对象能够直接使用下标访问数据,例如

map<string, string> record_map;
record_map.insert(make_pair("name", "TARS"));
// 构建并初始化对象
TC_Mysql::MysqlRecord record(record_map);
// 通过下标访问
cout << record["name"] << endl;

MysqlRecord 类的定义如下

    /**
     *  @brief mysql的一条记录
     */
    class MysqlRecord
    {
    public:
        /**
         * @brief 构造函数.
         * @param record
         */
        MysqlRecord(const map<string, string> &record);

        /**
         * @brief 获取数据,s一般是指数据表的某个字段名 
         * @param s 要获取的字段
         * @return  符合查询条件的记录的s字段名
         */
        const string& operator[](const string &s);
    protected:
        const map<string, string> &_record;
    };

可以看到 MysqlRecord 重载了 [] 运算符,实现了像 map 一样的下标访问数据的方式。MysqlRecord 还包含 map<string, string> 类型引用的成员变量 _record,因此实际保存记录的数据类型为 map<string, string>

MysqlData

保存一次查询的所有 mysql 数据记录,相当于 MYSQL_RES,每条记录即为一个 MysqlRecord 类型的数据。MysqlData 类型的数据能够通过下标访问每条记录,与数组相似,比如下面的例子

map<string, string> record_map;
record_map.insert(make_pair("name", "TARS"));
// 声明对象
TC_Mysql::MysqlData data;
// 添加记录
data.data().push_back(record_map);
// 通过下标获取 MysqlRecord 对象
TC_Mysql::MysqlRecord record = data[0];

cout << record["name"] << endl;
cout << data[0]["name"] << endl;

MysqlData 重载了 [] 运算符,实现了与数组一样的方式遍历和读取记录,每条记录类型为 MysqlRecord。实际保存数据的成员变量为 _data,类型为 vector< map<string, string> >,即通过 vector 容器来保存所有记录。定义如下

    /**
     * @brief 查询出来的mysql数据
     */
    class MysqlData
    {
    public:
        /**
         * @brief 所有数据.
         * @return vector<map<string,string>>&
         */
        vector<map<string, string> >& data();

        /**
         * @brief 数据的记录条数
         * @return size_t
         */
        size_t size();

        /**
         * @brief 获取某一条记录. 
         * @param i  要获取第几条记录 
         * @return   MysqlRecord类型的数据,可以根据字段获取相关信息,
         */
        MysqlRecord operator[](size_t i);

    protected:
        vector< map<string, string> > _data;
    };

数据库异常类 TC_Mysql_Exception

在前面 MySQL 的介绍中可以看到,常规 MySQL 的操作没有进行异常判断和处理,需要自己实现相关的逻辑。

tc_mysql.h 中定义了异常类 TC_Mysql_Exception,用于在 MySQL 操作出现错误时抛出异常,作为类 TC_Mysql 的辅助类被使用。例如下面是 TC_Mysql 中函数 queryRecord 的部分实现,返回的指针 pstRst 为空时抛出一个错误:

...
    MYSQL_RES *pstRes = mysql_store_result(_pstMql);

    if(pstRes == NULL)
    {
        throw TC_Mysql_Exception("[TC_Mysql::queryRecord]: mysql_store_result: " + sSql + " : " + string(mysql_error(_pstMql)));
    }
...

TC_Mysql 类中包含了异常处理逻辑,并将异常通过 TC_Mysql_Exception 抛出。方便用户在使用时,能够直接通过 try catch 来捕捉异常,比如

...

tars::TC_Mysql *mysql_ptr = new tars::TC_Mysql();

try 
{
    mysql_ptr->init("127.0.0.1", "root", "123456", "db_tars", "utf-8", 3306);
    mysql_ptr->connect();
}
catch (exception &e)
{
    // 获取异常信息
    cout << e.what() << endl;
    ...
}
...

总结

TC_Mysql 在 MySQL Connector C++ API 基础上进一步封装,对一些常用的流程例如 Insert、Update、Replace 等操作封装了独立的操作函数,使开发者无需自行拼接 SQL 语句,降低了出错的可能;同时屏蔽了错误处理等逻辑,开发者无需关注,仅通过 try catch 语句即可捕捉异常。另外,通过类 MysqlRecord, MysqlData 保存返回数据类型,极大的方便了数据的读取。这些都使得开发者能够更加方便地操作 MySQL,专注于业务代码的开发,进一步提高开发效率。

TARS可以在考虑到易用性和高性能的同时快速构建系统并自动生成代码,帮助开发人员和企业以微服务的方式快速构建自己稳定可靠的分布式应用,从而令开发人员只关注业务逻辑,提高运营效率。多语言、敏捷研发、高可用和高效运营的特性使 TARS 成为企业级产品。

TARS微服务助您数字化转型,欢迎访问:

TARS官网:https://TarsCloud.org

TARS源码:https://github.com/TarsCloud

Linux基金会官方微服务免费课程:https://www.edx.org/course/bu...

获取《TARS官方培训电子书》:https://wj.qq.com/s2/6570357/...

或扫码获取:

QR


TARS基金会
16 声望7 粉丝

2020年3月10日,Linux基金会正式宣布旗下的TARS开源项目将成立TARS基金会。TARS基金会是一个专注于微服务领域的开源基金会,致力于帮助企业在拓展新领域时拥抱微服务体系架构,解决在使用微服务方面可能出现的问...