Электронная библиотека книг Александра Фролова и Григория Фролова.
Shop2You.ru Создайте свой интернет-магазин
Библиотека
Братьев
Фроловых
[Назад] [Содержание] [Дальше]

Создание приложений с базами данных для Интернета и интрасетей: практическое руководство

(С) Александр Вячеславович Фролов, Григорий Вячеславович Фролов, 2000

6. Связь приложений с базами данных через ODBC

6. Связь приложений с базами данных через ODBC.. 1

Программный интерфейс ODBC.. 2

Структура приложения ODBC.. 2

Инициализация. 2

Инициализация среды выполнения. 3

Инициализация среды для установки соединения. 3

Установка соединения. 4

Подготовка и запуск команды.. 4

Получение идентификатора команды.. 4

Запуск команды.. 4

Обработка результата выполнения команды.. 5

Привязка полей к локальным переменным. 5

Цикл обработки записей. 8

Обработка ошибок.. 8

Извлечение диагностических записей. 8

Записи состояния. 9

Программа ODBCAPP. 10

Листинг 6-1 Вы найдете в файле ch6\odbcapp\odbcapp.cpp на прилагаемом к книге компакт-диске. 11

Глобальные определения и константы.. 11

Функция main.. 11

Функция GetErrorMsgConn.. 13

Функция GetErrorMsg.. 15

Запуск хранимых процедур. 16

Привязка параметров. 16

Запуск процедуры.. 17

Извлечение значений выходных параметров процедуры.. 18

Программа ODBCPARAM... 18

 

Последний метод доступа, о котором мы расскажем в нашей книге, это Microsoft Open Database Connectivity (ODBC). Интерфейс ODBC представляет собой набор предназначенных для доступа к базам данных функций программного интерфейса. Этот набор предполагает использование структурного языка запросов SQL.

Для того чтобы интерфейс ODBC стал доступен программам, необходимо установить драйвер ODBC. Такой драйвер имеется в составе Microsoft SQL Server, а также в составе многих других СУБД, рассчитанных на работу в среде операционных систем Microsoft Windows, Macintosh и некоторых версий Unix.

При создании приложений с базами данных для Интернета интерфейс ODBC пригодится Вам для связи расширений сервера Web (таких, как программы CGI и приложения ISAPI) с базами данных. При этом вызов функций ODBC будет выполняться как непосредственно из программ расширений, так и через дочерние процессы, запускаемые расширениями для обращения к базам данных (это иногда требуется для повышения устойчивости сервера Web к программным ошибкам, допущенным при обращении к базе данных).

Заметим, что в отличие от ADO, интерфейс ODBC не является объектным. Поэтому он недоступен из серверных сценариев JScript и VB Script, расположенных в страницах ASP.

В нашей книге мы не будем описывать все возможности ODBC — речь пойдет только о самых необходимых. Мы, в частности, расскажем о том, как посредством этого интерфейса программы могут выполнять предложения языка SQL и запускать хранимые процедуры с входными и выходными параметрами. Именно эти операции чаще всего нужны при создании реальных приложений.

Программный интерфейс ODBC

В этом разделе речь пойдет о назначении и параметрах основных функций программного интерфейса ODBC.

Помимо этих функций, обеспечивающих прямой доступ к ODBC, корпорация Microsoft разработала ряд программных интерфейсов и классов, предназначенных для обращения к ODBC. Это такие интерфейсы, как RDO и DAO, а также классы MFC и DAO.

Интерфейсы RDO и DAO предназначены главным образом для создания приложений Visual Basic, а классы MFC и DAO — для создания приложений на основе Microsoft Visual C++, к тому же имеющих интерактивный интерфейс пользователя.

При разработке приложений Интернет для интерактивной части проекта обычно применяется технология ASP, серверные сценарии и ADO, о чем мы подробно рассказывали в первых главах нашей книги. Что же касается расширений сервера Web, то они не имеют никакого непосредственного интерфейса пользователя. Поэтому классы MFC и DAO, ориентированные на автоматизированное создание диалоговых приложений средствами мастеров Microsoft Visual C++, не окажут Вам заметной помощи. Именно поэтому мы расскажем Вам только о непосредственном интерфейсе ODBC.

Структура приложения ODBC

Программа, обращающаяся к базам данных посредством интерфейса ODBC, обычно выполняет следующие действия:

·       инициализирует среду выполнения;

·       подключается к источнику данных;

·       создает и выполняет команды;

·       обрабатывает результат выполнения команды;

·       освобождает ресурсы, полученные для работы с ODBC.

Если команда предназначена для выполнения хранимой процедуры, приложение должно выполнить привязку входных и выходных параметров этой процедуры (а также при необходимости и кода завершения процедуры) к локальным переменным, определенным внутри тела программы.

Обработка результата также связана с привязкой данных к локальным переменным. Вы уже знакомы с этой процедурой из предыдущей главы, посвященной интерфейсу OLE DB. Хотя приложения ODBC выполняют эту операцию по-другому, смысл ее не меняется — устанавливается соответствие между локальными переменными и полями набора записей, полученных при выполнении команды SQL.

Нетрудно заметить, что ранее мы говорили о тех же самых действиях, что применяются для работы с базами данных, но выполняемых посредством методов доступа ADO и OLE DB. Действительно, набор действий остается тем же самым. Меняется только способ их реализации.

Рассмотрим назначение функций ODBC, вызываемых на различных этапах работы приложения ODBC.

Инициализация

Для выполнения инициализации среды выполнения приложение ODBC использует такие функции, как SQLAllocHandle и SQLSetEnvAttr. Первая из них отвечает за собственно инициализацию, а вторая позволяет устанавливать параметры среды исполнения.

Вот прототип функции SQLAllocHandle:

SQLRETURN SQLAllocHandle(SQLSMALLINT hType,
  SQLHANDLE inpHandle, SQLHANDLE* outpHandlePtr);

В качестве первого параметра hType функции SQLAllocHandle передается тип идентификатора. Это может быть идентификатор среды SQL_HANDLE_ENV, идентификатор соединения с источником данных SQL_HANDLE_DBC, идентификатор строки SQL_HANDLE_STMT или дескриптора SQL_HANDLE_DESC.

Второй параметр inpHandle определяет, в каком контексте нужно получить идентификатор. Здесь могут быть указаны константы SQL_NULL_HANDLE, SQL_HANDLE_ENV или SQL_HANDLE_DBC.

И наконец, третий параметр определяет адрес переменной, в которую записывается идентификатор, полученный функцией SQLAllocHandle.

При успешном завершении функция SQLAllocHandle возвращает значения SQL_SUCCESS или SQL_SUCCESS_WITH_INFO. Первая из этих констант возвращается, если операция завершилась успешно. Вторая константа также означает успешное завершение и, кроме того, сообщает дополнительную информацию.

В том случае, если функция SQLAllocHandle завершилась с ошибкой, возвращается значение SQL_INVALID_HANDLE или SQL_ERROR.

Перечисленные выше константы возвращаются многими функциями программного интерфейса ODBC. Немного позже мы расскажем о том, как программы обрабатывают ошибочные ситуации, извлекая коды ошибок и тексты сообщений об ошибках.

Перед завершением своей работы приложение должно освободить идентификаторы, полученные при вызове функции SQLAllocHandle. Для этого предназначена функция SQLFreeHandle. Она имеет два параметра:

SQLRETURN SQLFreeHandle (SQLSMALLINT hType, SQLHANDLE hHandle);

