MySQL++是一个针对MySQL C API的C++封装。它的目的是提供一个类似STL容易一样简单易用的接口,帮助你有效的避免在代码中使用复杂的SQL语句。

3.指导

前面的章节简要概述了MySQL++的基本情况。接下来我们将深入一些讲一些例子。我们从每个MySQL++必须处理的模块进行讲解,之后将一些更加复杂的模块。你可以读完本章节就停止了,因为后面我会只会将更多的复杂的高级特性。

3.1. 运行例子

如果你通过库源代码编译,那么当你编译完毕,例子也都应该被编译完毕了。如果你下载的是RPM格式,那么例子代码和一个makefile文件可能被安装在 /usr/share/doc/mysql++devel-/examples 里,当然,根据Linuxes不同也可能有些不同。 在你开始前,请根据平台,先阅读以下库内的 README.txt ,我们在本文档不再重复。 我们的许多例子需要一个 test 数据库,我们可以通过 resetdb 创建它。你可以输入如下命令:

resetdb [-s server_addr] [-u user] [-p password]

通常来说,MySQL++库应当已经编译完成,放置在操作系统动态链接器可以查找到的一个目录下了。(通常MySQL++不编译为静态的)如果你是下载的RPM格式,则需要根据Source编译一个Lib,然后建议你运行部分例子以保证库运行顺利。如果你的操作系统动态链接器无法找到MySQL++库,我们创建了一些脚本协助你运行这些例子。 MySQL++为Unix相关系统编写了一个 exrun 的Shell脚本,为Windows系统编写了一个 exrun.bat 批处理文件。你可以通过 exrun 脚本为这些例子创建一个环境。

./exrun resetdb [-s server_addr] [-u user] [-p password]

当然,如果在Windows里执行exrun.bat,可以去掉上述命令中的 ‘./’ 符号。 参数里 server_addr 可以填写 ? localhost - 本地(默认的) ? 192.168.1.224:3306 - 数据库IP和TCP端口 ? ServerName:SvcName - 这会优先从你的系统网络中查找指定名称的TCP服务,例如Unix下 /etc/services ,Windows下的C:\Windows\system32\drivers\etc\services, 然后从服务中查找到端口和你所建议的服务器名称。

如果参数里没有冒号和指定的端口,那么默认将使用3306端口。 如果参数里没有 –u 指定数据库用户名,则默认使用你当前的登录本机用户名。 如果参数没有 –p 指定数据库密码,则假设MySQL没有密码。

注意,我们要运行 resetdb, 用户名要求能够有足够权限创建 test 数据库。一旦这个数据库创建成功,你就可以运行多个例子去尝试DELETE,INSERT,SELECT,UPDATE 数据库。当然,或许你希望创建一个单独的用户去执行这些在 test 数据库上的测试操作,那么可以执行下述命令。

CREATE USER mysqlpp_test@‘%’ IDENTIFIED BY ’nunyabinness’; GRANT ALL PRIVILEGES ON mysql_cpp_data.* TO mysqlpp_test@‘%’;

这样你就创建一个用户,用户名 mysqlpp_test ,密码为 nunyabinness . 然后你可以继续执行

./exrun resetdb -u mysqlpp_test -p nunyabinness

实际运行 resetdb 这个例子,这个例子会为你创建一个 mysql_cpp_data 数据库,内有四个表。 之后,你可以看看 README-examoles.txt 来获取更多信息,运行其他例子看看吧.

3.2. 一个简单的例子 simple1.cpp

这个简单例子教我们如何去创建一个连接,执行一个查询并将结果显示出来。这个文件在 examples/simple1.cpp 中,代码如下:

#include “cmdline.h” #include “printdata.h” #include #include #include

using namespace std;

