3 CHN 08 1 数据库 DbClient
an-tao edited this page 2023-05-06 11:00:12 +08:00
This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

English | 简体中文

构建DbClient

构造DbClient对象有两种途径一个是通过DbClient类的静态方法在DbClient.h头文件可以看到定义如下

#if USE_POSTGRESQL
    static std::shared_ptr<DbClient> newPgClient(const std::string &connInfo, const size_t connNum);
#endif
#if USE_MYSQL
    static std::shared_ptr<DbClient> newMysqlClient(const std::string &connInfo, const size_t connNum);
#endif

得到DbClient实现对象的智能指针参数connInfo是个连接字符串采用key=value的形式设置一系列连接参数具体说明见头文件的注释。参数connNum是DbClient的连接数即该对象管理的数据库连接个数对并发有关键影响请根据实际情况设置。

通过这种方法得到的对象,用户要想办法持久化,比如放在某些全局容器内,创建临时对象,使用完再释放是非常不推荐的方案,理由如下:

  • 白白的浪费创建连接和断开连接的时间,增加了系统时延;
  • 该接口也是非阻塞接口也就是说用户拿到DbClient对象时它管理的连接还没建立起来框架没有故意的提供连接建立成功的回调接口难道还要sleep一下再开始查询么这和异步框架的初衷相违背。

所以应该在程序开始之初就构建这些对象并在整个生存周期持有并使用它。显然这个工作完全可以由框架来做因此框架提供了第二种构建方式就是通过配置文件构建或使用createDbClient接口创建配置方法见配置文件

需要使用时通过框架的接口获得DbClient的智能指针接口如下注意该接口必须在app.run()调用后才能得到正确的对象):

orm::DbClientPtr getDbClient(const std::string &name = "default");

参数name就是配置文件中的name配置项的值用以区分同一个应用的多个不同的DbClient对象。DbClient管理的连接总是断线重连的所以用户不用关心连接状态他们几乎总是正常连接的状态。

执行接口

DbClient对外提供了几种不同的接口列举如下

/// 异步接口
template <typename FUNCTION1,
          typename FUNCTION2,
          typename... Arguments>
void execSqlAsync(const std::string &sql,
                  FUNCTION1 &&rCallback,
                  FUNCTION2 &&exceptCallback,
                  Arguments &&... args) noexcept;

/// 异步future接口
template <typename... Arguments>
std::future<const Result> execSqlAsyncFuture(const std::string &sql,
                                             Arguments &&... args) noexcept;

/// 同步接口
template <typename... Arguments>
const Result execSqlSync(const std::string &sql,
                         Arguments &&... args) noexcept(false);

/// 流式接口
internal::SqlBinder operator<<(const std::string &sql);

因为涉及任意数量和类型的绑定参数,因此这些接口都是函数模板。

这些接口的性质如下表所示:

| 接口 | 同步/异步 | 阻塞/非阻塞 | 异常 | | :------------------------------------------* | :-------* | :--------------------------* | :--------------------------------* | | void execSqlAsync | 异步 | 非阻塞 | 不抛异常 | | std::future execSqlAsyncFuture | 异步 | 调用future的get方法时阻塞 | 调用future的get方法时可能抛异常 | | const Result execSqlSync | 同步 | 阻塞 | 可能抛异常 | | internal::SqlBinder operator<< | 异步 | 默认非阻塞,也可以阻塞 | 不抛异常 |

你可能对异步和阻塞的组合有点困惑一般而言同步接口涉及网络IO都是阻塞的异步接口则是非阻塞的不过异步接口也可以工作于阻塞模式意思是说这个接口会阻塞一直等到回调函数执行完毕才会退出。DbClient的异步接口工作于阻塞模式时回调函数会在同一个线程被执行然后该接口才执行完毕。