Через первый параметр функции SQLFreeHandle передается тип идентификатора (такой же, как и функции SQLAllocHandle), а через второй — освобождаемый идентификатор.

Инициализация среды выполнения

Рассмотрим фрагмент кода приложения, выполняющий инициализацию среды для выполнения запроса к базе данных.

Прежде всего программа должна получить идентификатор среды типа SQL_HANDLE_ENV:

SQLHENV hEnv = SQL_NULL_HENV;
RETCODE rc;
rc = SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &hEnv);

Он записывается в переменную hEnv.

На следующем этапе нам нужно установить атрибуты среды, в частности указать номер версии ODBC, равный 3. В результате приложение сможет воспользоваться особенностями этой версии драйвера. Так, драйвер ODBC будет работать с новыми кодами даты и времени SQL_TYPE_DATE, SQL_TYPE_TIME, SQL_TYPE_TIMESTAMP, SQL_C_TYPE_DATE, SQL_C_TYPE_TIME, SQL_C_TYPE_TIMESTAMP, а также — возвращать коды SQLSTATE при обработке ошибок.

Атрибуты среды устанавливаются с помощью функции SQLSetEnvAttr, имеющей четыре параметра:

rc = SQLSetEnvAttr(hEnv, SQL_ATTR_ODBC_VERSION,
  (SQLPOINTER)SQL_OV_ODBC3, SQL_IS_INTEGER);

Через первый параметр этой функции передается идентификатор среды, для которой выполняется настройка атрибутов.

Второй параметр задает атрибут, подлежащий редактированию. В частности, значение SQL_ATTR_ODBC_VERSION используется при настройке версии драйвера ODBC.

Третий параметр функции SQLSetEnvAttr задает значение атрибута (для численных значений) или адрес строки атрибута (для атрибутов, заданных в виде текстовой строки).

Смысл четвертого параметра зависит от типа значения атрибута, передаваемого через третий параметр. Если значение числовое (как в нашем случае), четвертый параметр определяет тип значения атрибута, а если строчное — длину строки.

Инициализация среды для установки соединения

На втором этапе инициализации Вам надо получить идентификатор соединения типа SQL_HANDLE_DBC, который создается в контексте только что созданного идентификатора среды SQL_HANDLE_ENV:

SQLHDBC   hDbc  = SQL_NULL_HDBC;
rc = SQLAllocHandle(SQL_HANDLE_DBC, hEnv, &hDbc);

Обратите внимание, что мы передаем функции SQLAllocHandle через второй параметр идентификатор hEnv, имеющий тип SQL_HANDLE_ENV.

Теперь, после выполнения второго этапа инициализации, у нас уже есть два идентификатора — hEnv (идентификатор среды выполнения) и hDbc (идентификатор среды соединения).

Установка соединения

Для установки соединения с источником данных Вам придется воспользоваться функцией SQLConnect, прототип которой приведен ниже:

SQLRETURN SQLConnect(SQLHDBC hConnection,
  SQLCHAR * ServerName,   SQLSMALLINT ServerNameLength,
  SQLCHAR * UserName,     SQLSMALLINT UserNameLength,
  SQLCHAR * Password,     SQLSMALLINT PasswLength);

Через первый параметр hConnection функции SQLConnect передается идентификатор соединения типа SQL_HANDLE_DBC, полученный на втором этапе инициализации.

Второй параметр ServerName определяет имя сервера, заданное в виде текстовой строки, а третий (с названием  ServerNameLength) — длину этой строки. Аналогично параметры UserName и Password задают имя пользователя и его пароль, а параметры UserNameLength и PasswLength — размер строк имени пользователя и пароля.

Вот фрагмент текста программы, выполняющий соединение с источником данных BookStore:

UCHAR szDSN[SQL_MAX_DSN_LENGTH + 1] = "BookStore";
UCHAR szUserName[MAXNAME] = "dbo";
UCHAR szPassword[MAXNAME] = "";
rc = SQLConnect(hDbc,
  szDSN,      (SWORD)strlen((const char*)szDSN),
  szUserName, (SWORD)strlen((const char*)szUserName),
  szPassword, (SWORD)strlen((const char*)szPassword));

В случае успешного соединения функция SQLConnect вернет значение SQL_SUCCESS или SQL_SUCCESS_WITH_INFO, а при ошибке — значение SQL_INVALID_HANDLE или SQL_ERROR.

Подготовка и запуск команды

Программный интерфейс ODBC предоставляет несколько возможностей для выполнения команд, здесь, однако, мы рассмотрим только один, связанный с использованием функции SQLExecDirect.

Выполнение команды с применением этой функции состоит из двух этапов. На первом нужно создать идентификатор команды типа SQL_HANDLE_STMT, а на втором — выполнить команду, вызвав функцию SQLExecDirect.

Получение идентификатора команды

Получение идентификатора команды реализуется обычным способом с применением функции SQLAllocHandle:

SQLHSTMT hStmt = SQL_NULL_HSTMT;
rc = SQLAllocHandle(SQL_HANDLE_STMT, hDbc, &hStmt);

Обратите внимание на то, что идентификатор команды создается в контексте идентификатора соединения типа SQL_HANDLE_DBC, поэтому через второй параметр мы передаем функции SQLAllocHandle значение hDbc. Идентификатор соединения hDbc был получен на предыдущем этапе инициализации.

Запуск команды

Для запуска команды на выполнение надо вызвать функцию SQLExecDirect, прототип которой расположен ниже:

SQLRETURN SQLExecDirect(SQLHSTMT hStatement,
  SQLCHAR* StatementText, SQLINTEGER StatementTextLength);

Через первый параметр этой функции передается идентификатор команды типа SQL_HANDLE_STMT, через второй — строка команды, а через третий — длина строки команды.

Если команда выполнена без ошибок, функция SQLExecDirect возвращает значение SQL_SUCCESS или SQL_SUCCESS_WITH_INFO, а при ошибке — значение SQL_INVALID_HANDLE или SQL_ERROR. Возможно также, что Вы получите коды возврата SQL_NEED_DATA, SQL_STILL_EXECUTING и SQL_NO_DATA.

Значение SQL_NEED_DATA возвращается при использовании так называемых параметров времени исполнения (data-at-execution parameter). Такой тип параметров предполагает использование для передачи данных функций SQLParamData и SQLPutData. Мы в своих программах не будем применять параметры времени исполнения.

Значение SQL_NO_DATA возвращается при выполнении команд, не вызывающих создания выходных наборов записей. Км ним относятся, например, команды удаления или обновления строк.

Ниже показан пример вызова команды, выполняющей выборку полей ManagerID, Name, Password, LastLogin и Rights из таблицы managers:

rc = SQLExecDirect(hStmt, (unsigned char*)
  "select ManagerID, Name, Password, LastLogin, Rights from managers", SQL_NTS);

В качестве длины строки, передаваемой функции SQLExecDirect через последний параметр, мы указали константу SQL_NTS. Эта константа означает, что длина строки определяется закрывающим ее двоичным нулем. Таким образом, нам не надо определять длину строки команды явным образом при помощи такой функции, как strlen.

Обработка результата выполнения команды

Если в результате выполнения команды создан набор записей, программа может его получить посредством соответствующих функций интерфейса ODBC. Однако перед тем как это сделать, она должна выполнить привязку полей набора записей к локальным переменным, определенным внутри программы.

Привязка полей к локальным переменным