int main(int argc, char argv[]) { //从命令行中获取数据库相关参数 const char db = 0, *server = 0, *user = 0, *pass = “”; if (!parse_command_line(argc, argv, &db, &server, &user, &pass)) { return 1; }

// 连接数据库
mysqlpp::Connection conn(false);
if (conn.connect(db, server, user, pass))
{
    // 从stock表中获取Item字段数据表并显示
    mysqlpp::Query query = conn.query("select item from stock");
    if (mysqlpp::StoreQueryResult res = query.store())
    {
        cout << "We have:" << endl;
        for (size_t i = 0; i < res.num_rows(); ++i)
        {
            cout << '\t' << res[i][0] << endl;
        }
    }
    else
    {
        cerr << "Failed to get item list: " << query.error() << endl;
        return 1;
    }
    return 0;
}
else
{
    cerr << "DB connection failed: " << conn.error() << endl;
    return 1;
}

}

这个例子里我们从数据库中查找 stock 表,获取其中的 item 列数据,并将数据逐一输出。 注意MySQL++中的 StoreQueryResult 起源于 std::vector ,而其中的 Row 又定义了类似 vector 一样的接口,意味着你可以进行下标访问,当然,也可以使用迭代器进行访问。 Row 比 vertor 更强大的一点是,它支持你使用字段标示进行访问,例如: res[i][“Item”] 这段代码中唯一不非常清楚的就是 parse_command_line() ,这个函数只是为了这些例子有一个统一的接口风格,你可以理解它是一个黑盒子,将 argc 和 argv 分割为不同的数据库参数。

3.3. 一个相对复杂一些的例子 Simple2.cpp

Simple1这个例子过于简单,没有太多价值,我们讲的深入一些,这里是 examples/simple2.cpp ,以下是代码:

#include “cmdline.h” #include “printdata.h” #include #include #include

using namespace std;

int main(int argc, char argv[]) { //从命令行中获取数据库相关参数 const char db = 0, *server = 0, *user = 0, *pass = “”; if (!parse_command_line(argc, argv, &db, &server, &user, &pass)) { return 1; }

// 连接数据库
mysqlpp::Connection conn(false);
if (conn.connect(db, server, user, pass))
{
    // 获取 stock 表内所有数据并存储
    mysqlpp::Query query = conn.query("select * from stock");
    mysqlpp::StoreQueryResult res = query.store();

    // 输出结果
    if (res)
    {
        // 输出标题列
        cout.setf(ios::left);

        cout << setw(31) << "Item" <<
            setw(10) << "Num" <<
            setw(10) << "Weight" <<
            setw(10) << "Price" << “Date” << endl << endl;

        // 取结果里的每一行输出
        for (size_t i = 0; i < res.num_rows(); ++i)
        {
            cout << setw(30) << res[i]["item"] << ' ' <<
                setw(9) << res[i]["num"] << ' ' <<
                setw(9) << res[i]["weight"] << ' ' <<
                setw(9) << res[i]["price"] << ' ' <<
                setw(9) << res[i]["sdate"] <<
                endl;
        }
    }
    else
    {
        cerr << "Failed to get stock table: " << query.error() << endl;
        return 1;
    }
    return 0;
}
else
{
    cerr << "DB connection failed: " << conn.error() << endl;
    return 1;
}

}

这个例子主要说明,我们使用对象字段标示进行访问,而不再使用下标。这样做会略微慢一些,但是可读性很好。

3.4. 异常

默认状态下,MySQL++会将异常视为一种错误,逐步传递下去。在我们的例子里面,所有异常将在 Connection 的构造函数处传递出来,使其构造失败,这样做会更加允许弹性的处理错误。在我们实际的项目中,我推荐我们将错误处理开启,有错误直接处理掉。这样我们也可以使用默认的 Connection 构造函数。 MySQL++中的所有异常均继承于一个Exception类,而它又是继承于 std::exception 类。所以,所有的标准C++错误都可能被MySQL++捕获。 若你强硬设置异常不进行抛出,那么异常将被视做一个错误返回。或者是一个int的错误码,或者是一个空对象指针,或者是一个false,也可能是一个对象中的一个错误标示。但是,你只能将继承于 OptionalException 接口的异常视为错误而不抛出,这些不被抛出的异常将别传递到 Connection 的构造函数处。 如果一个基于 OptionalException 接口继承的对象有一份拷贝对象,那么这个对象也将会拷贝源对象的一个异常标识。

