Сообщений 8    Оценка 90        Оценить  
Система Orphus

ODBC: проблема эффективного ввода/вывода.

Автор: К.М. Максимов
программист, кандидат экономических наук
Опубликовано: 05.09.2003
Исправлено: 13.03.2005
Версия текста: 1.0
Выбор технологии доступа к данным
Преимущества и перспективы ODBC API
Основы работы с ODBC API
Эффективный ввод/вывод через ODBC API
Функция db_printf
Функция db_scanf
Благодарности

Выбор технологии доступа к данным

Создавая программное обеспечение для операционных систем семейства Windows, и предусматривая работу с СУБД, разработчики стоят перед выбором: какие компоненты или библиотеки использовать для доступа к данным. Обычно приходится выбирать один компонент (иногда два и более) из состава Microsoft Data Access Components (MDAC). Вот из чего предлагается выбирать:

Компоненты обеспечивают независимость приложения от конкретного хранилища данных. Эта независимость достигается за счёт использования драйверов. Имеются драйверы двух типов – для работы с файлами данных (текстовыми, электронными таблицами) и для работы с СУБД. На сегодняшний день существует более сотни драйверов для работы со всеми основными типами файлов и баз данных.

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

Некоторую помощь в решении этой проблемы может оказать сравнительная таблица, которую можно найти в MSDN.

ODBCOLE DB
API для доступа к даннымКомпоненты для доступа к данным
API в стиле языка CCOM-модель
Табличные данныеТабличные и многомерные данные
SQL-стандартCOM-стандарт
Tаблица 1.

В общем случае, если вас терзают сомнения, я рекомендую прочитать статью в MSDN "Choosing Your Data Access Strategy". Очевидно лишь одно – в одной и той же программе использовать одновременно несколько компонент неэффективно.

Преимущества и перспективы ODBC API

