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 简单的进行限制,请读下一章节。