如果你的应用涉及高并发场景,请选择异步非阻塞接口,如果是低并发场景,比如一个网络设备的管理页面,则可以出于直观方便的考虑,选择同步接口。

  • execSqlAsync

    template <typename FUNCTION1,
            typename FUNCTION2,
            typename... Arguments>
    void execSqlAsync(const std::string &sql,
                    FUNCTION1 &&rCallback,
                    FUNCTION2 &&exceptCallback,
                    Arguments &&... args) noexcept;
    

    这是最常使用的异步接口,工作于非阻塞模式;

    参数sql是sql语句的字符串如果有需要绑定参数的占位符使用相应数据库的占位符规则比如PostgreSQL的占位符是$1,$2..而MySQL的占位符是

    不定参数args代表绑定的参数可以是零个或多个具体数据和sql语句的占位符个数一致类型可以是以下几类

    • 整数类型:可以是各种字长的整数,应和数据库字段类型相匹配;
    • 浮点类型:可以是float或者double,应和数据库字段类型相匹配;
    • 字符串类型:可以是std::string或者const char[],对应数据库的字符串类型或者其他可以用字符串表示的类型;
    • 日期类型:trantor::Date类型对应数据库的datedatetimetimestamp等字段类型。
    • 二进制类型:std::vector<char>类型对应PostgreSQL的bytea类型或者Mysql的blob类型

    这些参数可以是左值,也可以是右值,可以是变量,也可以是字面常量,用户可以自由掌握。

    参数rCallback和exceptCallback分别表示结果回调函数和异常回调函数它们有固定的定义如下

    • 结果回调函数调用类型为void (const Result &)符合这个调用类型的各种可调用对象std::function,lambda等等都可以作为参数传入
    • 异常回调函数调用类型为void (const DrogonDbException &),可传入和这个调用类型一致的各种可调用对象;

    sql执行成功后执行结果由Result类包装并通过结果回调函数传递给用户如果sql执行有任何异常异常回调函数被执行用户可以从DrogonDbException对象获得异常信息。

    我们举个例子:

    auto clientPtr = drogon::app().getDbClient();
    clientPtr->execSqlAsync("select * from users where org_name=$1",
                                [](const drogon::orm::Result &result) {
                                    std::cout << r.size() << " rows selected!" << std::endl;
                                    int i = 0;
                                    for (auto row : result)
                                    {
                                        std::cout << i++ << ": user name is " << row["user_name"].as<std::string>() << std::endl;
                                    }
                                },
                                [](const DrogonDbException &e) {
                                    std::cerr << "error:" << e.base().what() << std::endl;
                                },
                                "default");
    

    从例子中我们可以看出Result对象是个std标准兼容的容器支持迭代器它封装的结果集可以通过范围循环取到每一行的对象ResultRow和Field对象的各种接口请参考源码

    DrogonDbException类是所有数据库异常的基类具体的定义和它子类的说明请参考源码中的注释。

  • execSqlAsyncFuture

    template <typename... Arguments>
    std::future<const Result> execSqlAsyncFuture(const std::string &sql,
                                                Arguments &&... args) noexcept;
    

    异步future接口省略了前一个接口的中间两个参数使用future对象代替回调函数调用这个接口会立即返回一个future对象用户必须调用future的get()方法得到返回的结果异常要通过try/catch机制得到如果调用get()方法时没有try/catch并且整个调用堆栈中也没有try/catch则程序会在sql执行发生异常的时候退出。

    例如:

    auto f = clientPtr->execSqlAsyncFuture("select * from users where org_name=$1",
                                        "default");
    try
    {
        auto result = f.get(); // Block until we get the result or catch the exception;
        std::cout << result.size() << " rows selected!" << std::endl;
        int i = 0;
        for (auto row : result)
        {
            std::cout << i++ << ": user name is " << row["user_name"].as<std::string>() << std::endl;
        }
    }
    catch (const DrogonDbException &e)
    {
        std::cerr << "error:" << e.base().what() << std::endl;
    }
    
  • execSqlSync

    template <typename... Arguments>
    const Result execSqlSync(const std::string &sql,
                            Arguments &&... args) noexcept(false);
    

    同步接口是最简单直观的输入参数是sql字符串和绑定的参数返回一个Result对象调用会阻塞当前线程并且在出现错误时抛异常所以也要注意try/catch捕获异常。

    例如:

    try
    {
        auto result = clientPtr->execSqlSync("update users set user_name=$1 where user_id=$2",
                                            "test",
                                            1); // Block until we get the result or catch the exception;
        std::cout << result.affectedRows() << " rows updated!" << std::endl;
    }
    catch (const DrogonDbException &e)
    {
        std::cerr << "error:" << e.base().what() << std::endl;
    }
    
  • operator<<

    internal::SqlBinder operator<<(const std::string &sql);
    

    流式接口比较特殊它把sql语句和参数依次通过<<操作符输入,而通过>>操作符指定结果回调函数和异常回调函数比如前面select的例子使用流式接口是如下的样子

    *clientPtr  << "select * from users where org_name=$1"
                << "default"
                >> [](const drogon::orm::Result &result)
                    {
                        std::cout << result.size() << " rows selected!" << std::endl;
                        int i = 0;
                        for (auto row : result)
                        {
                            std::cout << i++ << ": user name is " << row["user_name"].as<std::string>() << std::endl;
                        }
                    }
                >> [](const DrogonDbException &e)
                    {
                        std::cerr << "error:" << e.base().what() << std::endl;
                    };
    

    这种写法和第一种异步非阻塞接口是完全等效的,采用哪种接口取决于用户的使用习惯。如果想让它工作于阻塞模式,可以使用<<输入一个Mode::Blocking参数,这里不再赘述。

    另外,流式接口还有一个特殊的用法,使用一种特殊的结果回调,可以让框架逐行的把结果传递给用户,这种回调的调用类型如下:

    void (bool,Arguments...);
    

    第一个bool参数为true时表示这次回调是一个空行也就是所有结果都已经返回了这是最后一次回调 后面是一系列参数对应一行记录的每一列的值框架会做好类型转换当然用户也要注意类型的匹配。这些类型可以是const型的左值引用也可以是右值引用当然也可以是值类型。

    我们再把上一个例子用这种回调重写一下:

    int i = 0;
    *clientPtr  << "select user_name, user_id from users where org_name=$1"
                << "default"
                >> [&i](bool isNull, const std::string &name, int64_t id)
                        {
                        if (!isNull)
                            std::cout << i++ << ": user name is " << name << ", user id is " << id << std::endl;
                        else
                            std::cout << i << " rows selected!" << std::endl;
                        }
                >> [](const DrogonDbException &e)
                    {
                        std::cerr << "error:" << e.base().what() << std::endl;
                    };
    

    可以看到select语句中的user_name和user_id字段的值被分别赋给了回调函数中的name和id变量用户无需自己处理这些转换这显然提供了一定的便利性用户可以在实践中灵活运用。

注意: 借着这个例子要强调一点异步编程必须注意的地方就是上面例子中的变量i用户必须保证在回调发生时变量i还是有效的因为它是被引用捕获的它的有效性并不是理所当然的回调会在别的线程被调用而回调发生时当前的上下文环境很可能已经失效了。类似的场景常常使用智能指针持有临时创建的变量再被回调捕获从而保证变量的有效性。

总结

每个DbClient对象有且仅有一个自己的EventLoop线程这个线程负责控制数据库连接IO通过异步或同步接口接受请求再通过回调函数返回结果。

它虽然也提供阻塞的接口这种接口只是阻塞调用者线程只要调用者线程不是EventLoop线程就不会影响EventLoop线程的正常运转。回调函数被调用时回调内的程序是运行在EventLoop线程的所以不要在回调内部进行任何阻塞操作否则会影响数据库的并发熟悉non-blocking I/O编程的人都应该明白这个约束。

08.2 事务