例如: mysqlpp::Connection con; // 这样的话,这个对象将会在构造时抛出异常。

但继承于NoException的对象是永远不会抛出异常的 例如: mysqlpp::NoExceptions ne(con); // 这里没有问题 if (!con.select_db(“a_db_that_might_not_exist_yet”)) { // 只会进入这里,表明某处有错误,但这个错误实际并非 select_db 函数的错误,而是 con 原本就为空。 }

但有些异常,MySQL++是不允许视为错误的,将会无条件抛出: - 索引访问错误。例如对一个只有5组数据的数组,进行 row[21] 的访问,你将会得到一个 BadIndex 异常。如果你进行row[“field”] 访问,而实际上没有 field 这个字段,你将会得到一个 BadFieldName 异常。在之前的版本中,由于过于模仿STL容器,所以这里可能会返回 std::range 异常,但是在MySQL++ v3.0.7 版本之后,应该是不会出现问题了。 - String转换时异常。例如,你尝试将”1.25”转换为int类型时,会抛出一个 BadConversion 异常。但是,如果你试图将”1.00”转换为int,则不会抛出异常。MySQL++能够判断哪些类型可以安全转换。 - 模板参数异常。如果你使用模板查询,而你又没有传入足够的参数,将会抛出一个 BadParamCount 异常。 - 类型查找异常。如果你使用一个C++数据结构,但是MySQL++又无法将其转换为SQL结构,MySQL++将抛出一个 TypeLookupFailed 异常。非常建议你使用 lib/sqltypes.h 内定义的类型。 当然,如果你拼错一个字母,或者强制转换一个奇怪类型,也将引发一些异常。

3.5. 引用和引号泄露

SQL语法解析经常需要依赖一些外界数据,例如下面这个查询:

SELECT * FROM stock WHERE item = ‘Hotdog Buns’

因为 “Hotdog Buns” 字符串需要被一对单引号包含,是一种引用,在MySQL++中,你通常不需要将这些引用特殊处理。

string s = “Hotdog Buns”; query << “SELECT * FROM stock WHERE item = “ << quote_only << s;

这个代码创建了同样的一个查询字符串。我们使用了MySQL++的quote_only操作符。这个操作符会在下一个单元加上单引号,再加入流中。这种方式在MySQL++类型适配器中可以进行完美解析。 引用还是比较简单的,但是SQL语法解析时经常还有一种“单引泄露”。假设一下,我们现在要查找一个数据为”Frank’s Hotdog Buns“这么一个数据表,我们的查询字符串将会是

SELECT * FROM stock WHERE item = ‘Frank’s Brand Hotdog Buns’

其实这样不是一个有效的查询语法,有效的语法应该是

SELECT * FROM stock WHERE item = ‘Frank”s Brand Hotdog Buns’

如你所料,MySQL++对这种引号泄露操作有特殊处理。我们可以用一个很简单的方法来处理它:

string s = “Frank’s Brand Hotdog Buns”; query << “SELECT * FROM stock WHERE item = “ << quote << s;

类似于quote_only,quote等相关的特定操作符在MySQL++中还有许多,可以参考代码中 manip.h 一些定义。 有一点非常重要,我们必须明白MySQL++这些操作符的实现机制。这些操作符在你没有将查询语句创建为一个流之前是完全无效的。另外这些操作符是一种建议的性质,不是命令。当MySQL++认为符合SQL语法时候,MySQL++可能无视这些操作符。 还有一点要注意,这些操作符在组成查询流的时候,以及在使用模板参数查询的时候意义是不完全一致的。

3.6. C++ vs. SQL数据类型

