构建DbClient

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

  1. #if USE_POSTGRESQL
  2. static std::shared_ptr<DbClient> newPgClient(const std::string &connInfo, const size_t connNum);
  3. #endif
  4. #if USE_MYSQL
  5. static std::shared_ptr<DbClient> newMysqlClient(const std::string &connInfo, const size_t connNum);
  6. #endif

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

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

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

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

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

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

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

执行接口

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

  1. /// 异步接口
  2. template <typename FUNCTION1,
  3. typename FUNCTION2,
  4. typename... Arguments>
  5. void execSqlAsync(const std::string &sql,
  6. FUNCTION1 &&rCallback,
  7. FUNCTION2 &&exceptCallback,
  8. Arguments &&... args) noexcept;
  9. /// 异步future接口
  10. template <typename... Arguments>
  11. std::future<const Result> execSqlAsyncFuture(const std::string &sql,
  12. Arguments &&... args) noexcept;
  13. /// 同步接口
  14. template <typename... Arguments>
  15. const Result execSqlSync(const std::string &sql,
  16. Arguments &&... args) noexcept(false);
  17. /// 流式接口
  18. 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

  1. template <typename FUNCTION1,
  2. typename FUNCTION2,
  3. typename... Arguments>
  4. void execSqlAsync(const std::string &sql,
  5. FUNCTION1 &&rCallback,
  6. FUNCTION2 &&exceptCallback,
  7. Arguments &&... args) noexcept;

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

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

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

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

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

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

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

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

我们举个例子:

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

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

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

execSqlAsyncFuture

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

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

例如:

  1. auto f = clientPtr->execSqlAsyncFuture("select * from users where org_name=$1",
  2. "default");
  3. try
  4. {
  5. auto result = f.get(); // Block until we get the result or catch the exception;
  6. std::cout << result.size() << " rows selected!" << std::endl;
  7. int i = 0;
  8. for (auto row : result)
  9. {
  10. std::cout << i++ << ": user name is " << row["user_name"].as<std::string>() << std::endl;
  11. }
  12. }
  13. catch (const DrogonDbException &e)
  14. {
  15. std::cerr << "error:" << e.base().what() << std::endl;
  16. }

execSqlSync

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

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

例如:

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

operator<<

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

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

  1. *clientPtr << "select * from users where org_name=$1"
  2. << "default"
  3. >> [](const drogon::orm::Result &result)
  4. {
  5. std::cout << result.size() << " rows selected!" << std::endl;
  6. int i = 0;
  7. for (auto row : result)
  8. {
  9. std::cout << i++ << ": user name is " << row["user_name"].as<std::string>() << std::endl;
  10. }
  11. }
  12. >> [](const DrogonDbException &e)
  13. {
  14. std::cerr << "error:" << e.base().what() << std::endl;
  15. };

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

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

  1. void (bool,Arguments...);

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

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

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

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

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

总结

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

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

08.2 事务