Такая операция выполняется функцией SQLBindCol:

SQLRETURN SQLBindCol(SQLHSTMT hStatement,
SQLUSMALLINT NumberOfColumn,
SQLSMALLINT   VariableType,
SQLPOINTER    VariableValuePtr,
SQLINTEGER    BufferSize,
SQLINTEGER*   cbSize);

Параметр hStatement задает идентификатор команды, для которой выполняется привязка. Это как раз тот идентификатор, с использованием которого функция SQLExecDirect выполняла команду.

Через параметр NumberOfColumn необходимо передать номер столбца набора данных, созданного в результате выполнения команды. Если в наборе нет столбца bookmark, то нумерация выполняется, начиная с единицы, а если есть — то с нуля.

Параметры VariableType и VariableValuePtr, задают соответственно тип локальной переменной, для которой выполняется привязка, и адрес этой локальной переменной.

С помощью параметра BufferSize необходимо указать размер области памяти, отведенной для локальной переменной с адресом VariableValuePtr (с учетом двоичного нуля, закрывающего текстовую строку). Это значение используется драйвером ODBC в качестве ограничителя, для того чтобы избежать записи данных за пределами буфера. Такая ситуация иногда возникает при извлечении из базы данных содержимого полей переменной длины.

Значение параметра BufferSize игнорируется при записи данных в переменные фиксированной длины типа целых чисел.

Параметр cbSize бывает как входным, так и выходным. Он задает адрес переменной, в которую записывается размер данных, прочитанных функцией извлечения записей SQLFetch (или SQLFetchScroll). Функция SQLBulkOperations использует этот параметр как входной.

В качестве примера приведем фрагмент кода, выполняющего привязку одного числового поля, одного текстового поля и одного поля, содержащего отметку о времени:

Локальные переменные, которые будут привязаны к полям, определены так:

SQLINTEGER nManagerID;
SQLCHAR       szName[MAXNAME + 1];
SQL_TIMESTAMP_STRUCT tsLastLogin;

Мы выполним привязку к полям таблицы managers с именами ManagerID, Name и LastLogin.

Вот фрагмент кода для первого из них:

SQLINTEGER cbManagerID;
rc = SQLBindCol(hStmt, 1, SQL_C_SLONG, &nManagerID,
  sizeof(nManagerID), &cbManagerID);

Здесь выполняется привязка первого столбца, поэтому мы передаем функции SQLBindCol через второй параметр значение 1. Константа SQL_C_SLONG задает тип данных, соответствующий стандартному типу C «long int». Константы для  других типов Вы найдете в таблице 6-1.

Хотя мы и указали в пятом параметре размер области памяти, выделенной для переменной nManagerID, он будет проигнорирован, так как тип SQL_C_SLONG занимает область памяти фиксированного размера.

Привязка переменной szName, в которую будет записана текстовая строка, выполняется аналогично:

SQLINTEGER cbName;
rc = SQLBindCol(hStmt, 2, SQL_C_CHAR, szName, MAXNAME, &cbName);

Здесь мы указали тип данных SQL_C_CHAR, соответствующий типу данных C «unsigned char*». Константа MAXNAME ограничивает длину буфера, не позволяя драйверу ODBC выйти за его границы в процессе записи данных из поля Name.

Для привязки поля LastLogin (содержащего отметку о дате и времени последнего подключения), мы указали тип данных SQL_C_TYPE_TIMESTAMP:

SQLINTEGER cbLastLogin;
rc = SQLBindCol(hStmt, 3, SQL_C_TYPE_TIMESTAMP, &tsLastLogin,
  sizeof(tsLastLogin), &cbLastLogin);

Значение, заданное нами в пятом параметре, будет проигнорировано. Мы указали его только для общности.

В таблице 6-1 мы перечислили идентификаторы типов, передаваемых функции SQLBindCol, для разных типов данных. Там же Вы найдете и типы данных ODBC, которые потребуются при вызове хранимых процедур SQL Server.

Таблица 6-1. Идентификаторы типов данных C и ODBC

Тип данных C

Идентификатор

Тип данных ODBC

char

SQL_C_STINYINT

SQLSCHAR

unsigned char

SQL_C_BIT
SQL_C_UTINYINT

SQLCHAR

unsigned char*

SQL_C_CHAR
SQL_C_BINARY
SQL_C_VARBOOKMARK

SQLCHAR*

short int

SQL_C_SSHORT

SQLSMALLINT

unsigned short int

SQL_C_USHORT

SQLUSMALLINT

long int

SQL_C_SLONG

SQLINTEGER

unsigned long int

SQL_C_ULONG
SQL_C_BOOKMARK

SQLUINTEGER
BOOKMARK

float

SQL_C_FLOAT

SQLREAL

double

SQL_C_DOUBLE

SQLDOUBLE
SQLFLOAT

_int64

SQL_C_SBIGINT

SQLBIGINT

unsigned _int64

SQL_C_UBIGINT

SQLUBIGINT

DATE_STRUCT

SQL_C_TYPE_DATE

SQL_DATE_STRUCT

TIME_STRUCT

SQL_C_TYPE_TIME

SQL_TIME_STRUCT

TIMESTAMP_STRUCT

SQL_C_TYPE_TIMESTAMP

SQL_TIMESTAMP_STRUCT

SQL_NUMERIC_STRUCT

SQL_C_NUMERIC

SQL_NUMERIC_STRUCT

SQLGUID

SQL_C_GUID

SQLGUID

В таблице есть ссылки на типы данных DATE_STRUCT, TIME_STRUCT, TIMESTAMP_STRUCT, SQL_NUMERIC_STRUCT и SQLGUID. Эти структуры предназначены для представления даты, времени, отметки о времени, числовых значений и глобальных уникальных идентификаторов GUID. Для удобства ниже мы приводим их определения:

struct tagDATE_STRUCT
{
  SQLSMALLINT   year;       // год
  SQLUSMALLINT month;      // месяц
  SQLUSMALLINT day;        // день
} DATE_STRUCT;

struct tagTIME_STRUCT
{
  SQLUSMALLINT hour;        // часы
  SQLUSMALLINT minute;      // минуты
  SQLUSMALLINT second;      // секунды
} TIME_STRUCT;

struct tagTIMESTAMP_STRUCT
{
  SQLSMALLINT   year;       // год
  SQLUSMALLINT month;        // месяц
  SQLUSMALLINT day;        // день
  SQLUSMALLINT hour;       // часы
  SQLUSMALLINT minute;     // минуты
  SQLUSMALLINT second;     // секунды
  SQLUINTEGER   fraction;   // доли секунды
} TIMESTAMP_STRUCT;

struct tagSQL_NUMERIC_STRUCT
{
  SQLCHAR   precision;       // точность
  SQLSCHAR scale;             // масштаб
  SQLCHAR   sign;            // знак
  SQLCHAR   val[SQL_MAX_NUMERIC_LEN];   // значение
} SQL_NUMERIC_STRUCT;

struct tagSQLGUID
{
  DWORD Data1;
  WORD   Data2;
  WORD   Data3;
  BYTE   Data4[8];
} SQLGUID;

Цикл обработки записей

После того как Ваше приложение выполнило привязку данных, оно может приступать к извлечению записей из набора, созданного в результате выполнения команды.

Эта операция выполняется в простом цикле, показанном ниже:

