Книги, научные публикации Pages:     | 1 |   ...   | 2 | 3 | 4 | 5 | 6 |   ...   | 11 |

мтт. ...

-- [ Страница 4 ] --

Часть II. Программные интерфейсы СУБД MySQL Программный интерфейс MySQL С API СУБД MySQL имеет клиентскую библиотеку, созданную на языке программирования С Ее можно использовать для создания клиентских программ, работающих с базами данных СУБД MySQL Библиотека обеспечивает интерфейс, обладающий следующими возможностями. Х Функциями управления подключением для начала и завершения сеанса связи с сервером. Х Функциями конструирования запросов, отсылки их на сервер и обработки полученных результатов. Х Функциями диагностики состояния и ошибок для определения точной причины возникновения ошибки. В этой главе продемонстрировано использование клиентской библиотеки в процессе написания программ. Одной из преследуемых здесь целей будет полное соответствие с уже существующими программами из дистрибуции СУБД MySQL, модульность и возможность повторного использования создаваемого кода. При этом предполагается, что читаГлава 6. Программный интерфейс MySQL С API тель имеет общее представление о программировании на языке С, не являясь при этом экспертом. В главе изложен процесс разработки нескольких клиентских программ от простого к сложному. Сначала разрабатывается скелет программы, который не делает ничего, кроме подключения к серверу и отключения от сервера. Причина этого заключается в том, что несмотря на то, что клиентские программы написаны для выполнения разных задач, они имеют одну общую функцию: функцию подключения к серверу. Разобьем процесс создания программы на несколько этапов. 1. Создание программного кода для подключения к базе данных и отключения от нее (clientl). 2. Добавление проверки на ошибки (client2). 3. Выделение кода подключения в отдельный модуль (clients). 4. Добавление возможности получения параметров подключения (узел, пользователь, пароль) во время работы (client4). Эта последовательность действий является достаточно общей, и ею можно воспользоваться для создания любой клиентской программы Сначала сделаем это, а после этого поразмыслим над тем, каким образом можно обрабатывать различные типы запросов. Сперва научимся обрабатывать жестко заданные операторы SQL, а потом разработаем программу, которая позволит обрабатывать произвольные запросы. После этого вставим программный код, обрабатывающий запросы, в костяк для разработки другой программы (клиента), подобного клиенту mysql. Кроме того, рассмотрим (и решим) более общие проблемы, например, каким образом можно получить информацию о структуре таблиц и хранить изображения в базе данных. В этой главе информация о функциях и типах данных клиентской библиотеки дана только по мере необходимости. Полный перечень функций и типов данных можно найти в приложении Е, "Программный интерфейс приложений С". Эти примеры можно найти на Web-узле и загрузить. Для этого обратитесь к приложению А, "Получение и инсталляция программного обеспечения". | Где можно найти примеры Самый распространенный вопрос где можно найти примеры клиентских программ, написанных на языке С? Конечно же, в этой книге Но иногда люди забывают, что сама дистрибуция СУБД MySQL содержит несколько клиентских программ (например mysql, mysqladmin И mysqldump), большинство из которых написано на С Исходные тексты дистрибуции доступны, поэтому СУБД MySQL уже сама по себе содержит примеры кода клиентских программ Поэтому, если это еще не было сделано, время от времени просматривайте дистрибуцию Клиентские программы СУБД MySQL общедоступны, поэтому их кодом свободно можно пользоваться в процессе создания своих собственных программ Часть II, Программные интерфейсы СУБД MySQL И в исходных текстах, приведенных как в этой главе, так и в дистрибуции СУБД MySQL, всегда можно что-нибудь позаимствовать для своих программ Для этого достаточно скопировать исходный текст из уже существующей программы и немного подкорректировать его Для того чтобы понять принцип работы клиентской программы, необходимо ознакомиться с материалом, изложенным в этой главе Но помните, что совсем необязательно все писать самому (Следует помнить, что одной из целей, преследуемых в этой главе, является возможность повторного использования исходных текстов ) Гораздо лучше воспользоваться результатами работы того, кто это уже когда-то делал Общая процедура создания клиентских программ Этот раздел содержит описание этапов компиляции и связывания программ, использующих клиентскую библиотеку СУБД MySQL. Набор команд, необходимых для построения клиентских программ в различных системах, несколько варьируется. Поэтому, возможно, вам понадобится немного видоизменить команды, предложенные здесь вашему вниманию. Однако это описание имеет общий характер, и его можно применить в процессе создания любой клиентской программы. Основные требования к системе Очевидно, что при программировании клиентских программ на языке С потребуется компилятор С В примерах, приведенных здесь, используется компилятор дсс. Кроме того, потребуется дополнение к исходным файлам. Х Файлы заголовков MySQL. Х Клиентская библиотека MySQL. Эти два компонента и составляют поддержку программирования клиента. Вероятно, они уже установлены в вашей операционной системе. В противном случае их надо получить. Здесь есть два варианта: если СУБД MySQL устанавливалась с исходной или двоичной дистрибуции, поддержка программирования клиентов должна быть установлена в процессе этой инсталляции. Если СУБД MySQL устанавливалась из RPM-файлов, эта поддержка не требуется, так как в таком случае установка производится с помощью соответствующего RPM-инструментария. Получить справку о процессе установки файлов заголовков и библиотеки можно в приложении А, "Получение и инсталляция программного обеспечения".

Глава 6. Программный интерфейс MySQL С API Компилирование и редактирование связей клиентской программы Для успешной компиляции и редактирования связей клиентской программы необходимо определить путь к файлам заголовков СУБД MySQL и клиентскую библиотеку. Обычно они находятся не там, где хранятся такие файлы по умолчанию. Предположим, что файл заголовков и клиентскую библиотеку можно найти, используя путь /usr/local/include/mysql и /usr/local/lib/mysql. Для того чтобы указать компилятору, как можно найти файл заголовка, ему нужно указать аргумент - I / u s r / l o c a l / i n c l u d e / m y s q l во время компилирования исходного файла в объектный файл. Например, можно воспользоваться следующей командной строкой:

% дсс -с -I/usr/local/include/mysql myclient.c Для того чтобы указать редактору связей, как можно найти клиентскую библиотеку, ему нужно указать аргументы -L/usr/local/lib/mysql и -Imysqlclient во время редактирования связей объектного файла при создании исполняемого двоичного кода следующей командной строкой:

% дсс -о myclient myclient.o -L/usr/local/lib/mysql -Imysqlclient Если клиентская программа состоит из нескольких объектных файлов, их все необходимо указать в командной строке. Если редактирование связей завершилось ошибкой из-за того, что редактор связей не смог найти функцию f l o o r ( ), подключите математическую библиотеку, добавив в конец строки -lm:

% дсс -о myclient myclient.o -L/usr/local/lib/mysql -Imysqlclient -lm Может потребоваться подключить таким же образом и другие библиотеки. Например, в системе Solaris может понадобиться -Isocket -Insl. Можно предположить, что уровень позволяет вам работать, избегая ввода командных строк с клавиатуры, и пользоваться командой make. Предположим, что ваш клиент myclient состоит из двух исходных файлов main, с и а и х. с и файла заголовка myclient. h. Простейший файл M a k e f i l e может выглядеть следующим образом:

СС = дсс INCLUDES = -I/usr/local/include/mysql LIBS = -L/usr/local/lib/mysql -Imysqlclient all: myclient main.о: main.с myclient.h $(CC) -c $(INCLUDES) main.с aux.o: aux.c myclient.h S(CC) -c $(INCLUDES) aux.c myclient: main.о aux.o $(CC) -o myclient main.о aux.o $(LIBS) clean: rm -f myclient main.о aux.o Часть II. Программные интерфейсы СУБД MySQL Если в этом случае требуется подключить математическую библиотеку, введите изменения в значение LIBS, добавив в конец -1т:

LIBS = -L/usr/local/lib/mysql -Imysqlclient -1m Если требуются другие библиотеки, такие как -Isocket -Insl, их тоже добавьте в параметр LIBS. С помощью m a k e f i l e можно перестраивать программу после внесения изменений в какой-либо из исходных файлов, вводя make. Это проще и влечет за собой меньше ошибок, чем при вводе длинной командной строки дсс. Client"! - подключение к серверу Наша первая клиентская программа MySQL Ч самая простая. Она подключается к серверу, отключается от него и завершает работу. Само по себе это не имеет смысла, но для того, чтобы работать с базой данных, надо к ней подключиться. Эта операция будет использоваться чаще всего во всех создаваемых вами клиентских программах. Кроме того, эта задача позволяет нам начать знакомство с процессом программирования клиентов с самой простой задачи. В дальнейшем мы будем наращивать функциональность программ по методу от простого к сложному. Исходный текст нашей первой клиентской программы содержится в одном-единственном файле c l i e n t l. с:

/* clientl.с */ #include tinclude #define def_host_name NULL /* узел, к которому производится подключение (умолчание = localhost)*/ #define def_user_name NOLL /* имя пользователя (умолчание = ваше имя регистрации в системе) */ #define def_password NULL /* пароль (умолчание = отсутствует) */ ftdefine def_db_name NULL /*база данных (умолчание = отсутствует)*/ MYSQL *conn;

/* указатель на дескриптор соединения */ int main (int argc, char *argv[]) { conn = mysql_init (NULL);

mysql_real_connect ( conn, /* указатель на дескриптор соединения */ def_host_name, /* узел, к которому осуществляется подключение*/ def_user_name, /* имя пользователя */ def_password, /* пароль */ def_db_name, /* используемая база данных */ О, /* порт (используется значение по умолчанию) */ NULL, /* разъем (используется значение по умолчанию) */ Глава 6. Программный интерфейс MySQL С API mysql_close exit(0);

} 0);

/* флаги (отсутствуют) */ (conn);

Исходный файл начинается со строк, подключающих заголовки stdio.h и mysql.h. Клиент MySQL может включать и больше файлов заголовков, но это является допустимым минимумом. Умолчание значенвй имен узла, пользователя базы данных и пароля прописываются прямо в коде. Позже эти значения будут параметризованы для того, чтобы была возможность указывать их в конфигурационных файлах или в командной строке. Функция main () чстанавливает и разрывает соединение с сервером. Установка соединения с сервером является процессом, который можно разделить на два этапа 1. Вызов функции mysql_init () для получения дескриптора соединения. Это структура, содержащая информацию о соединении. Переменные этого типа называются дескрипторами соединения. При передаче значения "NULL" функции mysql_init (), эта функция размещает переменную, инициализирует ее и возвращает указатель на нее. 2. Вызов функции mysql_real_connect () для установки соединения с сервером. Функция mysql_real_connect () принимает очень много параметров. Х Указатель на дескриптор соединения. Он не может быть равен "NULL'', а должен представлять собой значение, возвращенное функцией m y s q l _ i n i t ( }. Х Имя узла сервера. Если в качестве имени узла указано "NULL'" или узел "localtost", клиент подключится к серверу, запущенному на местном узле через стандартный разъем ОС UNIX. Если в качестве имени узла указано имя узла или IP-адрес узла Ч клиент подключится к указанному узлу, используя подключение TCP/IP. Это справедливо и для операционной системы Windows с одним исключением, что вместо разъема ОС UNIX используется подключение по протоколу TCP/IP. (В ОС Windows NT при указании в качестве имени узла значения "NULL" до использования протокола TCP/IP делается попытка использовать поименованный канал.) Х Имя пользователя и пароль. Если имя имеет значение "NULL'", клиентская библиотека посылает на сервер имя, с которым вы регистрировались. Если пароль имеет значение "NULL", он не посылается. Х Номер порта и файл разъема. Они равны 0 и "NULL", чтобы указать клиентской библиотеке воспользоваться значениями по 280 Часть II. Программные интерфейсы СУБД MySQL умолчанию. При этом эти значения должны быть определены таким образом, чтобы соединение осуществлялось непосредственно с нужным узлом. Детальнее об этом можно узнать в описании функции mysql_real_connect() в приложении Е, "Программный интерфейс приложений С". Х Значения флагов. Это значение равно 0. Это значит, что не будет использоваться никаких специальных опций соединения. Эти опции детально описываются в функции mysql_real_connect О в приложении Е. "Программный интерфейс приложений С". Для отключения соединения передайте указатель на дескриптор соединения функции mysql_close (). Дескриптор соединения, автоматически размещенный функцией mysql_init (), теперь автоматически отменяется функцией mysql_close (). Это приводит к прекращению соединения. Для того чтобы работать с clientl, откомпилируйте и отредактируйте связи, а затем запустите ее, просто выполнив команду: % clientl Эта программа подключается к серверу, разрывает соединение и завершает работу. Не слишком много, но это только начало. И тут очевидны два явных недостатка. Клиентская программа не диагностирует ошибки. Поэтому невозможно сказать, работает ли она вообще. Х Параметры соединения (имя узла, пользователя и т.д.) прописаны непосредственно в коде. Конечно, лучше дать возможность пользователю задавать эти параметры в конфигурационном файле или прямо в командной строке. Это не самые сложные задачи, и они будут решены на следующих этапах. Client2 - добавление диагностики ошибок Второй клиент аналогичен первому, но он уже сможет обрабатывать ошибочные ситуации, возникающие при установлении соединения. Достаточно часто можно прочитать в книгах по программированию о том, что диагностика ошибок "отдается на откуп'' читателю для упражнений в программировании. Давайте посмотрим правде в глаза: программирование диагностики ошибок Ч очень нудное дело. Но автор придерживается другого мнения. Клиентские программы должны реагировать на возникновение ошибок и реагировать на них соответствующим образом. Все вызываемые функции возвращают значения, свидетельствующие о том или ином исходе операции. Глава 6. Программный интерфейс MySQL С API 281 Х Рассмотрим нашу программу clientl. Как можно узнать, было или нет сделано подключение к серверу? Журнал регистрации подключений позволяет судить об этом по событиям "подключение" и "выход", произошедших примерно в это время, когда был сделан запуск программы:

990516 21:52:14 20 Connect 20 Quit paulglocalhost on В противном случае можно увидеть сообщение об отказе соединения:

990516 22:01:47 'paul@localhost' 21 Connect Access denied for user: (Using password: NO) Такое сообщение свидетельствует о том, что соединение не состоялось. К сожалению, сама программа clientl не информирует, какой из этих двух исходов операции произошел. В любом случае сложилась ненормальная ситуация: чтобы узнать о том, как отработала программа, приходится обращаться к системному журналу! Давайте исправим эту ситуацию. Функции клиентской библиотеки MySQL, возвращающие значения, обычно отображают успешное или неуспешное завершение операции одним из двух способов. Х Функции, получающие в качестве параметра указатель на значение, возвращают ненулевое значение в случае успешного завершения операции и '"NULL" в случае ошибки. (В этом контексте "NULL" означает "пустой указатель", а не пустое значение столбца СУБД MySQL.) Все клиентские функции, которыми мы пользовались до сих пор, будь то mysql i n i t ( ) или m y s q l _ r e a l _ c o n n e c t ( ), в случае успешного завершения операции возвращают указатель на дескриптор соединения и "NULL" в случае неудачного завершения операции. Х Функции, получающие в качестве параметра целые числа, возвращают значение 0, в случае успешного завершения операции и значение, отличное от нуля в случае ошибки. При этом важно понимать, что в случае ошибки не следует проверять на конкретные ненулевые значения. Такая функция может не возвратить никакого значения в случае неуспешного завершения операции. Ниже приведен код, который неправильно обрабатывает возвращаемое значение:

if (mysql_XXX() == -1)/* это пример неправильной проверки */ fprintf (stderr, "случилось что-то страшное\п");

Такая проверка может сработать, а может и не сработать. Интерфейс MySQL API не определяет никакого конкретного ненулевого значения, возвращаемого в качестве ошибки за исключением того, что оно должно Часть II. Программные интерфейсы СУБД MySQL быть ненулевым. Проверку на ошибочное завершение операции следует переписать следующим образом: if ( m y s q l _ X X X ( ) ) /* это правильная проверка */ f p r i n t f (stderr, "случилось что-то страшное\п");

или вот так:

if (mysql_XXX() != 0) /* это правильная проверка */ fprintf (stderr, "случилось что-то страшноеХп");

Это две равноценные проверки. И если просмотреть исходные тексты самой MySQL, можно увидеть, что чаще всего используется проверка первого типа, так как она короче. Но далеко не каждый интерфейс возвращает значение. К числу таких можно причислить другую функцию, которая была использована при написании клиентской программы Ч mysql_close (). (Как она может не сработать? И если это произошло, что из того? В любом случае соединение обрывается.) Для выяснения причин сбоя можно воспользоваться вызовом двух функций. Это функция mysql_error(), возвращающая строку, содержащую сообщение об ошибке, и функция mysql_errno (), возвращающая цифровой код ошибки. Их надо вызывать сразу же после обнаружения ошибки потому, что если это сделать после отработки какой-либо другой функции, она вернет уже какой-то свой статус, и вся информация, которая будет получена функциями mysql_error (} или mysql_errno (), будет относиться к функции, вызванной позже. В общем случае пользователю программ значительно удобнее получать строку, содержащую информацию об ошибке, чем числовой код. Для целостности картины воспользуемся обеими функциями. Принимая во внимание все вышесказанное, напишем вторую клиентскую программу client2. Эта программа аналогична clientl, но добавлена соответствующая обработка ошибок. Исходный текст c l i e n t 2. c имеет следующий вид:

/* client2.c */ finclude tinclude ftdefine def_host_name NULL /* узел, к которому производится подключение (умолчание = localhost) */ ^define def_user_name NULL /* имя пользователя (умолчание = ваше имя регистрации в системе) */ #define def_password NULL /* пароль (умолчание = отсутствует) */ ^define def_db_name NULL /*база данных(умолчание = отсутствует)*/ MYSQL *conn;

