MySQL & mSQL

Объектно-ориентированный доступ к базам данных на C++


С API прекрасно работают в процедурном программировании на С. Однако они не очень хорошо вписываются в объектно-ориентированную среду C++. Чтобы показать, как реально использовать в программе эти два API, в оставшейся части главы мы создадим с их помощью C++ API для объектно-ориентированного программирования баз данных.

Рис. 13-1. Библиотека объектно-ориенитрованного доступа к базе данных

Поскольку мы занимаемся освещением доступа к базам данных MySQL и mSQL, то сосредоточимся на специфичных для MySQL и mSQL темах и не будем пытаться создать совершенный общий C++ API. Работу с MySQL и mSQL описывают три главных понятия: соединение, результирующий набор и строки результирующего набора. Мы будем использовать эти понятия как ядро объектной модели, на которой будет основываться наша библиотека. Рис. 13-1 показывает эти объекты на UML-диаграмме.*

Соединение с базой данных

В любой среде доступ к базе данных начинается с соединения. Как вы видели в первых двух примерах, MySQL и mSQL по-разному представляют одно и то же понятие - соединение с базой данных. Создание нашей объектно-ориентированной библиотеки мы начнем с абстрагирования от этого понятия и создания объекта Connection . Объект Connection должен уметь устанавливать соединение с сервером, выбирать нужную базу данных, посылать запросы и возвращать результаты. Пример 13-3 показывает заголовочный файл, в котором объявлен интерфейс к объекту Connection.

UML - это новый Унифицированный язык моделирования, созданный Гради Бучем, Айваром Якобсоном и Джеймсом Рамбо (Grady Booch, Ivar Jacobson, James Rumbaugh) в качестве нового стандарта для документирования объектно-ориентированного проектирования и анализа.

Пример 13-3. Заголовок класса Connection

#ifndef l_connection_h

#define l_connection_h

#include <sys/time.h>

#if defined(HAS_MSQL)

#include <msql. h>



#lelif defined(HAS_MYSQL)

#include <mysql.h>

#endif

#include "result.h"