while((rc = SQLFetch(hStmt)) != SQL_NO_DATA)
{
  // nManagerID, szName, szPass, szRights
  // tsLastLogin.day, tsLastLogin.month, tsLastLogin.year,
  // tsLastLogin.hour, tsLastLogin.minute, tsLastLogin.second
}

Здесь для извлечения очередной записи набора мы использовали функцию SQLFetch. В качестве единственного параметра этой функции передается идентификатор команды.

Когда все записи набора будут получены, функция SQLFetch вернет значение SQL_NO_DATA. Это ее свойство мы использовали для завершения цикла. Вы также можете усовершенствовать цикл, прерывая его работу в случае возникновения ошибок.

Внутри цикла программа непосредственно обращается к переменным, для которых выполняется привязка, с целью извлечения значений.

Обработка ошибок

Как мы уже говорили, в случае возникновения ошибок функции ODBC возвращают значения, такие, как SQL_INVALID_HANDLE или SQL_ERROR. Однако в большинстве случаев такой информации недостаточно.

Для того чтобы получить развернутое описание ошибки, необходимо запросить у драйвера ODBC диагностические записи. Эта операция выполняется с применением функции SQLGetDiagRec. Отдельные поля диагностических записей извлекаются при помощи функции SQLGetDiagField.

Извлечение диагностических записей

Логика обработки ошибок, рекомендуемая в руководстве Microsoft, предполагает, что функции получения диагностических записей вызываются всякий раз, когда функции ODBC возвращают значения, отличные от SQL_SUCCESS. Однако в некоторых случаях Вы можете проигнорировать дополнительные информационные сообщения, возникающие при коде возврата SQL_SUCCESS_WITH_INFO, не рассматривая данную ситуацию как ошибочную.

Так как с одной ошибочной ситуацией иногда связано несколько диагностических записей, функция SQLGetDiagRec должна извлекать эти записи в цикле.

Рассмотрим прототип функции SQLGetDiagRec:

SQLRETURN SQLGetDiagRec(
  SQLSMALLINT      hType,
  SQLHANDLE        hHandle,
  SQLSMALLINT      nRecord,
  SQLCHAR*         SQLState,
  SQLINTEGER*      pNativeErrorPtr,
  SQLCHAR*         pMessageText,
  SQLSMALLINT      cbBuffer,
  SQLSMALLINT*     pcbText);

Через параметр hType Вы передаете тип идентификатора, для которого нужно получить диагностику. Здесь Вы можете указать хорошо знакомые значения SQL_HANDLE_ENV, SQL_HANDLE_DBC, SQL_HANDLE_STMT и SQL_HANDLE_DESC. Например, если ошибка возникла при выполнении команды, первый параметр должен содержать тип идентификатора команды SQL_HANDLE_STMT, если при создании соединения с источником данных — тип SQL_HANDLE_DBC, и т. д.

Параметр hHandle используется для передачи самого идентификатора.

Через параметр nRecord Вы должны передать номер извлекаемой записи. Обычно значение этого параметра последовательно, начиная с единицы, увеличивается в цикле.

Параметр SQLState должен содержать указатель на буфер, в который записывается код состояния SQLSTATE. Первые два символа в этом буфере указывают класс состояния, а следующие три — подкласс. Подробную информацию о кодах состояния Вы найдете в документации на Microsoft SQL Server.

Через параметр pNativeErrorPtr необходимо передать указатель на буфер, в котором будет храниться естественный код ошибки, зависящий от источника данных.

Текст сообщения об ошибке записывается в буфер, адрес которого передается функции SQLGetDiagRec через параметр pMessageText. Размер этого буфера должен быть указан с помощью параметра cbBuffer.

И наконец, последний параметр pcbText используется для передачи указателя на переменную, в которую будет записана длина упомянутого выше сообщения об ошибке.

Вот как, например, выглядеть цикл извлечения диагностических записей:

RETCODE     rc = SQL_SUCCESS;
SDWORD    sdwNativeError   = 0L;
SWORD     pcbErrorMessage = 0;
UCHAR     szSQLState[256] = "";
UCHAR     szErrorMessage[256]  = "";
SQLSMALLINT   nRecordNumber    = 1;

while(rc != SQL_NO_DATA_FOUND)
{
  rc = SQLGetDiagRec(nHandleType, sqlhHandle,
     nRecordNumber, szSQLState, &sdwNativeError,
     szErrorMessage, 255, &pcbErrorMessage);
  . . .
 
nRecordNumber++;
}

Записи состояния

Указанным выше методом можно получить первый из двух типов диагностических записей, а именно записи заголовка. Они создаются при возникновении любых ошибок, кроме тех, что вызывают появление кода завершения SQL_INVALID_HANDLE.

При использовании драйверов ODBC версии 3.x становятся доступными диагностические записи второго типа, называемые записями состояния. Они генерируются отдельно для каждой записи заголовка и могут быть получены посредством функции SQLGetDiagField:

SQLRETURN SQLGetDiagField( 
  SQLSMALLINT   hType,
  SQLHANDLE     hHandle,
  SQLSMALLINT   nRecord,
  SQLSMALLINT   nDiagId,
  SQLPOINTER    pDiagInfo,
  SQLSMALLINT   nBufferSize,
  SQLSMALLINT*  pStringSize);

Через первый параметр hType Вы должны передать функции SQLGetDiagField тип идентификатора, для которого нужно получить диагностику. Это константы SQL_HANDLE_ENV, SQL_HANDLE_DBC, SQL_HANDLE_STMT и SQL_HANDLE_DESC.

Параметр hHandle используется для передачи идентификатора.

Через параметр функции SQLGetDiagField передают номер записи заголовка, для которой извлекаются записи состояния. Это тот самый номер, что передается через одноименный параметр функции SQLGetDiagRec.

Параметр nDiagId позволяет указать идентификатор извлекаемой записи состояния и задается в виде одной из предопределенных констант.

С помощью параметра pDiagInfo Вы должны передать функции указатель на буфер, в которую будет записана диагностическая информация. Размер этого буфера задается параметром nBufferSize. Значение nBufferSize игнорируется в том случае, если указатель pDiagInfo ссылается на буфер данных фиксированного размера.

Для текстовых строк допускается также указывать в параметре nBufferSize константу SQL_NTS. Она означает, что буфер содержит текстовую строку, закрытую двоичным нулем.

И наконец, в переменную, адрес которой задается параметром pStringSize, записывается количество символов в извлеченном диагностическом сообщении (без учета двоичного нуля, закрывающего текстовую строку).

Вот фрагмент программы, в котором для записи заголовка извлекаются записи состояния:

rc = SQLGetDiagRec(nHandleType, sqlhHandle,
  nRecordNumber, szSQLState, &sdwNativeError,
  szErrorMessage, 255, &pcbErrorMessage);

if(rc != SQL_NO_DATA_FOUND)
{
  rc = SQLGetDiagField(nHandleType, sqlhHandle, nRecordNumber,
    SQL_DIAG_ROW_NUMBER, &nRowNumber, SQL_IS_INTEGER, NULL);

  rc = SQLGetDiagField(nHandleType, sqlhHandle, nRecordNumber,
    SQL_DIAG_SS_LINE, &nLine, SQL_IS_INTEGER, NULL);

  rc = SQLGetDiagField(nHandleType, sqlhHandle, nRecordNumber,
    SQL_DIAG_SS_MSGSTATE, &sdwMessageState, SQL_IS_INTEGER, NULL);

  rc = SQLGetDiagField(nHandleType, sqlhHandle, nRecordNumber,
    SQL_DIAG_SS_SEVERITY, &sdwSeverity, SQL_IS_INTEGER, NULL);

  rc = SQLGetDiagField(nHandleType, sqlhHandle, nRecordNumber,
    SQL_DIAG_SS_PROCNAME, &schProcName,
     sizeof(schProcName), &cbProcName);

  rc = SQLGetDiagField(nHandleType, sqlhHandle, nRecordNumber,
    SQL_DIAG_SS_SRVNAME, &schServerName, sizeof(schServerName),
    &cbServerName);
  . . .
}