C++和SQL数据类型有些不同,这就导致你在使用MySQL++时候可能会引发一些问题。当然,你在使用其它库的时候也可能出现这些问题。 大多数数据类型都可以保存在SQL数据库中,但是SQL自身是一个文本格式化语言,所以数字类型或者其他类型都要求被转化为文本类型进行数据存储。因此,MySQL++做了很多文本化操作,以便将C++的一些数字型数据简便的进行文本化处理。 一些用户会担心类型转变会引发部分的数据丢失,但显然这不成问题,MySQL++承诺对二进制数据以及数字类型的转化完全不会有信息丢失。 (但是最大的问题还是浮点类型数据。FLOAT 和 DOUBLE 类型的SQL数据类型,我们依旧有些麻烦……)(译者笃志按:原文这句话用小八号字体写在小角落里,这不吭爹么……) 关于类型转换,我们还有一个已知的问题就是SQL的DECIMAL类型转化为C++浮点型时可能会有溢出,显然这种情况我们很难遇到,这是为什么我们暂时没有解决这个BUG的原因。 避免数据类型转换的最好方法依然是使用MySQL++里Lib/sqltypes里的标准格式。 MySQL++不会强制你使用这些typedef。所以你可以在自己的程序里写满 int 而绝不使用 mysqlpp::sql tinyint unsigned。但是,使用MySQL++给你带来以下一些好处: - 空间消耗小。MySQL++类型完全基于MySQL数据类型,没有额外附属信息。 - 平台无关。如果你的程序需要在不同的操作系统上运行,甚至在32位和64位平台上切换运行,使用MySQL++类型将使你的代码平台无关化。 - 清晰易懂。C++类型和SQL类型同时使用的话,将使你的代码混乱不堪,统一为MySQL++类型将使你的代码类型简单易懂。 - 安全。使用C++类型进行数据转换时,可能会引发 TypeLookupFailed 异常,甚至更糟糕。 类型兼容不仅仅对你当前代码很重要,对你以后的代码维护也非常重要。我们会定期的修改MySQL++的类型定义,以更好的适应不同系统平台的C++和不同版本的SQL平台。例如,如果你使用 sql_decimal 替代 DECIMAL,那么当数据库对double不同处理时,我们会自动的使你代码重新编译以适应新的数据库平台。 许多类型定义是使用标准C++数据类型,但是有些是MySQL++自定义的类型。例如:在SQL里的DATETIME类型在MySQL++里被定义为 mysqlpp::DateTime ,为了一致性考虑,在 sql_types.h 里再次将 DateTime 类型定义为 mysqlpp::sql_datetime 类型。 但是MySQL++并没有定义一些非C++标准和SQL的类型,例如空间坐标Vector等类型,使用这些类型时请注意小心。

3.7. 处理SQL的NULL

C++和SQL 都有一类数据称为NULL,但是它们某些情况下是不同的。因此,MySQL++在这里做了一些支持封装。 Both C++ and SQL have things in them called NULL, but they differ in several ways. Consequently, MySQL++ has to provide special support for this, rather than just wrap native C++ facilities as it can with most data type issues.

当SQL NULL 时一个类型修饰时

在SQL中,“NULL”可能是一个类型修饰,它是一个特殊的值。 为适应SQL中的NULL,MySQL++提供一个空的模板去实现类似于C++的NULL类型。例如,我们有一个TINYINT UNSIGNED类型里存储了一个C++的空值。那么可以这样写:

mysqlpp::Nullmysqlpp::sql_tinyint_unsigned myfield;

在MySQL++ v3.1中,我们可以更简单一些,如下去写:

mysqlpp::sql_tinyint_unsigned_null myfield;

这些类型都定义在 lib/sql_types.h 中。你可以自行查看。模板实例化是C++的一个高级特性,这里使用该特性可以更大程度的避免C++和MySQL之间的NULL不同定义问题。

当SQL NULL是一个唯一值时

在SQL中和标准C++中的NULL第二个明显差异就是:SQL可能是一种特殊类型值,它既不是0,也不是false,也不是空字符串,而代表一种“未知/未定义”。此时我们不能用C++的NULL和它画上等号。这种NULL,在MySQL++中我们使用一个全局的 null 对象表示。

