Обработка данных является главной задачей инструмента, обеспечивающего взаимодействие с СУБД. При этом важное значение имеет установление фактических соответствий между типами данных, применяемых в СУБД (в нашем случае - MySQL), и типами данных языка программирования C++. В предыдущих статьях цикла уже рассматривались примеры, в которых использовались числовые данные (целочисленные и с плавающей точкой) и простые строки без каких бы то ни было проблем.
Тем не менее, при работе с данными более сложных типов возникают определённые вопросы. Даже с теми же строками не всегда всё так просто и ясно. Поэтому сейчас мы рассмотрим различные методы работы со строками, предлагаемыми библиотекой MySQL++, а затем перейдём к вопросам обработки бинарных данных и изображений в BLOB-полях.
1. Различные типы строк в MySQL++
В библиотеке MySQL++ есть два класса, поведение которых в большой степени похоже на поведение std::string - стандартного класса в языке C++: String и SQLTypeAdapter. Функциональные свойства этих классов специализированы по сравнению с std::string, но они не являются производными от этого стандартного класса. Вообще говоря, в коде приложения нет особой необходимости напрямую использовать вышеуказанные классы, поскольку в качестве обобщённого средства обработки строк std::string подходит лучше, к тому же сама MySQL++ в большинстве случаев обращается именно к применению std::string. Но варианты использования специализированных строковых типов также заслуживают внимания, поскольку повышают эффективность функционирования MySQL++.
Начнём с класса SQLTypeAdapter, так как он более прост в применении.
По его наименованию можно понять, что данный класс предназначен для преобразования ("адаптации") других типов данных с тем, чтобы их можно было использовать в SQL. SQLTypeAdapter содержит набор конструкторов преобразования для всех типов данных, которые предположительно могут встретиться в SQL-запросе. Сами по себе SQL-запросы представляют собой строки, поэтому конструкторы преобразования в строковые типы просто копируют строку запроса, но все прочие конструкторы действительно выполняют преобразования значений из "строковой" формы в формат, требуемый для SQL.
Конструкторы преобразований сохраняют информацию о типе, тем самым гарантируя, что любая важная информация, связанная с процессом адаптации типа, не будет утеряна.
Класс SQLTypeAdapter применяется там, где MySQL++ необходимо принять любой (произвольный) из нескольких типов данных для использования в SQL-запросе. В основном это работа механизма шаблонов запросов (template query) и подключение механизма заключения в кавычки и экранирования символов в потоке Query. Каждый раз, когда вы передаёте значения в методы MySQL++ для формирования SQL-запроса, эти данные проходят через объект SQLTypeAdapter. Поэтому, подводя итог, можно сказать, что класс SQLTypeAdapter является ключевым элементом библиотеки MySQL++, отвечающим за генерацию синтаксически корректных SQL-запросов.
Класс String представляет наиболее обобщённый строковый тип внутри библиотеки MySQL++, но в текущей версии (на момент написания статьи - 3.0.9) пока ещё не обладает функциональностью, достаточной для того, чтобы рекомендовать его для повсеместного использования.
Главным "преимуществом" класса String над стандартным std::string является возможность преобразования строк, содержащих значения в SQL-форматах, в типы данных языка C++. Например, если имеется строка "2010-01-20", то вы можете преобразовать эту String-строку в тип Date; и не потому, что Date-объект знает, как использовать String для собственной инициализации, а как раз наоборот: String содержит набор операторов неявного (скрытого) преобразования, определённых для каждого типа.
С учётом того, что оператор обращения к элементу данных в строке Row::operator[] возвращает значение типа String, можно написать, например, следующую инструкцию кода:
int x = row["x"]; |
Разумеется, элемент x строки данных должен содержать корректное значение, которое после неявного преобразования через String должно стать целым числом.
Можно сказать, что String является противоположностью SQLTypeAdapter. Если String выполняет преобразование строк SQL-значений в типы данных C++, то SQLTypeAdapter конвертирует типы данных C++ в строки, содержащие SQL-значения.
Чаще всего String используется для извлечения значения поля из строки Row, как это было показано выше. То есть, во всех случаях, когда MySQL++ извлекает данные из базы данных, они проходят через String-объект со всеми соответствующими преобразованиями, необходимыми для получения корректных типов C++.
Поскольку String является конечной формой данных в наборе результатов перед тем, как они попадут в пользовательский код, возможно возникновение ситуаций, в которых будут создаваться копии этих наборов данных. Если приложение обращается к серверу базы данных в сетевой среде, и в таблицах БД содержатся BLOB-объекты, то подобное копирование приведёт к снижению эффективности работы, а в некоторых случаях просто недопустимо. Здесь на помощь приходит замена копий ссылками.
Для того, чтобы избежать нерационального копирования буфера данных, и SQLTypeAdapter, и String реализованы с использованием механизма учёта ссылок (reference counting) по схеме "copy-on-write". Оба класса совместно используют один и тот же внутренний механизм, поэтому взаимодействие между ними вполне возможно. Таким образом, если вы создаёте один из этих объектов, как производный от другого, то данные не копируются в полном объёме, а в новом объекте создаётся указатель на буфер данных, и счётчик ссылок на этот буфер увеличивается на единицу. Если данные в производном объекте обновляются или ещё как-либо модифицируются, то счётчик ссылок уменьшается на единицу, и создаётся собственный, независимый экземпляр буфера с изменёнными данными. Это позволяет свести к минимуму количество операций копирования данных.
Главная особенность работы с бинарными данными при помощи библиотеки MySQL++ состоит в том, что эти данные практически не поддаются преобразованию в формат C-строки. Любой нулевой байт (а нулевые байты достаточно часто встречаются в массиве бинарных данных) интерпретируется, как символ конца строки. Поэтому обработку больших массивов бинарных данных (BLOB - Binary Large OBject) приходится производить по особой методике.
2.1. Загрузка бинарного файла в поле типа BLOB
Рассмотрим работу с бинарными данными на практическом примере. Предположим, что в дополнение к таблице хранимых товаров (с которой мы работали в предыдущей статье) в нашей базе данных имеется справочная таблица, содержащая фотографии каждого товара, определяемого уникальным идентификатором. Эта таблица с именем photos имеет следующую структуру:
id INT NOT NULL PRIMARY KEY AUTO_INCREMENT image MEDIUMBLOB |
В поле image могут содержаться двоичные данные с максимальным размером 16 Мбайт. Для упрощения примера допустим, что требуемый файл с именем comp_table.jpeg находится в текущем рабочем каталоге программы, исходный код которой приведён ниже (сохраните его в файле с именем load_jpeg.cpp):
#include <mysql++.h>
#include <fstream>
#include <string.h>
using namespace std;
using namespace mysqlpp;
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[] )
{
try
{
// установление соединения с тестовой базой данных
mysqlpp::Connection con( "test_db", "localhost", "tdb_user", "tdb_password" );
string img_name, img_data;
img_name = "comp_table.jpeg";
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 << " не содержит JPEG-изображение" << endl;
delete[] img_buffer;
}
else
cerr << "JPEG-файл должен иметь больший размер" << endl;
}
if( img_data.empty() )
{
cerr << "Указанный файл не содержит данных" << endl;
return -1;
}
// Вставка данных в поле image (тип MEDIUMBLOB) таблицы photos.
Query query = con.query();
query << "INSERT INTO photos (image) VALUES (\"" <<
mysqlpp::escape << img_data << "\")";
SimpleResult res = query.execute();
// Данные вставлены в таблицу успешно
cout << "Файл " << img_name << " добавлен в таблицу photos. Размер в байтах: " <<
img_data.size() << ", идентификатор " << res.insert_id() << endl;
}
catch( const BadQuery &erq )
{
cerr << "Ошибка при выполнении запроса: " << erq.what() << endl;
return -1;
}
catch( const BadConversion &erc )
{
cerr << "Ошибка преобразования данных: " << erc.what() << endl <<
"\tразмер обработанных данных: " << erc.retrieved << endl <<
"\tдействительный размер данных: " << erc.actual_size << endl;
return -1;
}
catch( const Exception &ex )
{
cerr << "Ошибка: " ex.what() << endl;
return -1;
}
return 0;
}
|
При формировании запроса INSERT был использован манипулятор escape, так как для обрабатываемых в данном случае типов MySQL++ не выполняется автоматическое экранирование и взятие в кавычки специальных символов.
3. Работа с изображениями в BLOB-полях
В следующем примере демонстрируется извлечение бинарных данных, записанных в таблицу БД с помощью предыдущей программы load_jpeg, с использованием CGI на локальном web-сервере. Исходный код размещается в файле с именем cgi_jpeg.cpp:
#include <mysql++.h>
#include <ssqls.h>
#include <string.h>
#include <stdlib.h>
sql_create_2( photos, 1, 2,
mysqlpp::sql_int_unsigned, id,
mysqlpp::sql_mediumblob, image )
using namespace std;
int main()
{
unsigned int img_id = 0;
char *cgi_query = getenv( "QUERY_STRING" );
if( cgi_query )
{
if( (strlen(cgi_query) < 4) || memcmp( cgi_query, "id=", 3 ) )
{
cout << "Content-type: text/plain" << endl << endl;
cout << "ERROR: Bad query string" << endl;
return -1;
}
else
img_id = atoi( cgi_query + 3 );
}
else
{
cerr << "Поместите эту программу в каталог cgi-bin локального web-сервера." << endl;
cerr << "Обратитесь к ней с помощью URL, например:" << endl;
cerr << "http://localhost/cgi-bin/cgi_jpeg?id=1" << endl;
cerr << "При этом будет извлечено изображение с идентификатором 1" << endl;
return -1;
}
try
{
mysqlpp::Connection con( "test_db", "localhost", "root", "hardtocrack" );
// Пароль следует заменить на реальный пароль администратора СУБД
mysqlpp::Query query = con.query();
query << "SELECT * FROM photos WHERE id = " << img_id;
mysqlpp::UseQueryResult res = query.use();
if( res )
{
photos pht = res.fetch_row();
cout << "Content-type: image/jpeg" endl;
cout << "Content-length: " << pht.data.length() << "\n\n";
cout << pht.data;
}
else
{
cout << "Content-type: text/plain" << endl << endl;
cout << "ERROR: No such image with ID = " << img_id << endl;
}
}
catch( const mysqlpp::BadQuery &erq )
{
cout << "Content-type: text/plain" << endl << endl;
cout << "QUERY ERROR: " << erq.what() << endl;
return -1;
}
catch( const mysqlpp::Exception &ex )
{
cout << "Content-type: text/plain" << endl << endl;
cout << "GENERAL ERROR: " << ex.what() << endl;
return -1;
}
return 0;
}
|
Скомпилированную программу cgi_jpeg следует поместить в специальный каталог для CGI-модулей вашего локального web-сервера. Затем с помощью любого браузера вызовите её через соотвествующий URL, например, указав в поле адреса http://localhost/cgi_jpeg?id=1. После этого из таблицы photos базы данных test_db будет извлечено JPEG-изображение с идентификатором, равным 1, и выведено в вашем браузере.
Обработка строк различных форматов и больших массивов бинарных данных требует применения специализированных методик, которые мы рассмотрели на конкретных примерах. Подводя итоги, можно отметить, что практическая реализация описанных методик достаточно проста и понятна, следовательно библиотека MySQL++ предлагает эффективное решение и этой задачи.
В данной статье рассматривались различные типы данных и работа с ними. Специализированным формам запросов уделяется особое внимание в следующей, шестой статье. В заключительной, седьмой статье цикла будет продемонстрировано практическое применение библиотеки MySQL++ в многопоточных приложениях.