В этой статье рассматривается "долгожитель" – Open Data-Base Connectivity (ODBC), а именно некоторые аспекты ввода-вывода с использованием этого API. Если кто-то не знает или забыл, то MFC-классы CDatabase и CRecordset (и соответствующие wizard'ы) используют вызовы ODBC API.

ODBC – это программный интерфейс для доступа к данным, использующий язык SQL. Основной средой функционирования ODBC считается Windows, хотя существуют реализации ODBC для других операционных систем – OS/2, Unix, MacOS и др.

Приложения, использующие ODBC API, могут работать с различными по своей природе источниками данных. Это могут быть реляционные, иерархические и гетерогенные СУБД, файлы с данными и любые другие источники данных. Такую возможность обеспечивают специальные модули – ODBC-драйверы. Менеджер драйверов (Driver Manager) взаимодействует с приложением и обеспечивает загрузку драйвера, необходимого для доступа к конкретному источнику данных. Таким образом, приложение работает с менеджером драйверов, который в свою очередь направляет вызовы API-функций в соответствующий ODBC-драйвер, который обрабатывает их специфично для конкретной СУБД. Для приложения работа с источником данных совершенно прозрачна. Вы всегда легко можете настроить своё приложение для работы с любой СУБД, для которой имеется драйвер. Перекомпилировать или изменять исходный код не требуется.

Первая версия Microsoft ODBC вышла в свет в 1992 году. Сейчас повсеместно используется третья версия ODBC, которая была представлена в 1996 году. Не следует использовать старые версии, потому что именно третья версия соответствует стандартам и спецификациям X/Open и ISO/IEC.

Библиотека ODBC получила широкое признание у программистов всего мира. С одной стороны она предоставляет возможность использовать стандартные SQL-операторы для запросов к базе данных. С другой – она является достаточно низкоуровневой, гибкой и настраиваемой на любой источник данных. Немаловажно и то, что ODBC – это стандартный интерфейс, который существует во многих операционных системах, а в Windows 95 и последующие версии Windows он встроен. К основным преимуществам ODBC API следует отнести высокую скорость работы, гибкость, переносимость исходного кода, наличие тесной связи с языком С/С++.

Основы работы с ODBC API

Прежде чем перейти к проблеме организации эффективного ввода-вывода, давайте рассмотрим основные этапы работы с ODBC API. Для доступа к данным при помощи ODBC любая программа вызывает API-функции, причем в определённой последовательности:

Для соединения с источником данных с помощью функции SQLAllocHandle следует создать "хэндлы" для среды (environment) и соединения (connection).

ПРИМЕЧАНИЕ

Объявления ODBC-функций и констант находятся в файлах sql.h и sqlext.h, библиотечный файл – odbc32.lib.

Необходимо также указать, что работать мы будем с третьей версией ODBC API. Затем можно подключиться к источнику данных функцией SQLConnect. Этой функции передаются имя источника данных (Data Source Name, DSN), имя пользователя (login), пароль (password) и длины этих строк.

Для строк языка С, которые заканчиваются нулём, можно передавать константу SQL_NTS (Null-Terminated String).

DSN – обязательный параметр, без которого дальнейшая работа программы невозможна. Обычно DSN создают при установке приложения. Например, инсталлятор InstallShield легко справляется с этой задачей, также он устанавливает необходимые ODBC-драйверы.

В этом месте стоит упомянуть о возможности создания "на лету" имени для источника данных (DSN). Функция SQLConfigDataSource позволяет программным путём создать DSN и избавляет конечного пользователя от процесса настройки DSN.

Все последующие этапы связаны с подготовкой и выполнением SQL-запросов. Для выполнения запроса требуется хэндл, который можно получить с помощью функции SQLAllocHandle. Далее может следовать так называемый прямой запрос, который выполняет функция SQLExecDirect, а может – сложный. В последнем случае запрос сначала подготавливается с помощью SQLPrepare, затем для передачи исходных данных или установки связи между переменными и параметрами SQL-оператора применяется функция SQLBindParameter. Когда всё готово для выполнения запроса, вызывают функцию SQLExecute. Для чтения данных обычно используют пару функций SQLFetch и SQLGetData, хотя существуют и другие способы. Например, для быстрого чтения данных из таблиц используют SQLBindCol. По окончании работы с запросом ресурсы следует освободить функцией SQLFreeHandle. Нужно не забыть отключиться от источника данных (функция SQLDisconnect) и освободить все ресурсы (функция SQLFreeHandle).

Всегда следует проверять значения, которые возвращают функции ODBC API. Функции в случае успешного выполнения возвращают значения SQL_SUCCESS или SQL_SUCCESS_WITH_INFO. Для того чтобы не выполнять две операции сравнения, существует удобный макрос SQL_SUCCEEDED.


Рисунок 1. Таблица users в базе данных

Проиллюстрируем работу с ODBC API на примере добавления записи в таблицу. Пусть у нас имеется таблица users. Эту таблицу мы будем использовать и для других примеров. В таблице три поля – идентификатор (номер) пользователя, его имя (name) и величина зарплаты (salary). В таблице используются поля трёх наиболее часто используемых типов – целое число, строка символов и число с плавающей точкой.

SQLHANDLE hEnv, hDbc;
SQLRETURN res;

// --== ИНИЦИАЛИЗАЦИЯ СОЕДИНЕНИЯ С БД ЧЕРЕЗ ODBC ==--
// Получаем хэндл ODBC-среды.
res = SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &hEnv);
if( !SQL_SUCCEEDED(res) ) return -1;
// Запрашиваем третью версию.
SQLSetEnvAttr(hEnv, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0);
// Получаем хэндл для соединения.
SQLAllocHandle(SQL_HANDLE_DBC, hEnv, &hDbc);
// Подключаемся к источнику данных.
res = SQLConnect(hDbc, "Sample_DB", SQL_NTS, "", SQL_NTS, "", SQL_NTS);
if( SQL_SUCCEEDED(res) )
{
  // --== ВЫПОЛНЕНИЕ SQL-ЗАПРОСА ==--
  SQLHSTMT hStmt;
  // SQL-оператор для добавления записи в БД.
  SQLCHAR szSQL[]="INSERT INTO users (id, name, salary) VALUES (1, 'Bill', 100);";
  // Получаем хэндл для SQL-запроса/оператора. 
  SQLAllocHandle(SQL_HANDLE_STMT, hDbc, &hStmt);
  // Простейший прямой SQL-запрос/оператор.
  SQLExecDirect(hStmt, szSQL, SQL_NTS);
  // Освобождаем ресурсы.
  SQLFreeHandle(SQL_HANDLE_DBC, hDbc);
}
// --== ЗАВЕРШЕНИЕ РАБОТЫ С ODBC ==--
// Отключаемся от источника данных.
SQLDisconnect(hDbc);
// Освобождаем ресурсы.
SQLFreeHandle(SQL_HANDLE_DBC, hDbc);
SQLFreeHandle(SQL_HANDLE_ENV, hEnv);