myfield = mysqlpp::null;

如果你将SQL的Null填充到C++的IO流中,输出时候你会得到一个类似于普通字符串的“(NULL)”,这样可以适度的保留SQL的NULL值得特性。 如果你尝试保存一个SQL的NULL,你可以使用Null模板。默认该模板第二个参数是 mysqlpp::NullIsNull 。这会自动标记 mysqlpp::null 为SQL的NULL。 我们看下面代码

mysqlpp::Null myfield(mysqlpp::null); cout << myfield << endl; cout << int(myfield) << endl;

这样的话,第一行会输出”(NULL)”,第二行甚至不会被编译通过,而抛出一个错误 CannotConvertNullToAnyOtherDataType 。 但是,如果你希望存储一个标准的 unsigned char 类型的NULL,即0.你可以这样做

mysqlpp::Null myfield(mysqlpp::null); cout << myfield << endl; cout << int(myfield) << endl;

这样则会输出两次 0 。 请注意模板的第二个参数。

3.8. MySQL++的特殊的String类型

MySQL++有两个类很类似 std::string : String 和 SQLTypeAdapter 。 这两个类提供了类似 std::string 的方法并额外提供了一些其他方法,但他们都不是 std::string 的继承子类,也不是一个封装。因此很多用户很喜欢直接使用 std::string 而不使用这两个类。但是这两个类的确对MySQL++而言非常重要,下面我们将用些时间去熟悉他们。

SQLTypeAdapter

这俩类中,相对简单一些的是 SQLTypeAdapter,也可简写为 STA。 如它名字所言,这个类的目的是将其他类型转化为SQL理解的类型。它有一系列的类型转换构造函数,可以将许多其他类型转换为 SQLTypeAdapter 类型。在构造函数的转换过程中,SQLTypeAdapter会记录原有的数据类型信息,所以这种转换不会丢弃任何重要信息。 STA 在MySQL++里创建SQL查询时被经常使用。即使你使用模板查询,也同样会在底层使用 STA。

String

如果说MySQL++有自定义类型,那么只能是 String。但是这个类不是足够被大众习惯使用。可能以后MySQL++将更多的开放基于 std::string 的功能,在V2.3版本之前,String 类名是 ColData ,但是后来发现它的功能不仅仅是保存一行数据,于是就修改类名为 SQLString 了。 String 比 std::string 功能强大的一点是,它知道能够将一些SQL的字符串转换为C++格式的一些特殊类型。例如:如果你使用String类型初始化”2007-11-19”,String 可以将其转换为 Date 类型,通样,Date 类型也可以简便的转换为 String 类型。 因为 Row::operator[] 返回的是 String 类型,所以你可以这样编码:

int x = row[“x”];

在一些情况下,String 其实和 STA 是相反的: String 可以将SQL类型字符串转换为C++数据类型。STA 可以将C++数据类型转换为SQL 类型字符串。

String 主要有俩个用途: 首先它经常被 Row 使用,例如上面的例子,它不仅仅作为 Row::operator[] 返回值,它也是Row内部的核心组成。所以,当MySQL++从数据库取得数据后,都会通过 String 的转换然后再转为你所需要的C++数据类型。 另外,因为String是从数据库取值转换为我们自定义数据的最后一道也是唯一的接口,所以它是需要拷贝出来提供给我们使用的,此时,如果是MySQL++的sql_blob类型,拷贝的代价就过于昂贵了,所以String采用了引用计数。

引用计数

为了减少不必要的内存拷贝,STA和String都采用了 引用计数 和 写时拷贝 技术。写时拷贝 意味着你使用拷贝构造时,并没有真正的拷贝数据,仅仅是拷贝了一个原本对象的数据内存指针,并修改了引用计数。只有当新的数据发生更变的时候,才减少源内存的引用计数并进行真正的拷贝。这样做可以大幅减少内存拷贝的代码,是很有意义的。例如你进行 Row::operator[] 返回String类型的话,将不会进行真正的内存拷贝。