MySQL++ V3.1.0 用户手册3
MySQL++是一个针对MySQL C API的C++封装。它的目的是提供一个类似STL容易一样简单易用的接口,帮助你有效的避免在代码中使用复杂的SQL语句。
3.9. 处理二进制数据
一个SQL新手很容易犯的错误就是将数据库服务器看成是一个文件系统。一个平台内部的确将数据库引擎用作一个文件系统,但是SQL基本设计意图不是这个工作。如果你有一个更好一些的文件管理系统,你应该用它去管理那些庞大的,完整的文件块以及二进制流数据。 一个常见的例子是用户讨论数据库后台的WEB应用程序。如果你用数据库保存这些图片信息,那意味着你要编码从数据库中读取这些数据并发送给客户端,这将更加低效的使用了的 I/O Cache 系统。如果你将这些图片信息保存在文件系统中,你需要做的只是告诉WEB服务器这个图片位置,并在你生成的HTML中为图片设置一个URL,因为你给了WEB服务器一个直接访问硬盘的通道,操作系统将更加高效的进行存取操作,WEB服务器将可以很方便的从硬盘中取得数据并发送到网络中。另外补充一句,你需要避免将这些数据通过高级语言进行传输,典型的避免使用解释性语言。 当然,还有许多人会坚持这点,认为使用数据库引擎具有更高的安全性,其实操作系统和WEB服务器同样可以象数据库系统一样进行安全正确的数据控制管理。
当然有时候你或许真需要存储一些庞大的二进制数据到数据库中。考虑到这点,近代的一些SQL数据库服务支持BLOB数据格式,用来支持Binary Large Object。有时候BLOB也简称为 Binary data,二进制数据。
在MySQL++中,处理二进制数据还是相对容易的,没有 C string 那么复杂。C string 中将空字符串视为一个特殊字符’/0’结尾的字符,但是再二进制数据中没有这种规定。我们下面将一些例子来说明,如果去正确处理BLOB数据。
从二进制文件中读取数据保存到BLOB字段中
在上文,我认为将图像数据存储到数据库中是不正确的,特别是在WEB应用中。但是下面我们仅仅是用图像存储进行讲解。 我们不再使用简单的例子,这里我们首先对JPEG格式图像数据进行分析,将其载入到内存中,然后保存到数据库的BLOB字段里。
下面是examples/load_jpeg.cpp:
#include "cmdline.h"
#include "printdata.h"
#include <mysql++.h>
#include <fstream>
using namespace std;
using namespace mysqlpp;
extern int ag_optind;
// 检查文件头标识是否是JPEG格式文件
static bool is_jpeg(const unsigned char* img_data)
{
return (img_data[0] == 0xFF) && (img_data[1] == 0xD8) &&
((memcmp(img_data + 6, "JFIF", 4) == 0) ||
(memcmp(img_data + 6, "Exif", 4) == 0));
}
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,
"[jpeg_file]"))
{
return 1;
}
try
{
// 建立和数据库服务器的连接
mysqlpp::Connection con(db, server, user, pass);
// 假设命令行参数最后是一个文件名。
// 将文件数据读取到img_data 中,然后检查是否是JPEG格式文件。
string img_name, img_data;
if (argc - ag_optind >= 1)
{
img_name = argv[ag_optind];
ifstream img_file(img_name.c_str(), ios::ate);
if (img_file)
{
size_t img_size = img_file.tellg();
if (img_size > 10)
{
img_file.seekg(0, ios::beg);
unsigned char* img_buffer = new unsigned char[img_size];
img_file.read(reinterpret_cast<char*>(img_buffer),
img_size);
if (is_jpeg(img_buffer))
{
img_data.assign(
reinterpret_cast<char*>(img_buffer),
img_size);
}
else
{
cerr << '"' << img_file <<
"\" isn't a JPEG!" << endl;
}
delete[] img_buffer;
}
else
{
cerr << "File is too short to be a JPEG!" << endl;
}
}
}
if (img_data.empty())
{
print_usage(argv[0], "[jpeg_file]");
return 1;
}
// 将图形数据填充到BLOB 字段中。
// 我们将img_data视为一个类似std::string类型的数据进行存储。
// 但并没有使用类似C string一样的'/0'特殊标识。
Query query = con.query();
query << "INSERT INTO images (data) VALUES(\"" <<
mysqlpp::escape << img_data << "\")";
SimpleResult res = query.execute();
// 如果执行到这里,插入成功
cout << "Inserted \"" << img_name <<
"\" into images table, " << img_data.size() <<
" bytes, ID " << res.insert_id() << endl;
}
catch (const BadQuery& er)
{
// 查询错误
cerr << "Query error: " << er.what() << endl;
return -1;
}
catch (const BadConversion& er)
{
// 数据转换错误
cerr << "Conversion error: " << er.what() << endl <<
"\tretrieved data size: " << er.retrieved <<
", actual size: " << er.actual_size << endl;
return -1;
}
catch (const Exception& er)
{
// 所有其他的SQL++错误
cerr << "Error: " << er.what() << endl;
return -1;
}
return 0;
}
注意,我们在上面创建INSERT查询时使用了 mysqlpp::escape 标识符。因为 mysqlpp::sql_blob 仅仅是MySQL++ string的一个别名,它其实完全就是 String 类型。
从BLOB字段中获取图像
这有一个也非常短小的例子,它从一个给定的字符串中CGI解析得到图像ID。然后根据ImageID从数据库中获取到详细的图像信息。 这个例子是 examples/cgi_jpeg.cpp :
#include "cmdline.h"
#include "images.h"
#define CRLF "\r\n"
#define CRLF2 "\r\n\r\n"
int main(int argc, char* argv[])
{
mysqlpp::examples::CommandLine cmdline(argc, argv, "root",
"nunyabinness");
if (!cmdline)
{
return 1;
}
// 解析CGI字符串环境变量以获得图像ID
unsigned int img_id = 0;
char* cgi_query = getenv("QUERY_STRING");
if (cgi_query)
{
if ((strlen(cgi_query) < 4) || memcmp(cgi_query, "id=", 3))
{
std::cout << "Content-type: text/plain" << std::endl << std::endl;
std::cout << "ERROR: Bad query string" << std::endl;
return 1;
}
else
{
img_id = atoi(cgi_query + 3);
}
}
else
{
std::cerr << "Put this program into a web server's cgi-bin "
"directory, then" << std::endl;
std::cerr << "invoke it with a URL like this:" << std::endl;
std::cerr << std::endl;
std::cerr << " http://server.name.com/cgi-bin/cgi_jpeg?id=2" <<
std::endl;
std::cerr << std::endl;
std::cerr << "This will retrieve the image with ID 2." << std::endl;
std::cerr << std::endl;
std::cerr << "You will probably have to change some of the #defines "
"at the top of" << std::endl;
std::cerr << "examples/cgi_jpeg.cpp to allow the lookup to work." <<
std::endl;
return 1;
}
// 根据ID从数据库中获取Image信息
try
{
mysqlpp::Connection con(mysqlpp::examples::db_name,
cmdline.server(), cmdline.user(), cmdline.pass());
mysqlpp::Query query = con.query();
query << "SELECT * FROM images WHERE id = " << img_id;
mysqlpp::StoreQueryResult res = query.store();
if (res && res.num_rows())
{
images img = res[0];
if (img.data.is_null)
{
std::cout << "Content-type: text/plain" << CRLF2;
std::cout << "No image content!" << CRLF;
}
else
{
std::cout << "X-Image-Id: " << img_id << CRLF;
std::cout << "Content-type: image/jpeg" << CRLF;
std::cout << "Content-length: " <<
img.data.data.length() << CRLF2;
std::cout << img.data;
}
}
else
{
std::cout << "Content-type: text/plain" << CRLF2;
std::cout << "ERROR: No image with ID " << img_id << CRLF;
}
}
catch (const mysqlpp::BadQuery& er)
{
std::cout << "Content-type: text/plain" << CRLF2;
std::cout << "QUERY ERROR: " << er.what() << CRLF;
return 1;
}
catch (const mysqlpp::Exception& er)
{
std::cout << "Content-type: text/plain" << CRLF2;
std::cout << "GENERAL ERROR: " << er.what() << CRLF;
return 1;
}
return 0;
}
当你亲自运行中这个例子的时候,你最好将它安装到一个WEB服务器CGI项目目录下,然后调用一个URL例如:http://freedomknightduzhi.com/cgi-bin/cgi_jpeg?id=1 .它将会从数据库查询到ID为1的一个JPEG图片并发送给WEB服务器然后进行公布。 我们已经可以使用MySQL++进行读取和存储图像二进制流数据了,你还可以尝试自己存储下其他的图片,例如 examples/logo.jpg
3.10. 使用事务
MySQL++的事务类比直接使用SQL的事务更加安全。通常你在堆栈中创建一个事务,然后将执行事务中的查询,你可以调用 Transaction::commit() 函数提交事务中的查询组。如果在你提交之前,事务对象已经出了自己的有效范围,该事务将会进行回滚。这可以确保在下一个事务处理之前,如果有错误可以及时抛出。 这里我们可以看 examples/transaction.cpp 的代码如下:
#include "cmdline.h"
#include "printdata.h"
#include "stock.h"
#include <iostream>
#include <cstdio>
using namespace std;
int main(int argc, char *argv[])
{
// 从命令参数中获取数据库信息
mysqlpp::examples::CommandLine cmdline(argc, argv);
if (!cmdline)
{
return 1;
}
try {
// 连接数据库服务器
mysqlpp::Connection con(mysqlpp::examples::db_name,
cmdline.server(), cmdline.user(), cmdline.pass());
// 获取数据库基本初始状态
mysqlpp::Query query = con.query();
cout << "Initial state of stock table:" << endl;
print_stock_table(query);
// 增加一行事务
{
// 使用一个比MySQL默认隔离级别高的事务。
// 我们要求这个事务高于所有其他的DB连接事务,
// 所以这个事物也会影响下一个事务,即使这个事务我们没有托付。
mysqlpp::Transaction trans(con,
mysqlpp::Transaction::serializable,
mysqlpp::Transaction::session);
stock row("Sauerkraut", 42, 1.2, 0.75,
mysqlpp::sql_date("2006-03-06"), mysqlpp::null);
query.insert(row);
query.execute();
cout << "\nRow inserted, but not committed." << endl;
cout << "Verify this with another program (e.g. simple1), "
"then hit Enter." << endl;
getchar();
cout << "\nCommitting transaction gives us:" << endl;
trans.commit();
print_stock_table(query);
}
// 测试回滚
{
// 进行一个新的事务,设置和上一个事务同样的隔离级别,直到上个事务释放session锁。
mysqlpp::Transaction trans(con);
cout << "\nNow adding catsup to the database..." << endl;
stock row("Catsup", 3, 3.9, 2.99,
mysqlpp::sql_date("2006-03-06"), mysqlpp::null);
query.insert(row);
query.execute();
}
cout << "\nNo, yuck! We don't like catsup. Rolling it back:" <<
endl;
print_stock_table(query);
}
catch (const mysqlpp::BadQuery& er)
{
cerr << "Query error: " << er.what() << endl;
return -1;
}
catch (const mysqlpp::BadConversion& er)
{
cerr << "Conversion error: " << er.what() << endl <<
"\tretrieved data size: " << er.retrieved <<
", actual size: " << er.actual_size << endl;
return -1;
}
catch (const mysqlpp::Exception& er)
{
cerr << "Error: " << er.what() << endl;
return -1;
}
return 0;
}
如果两个进程都想使用同一个事务对同样的两行数据进行存储访问,但是两者操作又无法兼容,此时可能会产生死锁。两者各自持有其中一行数据的锁权,同时它们又得不到另外一行数据的锁权。 MySQL服务器可以发现这个问题,但是我们最好的解决方法也只能是中断第二个事务,这样就可以允许第一个事务正常执行,但是这样第二个事务所在的进程就必须进行事务处理失败的处理了。 在MySQL++里,出现死锁状态,默认情况下会获得这个异常,通过 Query::errnum() 可以得到一个 ER_LOCK_DEADLOCK 异常。但是实际上,事务处理的结果返回值可能是 0,意味着“没错误”。这是为什么呢。因为,MySQL++底层在捕获这个异常后,默认会实现一个回滚查询,所以,你得到的返回值,不再是出现死锁的那个事务返回值,而是回滚查询事务的处理返回值,所以,在MySQL++里不是所有的死锁事务一定会引发一个异常。 但是这个异常我们希望得知,并不希望被隐藏,我们需要对另外一个错误进行检查。不再是 Connection::errnum() 而是Query::errnum()。请查看 examples/deadlock.cpp 说明这个问题,代码如下:
#include "cmdline.h"
#include <mysql++.h>
#include <mysqld_error.h>
#include <iostream>
using namespace std;
extern int run_mode;
int main(int argc, char *argv[])
{
// 从控制台获取数据库参数
mysqlpp::examples::CommandLine cmdline(argc, argv);
if (!cmdline)
{
return 1;
}
// 检查命令行参数是否有意义
const int run_mode = cmdline.run_mode();
if ((run_mode != 1) && (run_mode != 2)) {
cerr << argv[0] << " must be run with -m1 or -m2 as one of "
"its command-line arguments." << endl;
return 1;
}
mysqlpp::Connection con;
try {
// 创建一个和数据库服务器的连接
mysqlpp::Connection con(mysqlpp::examples::db_name,
cmdline.server(), cmdline.user(), cmdline.pass());
// 创建一个事务列。
// 这个事务将为修改的行数据创建锁,一旦两个程序在同一时间意图修改同一行,
// 将出现死锁。MySQL捕获这个问题将通知MySQL++ 使其抛出一个BadQuery 的异常。
// 如果你系那个捕获这个异常,你需要检查BadQuery::errnum() 而不是Connection::errnum(),
// 因为MySQL++创建的回滚查询可能已经执行成功。只有之前的死锁查询才知道本次事务曾失败过。
mysqlpp::Query query = con.query();
mysqlpp::Transaction trans(con);
// 创建并执行查询。
// 只有当第一个程序实例输入回车,否则第二个程序就一直处于死锁状态
char dummy[100];
for (int i = 0; i < 2; ++i) {
int lock = run_mode + (run_mode == 1 ? i : -i);
cout << "Trying lock " << lock << "..." << endl;
query << "select * from deadlock_test" << lock <<
" where x = " << lock << " for update";
query.store();
cout << "Acquired lock " << lock << ". Press Enter to ";
cout << (i == 0 ? "try next lock" : "exit");
cout << ": " << flush;
cin.getline(dummy, sizeof(dummy));
}
}
catch (mysqlpp::BadQuery e)
{
if (e.errnum() == ER_LOCK_DEADLOCK)
{
cerr << "Transaction deadlock detected!" << endl;
cerr << "Connection::errnum = " << con.errnum() <<
", BadQuery::errnum = " << e.errnum() << endl;
}
else
{
cerr << "Unexpected query error: " << e.what() << endl;
}
return 1;
}
catch (mysqlpp::Exception e)
{
cerr << "General error: " << e.what() << endl;
return 1;
}
return 0;
}
这个例子和其他的例子有些不同。你需要先执行一次这个程序实例,它会暂停等待你输入Enter,你需要再执行一次这个程序实例,此时才会有其中一个实例发生死锁异常,你可以看到错误信息输出。
3.11. 使用哪种查询方式?
MySQL++有三种主要的执行查询语句的方式: Query::execute(), Query::store(), Query::use() ,什么情况下需要用哪个?
execute() 主要服务于那些没有数据返回值的查询。例如:CREATE INDEX。这个查询会通过MySQL获得一些信息,而 execute() 函数返回它的调用者一个 SimpleResult 对象,这个对象里包含了一些 例如 语句是否执行成功,行数 等信息也会提供给你。 如果你仅仅是想知道语句是否执行成功,而不需要其他 例如行数 这些信息的话,有一个更简单的接口 Query::exec() ,它会简单的返回一个 bool 告知是否执行成功。
如果你需要从数据库中获取一些数据信息,需要使用 store() 接口,(我们之前的许多例子均使用该接口),这个接口会返回一个 StoreQueryResult 对象,对象内记录了全部的结果表。这个 StoreQueryResult 对象继承于 std::vector< mysqlpp::Row > ,所以它可以使用STL操作方式去获取结果表,也可以很方便的使用下标访问,包括使用前迭代和后迭代等等。 如果你愿意将查询结构保存在一个STL容器中,但是不希望是 std::vector 中,你可以调用 Query::storein() ,这个接口允许你将结果保存到任何一种标准STL容器中,当然,这样的话,你将无法获得 StoreQueryResult 对象,同时也将丢失 StoreQueryResult 对象中的一些其他附属信息。 使用store()接口的确很方便,但是它将消耗额外的内存,这可能是一个惊人的消耗。事实上,MySQL数据库将数据是保存在硬盘中的,它对客户端返回的是一个数据结构,对于MySQL++和底层的C API 库接口来说,它们有自己的一块内存区进行数据保存,所以,如果你有百万条数据,每条数据又大于1KB,那么很容易消耗上GB的内存。
对于这种巨大量级的查询,我们建议使用 use() 接口。它会返回一个 UseQueryResult 对象,它和 StoreQueryResult 很类似,但是它不具备 StoreQueryResult 的随机存储特性。它会要求数据库每次只返回一个数据。返回顺序是线性的,和Vertor提供的随机迭代正好相反,有些类似C++流中的输入迭代. 因为这种限制,你可能需要自己创建一些大点的容器去保存这些数据表。 我们看下使用 use() 接口的例子代码,在 examples/simple3.cpp 中。代码如下:
#include "cmdline.h"
#include "printdata.h"
#include <mysql++.h>
#include <iostream>
#include <iomanip>
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 内全部数据并进行显示。这个例子类似simple2 那个例子。
// 但使用use() 接口查询。所以我们需要逐行进行输出。
// 无法像simple2 那样存储在一个内存容器中遍历输出。
mysqlpp::Query query = conn.query("select * from stock");
if (mysqlpp::UseQueryResult res = query.use())
{
// 输出行首字符标识
cout.setf(ios::left);
cout << setw(31) << "Item" <<
setw(10) << "Num" <<
setw(10) << "Weight" <<
setw(10) << "Price" <<
"Date" << endl << endl;
// 获取每一行结果,输出。
while (mysqlpp::Row row = res.fetch_row())
{
cout << setw(30) << row["item"] << ' ' <<
setw(9) << row["num"] << ' ' <<
setw(9) << row["weight"] << ' ' <<
setw(9) << row["price"] << ' ' <<
setw(9) << row["sdate"] <<
endl;
}
if (conn.errnum())
{
cerr << "Error received in fetching a row: " <<
conn.error() << endl;
return 1;
}
return 0;
}
else
{
cerr << "Failed to get stock item: " << query.error() << endl;
return 1;
}
}
else
{
cerr << "DB connection failed: " << conn.error() << endl;
return 1;
}
}
这个例子和 simple2 例子做了相同的事,仅仅是使用 use() 接口替代了 store() 接口。 就如 use() 接口的设计思路一样,你在解决问题的时候,不应该试图第一时间去考虑如果使用内存解决这些问题。如果能够不将这么庞大的数据保存在内存中,将会更好。或许你会说 SELECT * 得到的结果并不都是你所需要的,你还要使用C++代码去清除一些不需要的数据,那么建议你在查询之前就做出筛选,例如对 SELECT 加上 WHERE 限制,这不仅仅节省了内存,同时也减少了数据库服务器和客户端之间的带宽负载,同样也节省了CPU时间周期。 如果你所需要的限制不能用一个 WHERE 简单的进行限制,请读下一章节。