Здесь используются константы, перечисленные в таблице 6-2.

Таблица 6-2. Идентификаторы типов данных C и ODBC

Константа

Описание

SQL_DIAG_ROW_NUMBER

Номер строки в наборе записей или номер параметра в наборе параметров, с которым связана данная запись

SQL_DIAG_SS_PROCNAME

Имя хранимой процедуры, вызвавшей ошибку

SQL_DIAG_SS_LINE

Номер строки в хранимой процедуре, вызвавшей появление ошибки

SQL_DIAG_SS_MSGSTATE

Значение состояния, полученное от оператора RAISERROR

SQL_DIAG_SS_SEVERITY

Степень тяжести ошибки

SQL_DIAG_SS_SRVNAME

Имя сервера, на котором произошла данная ошибка

Программа ODBCAPP

Программа ODBCAPP, исходные тексты которой мы описываем в этом разделе, выводит на консольный экран все записи из файла managers базы данных нашего Интернет-магазина:

1 |    frolov |    123 | Administrator   | 21.12.1999 11:24:44
2 |    Petrov |    123 |      sh_manager | (not logged yet)
3 |  Sidoroff   |    123 | Sales_manager   | (not logged yet)
5 | TestAdmins   | 1234 | Sales_manager   | (not logged yet)
4 |       admin |    123 | Sales_manager   | 05.12.1999 09:50:09

Полные исходные тексты программы ODBCAPP находятся в листинге 6-1.

Листинг 6-1 Вы найдете в файле ch6\odbcapp\odbcapp.cpp на прилагаемом к книге компакт-диске.

Глобальные определения и константы

Для обращения к функциям и константам программного интерфейса ODBC мы включили в исходные тексты нашего приложения четыре файла:

#include <sql.h>
#include <sqlext.h>
#include <sqltypes.h>
#include <odbcss.h>

Для формирования строки сообщения об ошибке используется шаблон stirng из стандартной библиотеки шаблонов STL. Поэтому в исходном тексте мы сделали такие определения:

#include <string>
using namespace std;

В области глобальных переменных задали следующие переменные:

SQLHENV   hEnv   = SQL_NULL_HENV;
SQLHDBC   hDbc   = SQL_NULL_HDBC;
SQLHSTMT hStmt = SQL_NULL_HSTMT;

В переменной hEnv хранится идентификатор среды исполнения, в переменной hDbc — идентификатор соединения с источником данных, а в переменной hStmt — идентификатор команды.

Функция main

Функция main выполняет инициализацию, установку соединения, создание и запуск команды, извлечение и вывод на экран результатов ее выполнения.

В области локальных переменных функции main мы определили переменную rc, предназначенную для хранения кода возврата функций программного интерфейса ODBC, три строки, необходимые для подключения к источнику данных (содержащих имя источника данных, им пользователя и пароль), а также строку класса string для хранения сообщения об ошибке:

RETCODE rc;
UCHAR szDSN[SQL_MAX_DSN_LENGTH + 1] = "BookStore";
UCHAR szUserName[MAXNAME] = "dbo";
UCHAR szPassword[MAXNAME] = "";
string sErrMsg;

Получив управление, функция main инициализирует среду выполнения программы, получая соответствующие идентификаторы:

rc = SQLAllocHandle(SQL_HANDLE_ENV, NULL, &hEnv);

if(rc != SQL_SUCCESS && rc != SQL_SUCCESS_WITH_INFO)
  return 1;

rc = SQLSetEnvAttr(hEnv, SQL_ATTR_ODBC_VERSION,
  (SQLPOINTER)SQL_OV_ODBC3, SQL_IS_INTEGER);

if(rc != SQL_SUCCESS && rc != SQL_SUCCESS_WITH_INFO)
  return 1;

rc = SQLAllocHandle(SQL_HANDLE_DBC, hEnv, &hDbc);

if(rc !=
SQL_SUCCESS && rc != SQL_SUCCESS_WITH_INFO)
  return 1;

Обратите внимание, что пока мы не установили соединение с источником данных, обработка ошибок заключается в простом завершении программы с кодом 1. Дело в том, что на данном этапе средства расширенной диагностики ошибок еще не доступны.

Далее программа устанавливает соединение с источником данных:

rc = SQLConnect(hDbc,
  szDSN,      (SWORD)strlen((const char*)szDSN),
  szUserName, (SWORD)strlen((const char*)szUserName),
  szPassword, (SWORD)strlen((const char*)szPassword));

if(rc != SQL_SUCCESS && rc != SQL_SUCCESS_WITH_INFO)
{
  GetErrorMsg(SQL_HANDLE_DBC, hDbc, sErrMsg);
  printf("SQLConnect error\n%s", sErrMsg.c_str());
  return 1;
}

Если функция SQLConnect вернула код ошибки, мы вызываем функцию GetErrorMsg, определенную в нашей программе. О ней мы поговорим позже, а сейчас только скажем, что эта функция извлекает заголовки диагностических записей и формирует из них текстовую строку sErrMsg, которая затем отображается на консольном экране.

В том случае, если функции main удалось открыть соединение с источником данных, она создает идентификатор команды и запускает команду на выполнение:

rc = SQLAllocHandle(SQL_HANDLE_STMT, hDbc, &hStmt);

if(rc != SQL_SUCCESS && rc != SQL_SUCCESS_WITH_INFO)
{
  GetErrorMsgConn(SQL_HANDLE_DBC, hDbc, sErrMsg);
  printf("SQLAllocHandle Error\n%s", sErrMsg.c_str());
  return 1;
}
 
rc = SQLExecDirect(hStmt, (unsigned char*)
  "select ManagerID, Name, Password, LastLogin, Rights from managers", SQL_NTS);

if(rc != SQL_SUCCESS && rc != SQL_SUCCESS_WITH_INFO)
{
  GetErrorMsgConn(SQL_HANDLE_STMT, hStmt, sErrMsg);
  printf("SQLExecDirect Error\n%s", sErrMsg.c_str());
  return 1;
}

В качестве команды мы применили здесь простой оператор SELECT, извлекающий несколько полей из таблицы managers.

Для обработки ошибочных ситуаций здесь вызывается функция GetErrorMsgConn. Она извлекает не только заголовки диагностических записей, но и поля записей состояния, формируя из этой информации строку сообщения об ошибке. Записи состояния становятся доступными только после установления соединения с источником данных, поэтому мы и подготовили две разные функции обработки ошибок.

Следующий этап — привязка локальных переменных к полям набора записей, извлеченных в результате выполнения команды:

SQLINTEGER nManagerID;
SQLINTEGER cbManagerID;
SQLCHAR     szName[MAXNAME + 1];
SQLINTEGER cbName;
SQLCHAR     szPass[MAXNAME + 1];
SQLINTEGER cbPass;
SQL_TIMESTAMP_STRUCT tsLastLogin;
SQLINTEGER cbLastLogin;
SQLCHAR     szRights[MAXNAME + 1];
SQLINTEGER cbRights;