Эффективный ввод/вывод через ODBC API

В процессе работы над проектом, который строился по технологии клиент-сервер, я столкнулся с проблемой организации эффективного ввода/вывода информации из базы данных. Под эффективностью понималась не только быстрая работа программ (клиентской и серверной), но и хорошая организация исходного кода, его мобильность и гибкость. Учитывая высокую скорость работы ODBC API, было решено использовать его для реализации серверной части программы, которая работала с СУБД.

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

Всем известно, что существует несколько программных "обёрток" для ODBC API, которые призваны облегчить процесс доступа к данным. Из них наиболее популярна библиотека MFC, а точнее её классы CDatabase, CRecordset и соответствующие wizard'ы. Следует напомнить о довольно удобном в использовании классе CODBCRecordset, которому ранее бала посвящена статья “Универсальный ODBC Recordset” в журнале "Программист" №2/2001.

Казалось бы, MFC-классы упрощают программный код. На самом же деле wizard MFC для каждого SQL-запроса создаёт производный класс, а это не всегда удобно, особенно когда требуется выполнять много различных SQL-запросов. Можно самому создать несколько производных классов, но, по-моему, это не лучшее решение. И базовые, и производные классы не отличаются функциональностью, а область их применения сильно ограничена. Таких классов в большом проекте может стать очень много. Такая организация программы, как мне кажется, усложняет процесс разработки, отладки и сопровождения. Кроме того, на начальных этапах разработки почти всех проектов изменяется структура базы данных. А еженедельно вносить изменения в классы, которые сгенерировал wizard – утомительно и неэффективно. Это главная причина, по которой пришлось отказаться от wizard'ов и MFC-классов для работы с базой данных.

Можно было бы использовать класс CODBCRecordset, однако и от него пришлось отказаться. Во-первых, исходный код начал превращаться в сплошной блок "try-catch", в котором одна половина кода "ловила" исключительные ситуации, а другая половина выполняла полезную работу. Во-вторых, и в этом классе были обнаружены недочёты.

В конце концов, весь исходный код для работы с базой данных был переписан без использования классов. Однако результат меня не устраивал. В программе довольно много SQL-операторов, для каждого из которых требовалось писать "обвязочный" код вроде последовательности SQLAllocHandle – SQLExecDirect – SQLFetch – SQLGetData – SQLFreeHandle.

Недолго думая, я решил выявить ту общую часть "обвязочного" кода, которая многократно повторяется, и перенести её в отдельную функцию. Осталась одна проблема – требовалось создать универсальную "начинку" для такой функции, чтобы иметь возможность выполнять различные SQL-операторы.

Таким образом, появились две небольшие функции db_printf и db_scanf. В именах функций отражена их суть – они предназначены для вывода и ввода данных. О них пойдёт речь далее. Здесь существует большая аналогия с библиотечными функциями printf и scanf. И те, и другие принимают переменное число аргументов и работают со спецификаторами. Время показало, что функции db_printf и db_scanf позволяют эффективно выполнять SQL-запросы, вызывать и передавать параметры в хранимые процедуры, манипулировать таблицами, создавать, изменять или удалять какие-либо записи в таблице. Исходный текст любого приложения становится намного компактнее, появляется возможность оперативно вносить в него изменения.

Функция db_printf

Эта функция осуществляет вывод (вставку и модификацию) данных любого типа посредством выполнения SQL-оператора.

Объявление функции:

int db_printf(SQLHANDLE hDbc, LPTSTR szSQL, LPCTSTR szFmt [, argument] ...);

Параметры:

hDbc – хэндл (дескриптор) соединения с источником данных;
szSQL – строка с SQL-оператором для выполнения запроса;
szFmt – строка формата с управляющими символами-спецификаторами;
argument – один или более аргументов для SQL-оператора.

Возвращаемое значение:

Количество обработанных аргументов или -1 при возникновении ошибки.

Область применения функции очень обширна. Приведём несколько примеров, в которых предполагается, что вы уже подключились к базе данных и в переменной hDbc лежит хэндл этого соединения. Контроль за подключением (отключением) к (от) источнику данных остаётся за программистом, остальную часть работы по выполнению SQL-запросов берут на себя функции db_printf и db_scanf. Предположим, что требуется программным путём создать таблицу users, изображённую на рисунке 1. Нет ничего проще:

db_printf(hDbc, "CREATE TABLE users (id INTEGER, name CHAR(40), salary FLOAT);", "");

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

  char *sql = "INSERT INTO users (id, name, salary) VALUES (?, ?, ?);";
  db_printf(hDbc, sql, "%d %s %f", 10, "Bill", 1003.14);

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

Напомню, что в языке SQL, который реализован в ODBC, чтобы указать драйверу на то, что значения можно получить у вызывающего приложения, используется символ "?". Обратите внимание на строку спецификаторов и на то, что в примере в функцию передаются аргументы трёх разных типов. Именно спецификаторы указывают соответствующий тип аргумента. Отличительной особенностью функции db_printf является то, что она работает с аргументами, которые имеют машинное (двоичное) представление. Исходные данные и параметры могут быть строкой символов, а могут быть в двоичном/машинном виде. То есть float n = 10.25; это совсем не то что char n[] = "10.25". Разницу чувствуете? SQL-оператор – это строка символов. Как туда передавать число, время и т.д. Есть различные форматы. Чтобы избежать использования этих форматов и конвертирования (замедления) можно работать с двоичными/машинными объектами. У вас никогда не возникнет проблем с преобразованием типов аргументов, которые иногда случаются в других программах. Например, преобразование строки "1003.14" в число не всегда проходит гладко, потому что в качестве разделителя целой и дробной частей числа может быть или точка, или запятая, или любой другой символ.

Теперь, когда вы знаете, как использовать функцию db_printf, можно посмотреть как она устроена.

int db_printf(SQLHANDLE hDbc, LPTSTR szSQL, LPCTSTR szFmt, ...)
{
  va_list p_arg; // Список аргументов функции на стеке.
  SQLHSTMT hStmt = SQL_NULL_HSTMT;
  if( SQL_SUCCEEDED(SQLAllocHandle(SQL_HANDLE_STMT, hDbc, &hStmt)) )
  {
    if( SQL_SUCCEEDED(SQLPrepare(hStmt, szSQL, SQL_NTS)) )
    {
      int iParam = 0; // Счётчик параметров
      va_start(p_arg, szFmt); // Начинаем выборку аргументов со стека
      for( const TCHAR *str = szFmt; *str; str++ )
      {
        // Обрабатываем спецификаторы типов
        if( *str==_T('%') )
        {
          str++;
          switch( *str )
          {
          case _T('%'):
            str++; break;
          case _T('s'): // Строка
          {
            // Вот её она – на стеке
            LPTSTR &ptr = va_arg(p_arg, LPTSTR);
            // Установим связь
            SQLBindParameter(hStmt, ++iParam, SQL_PARAM_INPUT, SQL_C_TCHAR, SQL_VARCHAR, lstrlen(ptr)+1, 0, ptr, 0, 0);
          } break;
          case _T('d'): // Целое число 32-бита
          {
            LONG &val = va_arg(p_arg, LONG);
            SQLBindParameter(hStmt, ++iParam, SQL_PARAM_INPUT, SQL_C_LONG, SQL_INTEGER, 0, 0, &val, 0, 0);
          } break;
          case _T('f'): // Число с плавающей точкой
          {
          double &val = va_arg(p_arg, double);
          SQLBindParameter(hStmt, ++iParam, SQL_PARAM_INPUT, SQL_C_DOUBLE, SQL_DOUBLE, 0, 0, &val, 0, 0);
          } break;
          // case ... здесь могут быть другие типы, TIMESTAMP и др.
        }
      }
    }
    // заканчиваем работу со стеком
    va_end(p_arg);
    // Выполняем запрос
    if( SQL_SUCCEEDED(SQLExecute(hStmt)) )
    {
      // Если успешно, то освобождаем ресурсы, отвязываем от стека, возврат.
      SQLFreeHandle(SQL_HANDLE_STMT, hStmt);
      return iParam;
    }
  }
  SQLFreeHandle(SQL_HANDLE_STMT, hStmt);
  }
 }
 return -1; // видать были какие-то проблемы
}