/* указатель на дескриптор соединения */ int main (int argc, char *argv[]) { conn = mysql_init (NULL);

if (conn == NULL) {fprintf (stderr, "mysql_init() ошибка (вероятно, не хватает памяти)\n");

exit (1);

if (mysql_real_connect ( Глава 6. Программный интерфейс MySQL С API conn, /* указатель на дескриптор соединения */ def_host_name, /* узел, к которому осуществляется подключение*/ def_user_name, /* имя пользователя */ def_password, /* пароль */ def_db_name, /* используемая база данных */ О, /* порт (используется значение по умолчанию) */ NULL, /* разъем (используется значение по умолчанию) */ 0);

/* флаги (отсутствуют) */ == NULL) { fprintf (stderr, "ошибка mysql_real_connect():\nError %u (%s)\n", mysql_errno (conn), mysql_error (conn));

exit (I);

) } mysql_close (conn);

exit (0) ;

Логика проверки на ошибку заключается в том, что в случае ошибочного завершения функции mysql_init () и mysql_real_connect () возвращают значение "NULL". Обратите внимание, что несмотря на то, что программа анализирует значение, возвращаемое функцией mysql_init (), в случае его сбоя функция диагностики ошибок не вызывается. Это происходит потому, что в случае сбоя функции mysql_init () дескриптор соединения не может содержать информации, достойной внимания. Однако при сбое функции mysql_real_connect () дескриптор соединения не отражает реального соединения, содержит информацию об ошибке, которую можно передать функции диагностики ошибок. (Но не передавайте дескриптор другим клиентским программам! Это недопустимо потому, что они предполагают успешное соединение, что, в свою очередь, может стать причиной сбоя программы.) Откомпилируйте и отредактируйте связи client2, а затем запустите программу: % client2 Если client2 не дает никаких сообщений (как показано выше), соединение было успешным. При возникновении сбоя мы увидим следующее:

% client2 mysql_real_connect() ошибка (вероятно, не хватает памяти): Error 1045 (Access denied for user: 'paul@localhost' (Using password: NO)) Это свидетельствует о том, что соединение не было установлено и причину, по которой это произошло. Это также означает, что наша первая программа, clientl, никогда успешно не подключалась к серверу. 284 Часть II. Программные интерфейсы СУБД MySQL (Ведь client 1 использует те же параметры подключения.) Об этом ничего не было известно, так как программа client! "не заботилась" о проверке ошибок. Программа client2 делает такую поверку, поэтому она может сказать, когда что-то происходит не так. Вот почему всегда надо проверять значения, возвращаемые функциями программного интерфейса. Ответы на многие вопросы можно найти при проверке ошибок. Вот типичные вопросы: почему моя программа всегда сбоит при выдаче SQLзапроса или почему мои запросы ничего не возвращают. Во многих случаях программа, о которой идет речь, не проверяет, было ли до генерирования запроса вообще установлено соединение или был ли завершен успешно запрос. Не допускайте такой ошибочной практики, когда априори считается, что каждое обращение к клиентской библиотеке происходит успешно. Все остальные примеры из этой главы обязательно производят проверку на ошибки, и вы, как разработчик аналогичных программ, тоже обязаны делать это. Это может показаться сложным, но, в конечном счете это сокращает время разработки программного обеспечения. Аналогичный подход приводится в последующих главах 7, "Программный интерфейс Perl API", и 8, ''Интерфейс API для языка написания сценариев РНР". Теперь предположим, что отработка программы client2 завершилась сообщением "Access denied" ("В доступе отказано"). Каким образом решить эту проблему? Можно, например, внести изменения в строки #defme, указав там в качестве имен сервера, пользователя и пароля такие, которые позволят осуществить доступ к серверу. Это может помочь хотя бы в том, что появится возможность подключения к серверу. Но в этом случае эти значения жестко прописаны в программном коде. Я не рекомендую пользоваться таким подходом, особенно при указании значения пароля. Может казаться, что в этом случае пароль надежно спрятан потому, что он скомпилирован в двоичном виде, но это совсем не так. (Не говоря о том, что любой, кто имеет право чтения ваших исходных файлов, без особого труда сможет узнать ваш пароль.) О проблеме доступа мы поговорим в разделе "Client4 Ч получение параметров соединения во время выполнения". Но сначала рассмотрим другие способы написания программы подключения. Clients -модульный стиль программирования В третьей программе-клиенте мы сделаем код подключения к серверу и разрыва соединения модульным, запрограммировав его в функциях do_connect() и do_disconnect (), что облегчит его использование другими клиентскими программами. Это альтернатива включению кода подключения в функцию main (). Это вообще хороший прием для любого коГлава 6. Программный интерфейс MySQL С API да. Старайтесь создать функцию, которая будет доступна из любой программы, а не программируйте функцию каждый раз заново. Если в функции будет найдена и исправлена ошибка или сделана какая-то полезная модификация, это лучше делать один раз. Это изменение будет внесено во все программы, использующие данную функцию, сразу же после перекомпиляции. Кроме того, некоторые клиентские программы написаны таким образом, что они подключаются к серверу и отключаются от него по несколько раз за время своего выполнения. Такую клиентскую программу значительно проще создавать и модифицировать, придерживаясь модульного стиля написания программного кода. Стратегию выделения кода в отдельные функции можно описать следующим образом. 1. Разбить общий код на отдельные функции и поместить их в отдельный исходный файл common.с. 2. Создать файл заголовка common.h, содержащий прототипы общих процедур. 3. Включить common.h в исходные файлы программ-клиентов, которые используют общие процедуры. 4. Скомпилируйте общий исходный текст в объектный файл. 5. Отредактируйте связи общего объектного файла и клиентской программы. Создадим процедуры do_connect() И do_disconnect (), применив эту стратегию. Вызов процедуры do_connect () заменит собой вызовы функций m y s q l _ i n i t ( ) и mysql_realconnect () и код анализа ошибки. Вызов новой функции аналогичен mysql_real_connect (), за исключением того, что ему не передается дескриптор соединения. Наоборот, процедура do_connect () сама размещает и инициализирует дескриптор и возвращает указатель на него после соединения. Если происходит ошибка при выполнении процедуры do_connect (), она после диагностического сообщения возвращает ''NULL". (Таким образом, любая программа, которая после выполнения процедуры do_connect() получает значение "NULL", может без какого-либо дополнительного диагностического сообщения просто завершать свою работу.) Процедура do_disconnect (} принимает указатель на дескриптор соединения и вызывает функцию mysql_close (). Вот исходный текст common. с: ^include #include #include "common, h" MYSQL * do_connect (char *host_name, char *user_name, char *password, char *db_name, unsigned int port_nura, char *socket_name, unsigned int flags) { Часть II. Программные интерфейсы СУБД MySQL MYSQL *conn;

/* указатель на дескриптор соединения */ conn = mysql_init (NOLL);

/* размещение и инициализация дескриптора соединения */ if (conn == NULL) { fprintf (stderr, "сбой mysql_init()\n") ;

return (NULL);

} if (mysql_real_connect (conn, host_name, user_name, password, db_name, portnum, socketname, flags) == NULL) { fprintf (stderr, "сбой mysql_real_connect(): \nError %u (%s)\n", mysql_errno (conn), mysql_error (conn));

return (NULL);

~ }return (conn);

/* связь установлена */ void do_disconnect (MYSQL *conn) { mysql_close (conn);

} common.h объявляет common. с".

прототипы процедур, находящихся в программе MYSQL * do_connect (char *host_name, char *user_name, char *password, char *db_name, unsigned int port_num, char *socket_name, unsigned int flags);

void do_disconnect (MYSQL *conn);

Для того чтобы получить доступ к общим процедурам, подключите в исходном файле common.h. Обратите внимание на то, что common, с тоже содержит common, h. В этом случае компилятор выдаст предупреждение, если объявление функций в исходном файле common. с не совпадает с объявлениями в файле заголовка. Также если будет изменена последовательность вызовов в тексте программы common.с без соответствующих изменений в common. h, компилятор обязательно сделает предупреждение во время перекомпилирования common.с. Конечно, возникает резонный вопрос, зачем вообще изобретать функцию do_disconnect (), которая делает так мало. Абсолютно верно, что процедура do_disconnect О и функция mysql_close () эквивалентны. Но давайте предположим, что когда-нибудь позднее понадобится запрограммировать дополнительные операции по очистке памяти в момент отключения. В случае, если есть унифицированная процедура, достаточно внести изменения только в нее. Это сделать будет невозможно, если функция mysql_close () вызывается непосредственно в теле главной процедуры. Ранее я утверждал, что модульный стиль программирования очень удобен и позволяет использовать модульные процедуры в нескольких Глава 6. Программный интерфейс MySQL С API программах или в нескольких местах одной программы Предыдущий абзац объясняет одну причину, почему это так. Вот два примера, которые обеспечат дополнительное обоснование этому. Х Пример 1. В версиях СУБД MySQL, предшествующих 3.22, вызов функдии mysql_real_connect () производится не совсем так, как в последней версии. Отсутствует параметр с именем базы данных. И если попробовать применить процедуру do_connect () со старой клиентской библиотекой СУБД MySQL, она не заработает. Но процедуру do_connect () можно исправить так, чтобы она работала и с более ранними версиями. Это значит, что, изменяя процедуру docor.nect (), мы можем увеличить переносимость всех программ, которые ее используют. Если программный код включать непосредственно в каждый клиент, придется вносить изменения во все программы. Для того чтобы процедура do_connect () могла работать с более ранними версиями функции mysql_realconnect (), нужно воспользоваться макросом MYSQL_VERSION_ID, который содержит номер текущей версии. Модифицированный do_connect () проверяет значение MYSQL_VERSION_ID, и в зависимости от этого обращается к соответствующей версии функции mysql_real_connect (): MYSQL * do_connect (char *host_name, char *user name, char *password, char *db_name, unsigned int port_num, char *socket_name, unsigned int flags) { MYSQL *conn;

/* указатель на дескриптор соединения */ conn = mysql_anit (NULL);

/* размещение и инициализация дескриптора соединения */ if (conn == NULL) { fprintf (stderr, "сбой mysql_init()\n");

return (NULL);

} fif defined(MYSQL_VERSION_ID) &S MYSQL_VERSION_ID >= 32200 /* 3.22 и выше */ if (mysql_real_connect (conn, host_name, user_name, password, db_name, portnum, socket_name, flags) == NULL) ( fprintf (stderr, } #else if { "сбой mysql_real_connect(): \nError %u ( % s ) \ n ", mysql_errno (conn), mysql_error ( c o n n ) ) ;

return (NULL);

/* до версии 3.22 */ (mysql_real_connect (conn, host_name, username, password, portnum, socket_name, flags) == NULL) fprintf "сбой mysql_real_connect():

(stderr, \nError %u (%s)\n", Часть II. Программные интерфейсы СУБД MySQL mysql_errno (conn));

(conn), mysql_error return (NULL);

} if (db_name '= NULL)/*имитация действия параметра dbname*/ { if (mysql_select_db (conn, dbname) != 0) { fprintf (stderr, ^endif return (conn) ;

/* соединение установлено */ } Модифицированная версия процедуры do_connect() аналогична предыдущей версии за исключением двух моментов. Х Она не передает параметр db_name старой версии функции mysql_real_connect () потому, что эта версия не имеет такого параметра. Х Если имя базы данных непустое, процедура do_connect (} вызывает функцию mysql_select_db () для того, чтобы сделать указанную базу данных текущей. (Это имитирует реакцию на отсутствие параметра db_name.) Если база данных не находится, процедура do_connect () выдает сообщение об ошибке, завершает соединение и возвращает значение "NULL" как свидетельство об ошибке Пример 2. Этот пример базируется на изменениях, внесенных в процедуру do_connect (), по сравнению с первым примером. В результате этих изменений появилось три вызова диагностических функций mysql_errno О и mysql_error (). Действительно утомительно программировать детальное сообщение о возникающих ошибках. Номер ошибки тоже трудночитаем. Значительно проще прочитать что-то наподобие: print_error (conn, "сбой m y s q l _ r e a l _ c o n n e c t ( ) " ). Так что соберем диагностику ошибок в процедуру pnnt_error (). Ею можно воспользоваться для того, чтобы выполнить какие-то осмысленные действия, даже если переменная conn будет равняться "NULL'". Таким же образом можно воспользоваться процедурой print_error (), если функция mysqlinit () возвращает ошибку. Таким образом мы избежим путаницы вызовов (одни f p r i n t f () и другие print error ( ) ).

"сбой mysql_select_db(): \nError %u ( % s ) \ n ", mysql_errno (conn), mysql_error ( c o n n ) ) ;

mysql_close (conn);

return ( N U L L ) ;

Глава 6, Программный интерфейс MySQL С API Ю-172У Кто-то может возразить, что при этом программный код делается нечитаемым только потому, что надо показать, что модульный стиль программирования лучше. Ведь совсем не обязательно писать весь программный код обработки ошибочных ситуаций каждый раз, когда это необходимо. Достаточно просто скопировать его. Все это правильно, но у меня есть возражения. Х Даже если использовать копирование, значительно проще копировать короткие блоки исходного текста. Х Независимо от того, предпочитаете вы или нет вызывать обе функции каждый раз, когда необходимо диагностировать ошибки, постоянная запись диагностирующего кода может привести к внесению изменений и порождает неуверенность при диагностировании ошибок. Заключение диагностирующего кода в отдельную функцию уменьшает этот соблазн и повышает однозначность программного кода. Х При необходимости изменить формат сообщений об ошибках, это проще сделать в одном месте. Или если принято решение выводить сообщение об ошибках в регистрационный журнал, а не на устройство stderr (или в дополнение к нему), это проще сделать только в процедуре print_error ( ). Этот подход позволяет уменьшить количество ошибок и увеличивает уверенность в положительных результатах своего труда. Х Если при тестировании программы используется отладчик, то можно порекомендовать один очень удобный прием: поместить точку останова внутри функции диагностики ошибок. Вот текст диагностирующей функции print_error ( ) : void print_error (MYSQL *conn, char *message) { fprintf (stderr, "%s\n", message);

if (conn != NULL) { fprintf (stderr, "Error %u (%s)\n", mysql_errno (conn), mysql_error (conn) ) ;

Процедура print_error () находится в common, с, поэтому добавим ее прототип в common. h: void print_error (MYSQL *conn, char *message) ;

Теперь внесем изменения в do_connect ( ) и добавим вызов процедуры print_error ( ) : MYSQL * do_connect (char *host_name, char *user_name, char *password, char *db name, Часть II. Программные интерфейсы СУБД MySQL unsigned int port_num, char *socket_name, unsigned int flags) { MYSQL *conn;

/* указатель на дескриптор соединения */ conn = mysql_init (NULL);

/* размещение и инициализация дескриптора соединения */ if (conn == NOLL) { print_error (NULL, "сбой mysql_init() (вероятно, не хватает памяти)");

return (NULL);

} #if defined (MYSQL_VERSION_ID) && MYSQL_VERSION_ID >= 32200 /* 3.22 и выше */ if (mysql_real_connect (conn, host_name, user_name, password, db_name, port_num, socket_name, flags) == NULL) { print_error (conn, "сбой mysql_real_connect()") ;

return (NULL) ;

~~ ~ } #else /* до-3.22 */ if (mysql_real_connect (conn, host_name, user_name, password, portnum, socket_name, flags) == NULL) { print_error (conn, "сбой mysql_real_connect()");

return (NULL);

~ ~ } if (db_name != NULL) /* имитация действия параметра dbname */ { if (mysql_select_db (conn, dbname) != 0) { { print_error (conn, "сбой mysql_select_db()");

mysql_close (conn);

return (NULL);

#endif return (conn);

/* соединение установлено */ } Основной исходный файл clients.с аналогичен первоначальному исходному файлу client2. с, но та часть программы, которая отвечает за соединение с сервером и отключение от него, теперь заключена в процедурах. Следовательно, этот код принимает вид: /* clients.с */ ^include ^include finclude "common.h" tdefine def_host_name NULL /* узел, к которому производится подключение (умолчание = localhost) */ ttdefine def_user_name NULL /* имя пользователя (умолчание = ваше имя регистрации в системе) */ #define def_password NULL /* пароль (умолчание = отсутствует) */ Idefine def_port_num 0 /* использовать порт по умолчанию */ Глава 6. Программный интерфейс MySQL С API 10 ttdefine def_socket_name NULL /* использовать разъем по умолчанию */ idefine def_db_name NULL /* база данных (умолчание = отсутствует) */ MYSQL *conn;

/* указатель на дескриптор соединения */ int main (int argc, char *argv[J) { conn = do_connect (def_host_name, def_user_name, def_password, def_db_name, def_port_num, def_socket_name, 0) ;

if (conn == NULL) exit (1);

/* Здесь основное тело программы */ do_disconnect (conn);

exit (0);

Client4 - получение параметров соединения во время выполнения Отлично, теперь мы имеем легкомодифицируемый и "пуленепробиваемый" в смысле устойчивости к ошибкам код. Попробуем изобразить что-нибудь более интеллектуальное, чем использование пустых параметров соединения, чтобы дать пользователю возможность определять эти параметры во время выполнения программы. Предыдущий клиент clients имеет один существенный недостаток Ч параметры соединения "зашиты" внутри него. Для того чтобы их изменить, необходимо модифицировать исходный текст и перекомпилировать его. Это не совсем удобно, особенно тогда, когда программа предназначается для использования другими людьми. Обычным методом определения параметров соединения во время выполнения является использование опций в командных строках. Программы из дистрибуции СУБД MySQL принимают параметры соединения в любой форме из двух, показанных в табл. 6 1. Таблица 6.1. Стандартные опции командной строки СУБД MySQL Параметр Имя узла Имя пользователя Краткая форма - h имя_узла - и имя пользователя Длинная форма Ч поз1=1;

/ия_узла Ч изег=имя пользователя Часть II. Программные интерфейсы СУБД MySQL Окончание табл 6 1 Параметр Пароль Номер порта Имя разъема Краткая форма - р или -рваш_пароль -Р номер_порта -S имя_разъема Длинная форма Ч password или Ч ра55\тогс1=ваш_пароль Ч рог(=номер_порта Ч эосКе1=имя_разъема Для обеспечения совместимости со стандартами СУБД MySQL клиент, создаваемый нами, будет воспринимать данные в аналогичном формате. Это достигается достаточно просто, так как клиентская библиотека включает функцию, которая осуществляет синтаксический анализ вводимых опций. Кроме того, наша программа-клиент получит возможность получать информацию из конфигурационных файлов. Это позволит разместить параметры соединения в файле ~/. m y. c n f (в файле. m y. c n f, находящемся в вашем корневом каталоге) и освободит от необходимости указывать их в командной строке. Клиентская библиотека позволяет проверять конфигурационные файлы СУБД MySQL и выбирать любые значения из них. Добавив только несколько строк в текст своей программы, можно ее сделать более интеллектуальной, и для этого совсем не надо изобретать велосипед. Синтаксис конфигурационного файла описан в приложении Д, "Программы MySQL". Доступ к содержимому конфигурационного файла Для чтения конфигурационного файла можно воспользоваться функцией load_defaults (). Функция l o a d _ d e f a u l t s ( ) просматривает конфигурационные файлы, производит синтаксический анализ их содержимого, обновляя вектор аргументов вашей программы (массив argv[]). Таким же образом анализируются опции, заданные в командной строке. Поэтому при синтаксическом анализе опций командной строки мы получаем параметры как часть обычного цикла анализа опций. Опции добавляются в начало массива a r g v [ ]. Так что если опции задаются одновременно и в конфигурационном файле, и в командной строке, они попадают в массив argv [ ] позже, чем любая опция, которая добавляется функцией load_defaults ( ). Вашему вниманию предоставляется небольшая программа show_argv, показывающая работу с функцией load_defaults () и иллюстрирующая, к каким изменениям это приводит в векторе аргументов:

Глава 6. Программный интерфейс MySQL С API /* show_argv.c V #include tinclude char *groups[] = { "client", NULL };

main (int argc, char * a r g v [ ] ) ( int i;

my_init ();

int printf ("Первоначальный вектор аргументов:\п");

for (i = 0;

i < argc;

i++) printf ("arg%d: %s\n", i, argv[i]);

load_defaults ("my", groups, iargc, sargv);

printf ("Модифицированный вектор аргументов:\n");

for (i = 0;

i < argc;

i++) printf ("arg %d: %s\n", i, argv[i]);

exit (0) ;

} Конфигурационный файл содержит следующие данные. Х groups [ ] Ч это символьный массив, определяющий в каких группах конфигурационных файлов вы заинтересованы. Для клиентских программ как минимум определяется ''client'" (для группы [client]), а последний элемент обязательно должен иметь значение "NULL". Подпрограмма my_init () является инициализирующей подпрограммой, выполняющей операции установки, необходимые подпрограмме l o a d _ d e f a u l t s ( ). Подпрограмма load_defaults () принимает четыре аргумента: префикс конфигурационных файлов всегда должен быть "ту", массив с перечнем нужных групп и адреса счетчика аргументов программы и вектора. Не передавайте значения счетчика и вектора непосредственно. Передавайте их адреса, потому что load_defaults () должна менять их значения. Необходимо обратить внимание на следующее: несмотря на то, что argv является указателем, передается только &argv, или адрес указателя.

Х Х Подпрограмма show_argv печатает свои аргументы дважды, первый раз, когда они определяются в командной строке, а потом по мере изменения подпрограммой load_defaults (). Для того чтобы посмотреть, как работает подпрограмма load_defaults (), убедитесь в том, что в вашем корневом каталоге существует файл.my.cnf с определенными установками для группы [client]. Предположим, что файл.my.cnf имеет следующий вид:

[client] user=paul password=secret host=some_host Часть II. Программные интерфейсы СУБД MySQL В этом случае выполнение подпрограммы show_argv даст результат:

% show_argv а Ь Первоначальный вектор аргументов arg 0: show_argv arg 1: а arg 2: b Измененный вектор аргументов: arg 0: show_argv arg 1:

- -user=paul arg 2:

- -password=secret arg 3:

- -host=some_host arg 4: a arg 5: b Возможно, что в выводе появятся некоторые опции от show_argv, которых нет в командной строке или в конфигурационном файле ~/.my. cnf. В таком случае, возможно, что это опции, заданные в общесистемном конфигурационном файле. Подпрограмма load_defaults () в действительности сначала ищет файл /etc/my. cnf и my. cnf в каталоге данных СУБД MySQL и только потом читает файл.my. cnf в вашем пользовательском каталоге. (В ОС Windows подпрограмма load_defaults () производит поиск в следующей последовательности C : \ m y. c n f, C:\mysql\data\my.cnf и файл my. ini в системном каталоге ОС Windows.) Клиентские программы, работающие с подпрограммой load_def a u l t s (), почти всегда определяют "client" в списке групп (таким образом, они всегда получают все общие установки клиентов из конфигурационного файла). Но может возникнуть необходимость в значениях, характерных только для вашей программы. Измените строку и вместо: char *groups[] = { "client", NULL };

введите следующее: char * g r o u p s [ ] = { [client] user=paul password=secret host=some_host [show_argv] host=other_host "show_argv", "client", NOLL };

После этого добавьте группу [show_argv] в свой файл ~/.my. cnf:

После внесения этих изменений повторный вызов show_argv даст несколько другой результат:

% show_argv a b Первоначальный вектор аргументов arg 0: show_argv arg 1: а arg 2: b Измененный вектор аргументов: arg 0: show_argv Глава 6. Программный интерфейс MySQL С API arg ang arg arg arg arg 1: 2: 3: 4: 5: 6:

a b -user=paul -password=secret -host=some_host -host=other_host Порядок следования значений опций в массиве аргументов определяется порядком их следования в конфигурационном файле, а не порядком следования групп в массиве groups [ ]. Это означает, что вы, вероятно, хотите определить группы, которые характерны для данной программы, следующие за группой [client] в вашем конфигурационном файле. Таким образом, если определить опции в двух группах, преимуществом будут пользоваться опции, присущие данной программе. Это можно видеть по предыдущему примеру. Опция узла была задана как в группе [client], так и в группе [show_argv], но так как группа [show_argv] идет в конфигурационном файле последней, ее установки узла будут представлены позже в векторе. Программа load_defaults () не выбирает значения из ваших установок среды. Для того чтобы воспользоваться переменными установки среды, такими как MYSQL_TCP_PORT или MYSQL_UNIX_PORT, обратитесь к программе getenv (). Такую возможность добавлять не будем, но пример проверки значений ряда стандартных переменных, связанных со средой MySQL, привести можно:

extern char *getenv();

char *p;

int port_num;

char *socket_name;

if ( (p = getenv ("MYSQL_TCP_PORT")) != NULL) port_num = atoi (p);

if ((p = getenv ("MYSQL_UNIX_PORT")) != NULL) socket_name = p;

В стандартных программах MySQL значения стандартных переменных среды имеют более низкий приоритет, чем значения, заданные в конфигурационных файлах или в командной строке. Если есть определенные сомнения в значениях, принимаемых переменными среды, проверьте все установки среды до (а не после) вызова подпрограммы load_defaults () или обработки опций командной строки. Синтаксический анализ аргументов командной строки Теперь можно получить все параметры в вектор аргументов, но как их теперь обрабатывать? Функция getopt_long () запрограммирована для выполнения этих задач. Функция getopt_long() встроена в клиентскую библиотеку СУБД MySQL, таким образом необходимо получить доступ к ней. В исходный 296 Часть II. Программные интерфейсы СУБД MySQL файл необходимо включить файл заголовков get opt. h. Этот файл заголовка можно скопировать из дистрибуции исходных текстов СУБД MySQL в каталог, где будет разрабатываться клиентская программа. 8 Функция load_defaults () и безопасность Вероятно, у вас возникают вопросы, связанные с возможностью просмотра с помощью команды ps списка аргументов произвольного процесса. Тут никакой проблемы не возникает, так как ps отображает первоначальное содержание argv[]. Любой аргумент пароля, который создается функцией load_defaults (), содержит ссылку на область, которую он распределяет для себя. Эта область не является частью обычного вектора аргументов, поэтому ps ее "не видит" Однако пароль, указанный в командной строке, используется командой ps до тех пор, пока он не будет удален. Как это сделать, изложено дальше в f- этом разделе. Программа show_param читает установки с помощью load_defaults О конфигурационного файла, а затем вызывает функцию getopt_long () для синтаксического анализа вектора аргументов. Программа show_param иллюстрирует операции, происходящие на каждой фазе обработки аргументов, выполняя следующие действия. 1. Устанавливает значения по умолчанию для имени узла, имени пользователя и пароля. 2. Выводит первоначальный параметр соединения и значения вектора аргументов. 3. Вызывает функцию load_defaults О для того, чтобы изменить вектор аргументов в соответствии со значениями, заданными в конфигурационном файле, и распечатывает результирующий вектор. 4. Вызывает функцию getopt_long() для обработки вектора аргументов, а затем распечатывает полученные значения параметров и оставшихся в векторе аргументов величин. Программа show_param позволяет экспериментировать с различными вариантами параметров соединения (независимо от того, где они расположены Ч в конфигурационном файле или в командной строке) и увидеть полученные значения, которые будут использованы при соединении. Программа show_param удобна для получения представления о поведении нашей следующей клиентской программы, где мы реально подключим этот программный код обработки параметров к функции подключения do_connect(). Вот как выглядит программа show_param. с: /* show_param.c */ #include linclude /* нужен для a t o i ( ) */ ^include " g e t o p t. h " Глава 6. Программный интерфейс MySQL С API char *groups[] = { "client", NULL };

struct option long_options [ ] = { {"host", required_argument, ("user", required_argument, ("password", required_argument, NULL, {"port", required_argument, {"socket", required_argument, NULL, {"host", required_argument, { 0, 0, 0, 0 } NOLL, NULL, 'p'}, NULL, 'S'}, NOLL, 'h'), 'u'}, 'P'}, 'h'}, int main (int argc, char *argv[]) { char *host_name = NULL;

char *user_name = NULL;

char *password = NULL;

unsigned int port_num = 0;

char *socket_name = NULL;

int i;

int c, option_mdex;

my_init () ;

printf printf printf printf printf printf ( "Первоначальные параметры соединения : \n" ) ;

("имя узла: %s\n", host_name ? host_narae : "(null)");

("имя пользователя: %s\n", user_name ? user_name : "(null)");

("пароль: %s\n", password ? password : "(null)");

("номер порта: %u\n", port_num) ;

("имя разъема: %s\n", socket_name ? socket_name : "(null)");

printf ( "Первоначальный вектор аргументов: \п");

for (i = 0;

i < argc;

i++) printf ("arg %d: %s\n", i, argv[i]);

load_defaults ("my", groups, sargc, sargv) ;

printf ("Измененный вектор аргументов после load_defaults () :\n") ;

for (i = 0;

i < argc;

i++) printf ("arg %d: %s\n", i, argv[i]);

while ( (c = getopt_long (argc, argv, "h:p: :u:P:S: ", long_options, &option_index) ) '= EOF) { switch (c) { case ' h ' : host_name = optarg;

break;

case 'u' : user_name = optarg;

break;

case 'p' : password = optarg;

break;

case 'P' : 298 Часть II. Программные интерфейсы СУБД MySQL port_num = (unsigned int) atoi (optarg) ;

break/case ' S ' : socket_name = optarg;

break;

argc -= optind;

/* перемещение указателя на аргументы, которые еще не обработаны */ argv += optind;

/* функцией getopt_long ( ) */ printf printf printf printf printf printf ("Параметры соединения после 7функции getopt_long ( ) : \п") ;

("имя узла: %s\n", host_name host_name : "(null)");

("имя пользователя: %s\n", user_name 9 user_name : "(null)");

("пароль: %s\n", password ? password : "(null)");

("номер порта: %u\n", port_num) ;

? ("имя разъема: %s\n", socket_name socket_name : "(null)");

printf ("вектор аргументов после getopt_long ( ) : \n") ;

for (i =0;

i < argc;

i++) printf ("arg %d: %s\n", i, argv[i]);

exit (0);

} Для обработки вектора аргументов show_argv вызывает в цикле функцию getopt_long ( ) while ((с = getopt_long (argc, argv, " h : p : :u : P: S : ", long_options, Soption_index) ) '= EOF) { /* обработка */ } Два первых аргумента, передаваемых getopt_long (), Ч это счетчик аргументов и вектор Третий аргумент содержит перечень распознаваемых опций Опции представлены в форме коротких имен (букв) За буквами могут следовать двоеточия, двойные двоеточия или их может не быть вовсе Это делается для того, чтобы показать, что за опцией должна следовать опция, может следовать опция или опция не должна следовать соответственно Четвертый аргумент long_options является указателем на массив структуры опций, каждая из которых определяет информацию для опции, которую должна распознавать программа Ее назначение аналогично строке опций в третьем аргументе Четвертый элемент каждой структуры long_options [ ] может быть таким. Х Длинное имя опции. Х Значение опции. Значение может равняться required_argument. Параметры optional_argument, или no_argument показывают, что за опцией будет следовать опция, может следовать опция или будет отсутствовать опция (Назначение его аналогично двоеточию, двойному двоеточию или отсутствию двоеточия в строке третьего аргумента ) Глава 6. Программный интерфейс MySQL С API Х Флаг. Он используется для запоминания указателя на переменную. Если опция определена, getopt_long () сохраняет в переменной значение, указанное в четвертом аргументе. Когда флаг имеет значение "NULL", функция getopt_long () присваивает переменной optarg значение указателя на значение, следующее за опцией, и возвращает короткое имя опции. Наш массив long_options [ ] задает значение "NULL" для всех опций. Значит, функция opt^long () возвращает каждый аргумент таким образом, чтобы их можно было обрабатывать в операторе switch. Х Короткое имя опции (состоящее из одного символа). Короткие имена, хранящиеся в массиве long_options [ ], должны соответствовать буквам, указанным в строке опций, которые передаются в третьем аргументе функции getopt_long (). В противном случае, программа не сможет обработать командную строку соответствующим образом. Массив long_options [] завершается структурой, все элементы которой равны 0. Пятый аргумент getopt_long () является указателем на переменную типа int. Функция getopt_long () сохраняет в эту переменную индекс структуры long_options [ ], которая имеет отношение к последней перечисленной опции. (showparam это значение не изменяет.) Опция пароля (обозначенная - -password или -р) может принимать различные значения, т е. ее можно задать как - -password или -password=eufM<_лa/KMb в случае использования длинной формы, или -р, или -рваш_пароль в случае использования короткой формы. Факультативная природа значения пароля отражена повторением двоеточия после буквы '"р" в строке аргумента optional_argument в массиве long_options []. Клиенты СУБД MySQL позволяют опускать значение пароля в командной строке, а затем запрашивают его. Это позволяет не указывать пароль в командной строке, сохраняя его таким образом в тайне. При написании следующей клиентской программы, client4 это будет обязательно учтено и добавлен соответствующий код. Вот пример вызова программы show_param и полученных результатов (предполагается, что содержимое конфигурационного файла - /. m y. c n f по сравнению с примером show_argv не изменилось).

% show_param -h yet_another_host x Первоначальный вектор аргументов: host name: (null) user name: (null) password: (null) port number: 0 socket name: (null) Измененный вектор аргументов: arg 0: show_param arg 1:

-h arg 2: yet_another_host Часть II. Программные интерфейсы СУБД MySQL arg 3 x Измененный вектор аргументов после load defaults)): arg 0 show_param arg 1 --user=paul arg 2 --password=secret arg 3 --host=sorae_host arg 4 -h arg 5 yet_another_host arg 6 x Параметры соединения после getopt_long(): host name: yet_another_host user name: paul password: secret port number: 0 socket name: (null) Вектор аргументов после getopt_long(): arg 0: x Этот результат показывает, что имя узла выбирается из командной строки (перекрывая значение, заданное в конфигурационном файле), а имя пользователя и его пароль Ч из конфигурационного файла. Процедура getopt_long () анализирует принятые опции, независимо от способа их задания в короткой (-h имя_узла) или длинной (- -user=paul, - -password=secret) форме. Теперь давайте с помощью простых данных посмотрим, как работают подпрограммы обработки полученных опций, а потом используем его как основу для программы, которая подключается к серверу, основываясь на любых данных, будь то данные, полученные из командной строки или из конфигурационного файла. Полученный исходный текст c l i e n t 4. c приобретет следующий вид:

/* client4.c */ #include #include /* для atoi() */ ^include #include "common.h" ftinclude "getopt.h" #define def_host_name NOLL /* узел, к которому необходимо подключиться (умолчание = localhost) */ #define def_user_name NULL /* имя пользователя = имени регистрации в системе */ #define def_password NULL /* пароль (умолчание = отсутствует )*/ ttdefine def_port_num 0 /* использовать порт по умолчанию */ ftdefine def_socket_name NULL /* использовать разъем по умолчанию */ #define def_db_name NULL /* база данных (умолчание = отсутствует )*/ char *groups[] = { "client", NULL };

struct option long_options[] = { {"host", {"user", required_argument, required_argument, NULL, ' ', h} NULL, 'u'}/ Глава 6. Программный интерфейс MySQL С API };

{"password", {"port", {"socket", {"host", { 0, 0, 0, required_argument, required_argument, required_argument, required_argument, } NULL, NULL, NULL, NULL, 'p'}, 'P'}, 'S'}, 'h'}, MYSQL int *conn;

/* указатель на дескриптор соединения */ main (int argc, char *argv[j) { char *host_name = def_host_name;

char *user_name = def_user_name;

char *password = defjpassword;

unsigned int port_num = def_port_num;

char *socket_name = def_socket_name;

char *db_name = de f _db_name ;

char passbuf [100] ;

int ask_password = 0;

int c, option_index=0;

int i;

my_init load_defaults ("my", groups, $argc, Sargv) ;

while ( (c = getopt_long (argc, argv, "h:p: :u:P:S: ", long_options, &option_index) ) != EOF) { switch (c) { case 'h' :

host_name = optarg;

break;

case 'u' :

*optarg++ = ' ' ;

) break;

case 'P1 : port_num = (unsigned int) atoi (optarg) ;

break;

case 'S' : socket_name = optarg;

break;

user_name = optarg;

break;

case 'p 1 : if (! optarg) /* значение отсутствует */ ask_password = 1;

else /* скопировав пароль, удалите оригинал */ { (void) strncpy (passbuf, optarg, sizeof (passbuf ) -1 } ;

passbuf [sizeof (passbuf )-l] = ' \ 0 ' ;

password = passbuf;

while (*optarg) argc -= optind;

/* перемещение указателя на аргументы, Часть II. Программные интерфейсы СУБД MySQL которые еще не обработаны */ argv += optind;

/* функцией getopt_long() */ if t } (argc > 0) db_name = argv(0];

--argc;

++argv;

if (ask_password) password = gettty_password (NOLL) ;

conn = do_connect (host_name, user_name, password, db_name, port_num, socket_name, 0);

if (conn == NOLL) ~ exit (1);

/* здесь размещается код, выполняющий основные операции */ do_disconnect (conn);

exit ( 0 ) ;

) По сравнению с программами clientl, client2 и clients, разработанными нами ранее, client4 имеет следующие особенности. Х Позволяет задавать имя базы данных прямо в командной строке. Это соответствует интерфейсу стандартных программ, которые поставляются с дистрибуцией СУБД MySQL. Х Удаляет любой пароль в векторе аргументов сразу же после его копирования. Таким образом, максимально уменьшается временной интервал, на протяжении которого пароль, заданный в командной строке, будет "виден" команде ps или другим программам, имеющим доступ к информации о состоянии системы. (Интервал уменьшается, а не ограничивается. И все же такой метод, при котором пароль задается в командной строке, остается очень рискованным с точки зрения безопасности.) Х Опция пароля может задаваться без значения. В таком случае клиент запрашивает пароль с помощью подпрограммы get_tty_password(). Эта подпрограмма из клиентской библиотеки запрашивает пароль, не повторяя его на экране. (В клиентской библиотеке таких "штучек" хватает. Рекомендуется также просматривать исходные тексты программы СУБД MySQL. Это позволит узнать больше о возможностях этих подпрограмм.) Правомочен вопрос: почему просто не вызвать функцию getpass () ? Но далеко не все системы имеют эту функцию (в ОС Windows, например, такая возможность отсутствует). Функция get_tty_password () работает в любой системе. Программа client4 работает в соответствии с заданными ей опциями. Для упрощения предположим, что конфигурационный файл отсутствует. Запустив client4 без аргументов, вы подключаетесь к узлу localhost без пароля, под именем, под которым вы зарегистрировались Глава 6. Программный интерфейс MySQL С API в ОС UNIX. Запустите client4 с помощью командной строки, приведенной ниже, программа запросит пароль (сразу же после ключа -р пароль отсутствует), соединит вас с узлом some_host и передаст имя пользователя some_user на сервер вместе с введенным вами паролем:

% client4 -h some_host -u some_user -p some_db Программа client4 передает имя базы данных some_db на do_connect (} и делает ее текущей базой данных. Если конфип рационный файл существует, обрабатывается его содержимое, которое затем используется для соответствующего изменения параметров соединения. Ранее, создавая функции подключения и отключения от сервера, мы прибегали к модульному методу. Резонен вопрос, можно ли включить программный код, занимающийся анализом опций (ключей), в эти функции. Сделать это можно, но я не рекомендую. Программный код анализа опций не так постоянен, как код подключения. Программы часто поддерживают в дополнение к стандартным опциям (ключам) дополнительные опции. Различные программы, вероятнее всего, поддерживают различные наборы дополнительных опций. Это делает затруднительным написание универсального процесса обработки опции. Кроме того, в отличие от процедуры установки соединения, которая может выполняться несколько раз во время выполнения программы, анализ опций производится только один раз в начале программы. Выполненная нами операция является универсальной для любой клиентской программы MySQL: подключение к серверу с помощью соответствующих параметров. Конечно, необходимо знать, как подключаться. Но теперь вы уже это знаете и детали этого процесса реализованы в коде клиентской программы (client4.c), так что об этом можно уже не беспокоиться. Это значит, что можно полностью сфокусировать внимание на самом главном Ч возможности получить доступ к данным, хранящимся в базе данных. Все действия программы должны размещаться между вызовами doconnect () и do_disconnect О. Но то, что у нас уже есть, можно использовать в качестве основы для создания различных программ. Для того чтобы написать новую программу, необходимо выполнить следующее. 1. Скопировать client4. с. 2. Внести изменения в цикл обработки параметров, если будут задействованы другие или дополнительные ключи. 3. Добавить код, реализующий ваш алгоритм между вызовами do_connect () и do_disconnect (). И все готово. Главное назначение заготовки клиентской программы заключается в том, что ее можно легко использовать для установки или разрыва соединения, а сам разработчик может сфокусировать свое внимание на том, что он действительно хочет сделать. Теперь это можно осуществить, тем самым демонстрируя довольно парадоксальный принцип, что дисциплина порождает свободу. 304 Часть II. Программные интерфейсы СУБД MySQL Обработка запросов Теперь нам известно, как можно начать и закончить "общение" с сервером. Рассмотрим как ведет себя само соединение. Этот раздел демонстрирует процедуру "общения" с сервером при обработке запросов. Каждый запрос к базе данных предполагает выполнение следующих шагов. 1. Создать запрос. Как это делается, зависит от содержимого запроса, в частности, содержит ли он двоичные данные. 2. Сделать запрос, послав его на сервер. 3. Обработать результат запроса. Зависит от типа сделанного запроса. Например, оператор SELECT возвращает строки с данными. Оператор INSERT не возвращает. Очень важную роль при создании запросов выполняет функция, с помощью которой запросы будут посылаться на сервер. Чаще всего используется подпрограммы mysql_real_query (). С их помощью запрос высылается в виде строки со счетчиком длины. Так как строка запроса передается вместе со счетчиком, ее содержимое может быть чем угодно, даже двоичными данными или пустыми байтами. В этом случае запрос не завершается символом "NULL". Другая функция передачи запросов, mysgl_query (), более ограничена в своих возможностях, но при этом и более удобна в использовании. Запросы, которые передаются функции mysql_query (), завершаются символом "NULL", что означает, что такие запросы не могут содержать в себе символ "NULL". (Наличие байтов со значением "NULL" просто приведет к ошибочной интерпретации их как более коротких, чем они есть на самом деле.) В общем, если запрос создается для того, чтобы передавать произвольные данные, и существует вероятность, что они будут содержать пустые байты, использования функции mysql_query{) следует избегать. Существенным преимуществом использования строк с ограничением символом "NULL" является возможность использования строковых функций, вероятно, хорошо уже вам знакомой стандартной библиотеки С, таких как strcpy () и sprintf (). Рассмотрим еще один фактор, с которым сталкиваешься при создании запросов: прибегаете ли вы к операциям, содержащим управляющие символы? Да, если создается запрос с использованием двоичных данных или других символов, таких как кавычки или обратная косая черта. Эта проблема будет обсуждаться в дальнейшем. Простейший набросок кода, обрабатывающего запрос, выглядит следующим образом: if (mysql_query (conn, query) != 0) { /* ошибка, сообщение об ошибке */ } else */ /* успешное выполнение;

определить результаты выполнения запроса */ Глава 6. Программный интерфейс MySQL С API Как функция m y s q l _ q u e r y ( ), так и функция mysql_real_query() возвращают 0 в случае успешного завершения запроса и ненулевое значение в случае ошибки. Успешное завершение запроса означает, что сервер не определил в нем ошибки и мог его выполнить. Это никак не свидетельствует о результате выполнения запроса. Например, это не значит, что запрос SELECT выбрал хоть одну строку или что оператор DELETE удалил хотя бы одну строку. Проверка результата, полученного ог выполнения запроса, требует дополнительной обработки. Запрос может дать ошибку по совершенно различным причинам. Вот самые вероятные из них. Х Он содержит синтаксическую ошибку. Х Он содержит семантическую ошибку, например, в запросе делается обращение к столбцу, не существующему в таблице. Х У вас нет прав для осуществления доступа к данным, указанным в запросе. Запросы можно сгруппировать в две общие категории: запросы, возвращающие результаты, и запросы, не возвращающие результаты. Такие запросы как INSERT, DELETE и UPDATE попадают в категорию запросов, "не возвращающих результата". Они ничего не возвращают даже для запросов, которые модифицируют базу данных. Единственной информацией, которую можно от них получить, является количество обработанных строк. Такие запросы, как SELECT и SHOW попадают в категорию запросов, "возвращающих результат", все-таки как никак их предназначением является возвращение какого-то результата. Набор строк, возвращаемый таким запросом, называется "результирующим набором". В СУБД MySQL это тип данных MYSQL_RES, представляющий собой структуру, содержащую значения данных в строках и метаданные о значениях (такие как названия столбцов и длины значений данных). Пустой результирующий набор (т.е. такой, который содержит ноль строк) нельзя идентифицировать как "нет результата". Обработка запросов, не возвращающих результатов Для обработки запросов, не возвращающих результирующих наборов, запрос генерируется с помощью функции mysql_query () или mysql_real_query (). При успешном завершении запроса можно будет узнать, сколько запросов было вставлено, удалено или модифицировано. Для этого нужно вызвать функцию mysql_af f ected_rows (). Это делается так: if (mysql_query ( c o n n, "INSERT INTO my_tbl SET name = 1 My Name ' " ) != 0) { print_error ("Сбой оператора INSERT ");

} else Часть II. Программные интерфейсы СУБД MySQL printf ( "Оператор INSERT успешно завершен, обработано: %lu строкХп", (unsigned long) mysql_affected_rows ( c o n n ) ) ;

} Обратите внимание, каким образом результат mysql_af fected_rows () "преобразуется" в переменную типа unsigned long для вывода на печать. Эта функция возвращает значение типа my_ulonglong. Так сделано потому, что печатать значение этого типа напрямую в некоторых системах нельзя. (Например, я убедился, что она работает под UNIX FreeBSD, но дает сбой при работе в Solaris.) Преобразование типа значения в тип unsigned long и указание типа формата "%lu" решает эту проблему. Подобное рассуждение применимо к любым функциям, возвращающим значения типа my_ulonglong, таким как mysql_num_rows () и mysql_insert_id (). Помните это, если хотите, чтобы ваши клиентские программы были переносимы. Функция mysql_rows_affected () возвращает количество строк, обработанных запросом, но значение самого термина "обработано строк" зависит от типа запроса. Для запросов INSERT, REPLACE или DELETE это количество добавленных, замещенных или удаленных строк соответственно. Для запроса UPDATE это означает количество реально модифицированных строк. СУБД MySQL не модифицирует строку, которая содержит те же данные, что и новые. Это значит, что хотя строка и может быть выбрана для модификации (с помощью предложения WHERE оператора UPDATE) она не будет реально изменена. Вообще, для оператора UPDATE термин "обработано строк" является неоднозначным. Это может означать "совпадающих строк", т.е. количество строк, выбранных для модификации, даже если операция модификации реально не изменит их значений. Существует возможность настроить систему таким образом. Для этого во время подключения к серверу нужно процедуре mysql_real_connect () передать значение флага CLIENT_FOUND_ROWS. Значение CLIENT_FOUND_ROWS также можно передать как аргумент и процедуре do_connect (), она передаст это значение процедуре mysql_real_connect(). Обработка запросов, возвращающих результаты Запросы, возвращающие данные, делают это в виде результирующего набора. Они могут быть получены после передачи запроса на сервер с помощью функций mysql_query() ИЛИ mysql_real_query(). Нужно помнить, что запрос SELECT в СУБД MySQL не является единственным запросом, возвращающим строки. К запросам такого рода можно отнести запросы SHOW, DESCRIBE и EXPLAIN. Для всех этих запросов необходимо Глава 6. Программный интерфейс MySQL С API произвести обработку полученных после запроса строк. Обработка результирующего набора предполагает следующие шаги. Х С помощью mysql_store_result() или mysql_use_result() получить результирующий набор. Эти функции возвращают указатель на MYSQL_RES в случае успешного завершения или значение "NULL" в случае ошибки. Позже мы остановимся на различиях между mysql_store_result () и mysql_use_result () и условиях, при которых можно будет одну функцию предпочесть другой. Здесь в примерах мы будем использовать функцию mysql_store_result (), немедленно возвращающую полученные строки с сервера и сохраняющую их в областях данных клиента. Х Для каждой строки результирующего набора вызовем mysql_fetch_row(). Эта функция возвращает значение MYSQL_ROW, которое представляет собой указатель на массив строк, представляющих значения каждого столбца строки. Что вы будете делать со строкой, зависит от задач приложения. Можно просто распечатать значения в столбцах, на их основе произвести какие-то статистические вычисления, или произвести какие-то другие действия. Функция mysql_fetch_row() возвратит значение "NULL", когда в результирующем наборе строк больше не осталось. Х После завершения всех операций с результирующим набором для очистки памяти, которую он использует, необходимо вызвать niysql_free_result(). Если этого не сделать, ваше приложение может вызвать переполнение оперативной памяти. (Очень важно не забывать это делать для приложений, работающих длительное время;

в противном случае скоро станет заметно, что система медленно, но верно все чаще будет занята процессами, которые будут поглощать все больше ресурсов системы.) В следующем примере рассмотрим обработку результирующего набора, полученного после выполнения запроса: MYSQL_RES *res_set;

(mysql_query (conn, "SHOW TABLES FROM mysql") '= 0) print_error (conn, "сбой raysql_query()");

else { res_set = mysql_store_result (conn);

/* генерировать результирующий набор */ if (res_set == NULL) print_error (conn, " Сбой mysql_store_result()") ;

else { if /* обработать результирующий набор, а затем удалить его */ process_result_set (conn, res_set);

mysql_free_result (res_set);

Часть II. Программные интерфейсы СУБД MySQL Здесь мы совсем обошли вниманием вызов функции p r o c e s s _ r e s u l t _ s e t (), помощью которой обрабатывается каждая строка из результирующего набора. Сделаем это. В общем случае функции обработки результирующего набора основаны на цикле, имеющем следующий вид: MYSQL_ROW row;

while ( ( r o w = mysql_fetch_row ( r e s _ s e t ) ) != NULL) /* произвести определенные операции над содержимым строки */ Функция mysql_f etch_row О возвращает в MYSQL_ROW значение, которое является указателем на массив значений. Таким образом доступ к каждому значению можно осуществить с помощью выражения row [ i ], где i лежит в диапазоне от 0 до количества столбцов в строке минус один. Вот несколько особенностей типа данных MYSQL_ROW, которые необходимо иметь в виду при работе с ним. Х Тип MYSQL_ROW является указательным типом. Таким образом переменные этого типа необходимо объявлять как MYSQL_ROW row, a не как MYSQL_ROW *row. Х Строки массива MYSQL_ROW завершаются пустым значением. Однако если столбец содержит двоичные данные, он может содержать и пустые байты. Такую строку нельзя рассматривать как строку, ограниченную пустым значением. Получите длину столбца, чтобы определить, какую длину может иметь значение столбца. Х Значения данных всех типов возвращаются как строки. Для обработки числовых значений как чисел их необходимо преобразовать из строкового формата в числовой самостоятельно. Х Значения "NULL" в массиве MYSQL_ROW представляются указателями "NULL". За исключением случая, когда столбец объявлен как "NOT NULL", всегда необходимо проверять, являются или не являются значения в столбцах указателями со значением "NULL". Приложения, создаваемые вами, могут делать все что угодно с данными в каждой строке. Для иллюстрации просто распечатаем строки с табуляциями между столбцами в качестве разделителей. Для этого нам потребуется дополнительная функция из клиентской библиотеки mysql_num_f ields (). Эта функция возвращает количество столбцов в строке. Вот программный код для process_result_set (): void process_result_set MYSQL_ROW row;

unsigned int i;

(MYSQL *conn, MYSQL_RES *res_set) Глава 6. Программный интерфейс MySQL С API } while ( ( r o w = mysql_fetch_row ( r e s _ s e t ) ) != NULL) { for (i =0;

i

i { if (i > 0) fputc { ' U ', stdout);

printf ("%s", row[i] != NULL ? row[i] : "NULL");

} fputc C \ n ', stdout);

} if (mysql_errno (conn) != 0) print_error (conn, "mysql_fetch_row{) f a i l e d " ) ;

else printf ("возвращено рядов %lu \ n ", (unsigned long) mysql_num_rows ( r e s _ s e t ) ) ;

Программа process_result_set () распечатывает каждую строку в формате разделения столбцов табуляцией (отображая значения "NULL" словом NULL) с последующим количеством выбранных строк. Это количество получено с помощью функции mysql_num_rows (). Как функции mysql_af fected_rows () и mysql_num_rows (), эта функция возвращает значение типа my_ulonglong. Таким образом преобразуем в тип unsigned long и с помощью формата "%lu" распечатаем его. За циклом выборки строк следует тестирование на ошибки. При создании результирующего набора с помощью mysql_store_result () значение "NULL", возвращаемое из m y s q l _ f e t c h _ r o w ( ), будет означать "больше строк нет". Однако при создании результирующего набора с помощью mysql_use_result () значение "NULL", возвращаемое из mysql_f etch_row (), будет означать как "больше строк нет", так и то, что произошла ошибка. Это тестирование просто дает возможность программе process_result_set () определить, произошла или нет ошибка, независимо от метода создания результирующего набора. Эта версия process_result_set () взяла на вооружение минималистический подход к выводу значений столбцов, который имеет определенные недостатки. Например, предположим, что выполняется следующий запрос: SELECT last_name, first_name, city, state FROM president Он даст следующий результат: Adams John Adams John Quincy Arthur Chester A. Buchanan James Bush George W. Carter James E. J r Cleveland Grover Braintree Braintree Fairfield Mercersburg Milton Plains Caldwell MA MA VT PA MA GA NJ Этот вывод можно улучшить, добавив наименования столбцов и выровняв столбцы по вертикали. Для этого необходимо узнать имена 310 Часть II. Программные интерфейсы СУБД MySQL столбцов и самое длинное значение в каждом столбце. Эта информация имеется в наличии, но не как часть данных, а как часть метаданных результирующего набора (данные о данных). После небольшого усовершенствования кода обработки запроса, в разделе "Метаданные результирующего запроса" мы сможем усовершенствовать формат вывода данных. Печать двоичных данных Значения столбцов, содержащих пустые данные, могут не сработать правильно с помощью спецификации формата "%s" оператора printf (). Оператор printf () ожидает строку, завершающуюся пустым значением, и распечатывает строку только до пустого значения. Для двоичных данных лучше всего воспользоваться длиной столбца. Таким образом можно будет распечатать все значение. Для этого можно воспользоваться операторами fwrite () или putc(). Обработка общих запросов Все предыдущие примеры обработки запросов были написаны исходя из того, возвращает или не возвращает оператор данные. Это было возможно благодаря тому, что текст самого запроса был включен в код. Мы использовали оператор INSERT, который не возвращал результирующий набор, и оператор SHOW TABLES, который возвращал. Однако зачастую нельзя заранее знать, какой тип оператора имеет запрос. Например, при выполнении запроса, который вводится с клавиатуры или из файла, это может быть произвольный оператор, т.е. заранее нельзя предсказать, возвращает или нет этот файл строки. Что делать? Проводить синтаксический анализ запроса для того, чтобы определить, какой тип он представляет? Это не очень просто. Совсем недостаточно проверить первое слово в строке, тем более, что это может быть комментарий:

/* комментарий */ SELECT...

К счастью, совсем не обязательно знать тип запроса заранее для того, чтобы обработать его соответствующим образом. Интерфейс MySQL С API позволяет создавать программы, безошибочно обрабатывающие запросы любого типа, независимо от того, возвращает или не возвращает он результирующий набор. Перед тем как его создать, определим как он работает. Х Делаем запрос. Если он дает сбой, то выходим. Х Запрос срабатывает без ошибок. Для выборки строки с сервера и создания результирующего набора вызываем процедуру mysql_store_result(). Х Сбой процедуры mysql_store_result () может быть вызван тем, что запрос не возвращает результирующий набор или в процессе выборки произошла ошибка. Различить эти два результата можно, если вызвать функцию mysql_f ield_count () и проверить результат, возвращаемый ею. Глава 6. Программный интерфейс MySQL С API Х Функция mysql_f ield_count ( ) возвращает значение, отличное от нуля. Запрос должен возвратить результирующий набор, но произошла ошибка. Это может случиться по самым разным причинам. Например, результирующий набор может быть слишком велик и для его размещения не хватило памяти или во время выборки результатов с сервера произошел какойто сбой в сети. Ситуацию немного усложняет тот факт, что функции mysql_field_count () до версии 3.22.24 не существовало. В более ранних версиях вместо нее можно воспользоваться функцией mysql_num_f ields ( ). Для того чтобы программа могла работать с более ранними версиями MySQL, в программный код, работающий с mysql_f ield_count (), включите следующий фрагмент:

#if !defined(MYSQL_VERSION_ID) || MYSQL_VERSION_ID<32224 Idefine mysql_field_count mysql_num_fields #endif Он позволит при работе с СУБД MySQL версии более ранней, чем 3.22.24, вместо вызова mysql_f ield_count () делать вызов mysql_num_f ields ( ). Х Процедура mysql_f ield_count ( ) возвращает 0, это означает, что запрос не возвращает результирующего набора. (Это был запрос INSERT, DELETE ИЛИ UPDATE). Х Процедура mysql_store_result () выполняется успешно. Запрос возвращает результирующий набор. Обрабатывайте строки с помощью процедуры mysql_fetch_row() до тех пор, пока она не возвратит значение "NULL"'. Ниже приведена распечатка программы, обрабатывающая любой запрос.

#if !defined(MYSQL_VERSION_ID) || MYSQL_VERSION_ID<32224 #define mysql_field_count mysql_num_f ields fendif process_query (MYSQL *conn, char *query) { MYSQL_RES *res_set;

unsigned int field_count;

if (mysql_query (conn, query) 1= 0) /* сбой запроса */ { print_error (conn, "Сбой process_query () ") ;

return;

/* запрос завершил работу успешно;

определить, вернул он данные или нет */ res_set = mysql_store_result (conn) ;

void 31 Часть II. Программные интерфейсы СУБД MySQL if (res_set == NULL) /* результирующий набор не возвращен */ { /* * означает ли отсутстве результирующего набора, что * произошла ошибка или не вернулся результирующий набор? */ if (mysql_f ield_count (conn) > 0) ( /* * ожидался результат, но процедура mysql_store_result ( ) * ничего не возвратила;

произошла ошибка */ print_error (conn, "Невозможно обработать результирующий набор");

else /* * результирующий набор не возвращен;

запрос не вернул данных * (это не был запрос SELECT, SHOW, DESCRIBE или EXPLAIN), * просто верните количество строк, обработанных запросом */ printf ("обработано %lu строкХп", (unsigned long) mysql_af f ected_rows (conn) ) ;

else { /* возвращен результирующий набор */ /* обработать строки и очистить результирующий набор */ process_result_set (conn, res_set) ;

mysql_f ree_result (res_set) ;

Альтернативные методы обработки запросов Эта версия процедуры process_query () имеет три особенности. Х С помощью mysql_query () она вызывает запрос. Х Для выборки результирующего набора используется процедура mysql_store_query(). Х Если результирующий набор не получен, вызывается процедура mysql_f ield_count (). Она должна распознать ситуацию, была ли это ошибка или результирующего набора вообще быть не должно. Для всех этих аспектов обработки запроса можно предложить альтернативные варианты. Х Вместо запроса, использующего ограниченные символом "NULL" строки и функции mysql_query (), воспользоваться строкой со счетчиком и функцией mysql_real_query (). Глава 6. Программный интерфейс MySQL С API Х Для получения результирующего набора необходимо заменить функцию mysql_store_result ( ) функцией mysql_use_result ( ). Х В процессе определения, произошла ли ошибка или просто этот запрос ничего не должен возвращать, отказаться от использования функции mysql_f ield_count ( ) в пользу функции mysql_error ( ). Любой из этих вариантов или сразу все можно использовать в процедуре process_query (). Вот процедура process_real_query {), которая аналогична процедуре process_query (), но в ней использовано сразу три варианта:

void process real_query (MYSQL *conn, char *query, unsigned int len) { MYSQL_RES *res_set;

unsigned int field_count;

if (mysql_real_query (conn, query, len) != 0) /* сбой запроса */ { print_error (conn, " сбой process_real_query () ") ;

return;

/* запрос завершил работу успешно;

определить, вернул он данные или нет */ res_set = mysql_use_result (conn) ;

if (res_set == NOLL) /* результирующий набор не возвращен */ ( /* * означает ли отсутствие результирующего набора, что * произошла ошибка или не вернулся результирующий набор? */ if (mysql_errno(conn) != 0) /* произошла ошибка */ print_error (conn, " Невозможно обработать результирующий набор " ) ;

} else { * результирующий набор не возвращен;

запрос не вернул данных * (это не был запрос SELECT, SHOW, DESCRIBE или EXPLAIN), * просто верните количество строк, обработанных запросом */ else { */ printf ("обработано %lu строк\п", (unsigned long) mysql_af fected_rows (conn) ) ;

/* возвращен результирующий набор */ /* обработать строки и очистить результирующий набор */ process_result_set (conn, res_set) ;

mysql_free_result (res_set) ;

Часть II. Программные интерфейсы СУБД MySQL Сравнение возможностей mysql_store_result () И mysql_useresult () Функции mysql_store_result О И mysql_use_result () подобны В том, что обе принимают аргумент дескриптора соединения и возвращают результирующий набор. Но между ними имеются существенные различия. Первое и главное различие заключается в том, каким образом результирующий набор выбирается с сервера. Функция mysql_store_result () выбирает все строки сразу же после ее вызова. Функция mysql_use_result () инициирует выборку, но в действительности не получает строк. Более того, ее использование предполагает, что после нее будет вызвана функция mysql_fetch_row () для последующей выборки записей. Эти различия в выборке строк являются причиной всех остальных различий между этими двумя функциями. В этом разделе они сравниваются для того, чтобы дать возможность осознанного выбора разработчику программного обеспечения. Функция mysql_store_result () в процессе выборки результирующего набора с сервера производит собственно выборку, распределяет для него память, сохраняет его на компьютере клиента. Последующий вызов mysql_fetch_row() никогда не возвращает ошибки, так как она просто выбирает строку из структуры данных, которые уже содержат результирующий набор. Возвращаемое значение "NULL" означает только, что достигнут конец результирующего набора. Однако mysql_use_result () никогда сама не выбирает строк. Она просто инициирует построчную выборку, которая будет завершена, если для каждой строки вызвать процедуру mysql_f etch_row (). В этом случае возвращение значения "NULL" из процедуры mysql_f etch_row () может означать как достижение конца результирующего набора, так и возникновение ошибки в процессе работы сервера. Разобраться в различиях МОЖНО, только вызвав mysql_errno () ИЛИ mysql_error (). Процедура mysql_store_result () потребляет больше памяти и вычислительных ресурсов, чем mysql_use_result(). Это происходит потому, что вся обработка результирующего набора производится программой. При необходимости выборки большого количества данных одновременно может произойти ситуация, когда лучше воспользоваться процедурой mysql_use_result (). Процедура mysql_use_result () требует меньший объем памяти: ей достаточно столько памяти, сколько необходимо для размещения одной строки. Эта функция будет работать быстрее, потому что не потребуется производить реконфигурации в зависимости от получаемых объемов. Но функция mysql_use_result () сильно перегружает сервер, который должен хранить все строки результирующего набора до тех пор, пока клиент не соберется их выбрать. Следовательно, функция mysql_use_result () Ч Глава 6. Программный интерфейс MySQL С API не самый лучший выбор в качестве инструмента разработки программ определенного класса. Х Интерактивные клиентские программы, производящие построчный просмотр по запросу пользователя. (Сервер не должен ожидать посылки следующей строки только потому, что пользователь делает короткий перерыв в работе.) Х Клиентские программы, производящие между выборками строк большой объем вычислений. В обоих случаях клиент не имеет возможности быстро выбрать все строки в результирующий набор. Это связывает сервер и может иметь отрицательное влияние на работу других клиентов, так как таблицы, из которых производится чтение, закрыты для чтения на все время, пока происходит выполнение запроса. Любой клиент, пытающийся внести изменения в эти таблицы или вставить новые строки, блокируется. Возмещением расходов на дополнительную память, требующуюся функции mysql_store_result (), является возможность получения доступа одновременно ко всему результирующему набору. Функции mysql_data_seek(), m y s q l _ r o w _ s e e k ( ) И mysql_row_tell() ПОЗВОЛЯЮТ осуществить доступ к строкам в любом нужном порядке. При работе с функцией mysql_use_result () доступ к строкам возможен только в том порядке, в котором они были выбраны функцией mysql_f etch_row (). При необходимости обрабатывать строки в любом другом порядке, отличающемся от последовательности, в которой они возвращены с сервера, нужно воспользоваться возможностями функции mysql_store_result (). Например, если разрабатываемое приложение должно разрешить пользователю просматривать выбранные строки вперед и назад, что-нибудь лучше, чем mysql_store_result (), найти трудно. Работая с функцией m y s q l _ s t o r e _ r e s u l t (), можно получить такую информацию о столбцах, которую невозможно получить с помощью mysql_use_result (). Количество строк, полученных в результирующем наборе, можно получить с помощью функции mysql_num_rows (). Максимальная длина значений в каждой колонке сохраняется в элементе max_width структуры MYSQL_FIELD, хранящей информацию о столбцах обрабатываемой таблицы. Функции m y s q l _ u s e _ r e s u l t () и mysql_num_rows () не возвращают точного значения до тех пор, пока не будут выбраны все строки, a max_width будет недоступна, так как она может быть просчитана только после того, как будут видны все данные во всех столбцах. Так как функция mysql_use_result О выполняет меньше обработки чем mysql_store_result (), она требует соблюдения особого условия: программа-клиент должна вызывать функцию mysql_f etch_row () для каждой строки в результирующем наборе. В противном случае, любая оставшаяся в наборе запись попадает в результирующий набор следующего запроса, что вызывает появление ошибки "out of sync" ("рассинхронизация"). Та316 Часть II. Программные интерфейсы СУБД MySQL кая ситуация не может возникнуть в принципе при использовании функции mysql_store_result (), так как в тот момент, когда эта функция завершает работу, все строки результирующего набора уже выбраны. Действительно, при работе с функцией mysql_store_result() необходимость вызывать функцию mysql_f etch_row (} полностью отпадает. Такая возможность может понадобиться при обработке запросов, результаты которых нас интересуют только с той точки зрения, что получен пустой или непустой результирующий набор данных, а не собственно результирующие данные. Например, для того, чтобы проверить факт существования таблицы my_tbl, нужно выполнить следующий запрос:

SHOW TABLES LIKE "my_tbl" Пусть после отработки функции m y s q l _ s t o r e _ _ r e s u l t () mysql_num_rows () возвратит ненулевое значение. Следовательно, таблица существует. Функцию mysql_f etch_row (} вызывать не надо. Можно предоставить пользователям самим выбирать способ обработки данных. Кстати программы mysql и mysqldump такой выбор предоставляют. По умолчанию они используют mysql_store_result (). Но имеется возможность переключения на режим использования mysql_use_result (). Для этого необходимо задать ключ - -quick. Метаданные результирующего запроса Результирующий набор содержит не только значения, хранящиеся в столбцах, но и информацию о данных. Эта информация называется метаданными результирующего набора и включает следующие элементы. Х Количество строк и столбцов в результирующем наборе, которые можно получить, вызвав функции m y s q l _ n u m _ r o w s ( ) и mysql_num_fields(). Х Длину каждого столбца в строке, которую можно получить, вызвав функцию m y s q l _ f e t c h _ l e n g t h s О. Х Такую информацию о каждой строке, как имя столбца и его тип, максимальную длину значения, хранящегося в каждом столбце, и имя таблицы, которой этот столбец принадлежит. Эти данные хранятся в структурах типа MYSQL_FIELD, которые можно получить, вызвав mysql_fetch_field (). Структура MYSQL_FIELD детально описана в приложении Е, "Программный интерфейс приложений С". Там же можно найти список всех функций, обеспечивающих доступ к информации, содержащейся в структуре. Доступность метаданных частично зависит от метода обработки результирующего набора. Как это показано в предыдущем разделе, если необходимо узнать количество строк или максимальные длины значений столбцов для создания результирующего набора, вызовите функцию m y s q l _ s t o r e _ r e s u l t ( ), а не m y s q l _ u s e _ r e s u l t ( ). Глава 6. Программный интерфейс MySQL С API Метаданные результирующего набора нужны, чтобы выбрать метод обработки данных результирующего набора. Имя и длина столбца нужны для планирования формата вывода данных. Х Количество строк результирующего набора нужно для программирования цикла обработки строк. Счетчик строки или столбцов можно использовать, если необходимо определить объемы памяти под структуры данных, которые будут получены в результирующем наборе. Х Есть возможность определить тип данных столбца. Это позволяет определить тип данных, хранящихся в столбце. Ранее в разделе "Обработка запросов, возвращающих результаты" была представлена версия процедуры process_result_set (), которая распечатывает строки результирующего набора в формате разделения столбцов символами табуляции. В определенных случаях это хорошо (тогда, когда данные импортируются в электронные таблицы), но это не совсем то, что нужно для визуального исследования или распечаток. Помните, результат ранней версии process_result_set () имел вид:

Adams John Braintree MA Adams John Quincy Braintree MA Arthur Chester A. FairfieldVT Buchanan James Mercersburg PA Bush George W. Milton MA Carter James E.Jr Plains GA Cleveland Grover Caldwell NJ Х Внесем изменения в процедуру process_result_set (). Теперь столбцы будут иметь заголовки, печатная форма будет разграфлена. Это более читабельная форма.

last name Adams Adams Arthur Buchanan Bush Carter Cleveland first name | city ] state John | Braintree John Quincy | Braintree Chester A. 1 Fairfield James | Mercersburg George W. | Milton James E.Jr | Plains Grover ] Caldwell I I 1 1 I 1 j MA MA VT PA MA GA NJ...

Основные операции алгоритма отображения данных следующие. 1. Определить ширину отображения каждого столбца. 2. Распечатать строку с наименованиями столбцов (ограничено вертикальными линиями, предваряется и завершается строками с тире).

Часть II. Программные интерфейсы СУБД MySQL 3. Распечатать значения каждой строки результирующего набора (каждый столбец которого расположен в соответствующей графе). Кроме того, цифры выравниваются по правому краю, и для пустых значений распечатывается "NULL". 4. В конце распечатывается счетчик выбранных строк. Это хороший пример того, каким образом можно использовать метаданные результирующего набора. Для того чтобы создать такое отображение данных, нужно знать не только значения данных, которые содержат строки, но и более детальную информацию. Конечно, можно подумать, что это очень напоминает то, как клиентская программа mysql отображает данные. Да, это так, но вы можете сравнить исходный текст mysql с кодом, который мы получим в конце концов в модифицированном коде process_result_set (). Они не похожи друг на друга. И будет очень полезно на реальном примере сравнить два подхода к решению одной и той же проблемы. Сначала необходимо определить длину отображаемых столбцов. Ниже приведена распечатка, показывающая, как это делается. Обратите внимание на тот факт, что все вычисления целиком основываются на метаданных результирующего набора данных, и нет никаких ссылок на значения данных в строках. MYSQL_FIELD * field;

unsigned int i, col_len;

/* определение длины отображаемых столбцов */ mysql_field_seek (res_set, 0);

for (i =0;

i < mysql_num_fields (res_set);

i++) ( field = mysql_fetch_field (res_set);

col_len = strlen (field->name);

if (col_len < field->max_length) col_len = field->max_length;

if (coljlen < 4 && !IS_NOT_NULL (field->flags)) col_len =4 ;

/* 4 = длине слова "NULL" */ field->max_length = col_len;

/* обновить информацию о столбце} } Здесь в последовательных итерациях вычисляется длина столбцов из структур MYSQL_FIELD для результирующего набора. Позиционирование на первую структуру осуществляется вызовом функции mysql_fetch_seek(). Последующие вызовы mysql_fetch_field () возвращают указатели на следующие столбцы. Ширина столбца выбирается как максимум трех величин, каждая из которых зависит от метаданных информационной структуры столбца. Х Длины f i e l d ->name титула столбца. Х Длины f i e l d ->max length самого длинного значения в столбце.

Глава 6. Программный интерфейс MySQL С API Х Длины строки "NULL", если столбец содержит пустое значение f ield->f lags указывает, может или нет столбец содержать пустые данные После того как длина отображения вычислена, она присваивается элементу max_length структуры, которую мы получаем из клиентской библиотеки. Допустим такой прием или элементы структуры MYSQL_FIELD должны быть открыты только для чтения9 В общем случае, "только для чтения'', но некоторые из клиентских программ дистрибуции СУБД MySQL ведут себя со значением max_length аналогичным образом Так что можно сказать, что все нормально. (Если вы предпочитаете не использовать max_length, объявите массив типа unsigned int и храните полученные значения длин в нем ) Вычисление длины столбца имеет один недостаток. Вспомним, что max_length не имеет никакого смысла, если результирующий набор создается с помощью функции mysql_use_result (). По этой причине (необходимость вычисления max_length) для реализации этого алгоритма лучше подходит функция mysql_store_result ( } '. Когда нам известна ширина столбца, мы готовы к печати Печать титульной части таблицы несложна Воспользуемся для этого информационной структурой столбца и с учетом ранее вычисленной длины распечатаем имя элемента структуры: printf (" %-*s I", field->max_length, field->name);

Для распечатки данных мы просто просмотрим строки в результирующем наборе, распечатывая при каждой итерации значение текущего столбца Это достаточно сложный процесс потому, что значение может быть равно "NULL" или может представлять число (в этом случае печать выравнивается по правому краю). Значения столбцов распечатывается следующим образом Здесь r o w [ i ] содержит значения данных, a field Ч указатель на информацию о столбце. if ( r o w [ i ] == NULL) printf ( " % - * s | ", field->maxlength, "NULL");

else if (IS_NUM(field->type)) printf ( " % * s | ", field->max_length, r o w [ i ] ) ;

else printf ( " % - * s I ", field->max_length, r o w [ i ] ) ;

Макрос i s _ N U M ( ) принимает значение "истина", если тип столбца, который указывается f ield->type, является числовым Ч INT, FLOAT или DECIMAL.

' Элемент, хранящий информацию о длине в структуре MYSQL_FIELD, хранит информацию о самой большой длине, которую могут иметь значения столбца Она может больше пригодиться в mysql_use_result (), чем в mysql_store_result () 320 Часть II. Программные интерфейсы СУБД MySQL Окончательный программный код отображения результирующего набора данных имеет следующий вид. Процедура печати тире сведена в отдельную функцию print_dashes ( ) : void print_dashes (MYSQL_RES *res_set) MYSQL_FIELD { unsigned int i, j;

* field;

mysql_field_seek (res_set, 0) ;

fputc С + ', stdout);

for (i =0;

i < mysql_num_fields (res_set) ;

i++) { field = mysql_fetch_field (res_set) ;

for (: = 0;

] < field->max_length + 2;

fputc (' -', stdout);

fputc ( ' + ', stdout) ;

} fputc ('\n', stdout) ;

void process_result_set { unsigned int (MYSQL *conn, MYSQL_RES *res_set) MYSQL_FIELD MYSQL_ROW * field;

row;

i, col_len;

/* определить длину отображаемого столбца */ mysql_field_seek (res_set, 0);

for ( i = 0 ;

i < mysql_num_fields (res_set) ;

i++) ( field = mysql_fetch_field (res_set);

col_len = strlen (field- >name) ;

if (col_len < field ->max_length) col_len = field ->max_length;

if (col_len < 4 && iIS_NOT_NULL (field->flags) ) ' col_len = 4 ;

/* 4 = длине слова "NULL" */ field->max_length = col_len;

/* обновить информацию о столбце) print_dashes (res_set);

fputc ( ' | ', stdout);

mysql_field_seek (res_set, 0);

for ( i = 0 ;

i < mysql_num_fields (res_set) ;

( field = mysql_fetch_field (res_set);

printf (" %-*s I", field ->max_length, field->name) ;

} fputc ( '\n', stdout) ;

print_dashes ( res_set ) ;

while ((row = mysql_fetch_row (res_set) ) !- NULL) { mysql_field_seek (res_set, 0);

Глава 6. Программный интерфейс MySQL С API 11-1729 fputc ( ' I ' / stdout) ;

for (i = 0;

i < mysql_num_fields ( r e s _ s e t ) ;

{ field = mysql_fetch_field (res_set);

if (row[i) ==~NULL) ~ printf (" %-*s I", field ->max_length, "NULL");

else if (IS_NUM (field->type) ) printf (" %*s I", field->max_length, row[i]);

else printf (" %-*s |", field->max_length, row[i]};

} fputc ( ' \n', stdout);

print_dashes ( r e s _ s e t ) ;

printf ("обработано %lu строк\п", (unsigned long) mysql_num_rows ( r e s _ s e t ) ) ;

} Клиентская библиотека СУБД MySQL поддерживает несколько способов доступа к структурам, содержащим информацию о столбцах. Например, код в предыдущем примере получает доступ к этим структурам несколько раз с помощью цикла, такого вида:

mysql_field_seek (res_set, 0);

for (i =0;

i < mysql_num_f ields (res_set);

{ field = mysql_fetch_f ield (res_set);

} Однако комбинация mysql_f ield_seek ( )/mysql_fetch_f i e l d ( ) является единственным методом доступа к структурам MYSQLFIELD. Более детальные данные можно найти в разделах, посвященных функциям mysql_fetch_f ields () И mysql_fetch_f ield_direct ( ) В приложении Е, ''Программный интерфейс приложений С". Clients - программа интерактивного ввода запросов Теперь соберем все сделанные нами наработки вместе и на базе этого попробуем создать простейшую программу-клиент, обладающую интерактивными возможностями. Ее задача заключается в вводе запросов, выполнении их с помощью обработчика запросов общего назначения process_query (), а затем отображении полученных результатов с помощью процедуры processresult_set ( ). Клиенты в некотором смысле аналогичны mysql, хотя, конечно, не имеют таких же широких возможностей. Есть некоторые ограничения на то, что эти клиенты позволяют в качестве ввода. Х 322 Каждая вводимая строка должна содержать один полный запрос. Часть II. Программные интерфейсы СУБД MySQL Х Запросы не должны завершаться двоеточием или "\д''. Х Для выхода нужно пользоваться комбинацией клавиш , a не командой "Exit". Оказывается, что клиентские программы очень просто написать (немногим более 10 строк нового кода). Почти все, что нам нужно, уже присутствует в "скелете" клиентской программы (client4.c) и другом программном коде, который уже нами был написан. Осталось только добавить цикл, который собирает входные строки и выполняет их. Для создания программы-клиента clients.с сначала скопируем "скелет" программы c l i e n t 4. c в файл clients, с. Затем добавим вызовы process_query(), process_result_set() и print_dashes (). Наконец, найдем следующую строку в main (): /* поместите сюда основные операции */ И заменим их следующими операциями: while (1) { char buf[1024] ;

printf (stderr, "query> ") ;

/* вывод подсказки */ if f f g e t s (buf, sizeof ( b u f ), stdin) == NULL) /* прочесть запрос */ break;

process_query (conn, but) ;

/* выполнение запроса */ ) Теперь откомпилируйте clients, с для того, чтобы получить clients, о, скомпонуйте clients, о с common, о и с клиентской библиотекой, и все сделано! В результате у нас в распоряжении появится новая клиентская программа, которая может выполнить любой запрос и отобразить полученные результаты. Разное Этот раздел посвящен некоторым проблемам, которые не попали в поле зрения при изучении клиентских программ clientl и clients. Х Вычисление результата на основании данных результирующего набора, после предварительной проверки с помощью метаданных, чтобы определить, насколько эти данные пригодны для проведения вычислений. Х Что делать с данными, которые нельзя вставить в запросы. Х Как обрабатывать изображения. Х Как получить информацию о структуре таблиц. Х Ошибки программирования в СУБД MySQL, которые встречаются наиболее часто, и способы их предотвращения. Глава 6. Программный интерфейс MySQL С API Выполнение вычислений на основе данных, полученных в результирующем наборе До этого момента все наши усилия были сфокусированы на метаданных результирующего набора, прежде всего на распечатке данных. Но определенно могут наступить времена, когда с данными потребуется произвести операции другого рода. Например, на основании этих данных можно подсчитать статистическую информацию, воспользовавшись метаданными для того, чтобы проверить, соответствуют ли данные нужным вам условиям. Какой тип условий? Для начала можно проверить, что столбец, который планируется использовать в цифровых вычислениях, действительно содержит числа! Ниже приведен листинг, представляющий простую функцию summary_stats (), которая на основании данных из результирующего набора и индекса столбцов вычисляет итоговую статистику значений, содержащихся в столбце. Функция также сообщает количество пустых значений, которые он определяет проверкой на значения "NULL". При этих вычислениях к данным выдвигается два требования, которые должны быть соблюдены. Функция suinmary_stats () проверяет их с помощью метаданных. Х Указанный столбец должен существовать (т.е. индекс столбца должен лежать в пределах диапазона номеров столбцов результирующего набора). Х Столбец должен содержать цифровые данные. Если эти условия не соблюдаются, summary_stats () распечатывает сообщение об ошибке. В этом случае программный код примет вид:

void summary_stats (MYSQL_RES *res_set, unsigned int col_num) { MYSQL_FIELD * field;

MYSQL_ROW row;

unsigned int n, missing;

double val, sum, sum_squares, var;

mysql_field_seek (res_set, 0);

field = mysql_fetch_field (res_set);

if (!IS_NUM (field->type)) { print_error (NULL, "столбец не является числовым");

/* проверка соответствия условиям */ if (mysql_num_fields (res_set) < col_num) { print_error (NULL, "Ошибочный номер столбца ");

return;

} return;

Часть II. Программные интерфейсы СУБД MySQL /* вычисление итоговой статистики */ п = 0;

missing = 0;

sum = 0;

sum_squares = 0;

mysql_data_seek (res_set, 0);

while ((row = mysql_fetch_row (res_set) ) != NULL) { if ( row [ col_num] == NULL) missing++;

else val = atof (row[col_num) ) ;

/* преобразование строки в число */ sum += val;

sum_squares += val * val;

if else { (n == 0) printf ("Наблюдения отсутствуют\п") ;

printf ("Количество наблюдений: %lu\n", n) ;

printf ("Наблюдения отсутствуют: %lu\n", missing);

printf ("Сумма: %g\n", sum);

printf ("Среднее: %g\n", sum / n) ;

printf ("Сумма квадратов: %g\n", sum_squares) ;

var = ( (n * sum_squares) Х (sum * sum)) / (n * (n - 1)) printf ("Дисперсия: %g\n", var);

printf ("Квадратичное отклонение: %g\n", sqrt (var)) Обратите внимание на вызов функции mysql_data_seek(), который предшествует циклу mysql_fetch_row(). Он здесь для того, чтобы дать возможность вызвать summary_stats ( ) несколько раз для обработки одного и того же результирующего набора (в случае, если требуется вычислить статистику для нескольких столбцов). Перед каждым вызовом summary_stats ( ) он делает "перемотку" на начало результирующего набора. (Это предполагает создание результирующего набора с помощью mysql_store_result (). В случае создания результирующего набора с помощью функции mysql_use_result (), строки можно обработать по порядку и только один раз.) Функция summary_stats ( ) является относительно простой функцией, но она может подсказать, как создать программу для более сложных вычислений, таких как средняя квадратичная регрессия двух столбцов или такой стандартной статистики, как t-test.

Глава 6. Программный интерфейс MySQL С API Кодирование проблемных данных в запросах Вставленные в запрос значения данных, содержащие кавычки, значения ''NULL" или обратная косая черта могут вызвать неоднозначность при выполнении запроса. Ниже излагается материал, в котором описывается природа этого явления и способы разрешения проблем. Предположим, что нам требуется создать запрос SELECT, обрабатывающий содержимое строки, завершающейся значением "NULL". Обращение к этой строке производится по имени:

char q u e r y [1024 ] ;

sprintf (query, name);

"SELECT * FROM my_tbl WHERE name='%s'", Значение имени может быть чем-то вроде "O'Malley, Brian". Из-за того, что апостроф находится внутри такой строки, будет получен ошибочный запрос:

SELECT * FROM my_tbl WHERE name=' O'Malley, Brian' Одинарную кавычку необходимо обрабатывать каким-то специальным способом для того, чтобы сервер не интерпретировал ее как конец имени. Одним из решений может быть дублирование кавычек в строке. Это соглашение ANSI SQL. СУБД MySQL распознает данные в соответствии с этим соглашением. Кроме того, стандарт допускает использование обратной косой черты до кавычек:

SELECT * FROM my_tbl WHERE name='О''Malley, Brian' SELECT * FROM my_tbl WHERE name='0\'Malley, Brian' Еще одна проблемная ситуация связана с использованием в запросе произвольных двоичных данных. Это происходит в приложениях, которые хранят изображения в базах данных. Ведь двоичные данные могут содержать любой символ, и поэтому их нельзя обрабатывать запросами в том виде, в каком они есть. Для решения этой проблемы воспользуйтесь функцией mysql_escape_string (), которая кодирует специальные символы с помощью кавычек. Функция mysql_escape_string() рассматривает как специальные следующие символы: символ "NULL", одинарную и двойную кавычки, обратную косую черту, символ новой строки, символ возврата каретки "". (Последний символ присущ ОС Windows.) В каком случае может использоваться m y s q l _ e s c a p e _ s t r i n g ( ) ? Самым безопасным ответом будет "всегда". Однако, если есть уверенность в данных и все нормально (возможно, потому, что вы выполнили предварительную проверку), необходимость в кодировке данных отпадает. Например, нет необходимости в вызове mysql_escape_string{) при работе со строками, которые, как известно, должны представлять телефонные номера, состоящие только из цифр и тире. В противном случае, вероятно, в этом существует определенная необходимость. 326 Часть II. Программные интерфейсы СУБД MySQL Функция mysql_escape_string () кодирует проблемные символы в символьные последовательности, состоящие из двух символов, начинающиеся с обратной косой черты. Так, байт '"NULL" преобразуется в "\0", где символ "О" является обычным печатаемым символом "ноль" в кодировке ASCII. А символы обратной косой черты, одинарная и двойная кавычка становятся " \ \ ", "\" и "\"". Функция mysql_escape_string () вызывается следующим образом:

to_len = mysql_escape_string (to_str, from_str, from_len);

Функция m y s q l _ e s c a p e _ s t r i n g ( ) кодирует from_str и записывает результат в to_str. Кроме того, она добавляет символ -'NULL" в конец строки. Это очень удобно, так как полученную строку можно будет обрабатывать функциями s t r c p y ( ) И s t r l e n ( ). Переменная from_str содержит указатель на буфер типа char, в котором хранятся данные, предназначенные для кодировки. Эта строка может содержать все что угодно, включая двоичные данные. Переменная to_str содержит указатель на существующий буфер типа char, куда и будет записываться закодированная строка. Ни в коем случае не передавайте не проинициализированный указатель или указатель, содержащий значение "NULL". Функция mysql_escape_string () сама не выделяет память. Длина буфера, адрес которого содержит to_str, должна быть по крайней мере (from_len*2) +1 байт. (Вполне возможно, что каждый символ в from_str потребует кодировки двумя символами;

плюс еще один байт для символа "NULL" в конце строки.) Переменные from_len и to_len являются переменными типа unsigned int, from_len содержит длину данных в fromstr. Эту длину необходимо знать потому, что строка f rom_str может содержать пустые байты, которые нельзя рассматривать как символ конца строки. Значение to_len, возвращаемое функцией mysql_escape_string (), является реальной длиной полученной закодированной строки, не считая символа "NULL" в конце строки. Функция mysql_escape_string () возвращает закодированную результирующую строку в to_str, которую можно обрабатывать как строку с символом "NULL" в конце, так как любое пустое значение, которое присутствовало в строке from_str, кодируется в виде последовательности символов "\0". Для того чтобы переписать программный код, конструирующий оператор SELECT так, чтобы он мог безошибочно преобразовывать даже имена, содержащие кавычки, необходимо сделать что-то вроде:

char query[1024], *p;

р = strcpy (query, "SELECT * FROM my_tbl WHERE name='");

p += strlen (p);

p += mysql_escape_string (p, name, strlen ( n a m e ) ) ;

p = strcpy (p, " ' ") ;

Глава 6. Программный интерфейс MySQL С API Да, этот код можно немного упростить, применив второй буфер:

char query[1024], buf(1024];

(void) mysql_escape_string (but, name, strlen (name));

sprintf (query, "SELECT * FROM my_tbl WHERE name='%s'", but);

Обработка изображений Основное назначение функции mysql_escape_string () Ч загрузка изображений в таблицы. Этот раздел полностью посвящен этой теме. (Материал, изложенный здесь, можно также применить и для других типов двоичных данных.) Предположим, что вы хотите прочесть изображения, хранящиеся в файле, и сохранить их с каким-то уникальным идентификатором в таблице. Для хранения двоичных данных лучше всего подходит тип BLOB, поэтому для этой цели можно воспользоваться такой спецификацией таблицы:

CREATE TABLE images ( image_id INT NOT NULL PRIMARY KEY, image_data BLOB ) При заданных идентификационном номере и указателе на открытый файл, содержащем данные изображения, загрузку двоичных данных в таблицу можно выполнить, воспользовавшись функцией load_image (): int load_image (MYSQL *conn, int id, FILE *f) { char query[1024*100], but[1024*10], unsigned int from_len;

int status;

*p;

sprintf (query, "INSERT INTO images VALUES (%d, "', id);

p = query + strlen (query);

while ((frora_len = fread (buf, 1, sizeof (but), f)) > 0) { /* не выходите за границы буфера запроса! */ if (p + (2*from_len) + 3 > query + sizeof (query)) { print_error (NULL, "изображение слишком велико");

return (1) ;

} } p } (void) status return += mysql_escape_string (p, buf, from_len);

strcpy (p, ' " ) " ) ;

= mysql_query (conn, query);

(status);

Функция load_image () не располагает очень большим буфером (100 Кбайт) для хранения изображений, поэтому она предназначается, 328 Часть II. Программные интерфейсы СУБД MySQL прежде всего, для небольших изображений. В реальном приложении буфер можно разместить динамическим способом, основываясь на размере файла изображения. Задача обработки изображений (как и любых других двоичных данных) не настолько трудна как, скажем, задача загрузки их в базу данных, так как значения данных можно получить в переменной MYSQL_ROW, а длину этих данных можно получить с помощью функции mysql_fetch_lengths (). Только необходимо убедиться в том, что это строки с известной длиной, а не ограниченные символом "NULL". Получение информации из таблицы С помощью одного из запросов СУБД MySQL позволяет получить информацию о структуре таблиц:

DESCRIBE tbl_name SHOW FIELDS FROM tbl_nam,e Оба оператора подобны оператору SELECT, так как они возвращают результирующий набор. Для того чтобы получить информацию о структуре таблицы, все, что надо сделать Ч это обработать строки результирующего набора данных. Например, оператор DESCRIBE images возвратит следующую информацию.

4Ч 1 1 Field 1 Type 1 Null | Key 1 Default 1 Extra image id 1 int (11) 1 image data 1 blob 0 1 PRI 1 PRI 1 1 1 -- 1 Ч 4 При выполнении такого же запроса с клиентского места будет получена аналогичная информация (но уже без рамки). Для получения информации только об одном столбце выполните запрос:

SHOW FIELDS FROM tbl_name LIKE "col_name" Этот запрос вернет аналогичные столбцы, но только одну строку (или ничего не вернет, если такой столбец отсутствует). Наиболее часто встречающиеся ошибки при программировании клиентских программ В этом разделе описаны некоторые наиболее "популярные" ошибки, происходящие в процессе программирования на языке С с использованием интерфейса MySQL С API, и методы, помогающие их избежать. (Эти проблемы часто обсуждаются на Web-узле MySQL, и я не буду их скрывать.) Глава 6. Программный интерфейс MySQL С API Ошибка № 1 - работа с не проинициализированными указателями дескрипторов соединения Во всех примерах, которые продемонстрированы в этой главе, функция m y s q l _ i n i t ( ) вызывалась с аргументом "NULL". Это разрешает mysql_init () создать и инициализировать структуру MYSQL и возвратить указатель на нее. Можно воспользоваться и другим подходом, при котором функции mysql_init () передается указатель на уже существующую структуру MySQL. В этом случае mysql_init () инициализирует эту структуру и вернет указатель на нее, не создавая при этом новой структуры. Выбирая второй подход, вы должны отдавать себе отчет в том, что он может вызвать определенные скрытые проблемы. И здесь они будут затронуты. Указатель, передаваемый функции mysql_init О, должен на что-то указывать. Рассмотрим следующий программный код: main () { MYSQL *conn;

mysql_init (conn) ;

Проблема заключается в том, что функция mysql_init ( ) принимает указатель, но этот указатель не указывает на что-либо имеющее значение. Переменная conn является локальной переменной, она не проинициализирована. Следовательно, она может указывать на что угодно в момент старта модуля mai n ( ). Это значит, что функция mysql_init ( } будет указывать на совершенно произвольное место в памяти. Хорошо, если указатель conn будет указывать на область памяти, находящуюся вне адресного пространства вашей программы, вследствие чего система тотчас ее остановит. Таким образом сразу же станет ясно, что в программе содержится какая-то ошибка. Но если удача от вас отвернулась, указатель conn будет указывать на область данных, которая до поры до времени программой не используется, и вы просто ничего не заметите. В таком случае проблема возникнет позже во время выполнения программы. В таком случае диагностировать и устранить такую ошибку будет значительно труднее. Вот аналогичная часть ошибочного программного кода: MYSQL * conn;

main () mysql_init (conn) ;

mysql_real_connect (conn,... ) mysql_query (conn, "SHOW DATABASES");

В этом случае переменная conn является глобальной. Она инициализируется со значением 0 (т.е. "NULL"') до момента старта программы. Функция m y s q l _ i n i t ( ) принимает аргумент со значением "NULL" и та330 Часть II. Программные интерфейсы СУБД MySQL ким образом инициализирует новый дескриптор соединения. К сожалению, переменная conn все еще имеет значение "NULL", поскольку ей так и не присвоено никакого значения. А так как значение conn уже передано функции интерфейса MySQL С API, которая требует, чтобы дескриптор соединения содержал значение, отличное от "NULL", программа прекратит выполнение из-за ошибки. Для того чтобы исправить обе программы, необходимо убедиться, что переменная conn содержит допустимое значение. Например, можно присвоить ей адрес уже существующей структуры MYSQL: MYSQL c o n n _ s t r u c t, *conn = &conn_struct;

raysql_init (conn);

Однако рекомендуемым (и самым простым!) решением будет просто задание явным образом функции mysql_init О значения "NULL", вынудив тем самым выделить структуру MYSQL и присвоить переменной conn возвращаемое значение:

MYSQL *conn;

conn = mysql_init (NULL);

В любом случае, не забывайте проверять значение, возвращаемое функцией mysql_init {), чтобы убедиться, что оно не равно "NULL". Ошибка № 2 - непроверенный результирующий набор Помните о том, что необходимо проверять состояние вызываемых функций, от которых ожидается получение результирующего набора. Вот программа, которая этого не делает:

MYSQL_RES *res_set;

MYSQL_ROW row;

res_set = mysql_store_result (conn);

while ((row = mysql_fetch_row (res_set)) != NOLL) { } /* обработка строки */ Очевидно, что в случае ошибочного завершения функции mysql_store_result () значение переменной res_set будет "NULL", и цикл while даже не будет выполнен. Таким образом, можно подытожить: обязательно проверяйте значения, возвращаемые функциями, которые возвращают результирующий набор, чтобы было с чем работать. Ошибка № 3 - ошибка при выявлении столбцов, имеющих значение "NULL" Не забывайте проверять значения столбцов в массиве MYSQL_ROW, возвращаемые функцией mysql_f etch_row (). Они не должны быть Глава 6. Программный интерфейс MySQL С API "NULL". Вот программный код, который сбоит на некоторых машин;

когда значение row [i] равно "NULL": for (i =0;

i < mysql_num_fields ( r e s _ s e t ) ;

1++) { if (i > 0) fputc ( ' \ t ', stdout);

printf ( " % s ", r o w [ i ] ) ;

Ij > fputc C \ n ', stdout);

' Самое плохое в данном случае Ч то, что операторы printf ( ) в некоторых версиях "прощают" эту ошибку и печатают " (null) " для указателей, имеющих значение "NULL". Это позволяет просто уйти от решения проблемы. Если передать эту программу другу> имеющему более "строгий" оператор printf (), программа даст сбой, и ваш друг сделает вывод, что вы плохой программист. Этот цикл нужно переписать следующим образом: for (i = 0;

i< mysql_num_f lelds ( r e s _ s e t ) ;

if (i > 0) fputc ( ' \ t ', s t d o u t ) ;

printf ( " % s ", row[i] >= NULL ? row[i] : "NULL");

} fputc ( ' \ n ', stdout);

Единственным случаем, когда не надо проверять значение столбца на значение "NULL", является случай, когда из атрибута этого столбца ясно, что может принимать пустые значения. Ошибка № 4 - передача бессмысленных результирующих значений Функции из клиентской библиотеки, ожидающие буферы, предполагают, что они действительно существуют. Вот код, нарушающий этот принцип: char *from_str = "some string";

char *to_str;

unsigned int len;

len = mysql_escape_string (to_str, from_str, strlen ( f r o m _ s t r ) ) ;

В чем здесь дело? Указатель to_str должен содержать адрес реально существующего буфера В этом примере буфер не существует, и указатель содержит адрес какого-то произвольного места в памяти. Не передавайте не проинициализированный указатель, как это было в примере с аргументом to_str функции mysql_escape_stnng ( ), конечно, если не хотите "разгуливать" по какой-то произвольной области памяти.

Часть II. Программные интерфейсы СУБД MySQL t\ Программный интерфейс Perl API В этой главе описываются принципы работы интерфейса Perl DBI с СУБД MySQL Здесь не обсуждается философия и архитектура интерфейса с базами данных. Информацию об этих аспектах интерфейса можно почерпнуть в главе 5, "Введение в программирование в СУБД MySQL" (где материал дается в сравнении с интерфейсами языков С и РНР). Для более осознанного восприятия материала, изложенного в этой главе, достаточно иметь общее представление о языке написания сценариев Perl Если его нет, эту главу можно пропустить и просто скопировать примеры, приведенные здесь. Кстати, в качестве примеров используется хорошо известная по прежним главам база данных samp_db. В качестве хорошего руководства по языку Perl можно назвать книгу Освой самостоятельно Perl за 24 часа, выпущенную Издательским домом "Вильяме" в 2000 г. Сейчас используется версия интерфейса 1 13, но большая часть материала этой главы опирается на более ранние версии l.xx. Возможности, которые здесь описаны, но не представлены в ранних версиях, будут отмечены Глава 7. Программный интерфейс Perl API Для интерфейса СУБД MySQL требуется версия языка Perl не меньше 5.004_05. Кроме клиентской библиотеки языка С СУБД MySQL и файлов заголовков, потребуется установка модулей Msql-Mysql и Data1Dumper языка Perl. Если планируется написание сценариев интерфейса на базе Web, может потребоваться подключение модуля CGI.pm. В этой главе этот модуль используется совместно с Web-сервером Apache. При необходимости получить эти пакеты обратитесь к приложению А, "Получение и инсталляция программного обеспечения", где описано, каким образом можно получить исходные тексты этих сценариев. Их можно просто загрузить с Web-узла. Методы и переменные интерфейса Perl описаны здесь только по мере необходимости. Более полный перечень методов и переменных интерфейса Perl дан в приложении Ж, "Программный интерфейс DBI языка написания сценариев Perl". Это приложение можно использовать также в качестве источника получения базовых данных о любом интерфейсе, который будет использоваться в дальнейшем. Получить документацию в режиме on-line можно с помощью следующих команд:

% perldoc DBI % perldoc DBI::FAQ % perldoc DBD:rmysql На уровне драйвера базы данных (DBD) драйвер для СУБД MySQL встроен в клиентскую библиотеку СУБД MySQL. Для получения более детальной информации об этой библиотеке можно обратиться к материалу, изложенному в главе 6, "Программный интерфейс MySQL С API". Свойства сценариев языка Perl Сценарии языка Perl представляют собой обычные текстовые файлы, поэтому их можно создать с помощью любого текстового редактора. Все приведенные в этой главе сценарии Perl созданы в соответствии с соглашением, принятым в ОС UNIX, поэтому для запуска сценария используется строка, начинающаяся с " # ! ". Для этого сценария использовалась следующая строка: #! / u s r / b i n / p e r l Если путь на вашем компьютере отличается от приведенного здесь, внесите изменения в строку " # ! ", например, / u s r / l o c a l / b i n / p e r l 5 или /opt/bin/perl. В противном случае сценарии Perl на вашем компьютере работать не будут. После "|!" следует пробел, потому что некоторые системы интерпретируют последовательность символов " # ! / " как символ комментария, и сценарий рассматривается системой как сценарий оболочки. В ОС UNIX сценарий Perl должен быть исполнимым файлом. Таким образом, он может вызываться на выполнение простым вводом его имени. Для того чтобы это сделать, измените режим файла:

% chmod +x script_narae Часть II. Программные интерфейсы СУБД MySQL Сценарии не надо делать исполнимыми при работе с активным Perl 1под управлением ОС Windows. Вместо этого сценарий выполняется слеующим образом: :\> perl script_name Зсновы интерфейса базы данных Perl Этот раздел предоставляет основную информацию по интерфейсу, которой будет достаточно как для написания своих собственных сценариев, так и для понимания сценариев, написанных другими. В случае, если вы уже знакомы с интерфейсом, можно перейти прямо к разделу "Подготовка интерфейса к работе". Типы данных интерфейса DBI В некотором смысле использование интерфейса Perl DBI API аналогично работе с клиентской библиотекой языка С, описанной в главе 6, "Программный интерфейс MySQL С API". При работе с ней доступ к данным производится вызовом функций и обращением к данным с помощью указателей на структуры или массивы. При работе с интерфейсом Perl DBI API также производится вызов функций и используются указатели на структуры, с той лишь разницей, что функции называются методами (methods), указатели Ч ссылками (references), указательные переменные Ч дескрипторами (handles), а структуры, на которые указывает дескриптор, Ч объектами (objects). Интерфейс баз данных использует несколько типов дескрипторов. Имена дескрипторов присваиваются в соответствии с соглашением, приведенном в табл. 7.1. Способ их применения будет проясняться по мере повествования. Кроме того, существуют соглашения на некоторые переменные, не использующие дескрипторов (табл. 7.2). Не все из них будут упоминаться в этой главе, но знание этих соглашений очень поможет для понимания сценариев, написанных другими программистами. Таблица 7.1. Соглашение об именах переменных дескрипторов языка Perl DBI Название $dbh $sth $fh $h Значение Дескриптор объекта базы данных Дескриптор объекта оператора (запроса) Дескриптор открытия файла "Общий" дескриптор, смысл которого зависит от содержания Глава 7. Программный интерфейс Perl API Таблица 7.2. Соглашение о значении имен переменных, не использующих дескрипторы Название $гс $rv $rows @агу Значение Код, возвращаемый операциями, возвращающими "true" или "false" Код, возвращаемый операциями, возвращающими целое Код, возвращаемый операциями, возвращающими значение счетчика строк Массив, содержащий строки, возвращаемые запросом Простой DBI-сценарий Начнем с простого сценария dump_members, иллюстрирующего несколько приемов программирования с использованием интерфейса DBI, таких как подключение к серверу СУБД MySQL и отключение от него, создание запросов и выборка данных. Этот сценарий делает выборку членов "Исторической Лиги" в формате колонок с табуляцией. Нас сейчас не интересует формат как таковой, в фокусе нашего внимания лежат возможности интерфейса. Сценарий dump_members выглядит следующим образом: # ! /usr/bin/perl # dumpmembers - список членов "Исторической Лиги" use DBI;

use struct;

my ($dsn) = "DBI:mysql:samp_db:localhost";

my my my my ($user_name) = "paul";

($password) = "secret";

(Sdbh, $sth);

(@ary);

# # # # # наименование источника данных имя пользователя пароль дескрипторы базы данных и операторов массив строк, возвращаемых запросом # подключение к базе данных $dbh = DBI->connect ($dsn, $user_name, $password, { RaiseError => 1 });

# создать запрос $sth = $dbh->prepare ("SELECT last_name, first_name, suffix, email,". "street, city, state, zip, phone FROM member ORDER BY last_name");

$sth->execute() ;

# прочесть результаты, возвращаемые запросами, и очистить память while (Sary = $sth->fetchrow_array()) { print join ("\t", @ary), "\n";

} $sth->finish() ;

$dbh->disconnect();

exit(O);

Pages:     | 1 |   ...   | 2 | 3 | 4 | 5 | 6 |   ...   | 11 |    Книги, научные публикации