构建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 |
异步 | 调用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
类型,对应数据库的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对象获得异常信息。
我们举个例子:
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标准兼容的容器,支持迭代器,它封装的结果集可以通过范围循环取到每一行的对象,Result,Row和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编程的人都应该明白这个约束。