Необходимо дать некоторые пояснения к исходному коду. Взгляните на объявление функции db_printf и обратите внимание на многоточие в конце списка параметров. В языке С, как вы помните, это означает, что количество аргументов у функции может быть произвольным. Это позволило создать функцию, которая может выполнять почти любые SQL-операторы с переменным количеством параметров. Строка формата szFmt позволяет контролировать количество и тип этих параметров.

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

ПРИМЕЧАНИЕ

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

В функции db_printf используются макросы va_list, va_arg, которые извлекают из стека значения в соответствии с тем, что указано в строке спецификаторов. Центральное место занимает процесс обработки строки спецификаторов. В цикле последовательно перебираются символы, пока не закончится строка или пока не встретится управляющий символ %. Далее начинает работать конструкция switch, в которой имеется три ветви – для обработки аргументов трёх типов: строковых (%s), целых чисел (%d) и вещественных чисел (%f). Остальные символы в строке спецификаторов не обрабатываются. В общем случае совсем необязательно (но желательно) реализовывать поддержку всех типов данных, т.к. ODBC при необходимости осуществляет преобразование типов.

ПРИМЕЧАНИЕ

Таблица преобразования типов есть в MSDN в разделе "Converting Data from C to SQL Data Types".

Тип CHAR является универсальным, его можно использовать везде. Например, дату и время (TIMESTAMP) можно записать так: ts '2003-01-31 23:19:54.999'.

В цикле шаг за шагом формируется запрос к источнику данных, подставляются функцией SQLBindParameter исходные значения (адреса переменных привязываются к внутренним структурам ODBC-драйвера) и, наконец, выполняется запрос (функция SQLExecute).

На заключительном этапе вызывается функция SQLFreeHandle. Эта функция выполняет важную работу, в том числе отвязывает переменные от параметров SQL-запроса. Это серьёзный момент, потому что, мы установили связь между стеком, который постоянно изменяется, и параметрами SQL-запроса. В общем случае, устанавливая связь между автоматической переменной (ячейкой памяти), размещаемой на стеке, и параметром SQL-запроса (читай внутренней структурой ODBC-драйвера), программист должен контролировать время жизни такой переменной и своевременно удалять связи. Связь существует до тех пор, пока не будет корректно завершено выполнение запроса функцией SQLFreeHandle. Завершается функция оператором return, который возвращает количество обработанных аргументов или -1 в случае ошибки.

Функция db_scanf

При работе с базами данных часто требуется осуществлять выборку данных. Как вы догадались, настала очередь функции db_scanf. Функция считывает в переменные информацию из источника данных посредством выполнения SQL-оператора SELECT. Функция db_scanf годится для выполнения SQL-операторов, которые возвращают ограниченное число записей или значений.

Объявление функции:

int db_scanf(SQLHANDLE hDbc, LPTSTR szSQL, LPCTSTR szFmt[, argument] ...);

Параметры:

hDbc – хэндл (дескриптор) соединения с источником данных;
szSQL – строка с SQL-оператором для выполнения запроса;
szFmt – строка формата с управляющими символами-спецификаторами;
argument – адреса переменных, в которые считывается информация.

Возвращаемое значение:

Число считанных значений или -1 при возникновении ошибки.

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

Продемонстрируем на простых примерах использование функции db_scanf. В примере определяется число записей в таблице users. Результат записывается в переменную n.

  int n;
  db_scanf(hDbc, "SELECT COUNT(*) FROM users", "%d", &n);

В другом примере делается поиск клиента с id равным 10:

  int id;
  char name[64];
  double salary;
  db_scanf(hDbc, "SELECT id, name, salary FROM users WHERE id=10", "%d %s %f", &id, name, &salary);

Обратите внимание, что в функцию передаются не значения, а адреса переменных (оператор &). По указанным адресам функция db_scanf запишет данные, которые будут получены в результате выполнения SQL-оператора. Приведённых примеров должно быть достаточно, чтобы начать использовать функции db_printf и db_scanf для работы с базами данных.

Переходим к рассмотрению исходного текста функции db_scanf:

int db_scanf(SQLHANDLE hDbc, LPTSTR szSQL, LPCTSTR szFmt, ...)
{
 va_list p_arg; // Аргументы функции
 SQLINTEGER cb; // Счётчик прочитанных байт
 SQLHSTMT hStmt = SQL_NULL_HSTMT;
 if( SQL_SUCCEEDED(SQLAllocHandle(SQL_HANDLE_STMT, hDbc, &hStmt)) )
 {
  // Выполняем запрос
  if( SQL_SUCCEEDED(SQLExecDirect(hStmt, szSQL, SQL_NTS)) )
  {
   // Считываем порцию данных
   if( SQL_SUCCEEDED(SQLFetch(hStmt)) )
   {
    int iParam = 0;
    // Начинаем извлекать аргументы со стека
    va_start(p_arg, szFmt);
    for( const TCHAR *str = szFmt; *str; str++ )
    {
     if( *str==_T('%') )
     {
      str++;
      switch( *str )
      {
       case _T('%'):
         str++;
         break;
       case _T('s'): // Строка
       {
        void *ptr = va_arg(p_arg, void *);
        SQLGetData(hStmt, ++iParam, SQL_C_TCHAR, ptr, 1024, &cb);
        if( cb==0 ) *LPTSTR(ptr) = 0;
       }break;
       case _T('d'): // Целое число
       {
        void *ptr = va_arg(p_arg, void *);
        SQLGetData(hStmt, ++iParam, SQL_C_LONG, ptr, 0, &cb);
        if( cb==0 ) *PLONG(ptr) = 0;
       }break;
       case _T('f'): // Число с плавающей точкой
       {
        void *ptr = va_arg(p_arg, void *);
        SQLGetData(hStmt, ++iParam, SQL_C_DOUBLE, ptr, 0, &cb);
        if( cb==0 ) *(double *)ptr = 0.0;
       }break;
       // case ...
      }// end of switch()
     }
    }
    va_end(p_arg);
    SQLFreeHandle(SQL_HANDLE_STMT, hStmt);
    return iParam;
   }
  }
  SQLFreeHandle(SQL_HANDLE_STMT, hStmt);
 }
 return -1;
}

В комментариях исходный текст функции db_scanf не нуждается, потому что его внутренняя структура очень похожа на структуру предыдущей функции. Здесь точно также в цикле обрабатывается строка спецификаторов, используется такой же принцип извлечения параметров из стека. После выполнения прямого запроса (функция SQLExecDirect) результаты подготавливаются (функция SQLFetch) и считываются функцией SQLGetData. Спецификаторы (%s, %d или %f) указывают какой использовать тип данных. При необходимости ODBC самостоятельно выполнит необходимые преобразования типов. Переменная cb помогает контролировать процесс ввода и показывает количество полученных байтов.

Функцию db_scanf следует оценить критически и отметить, что область применения этой функции немного ограничена. Она не лучшим образом подходит для чтения больших таблиц. Явный недостаток этой функции – это невозможность корректно обработать SQL-оператор вида "SELECT * FROM users". Вместо целой таблицы функция сможет прочитать и вернуть только единственную последнюю запись. Для таких SQL-операторов можно написать свой код, с использованием ODBC API-функции или существующих классов, например CRecordset. Совместное использование функций db_printf/db_scanf и MFC-класса CDatabase удобно, потому что функции db_printf и db_scanf легко "прикручиваются" к этому классу. Известно, что в классе CDatabase есть переменная-член m_hdbc, которая и обеспечивает связь предложенных в этой статье функций и MFC-классов. Один из вариантов совместной работы:

CDatabase db;
db.Open("Sample_DB");
...
if( db.IsOpen() )
{
  double s = 999.99;
  char   name[] = "Bill";
  db_printf(db.m_hdbc, "UPDATE users SET salary=? WHERE name=?", "%f %s", s, name);
}

Вот и всё, о чём хотелось рассказать в статье. Напоследок лишь проинформирую тех читателей, которых волнует переносимость исходного кода. Существует хорошая реализация ODBC для операционных систем, отличных от Windows. На сайте http://www.unixodbc.org можно свободно загрузить такую версию и использовать её в соответствии с GNU-лицензией. Программисты, занимающиеся CGI-скриптами, по достоинству оценят эту библиотеку. Она снимает ещё одну проблему переноса исходных текстов на платформу или с платформы Unix. Вот ещё несколько серверов, где можно найти интересную информацию по теме: http://www.iodbc.org, http://www.microsoft.com/data/odbc/.

Благодарности

Выражаю благодарность Максиму Туйкину - главному редактору журнала “Программист” за помощь в оформлении и подготовке статьи.


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