rc = SQLBindCol(hStmt, 1, SQL_C_SLONG, &nManagerID,
  sizeof(nManagerID), &cbManagerID);
rc = SQLBindCol(hStmt, 2, SQL_C_CHAR, szName, MAXNAME, &cbName);
rc = SQLBindCol(hStmt, 3, SQL_C_CHAR, szPass, MAXNAME, &cbPass);
rc = SQLBindCol(hStmt, 4, SQL_C_TYPE_TIMESTAMP, &tsLastLogin,
  sizeof(tsLastLogin), &cbLastLogin);
rc = SQLBindCol(hStmt, 5, SQL_C_CHAR, szRights, MAXNAME, &cbRights);

Здесь мы последовательно привязываем пять локальных переменных nManagerID, szName, szPass, tsLastLogin, szRights к столбцам набора записей с именами ManagerID, Name, Password, LastLogin, Rights, соответственно. В этой главе мы уже рассказывали о том, как выполнить привязку данных с использованием функции SQLBindCol.

После выполнения привязки функция main запускает цикл извлечения записей из набора, полученного в результате выполнения команды:

while((rc = SQLFetch(hStmt)) != SQL_NO_DATA)
{
  if(cbLastLogin < 0)
  {
     printf("%4d | %10s | %10s | %15s | (not logged yet)\n",
       nManagerID, szName, szPass, szRights);
  }
  else
  {
     printf("%4d | %10s | %10s | %15s | %02d.%02d.%04d %02d:%02d:%02d\n",
       nManagerID, szName, szPass, szRights,
       tsLastLogin.day, tsLastLogin.month, tsLastLogin.year,
       tsLastLogin.hour, tsLastLogin.minute, tsLastLogin.second);
  }
}

Записи извлекаются средствами функции SQLFetch. В теле цикла мы выводим на консоль содержимое извлеченных полей.

Так как поле LastLogin (дата последнего подключения сотрудника магазина) может содержать значения NULL, мы обрабатываем этот случай отдельно. Признаком извлечения пустой записи NULL является отрицательное значение переменной cbLastLogin, содержащей размер извлеченных данных.

Освобождение ресурсов, полученных программой для обращения к базе данных, нужно выполнять следующим образом.

Вначале программа освобождает идентификатор команды, вызывая для этого функцию SQLFreeHandle:

SQLFreeHandle(SQL_HANDLE_STMT, hStmt);

Далее выполняется отключение от источника данных при помощи функции SQLDisconnect:

SQLDisconnect(hDbc);

И наконец, мы освобождаем идентификаторы соединения и среды исполнения:

SQLFreeHandle(SQL_HANDLE_DBC,  hDbc);
SQLFreeHandle(SQL_HANDLE_ENV,  hEnv);

Функция GetErrorMsgConn

Эта функция выполняет извлечение заголовка диагностической записи, а также полей записей состояния, созданных для данной диагностической записи.

Алгоритм извлечения записей мы уже рассматривали.

Опустив для краткости определения локальных переменных, приведем исходный текст цикла извлечения записей и формирования сообщения об ошибке:

while(rc != SQL_NO_DATA_FOUND)
{
  rc = SQLGetDiagRec(nHandleType, sqlhHandle,
     nRecordNumber, szSQLState, &sdwNativeError,
     szErrorMessage, 255, &pcbErrorMessage);

  if(rc != SQL_NO_DATA_FOUND)
  {
      rc = SQLGetDiagField(nHandleType, sqlhHandle, nRecordNumber,
        SQL_DIAG_ROW_NUMBER, &nRowNumber, SQL_IS_INTEGER, NULL);

      rc = SQLGetDiagField(nHandleType, sqlhHandle, nRecordNumber,
        SQL_DIAG_SS_LINE, &nLine, SQL_IS_INTEGER, NULL);

      rc = SQLGetDiagField(nHandleType, sqlhHandle, nRecordNumber,
        SQL_DIAG_SS_MSGSTATE, &sdwMessageState, SQL_IS_INTEGER,
        NULL);

      rc = SQLGetDiagField(nHandleType, sqlhHandle, nRecordNumber,
        SQL_DIAG_SS_SEVERITY, &sdwSeverity, SQL_IS_INTEGER, NULL);

      rc = SQLGetDiagField(nHandleType, sqlhHandle, nRecordNumber,
        SQL_DIAG_SS_PROCNAME, &schProcName, sizeof(schProcName),
        &cbProcName);

      rc = SQLGetDiagField(nHandleType, sqlhHandle, nRecordNumber,
        SQL_DIAG_SS_SRVNAME, &schServerName, sizeof(schServerName),
        &cbServerName);

      sErrMsg = "SQL State:         ";
      sErrMsg += (LPSTR)szSQLState;

      wsprintf(szBuf, "%d", sdwNativeError);
      sErrMsg += "\nNative Error:   ";
      sErrMsg += (LPSTR)szBuf;

      sErrMsg += "\nError Message:    ";
      sErrMsg += (LPSTR)szErrorMessage;

      wsprintf(szBuf, "%d", nRowNumber);
      sErrMsg += "\nODBC Row Number:  ";
      sErrMsg += (LPSTR)szBuf;

      wsprintf(szBuf, "%d", sdwMessageState);
      sErrMsg += "\nMessage State:    ";
      sErrMsg += (LPSTR)szBuf;

      wsprintf(szBuf, "%d", sdwSeverity);
      sErrMsg += "\nSeverity:         ";
      sErrMsg += (LPSTR)szBuf;

      sErrMsg += "\nProc Name:        ";
      sErrMsg += (LPSTR)schProcName;

      sErrMsg += "\nServer Name:      ";
      sErrMsg += (LPSTR)schServerName;
   }
   nRecordNumber++;
}

Заголовок диагностической записи извлекается функцией SQLGetDiagRec. Далее при помощи функции SQLGetDiagField извлекаются по очереди значения записей состояния. Среди них есть как численные значения, так и текстовые строки. Для формирования общей строки сообщения об ошибке мы преобразуем численные значения в строки и добавляем к строке sErrMsg, ссылка на которую передается функции GetErrorMsgConn.

Вот какая строка формируется в том случае, если мы допустили синтаксическую ошибку в операторе SELECT, указав, например, его как SELECTT:

SQLExecDirect Error
SQL State
:         42000
Native Error:    156
Error Message:     [Microsoft][ODBC SQL Server Driver][SQL Server]Incorrect synta
x near the keyword 'from'.
ODBC Row Number: 1
Message State:     1
Severity:        15
Proc Name:
Server Name:       FROLOV

Если же ошибка — в имени столбца, текст сообщения будет другим:

SQLExecDirect Error
SQL State
:         42S22
Native Error:    207
Error Message:     [Microsoft][ODBC SQL Server Driver][SQL Server]Invalid column
name 'ManagerIDv'.
ODBC Row Number: 1
Message State:     3
Severity:        16
Proc Name:
Server Name:       FROLOV

Как видите, диагностика ошибки достаточно полная.

Функция GetErrorMsg

Она вызывается для обработки ошибок, возникающих на ранней стадии работы программы до момента установления соединения с источником данных.

Внутри этой функции находится цикл, в котором выполняется извлечение заголовки диагностических записей при помощи функции SQLGetDiagRec:

while(rc != SQL_NO_DATA_FOUND)
{
  rc = SQLGetDiagRec(nHandleType, sqlhHandle,
     nRecordNumber, szSQLState, &sdwNativeError,
     szErrorMessage, 255, &pcbErrorMessage);

  wsprintf(szBuf, "%d", sdwNativeError);
  if(rc != SQL_NO_DATA_FOUND)
  {
     sErrMsg = "SQL State:   ";
     sErrMsg += (LPSTR)szSQLState;

     sErrMsg += "\nNative Error: ";
     sErrMsg += (LPSTR)szBuf;

     sErrMsg += "\nError Message: ";
     sErrMsg += (LPSTR)szErrorMessage;
  }
  nRecordNumber++;
}

Эта функция также формирует в переменной sErrMsg итоговое сообщение об ошибке.

Вот, например, какое сообщение появится на экране, если указать неправильное имя источника данных:

SQLConnect error
SQL State:       IM002
Native Error:    0
Error Message:   [Microsoft][ODBC Driver Manager] Data source name not found and no default driver specified

Запуск хранимых процедур

Как уже говорилось ранее, применение хранимых процедур позволяет упростить разработку и сопровождение проектов с базами данных за счет отделения программ от данных.

Практически при создании приложений для Интернета или интрасетей Вам потребуется вызвать хранимые процедуры из серверных сценариев JScript или VB Script, встроенных в страницы ASP, либо из расширений сервера Web, таких, как программы CGI или приложения ISAPI. В последнем случае мы рекомендуем обращаться к базе данных Microsoft SQL Server с применением интерфейса ODBC, а не ADO или OLE DB.

Именно поэтому, рассказывая о работе с методами доступа ADO и OLE DB в приложениях, написанных на C++, мы опустили материал о вызове хранимых процедур как второстепенный. Теперь же нам необходимо к нему вернуться, так как уже в следующей главе мы займемся созданием расширений сервера Web, обращающихся к базам данных через интерфейс ODBC.

Привязка параметров

Запуск процедур выполняется примерно таким же образом, что и запуск команд при помощи функции SQLExecDirect. Вы должны сформировать команду в виде шаблона хранимой процедуры и затем запустить ее.

Однако прежде чем рассказать о шаблонах хранимых процедур, мы займемся привязкой входных и выходных параметров, передаваемых процедуре при запуске. В ходе этой операции параметры процедуры привязываются к локальным переменным, определенным в программе.

Для привязки параметров хранимых процедур Вы должны использовать функцию SQLBindParameter, прототип которой приведен ниже:

SQLRETURN SQLBindParameter(
  SQLHSTMT      hStatement,
  SQLUSMALLINT  nParameter,
  SQLSMALLINT   nInputOutputType,
  SQLSMALLINT   nValueType,
  SQLSMALLINT   nParameterType,
  SQLUINTEGER   nColumnSize,
  SQLSMALLINT   nDecimalDigits,
  SQLPOINTER    pParameterValue,
  SQLINTEGER    cbBufferSize,
  SQLINTEGER*   pcbParamSize);

Через параметр hStatement программа должна передать функции SQLBindParameter предварительно созданный идентификатор команды запуска процедуры.

Параметр nParameter задает последовательный номер параметра процедуры, который будет привязан к локальной переменной. Нумерация параметров начинается с единицы.

С помощью параметра nInputOutputType функции SQLBindParameter передается одно из следующих значений, определяющих направление передачи данных: SQL_PARAM_INPUT (входной параметр), SQL_PARAM_OUTPUT (выходной параметр) или SQL_PARAM_INPUT_OUTPUT (входной и выходной параметр).

Параметр nValueType определяет идентификатор типа данных C локальной переменной, привязываемой к параметру. Здесь можно указывать такие константы, как SQL_C_CHAR или SQL_C_SSHORT. Полный список идентификаторов приведен в таблице 6-1.

Тип параметра хранимой процедуры указывается через параметр nParameterType функции SQLBindParameter. Здесь указываются такие типы данных SQL, как SQL_CHAR или SQLSMALLINT (см. таблицу 6-1).

Параметр nColumnSize определяет максимальное количество символов, цифр или точность данных, передаваемых через параметр.

С помощью параметра nDecimalDigits программа задает количество десятичных цифр маркера параметра.

Через параметр pParameterValue программа передает функции SQLBindParameter указатель на буфер локальной переменной, которая должна быть привязана к параметру хранимой процедуры. Длина этого буфера определяется параметром cbBufferSize.

И наконец, через параметр pcbParamSize Вы передаете указатель на переменную, в которую предварительно была записана длина параметра.

В следующем фрагменте кода мы выполняем привязку двух входных параметров и одного выходного параметра:

SQLCHAR     szAdminName[51];
SQLINTEGER cbAdminName = SQL_NTS;

rc = SQLBindParameter(hStmt, 1, SQL_PARAM_INPUT,
  SQL_C_CHAR, SQL_CHAR, 50, 0,
  szAdminName, 51, &cbAdminName);

SQLCHAR     szAdminPass[51];
SQLINTEGER cbAdminPass = SQL_NTS;

rc = SQLBindParameter(hStmt, 2, SQL_PARAM_INPUT,
  SQL_C_CHAR, SQL_CHAR, 50, 0,
  szAdminPass, 51, &cbAdminPass);

SQLCHAR     szAdminRights[51];
SQLINTEGER cbAdminRights = SQL_NTS;

rc = SQLBindParameter(hStmt, 3, SQL_PARAM_OUTPUT,
  SQL_C_CHAR, SQL_CHAR, 16, 0,
  szAdminRights, 51, &cbAdminRights);

Первый и второй параметры — входные. Они привязываются функцией SQLBindParameter с помощью константы SQL_PARAM_INPUT. Для третьего, выходного параметра мы указали константу SQL_PARAM_OUTPUT.

Все наши параметры являются текстовыми строками. Локальные переменные, которые мы будем к ним привязывать, расположены в массивах символов типа SQLCHAR. Соответственно через четвертый и пятый параметры мы передаем функции SQLBindParameter константы SQL_C_CHAR и SQL_CHAR.

Обратите внимание, как мы указываем количество символов в параметре (значение nColumnSize функции SQLBindParameter). Входные параметры предназначены для передачи имени пользователя и пароля, причем соответствующие столбцы в базе данных могут содержать до 50 символов. Выходной параметр предназначен для передачи строк, размером не более 16 символов.

Последние три параметра функции SQLBindParameter описывают буферы, привязываемые к соответствующим параметрам. Они имеют одинаковый размер (51 символ с учетом двоичного нуля, закрывающего строку).

Запуск процедуры

Как мы уже говорили, для запуска на выполнение хранимых процедур можно воспользоваться уже знакомой Вам функцией SQLExecDirect.

Ниже показан фрагмент кода, в котором запускается процедура с тремя параметрами:

rc = SQLExecDirect(hStmt,
  (unsigned char*)"{call ManagerLogin(?,?,?)}", SQL_NTS);

Обратите внимание, что через второй параметр мы передали функции SQLExecDirect строку шаблона хранимой процедуры, в которой параметры отмечены символом «?».

Помимо параметров процедура может возвращать значение. Для такой процедуры следует подготовить шаблон следующего вида:

{? = call ProcName(?,?,?)}

Напомним, что при выполнении привязки параметров хранимой процедуры функции SQLBindParameter необходимо указать номер привязываемого параметра nParameter. Если хранимая процедура возвращает значение, то для привязки локальной переменной к этому значению необходимо указать номер 1. При этом остальные параметры процедуры нумеруются, начиная со значения 2.

