Сообщений 8 Оценка 90 Оценить |
Создавая программное обеспечение для операционных систем семейства Windows, и предусматривая работу с СУБД, разработчики стоят перед выбором: какие компоненты или библиотеки использовать для доступа к данным. Обычно приходится выбирать один компонент (иногда два и более) из состава Microsoft Data Access Components (MDAC). Вот из чего предлагается выбирать:
Компоненты обеспечивают независимость приложения от конкретного хранилища данных. Эта независимость достигается за счёт использования драйверов. Имеются драйверы двух типов – для работы с файлами данных (текстовыми, электронными таблицами) и для работы с СУБД. На сегодняшний день существует более сотни драйверов для работы со всеми основными типами файлов и баз данных.
Какой компонент выбрать? Для этого необходимо знать их преимущества и недостатки. Все зависит от того, какая решается задача, кто решает эту задачу, в каких условиях предполагается эксплуатировать программный продукт.
Некоторую помощь в решении этой проблемы может оказать сравнительная таблица, которую можно найти в MSDN.
ODBC | OLE DB |
---|---|
API для доступа к данным | Компоненты для доступа к данным |
API в стиле языка C | COM-модель |
Табличные данные | Табличные и многомерные данные |
SQL-стандарт | COM-стандарт |
В общем случае, если вас терзают сомнения, я рекомендую прочитать статью в MSDN "Choosing Your Data Access Strategy". Очевидно лишь одно – в одной и той же программе использовать одновременно несколько компонент неэффективно.
В этой статье рассматривается "долгожитель" – 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-функции, причем в определённой последовательности:
Для соединения с источником данных с помощью функции 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, которые призваны облегчить процесс доступа к данным. Из них наиболее популярна библиотека 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-запросы, вызывать и передавать параметры в хранимые процедуры, манипулировать таблицами, создавать, изменять или удалять какие-либо записи в таблице. Исходный текст любого приложения становится намного компактнее, появляется возможность оперативно вносить в него изменения.
Эта функция осуществляет вывод (вставку и модификацию) данных любого типа посредством выполнения 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. Функция считывает в переменные информацию из источника данных посредством выполнения 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 Оценить |