class Connection { private:

int affected_rows;


#if defined(HAS_MSQL)

int connection;

#elif defined(HAS_MYSQL)

MYSQL mysql;

MYSQL 'connection; tfelse

#error База данных не определена,

#endif

public:

Connection(char *, char *);

Connection(char *, char *, char *, char *);

~Connection();

void Close();

void Connect(char 'host, char *db, char *uid, char *pw);

int GetAffectedRows();

char. *GetError();

int IsConnected();

Result *Query(char *);

};

#endif // l_connection_h

Методы, которые предоставляет класс Connection, одинаковы вне зависимости от используемой СУБД. Однако спрятанными в глубине класса окажутся закрытые члены, специфичные для той библиотеки, с которой он будет компилироваться. При установлении соединения единственными различными данными-членами станут те, которые представляют соединение с базой данных. Как отмечалось, mSQL для представления соединения использует величину типа int, a MySQL использует указатель на MYSQL и дополнительную величину типа MYSQL для установления соединения.

Установление соединения с базой данных

Всем приложениям, которые мы будем создавать с использованием этого API, для соединения с базой данных потребуется только создать новый экземпляр класса Connection с помощью одного из его конструкторов. Аналогично, приложение может отсоединиться, уничтожив экземпляр Connection . Оно может даже повторно использовать экземпляр Connection с помощью прямых обращений к методам Close() и Соnnect(). Пример 13-4 показывает реализацию конструкторов и метода Connect().

Пример 13-4. Соединение с MySQL и mSQL в классе Connection

#include "connection.h"

Connection::Connection(char *host, char *db) {

#if defined(HAS_MSQL)

connection = -1;

#elif defined(HASJIYSQL)

connection = (MYSQL *)NULL;

#else

#error Het соединения с базой данных,

#endif

Connect(host, db, (char *)NULL, (char *)NULL); }

Connection::Connection(char 'host, char *db, char *uid, char *pw) {

#if defined(HASJISQL)

connection = -1;

#elif defined(HASJIYSQL)

connection = (MYSQL *)NULL;



#else

#error Нет соединения с базой данных,

#endif

Connect(host, db, uid, pw);

}

void Connection: :Connect(char'host, char *db, char *uid, char *pw)

{

int state;

if( IsConnected() )

{

throw "Соединение уже установлено.";

}

#if defined(HAS_MSQL)

connection = msqlConnect(host);

state = msqlSelectDB(connection, db);

#elif defined (HAS.MYSQL) mysql_init(&mysql);

connection = mysql_real_connect(&mysql, host,

uid, pw,

db, 0, 0); #else

#error Нет соединения с базой данных.

#endif

if( !IsConnected() )

{

throw GetError();

}

if( state < 0 )

{

throw GetError();

}

}

Оба конструктора разработаны с учетом различия параметров, требуемых для соединений MySQL и mSQL. Тем не менее эти API должны разрешать обоим конструкторам работать с каждой из баз данных. Это достигается игнорированием ID пользователя и пароля при вызове конструктора с четырьмя аргументами. Аналогично при вызове конструктора с двумя аргументами, серверу MySQL в качестве значений ID пользователя и пароля передаются значения null. Фактическое соединение с базой данных происходит в методе Connect ().

Метод Connect() инкапсулирует все шаги, необходимые для соединения. Для MySQL он вызывает метод mysql_real_connect() . Для mSQL жe сначала вызывается метод msqlConnect(), а затем msqlSelectDB() . При неудаче на любом из этапов Connect() возбуждает исключительную ситуацию.

Отсоединение от базы данных

Другой логической функцией класса Connection является отсоединение от базы данных и освобождение скрытых от приложения ресурсов. Эту функцию осуществляет метод Close (). В примере 13-5 показано, как происходит отсоединение от MySQL и mSQL.

Пример 13-5. Освобождение ресурсов базы данных

Connection::"Connection() {

if( IsConnected() ) {

Close();

} }

void Connection::Close() {

if( !IsConnected() )

{

return;

}

#if defined(HAS_MSQL)

msqlClose(connection);

connection = -1;

#elif defined(HAS_MYSQL)

mysql_close(connection);

connection = (MYSQL *)NULL;



#else

#error Нет соединения с базой данных, tfendif }

Методы mysql_close() и msqlClose() освобождают ресурсы, используемые соединениями с MySQL и mSQL соответственно.

Выполнение обращений к базе данных

В промежутке между открытием соединения и закрытием базе данных обычно посылаются команды. Класс Connection делает это с помощью метода Query(), принимающего команду SQL в качестве аргумента. Если команда является запросом, она возвращает экземпляр класса Result из объектной модели, представленной на рио. 13-1. Если же команда обновляет данные, то метод возвращает NULL и устанавливает значение affected_rows равным количеству строк, в которых произведены изменения. В примере 13-6 показано, как класс Connection обрабатывает запросы к базам данных MySQL и mSQL.

Пример 13-6. Обработка запроса к базе данных

Result "Connection::Query(char *sql) { T_RESULT *res; int state;

// Если нет соединения, делать нечего

if( !lsConnected(-) ) { throw "Соединения нет.";

}

// Выполнить запрос

#if defined(HAS_MSQL)

state = msqlQuery(connection, sql);

#elif defined(HAS_MYSQL)

state = mysql_query(connection, sql);

#else

#error Нет соединения с базой данных,

#endif

// Если произошла ошибка

if( state < 0 ) { throw GetError();

}

// Забрать результаты, если таковые имеются

#if defined(HAS_MSQL)

res = msqlStoreResult();

#elif defined(HAS_MYSQL)

res = mysql_store_result(connection);

#else

#error Нет соединения с базой данных,

#endif

// Если результат null, это было обновление или произошла ошибка

// Примечание: mSQL не порождает ошибки в msqlStoreResult()

if( res == (T_RESULT *)NULL ) {

// Установить значение affected_rows равным возвращенному msqlQuery()

#if defined(HAS_MSQL)

affected_rows = state;

#elif defined(HAS_MYSQL)

// field_count != 0 означает, что произошла ошибка

int field_count = mysql_num_fields(connection);

if( field_count != 0 )

{

throw GetError();

}

else

{

// Запомнить affected_rows

affected_rows = mysql_affected_rows(connection); }



#else

#error Нет соединения с базой данных,

#endif

//Возвратить NULL в случае обновления

return (Result *)NULL; }

// Для запроса возвратить экземпляр Result

return new Result(res); }

В начале обращения к базе данных делается вызов метода mysql_query() или msqlQuery() с передачей ему команды SQL, которую нужно выполнить. В случае ошибки оба API возвращают отличное от нуля значение. На следующем этапе вызываются mysql_store_result() или msqlStoreResult() , чтобы проверить, получены ли результаты, и сделать эти результаты доступными приложению. В этом месте две СУБД несколько отличаются в деталях обработки.

В mSQL API метод msqlStoreResult() не генерирует ошибки. Эту функцию приложение использует для того, чтобы поместить полученный результирующий набор в хранилище, управлять которым будет приложение, а не mSQL API. Иными словами, при вызове msqlQuery() результаты запоминаются во временной области памяти, управляемой API. Последующие вызовы msqlQuery() затирают эту область памяти. Чтобы сохранить результат в области памяти вашего приложения, нужно вызвать msqlStoreResult() .

Поскольку метод msqlStoreResult() не генерирует ошибку, при его вызове нужно рассматривать две возможности. Если обращение к базе данных было запросом, создавшим результирующий набор, то msqlStoreResult() возвращает указатель на структуру m_result, с которой может работать ваше приложение. При всех других типах обращения (обновление, вставка, удаление или создание) msqlStoreResult() возвращает NULL. Узнать количество строк, обработанных неизвлекающим данные запросом, можно из значения, возвращенного исходным вызовом msqlQuery() .

Подобно msqlStoreResult() , метод mysql_store_result() используется для запоминания данных, возвращенных запросом, в области памяти приложения, но, в отличие от версии для mSQL, необходимо создать для mysql_store_result() некий обработчик ошибок. Именно, значение NULL, возвращенное mysql_store_result() , может означать и то, что запрос не предполагал возвращение результирующего набора, и ошибку при получении последнего. Вызов метода mysql__num_f ields() позволит определить истинную причину. Отличное от 0 значение счетчика полей свидетельствует о происшедшей ошибке. Число измененных строк можно определить при обращении к методу mysql_affected_rows() .*



Другие методы класса Connection

По всему классу Connection разбросаны два вспомогательных метода, IsConnected() и GetError(). Проверить состояния соединения просто — достаточно посмотреть значение атрибута connection. Оно должно быть не NULL для MySQL или отличным от -1 для mSQL. Напротив, сообщения об ошибках требуют некоторых пояснений.

Извлечение сообщений об ошибках для mSQL просто и безыскусно, нужно лишь использовать значение глобальной переменной msqlErrMsg . Ее значение точно совпадает с тем, что возвращает от mSQL метод GetError(). С MySQL дело обстоит несколько сложнее. При обработке любых сообщений об ошибках необходимо учитывать многопоточность. В многопоточной среде обработка ошибок осуществляется путем получения сообщений об ошибках с помощью функции mysql_error() . В примере 13-7 показаны обработка ошибок для MySQL и mSQL в методе GetError(), а также проверка соединения в методе IsConnected() .

Пример 13-7. Чтение сообщений об ошибках и другие вспомогательные задачи класса Connection

int Connection::GetAffectedRows() {

return affected_rows; }

char 'Connection::GetError() {

#if defined(HAS_MSQL)

return msqlErrMsg:

#elif defined(HAS_MYSQL)

if( IsConnected() ) {

return mysql_error(connection); }

else {

return mysql_error(&mysql); }

#else

#error Нет соединения с базой данных,

#endif }

int Connection::IsConnected() {

#if defined(HAS_MSQL)

return !(connection < 0);

#elif defined(HAS_MYSQL)

return !(iconnection);

#else

#error Нет соединения с базой данных,

#endif

)

Проблемы при обработке ошибок

Хотя обрабатывать ошибки, как это описано выше, несложно благодаря инкапсуляции обработки в простой вызов API в классе Connection , следует остерегаться некоторых потенциальных проблем. Во-первых, при работе с mSQL обработка ошибок осуществляется глобально в пределах приложения. Если приложение поддерживает несколько соединений, значение msqlErrMsg относится к последней ошибке последнего вызова какой-либо функции mSQL API. Следует также учесть, что хотя mSQL - однопоточное приложение, можно создавать многопоточные приложения, использующие mSQL, но проявлять крайнюю осторожность при извлечении сообщений об ошибках. Именно, необходимо написать собственный API, корректно работающий с потоками поверх mSQL С API, который копирует сообщения об ошибках и связывает их с соответствующими соединениями.



Обе СУБД управляют и сохраняют сообщения об ошибках внутри своих соответствующих API. Поскольку вы не распоряжаетесь этой деятельностью, может возникнуть другая проблема, связанная с запоминанием сообщений об ошибках. В нашем C++ API обработка ошибок . происходит сразу после их возникновения и до того, как приложение сделает новое обращение к базе данных. Если мы хотим продолжить обработку и лишь позднее заняться ошибками, сообщение об ошибке следует скопировать в область памяти нашего приложения.

Результирующие наборы

Класс Result абстрагируется от понятий результатов MySQL и mSQL. Он должен обеспечивать доступ как к данным результирующего набора, так и к сопутствующим этому набору метаданным. Согласно объектной модели на рис. 13-1, наш класс Result будет поддерживать циклический просмотр строк результирующего набора и получение числа строк в нем. Ниже в примере 13-8 приведен заголовочный файл класса Result.

Пример 13-8. Интерфейс класса Result в result.h

#ifndef 1_result_h

#define 1_result_h

#include <sys/time.h>

#if defined(HASJSQL)

#include <msq1.h>

#elif defined(HAS_MYSQl)

#include <mysq1.h>

#endif

#include "row.h"

class Result { private:

int row_count;

T_RESULT *result;

Row *current_row;

public:

Result(T_RESULT *);

~Result();

void Close();

Row *GetCurrentRow();

int GetRowCount();

int Next(); };

#endif // l_result_h

Перемещение по результатам

Наш класс Result позволяет работать с результирующим набором построчно. Получив экземпляр класса Result в результате обращения к методу Query() , приложение должно последовательно вызывать Next() и GetCurrentRow(), пока очередной Next() не возвратит 0. Пример 13-9 показывает, как выглядят эти действия для MySQL и mSQL.

Пример 13-9. Перемещение по результирующему набору

int Result::Next() { T_ROW row;

if( result == (T_RESULT *)NULL ) {

throw "Результирующий набор закрыт.";

}

#if defined(HAS_MSQL)

row = msqlFetchRow(result);

#elif defined(HAS_MYSQL)



row = mysql_fetch_row(result);

#else

#error Нет соединения с базой данных,

#endif if( ! row )

{

current_row = (Row *)NULL;

return 0;

}

else

{

current_row = new Row(result, row);

return 1;

}

}

Row 'Result::GetCurrentRow()

{

if( result == (T_RESULT *)NULL )

{ throw "Результирующий набор закрыт.";

}

return current_row; }

Заголовочный файл row.h в примере 13-11 определяет T_ROW и T_RESULT в зависимости от того, для какого ядра базы данных компилируется приложение. Перемещение к следующей строке в обеих базах данных осуществляется одинаково и просто. Вы вызываете mysql_fetch_row() или msqlFetchRow() . Если вызов возвращает NULL, значит, необработанных строк не осталось.

В объектно-ориентированной среде это единственный тип навигации, которым вы должны пользоваться. API для базы данных в объектно-ориентированном программировании существует лишь для обеспечения извлечения данных, а не их обработки. Обработка данных должна быть заключена в объектах доменов. Однако не все приложения являются объектно-ориентированными. MySQL и mSQL предоставляют функции, позволяющие перемещаться к определенным строкам в базе данных. Это методы mysql_data_seek() mnsqlDataSeek() соответственно.

Освобождение ресурсов и подсчет строк

Приложения баз данных должны освобождать после себя ресурсы. Обсуждая класс Connection, мы отметили, как результирующие наборы, порождаемые запросом, помещаются в память, управляемую приложением. Метод Close() класса Result освобождает память, занятую этим результатом. Пример 13-10 показывает, как освободить ресурсы, занятые результатом, и получить количество строк в нем.

Пример 13-10. Освобождение ресурсов и подсчет числа строк

void Result::Close() {

if( result == (T_RESULT *)NULL ) { return;

}

#if defined(HAS_MSQL)

msqlFreeResult(result);

#elif defined(HAS_MYSQL)

mysql_free_result(result);

#else

#error Нет соединения с базой данных, ftendif

result = (TJESULT *)NULL; '

}

int Result::GetRowCount()

{



if( result == (T_RESULT *)NULL )

{

throw "Результирующий набор закрыт.";

}

if( row_count > -1 )

{

return row_count;

}

else

{

#if defined(HAS_MSQL)

row_count = msqlNumRows(result);

#elif defined(HAS_MYSQL)

row_count = mysql_num_rows(result);

#else

#error Нет соединения с базой данных,

#endif

return row_count;

}

}

Строки

Отдельная строка результирующего набора представляется в нашей объектной модели классом Row. Класс Row позволяет приложению извлекать отдельные поля строки. В примере 13-11 показано объявление класса Row.

Пример 13-11. Объявление класса Row в row.h

#ifndef l_row_h

#define l_row_h

#include <sys/types.h>

#if defined(HAS_MSQL)

#include <msql.h>

#define T_RESULT m_result

#define T_ROW m_row

#elif defined(HAS_MYSQL)

#include <mysql.h>

#define T_RESULT MYSQL_RES

#define T_ROW MYSQL_ROW

#endif

class Row { private:

T_RESULT 'result;

T_ROW fields;

public:

Row(T_RESULT *, T_ROW);

~Row();

char *GetField(int);

int GetFieldCount();

int IsClosed();

void Close();

};

#endif // l_row_h

В обоих API есть макросы для типов данных, представляющие результирующий набор и строку внутри него. В обоих API строка является массивом строк, содержащих данные этой строки, и ничем более. Доступ к этим данным осуществляется по индексу массива в порядке, определяемом запросом. Например, для запроса SELECT user_id , password FROM users индекс 0 указывает на имя пользователя и индекс 1 -на пароль. Наш C++ API делает это индексирование несколько более дружественным для пользователя. GetField(1) возвратит первое поле, или f ields[0]. Пример 13-12 содержит полный листинг исходного кода для класса Row.

Пример 13-12. Реализация класса Row

#include <malloc.h>

#include "row.h"

Row::Row(T_RESULT *res, T_ROW row) {

fields = row;

result = res; }

Row::"Row() {

if( ! IsClosed() ) {

Close();

}

}

void Row::Close() {

if( IsClosed() ) {

throw "Строка освобождена.";



}

fields = (T_ROW)NULL;

result = (T_RESULT *)NULL;

}

int Row::GetFieldCount()

{

if( IsClosed() )

{

throw "Строка освобождена.";

} #if defined(HASJISQL)

return msqlNumFields(result);

#elif defined(HAS_MYSQL)

return mysql_num_fields(result);

#else

#error Нет соединения с базой данных,

#endif }

// При вызове этого метода нужно быть готовым

// к тому, что может быть возвращен

NULL, char *Row::GetField(int field)

{

if( IsClosed() )

{

throw "Строка освобождена.";

}

if( field < 1 || field > GetFieldCount() .)

{ throw "Индех лежит вне допустимых значений.";}

return fields[field-1]; }

int Row::IsClosed() {

return (fields == (T_ROW)NULL); }

Пример приложения, использующего эти классы C++, прилагается к книге.


Содержание раздела