TarsCpp 元件之 MySQL 操作

TARS基金會發表於2020-11-02

作者 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/building-microservice-platforms-with-tars

獲取《TARS官方培訓電子書》:https://wj.qq.com/s2/6570357/3adb/

或掃碼獲取:

QR

相關文章