При успешном запуске хранимой процедуры функция SQLExecDirect возвращает не только значения SQL_SUCCESS и SQL_SUCCESS_WITH_INFO но и SQL_NO_DATA.

Код завершения SQL_NO_DATA означает, что при выполнении процедуры не был создан набор записей. В зависимости от того, какие действия выполняет хранимая процедура, данную ситуацию можно считать нормальной или ошибочной. Например, если процедура обновляет или удаляет записи в базе данных или в зависимости от тех или иных условий возвращает выходные параметры, не изменяя базу данных, набор записей может и не создаваться.

Извлечение значений выходных параметров процедуры

Перед извлечением значений выходных параметров хранимой процедуры (а также значения кода возврата процедуры) необходимо выполнить обработку наборов записей, созданных при выполнении команды.

Если эти результаты не нужны, их можно проигнорировать, вызвав ее в цикле функцию SQLMoreResults:

while((rc = SQLMoreResults(hStmt)) != SQL_NO_DATA)
{
  if(rc != SQL_SUCCESS && rc != SQL_SUCCESS_WITH_INFO)
     break;
}

После завершения цикла, когда функция SQLMoreResults вернет значение SQL_NO_DATA, программа может получить выходные параметры из локальных переменных, к которым эти параметры были привязаны до запуска команды.

Программа ODBCPARAM

Применение описанных выше методик вызова хранимых процедур с параметрами демонстрируется на примере консольной программы ODBCPARAM.

Программа ODBCPARAM запрашивает у пользователя идентификатор и пароль, выполняет запрос к таблице managers базы данных BookStore и затем отображает права пользователя:

Login name: frolov
Password: 123

Your rights: Administrator

Если пароль или идентификатор указаны неправильно, вместо прав на консоли появляется строка «nothing»:

Login name: frolov
Password: 111

Your rights: nothing

Здесь мы не будем описывать глобальные определения и переменные программы ODBCPARAM, так как они аналогичны определениям, примененным в программе ODBCAPP (см. ранее в этой главе). Вместо этого мы сразу расскажем о функции main, выполняющей все основные действия.

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

rc = SQLAllocHandle(SQL_HANDLE_ENV, NULL, &hEnv);

rc = SQLSetEnvAttr(hEnv, SQL_ATTR_ODBC_VERSION,
     (SQLPOINTER)SQL_OV_ODBC3, SQL_IS_INTEGER);

rc = SQLAllocHandle(SQL_HANDLE_DBC, hEnv, &hDbc);

rc = SQLConnect(hDbc,
  szDSN,      (SWORD)strlen((const char*)szDSN),
  szUserName, (SWORD)strlen((const char*)szUserName),
  szPassword, (SWORD)strlen((const char*)szPassword));

Обработка ошибок опущена для краткости. Она выполняется точно таким же образом, как и в программе ODBCAPP.

На следующем этапе мы запустим хранимую процедуру ManagerLogin, с которой Вы познакомились в четвертой главе нашей книги. Для удобства мы повторим исходный текст этой процедуры, немного изменив его:

CREATE PROCEDURE ManagerLogin @User varchar(50), @Pass varchar(50), @Rights varchar(16) output AS

IF NOT EXISTS(SELECT * FROM managers)
  INSERT managers (Name, Password, Rights, LastLogin) VALUES(@User, @Pass, 'Administrator', GETDATE())

SELECT @Rights="nothing"
SELECT @Rights=Rights FROM managers WHERE Name=@User AND Password=@Pass
UPDATE managers SET LastLogin=GETDATE() WHERE Name=@User

Здесь откорректированы значения, возвращаемого в случае отсутствия записи сотрудника в базе данных. Вместо пустой строки теперь возвращается строка «nothing».

Как видите, процедура ManagerLogin имеет два входных и один выходной параметры. Получив управление, она проверяет существование записей в таблице managers. Если таблица пуста, в ней создается новая запись с правами администратора. Это нужно для начальной настройки системы при первом запуске.

В ходе дальнейшей работы процедура ManagerLogin ищет в таблице managers запись для сотрудника магазина, идентификатор и пароль которого был переда ей через параметры @User и @Pass. Если такая запись найдена, права сотрудника переписываются в выходной параметр @Rights. Если же записи нет, в этой параметр записывается строка «nothing».

Перед тем как запустить процедуру, нам нужно создать идентификатор команды и привязать три параметра, предназначенные для передачи имени, пароля и прав сотрудника.

Создание идентификатора команды выполняется, как и раньше, функцией SQLAllocHandle:

rc = SQLAllocHandle(SQL_HANDLE_STMT, hDbc, &hStmt);

Далее программа вводит с консоли и привязывает к соответствующему параметру хранимой процедуры ManagerLogin строку szAdminName, содержащую имя сотрудника:

SQLCHAR     szAdminName[51];
SQLINTEGER cbAdminName = SQL_NTS;

printf("\nLogin name: ");
gets((char*)szAdminName);

rc = SQLBindParameter(hStmt, 1, SQL_PARAM_INPUT,
  SQL_C_CHAR, SQL_CHAR, 50, 0,
  szAdminName, 50, &cbAdminName);

Аналогичным образом выполняется ввод пароля и привязка переменной szAdminPass, предназначенной для хранения пароля:

SQLCHAR     szAdminPass[51];
SQLINTEGER cbAdminPass = SQL_NTS;

printf("\nPassword: ");
gets((char*)szAdminPass);

rc = SQLBindParameter(hStmt, 2, SQL_PARAM_INPUT, SQL_C_CHAR,
  SQL_CHAR, 50, 0,
  szAdminPass, 50, &cbAdminPass);

В обоих случаях мы отмечаем входные параметры константой SQL_PARAM_INPUT.

Выходной параметр привязывается с применением константы SQL_PARAM_OUTPUT:

SQLCHAR     szAdminRights[51];
SQLINTEGER cbAdminRights = SQL_NTS;

rc = SQLBindParameter(hStmt, 3, SQL_PARAM_OUTPUT,
  SQL_C_CHAR, SQL_CHAR, 16, 0,
  szAdminRights, 16, &cbAdminRights);

К нему привязывается массив szAdminRights. Именно сюда будет записан результат работы хранимой процедуры ManagerLogin.

Для запуска хранимой процедуры на выполнение мы вызываем функцию SQLExecDirect, передавая ей идентификатор команды и шаблон процедуры ManagerLogin:

rc = SQLExecDirect(hStmt,
  (unsigned char*)"{call ManagerLogin(?,?,?)}", SQL_NTS);

while((rc = SQLMoreResults(hStmt)) != SQL_NO_DATA)
{
  if( rc == -1)
     break;
}

После того как программа убедится в отсутствии наборов записей (что в нашем случае не требуется делать, так как процедура ManagerLogin не создает никаких наборов), она выводит консоль полученные права сотрудника:

printf("\nYour rights: %s\n", szAdminRights);

Перед завершением работы программа освобождает идентификатор команды, отключается от источника данных, а затем освобождает идентификаторы источника данных и среды исполнения:

SQLFreeHandle(SQL_HANDLE_STMT, hStmt);
SQLDisconnect(hDbc);
SQLFreeHandle(SQL_HANDLE_DBC,  hDbc);
SQLFreeHandle(SQL_HANDLE_ENV,  hEnv);

[Назад] [Содержание] [Дальше]