• СТАТЬЯ Доступ к БД с использованием ODBC Часть 2
  • Программирование на Visual C++

    Выпуск №33 от 18 февраля 2001 г.

    Приветствую!

    Сегодня хочу предложить вашему вниманию заключительную часть статьи про ODBC нашего постоянного автора Александра Шаргина, которая, если вы помните, открывает цикл статей о технологиях доступа к данным.

    СТАТЬЯ

    Доступ к БД с использованием ODBC

    Часть 2

    Автор: Александр Шаргин 

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

    Перемещение на другие компьютеры

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

    Необходимые компоненты

    Сразу замечу, что некоторые приложения изначально разрабатываются для работы с произвольными БД. К таким приложениям относятся пакеты статистической обработки данных или электронные таблицы, способные импортировать данные из выбранной пользователем БД. Другой характерный пример – среда Visual C++. Если вы разрабатываете подобное приложение, можете смело пропустить этот раздел. Установка компонентов, необходимых для работы с конкретной базой данных – не ваша забота. Любому приложения, использующему ODBC, необходимы основные компоненты ODBC (core components) и ODBC-драйвер. К основным компонентам относятся менеджер драйверов (ODBC32.DLL), библиотека инсталлятора (ODBCCP32.DLL), библиотека курсоров (ODBCCR32.DLL) и администратор источников данных (ODBCAD32.EXE), а также несколько вспомогательных файлов. Драйвер состоит из двух DLL: библиотеки драйвера (driver DLL) и библиотеки настройки (setup DLL). Библиотека драйвера экспортирует все необходимые функции ODBC API, а библиотека настройки – функции ConfigDriver и ConfigDSN, используемые для конфигурирования самого драйвера и связанных с ним источников данных. Иногда обе библиотеки объединяют в одной DLL. Основные компоненты сейчас установлены практически на каждом компьютере, поэтому об их инсталляции я рассказывать не буду. Тем, кого интересует этот вопрос, советую обратиться к описанию функции SQLInstallDriverManager. Драйвер для каждой конкретной СУБД обычно распространяется со своей программой инсталляции. В этом случае вам нужно просто включить её в комплект поставки. Но предположим, что такая программа недоступна. Тогда можно воспользоваться функцией SQLInstallDriverEx, входящей в библиотеку инсталляции. Эта функция вызывается дважды: первый раз, чтобы определить целевую папку для драйвера, а второй раз, чтобы добавить необходимые записи в реестр. Копирование осуществляет вызывающая программа. Предположим, что драйвер "My Driver" состоит из файлов MYDRV.DLL и MYSETUP.DLL. Установку этого драйвера выполнит следующий код. 

    #include <windows.h>

    #include <odbcinst.h>

    #include <stdio.h> :


    char szPathIn[301];

    char szPathOut[301];

    DWORD dwUsageCount;

    int i, j;

    char szDriver[300] = "My Driver\0Driver=MYDRV.DLL\0Setup=MYSETUP.DLL\0";

    SQLInstallDriverEx(szDriver, NULL, szPathIn, 300, NULL, ODBC_INSTALL_INQUIRY, &dwUsageCount);

    // Копируем файлы в папку szPathIn.

    sprintf(szDriver, "My Driver;Driver=%s\\%s;Setup=%s\\%s;", szPathIn, "MYDRV.DLL", szPathIn, "MYSETUP.DLL");

    for (i = strlen(szDriver), j = 0; j < 0; j++) {

     if (szDriver[j] == ';') szDriver[j] = '\0';

    }

    SQLInstallDriverEx(szDriver, szPathIn, szPathOut, 300, NULL, ODBC_INSTALL_COMPLETE, &dwUsageCount);

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

    DWORD dwUsageCount;

    SQLRemoveDriver("My Driver", TRUE, &dwUsageCount);
     

    Как и в случае с SQLInstallDriverEx, физическое удаление файлов остаётся на вашей совести. Удалять файл следует только если счётчики использования как компонента, так и самого файла равны нулю. 

    За подробностями об установке компонентов ODBC следует обратиться к главам 18 и 23 из ODBC Programmer's Reference. 

    Программная регистрация источника данных

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

    Примечание: системный источник данных отличается от пользовательского тем, что он доступен всем пользователям компьютера, в то время как пользовательский доступен только создавшему его пользователю. Соответственно, информация о системных источниках данных хранится в реестре в разделе HKEY_LOCAL_MACHINE, а о пользовательских – в разделе HKEY_CURRENT_USER. 

    Функция SQLConfigDataSource получает имя драйвера, а также набор параметров, описывающих создаваемый источник данных. Состав этих параметров изменяется от драйвера к драйверу. Например, драйверу MS Access нужно сообщить, по крайней мере, имя источника данных (DSN) и имя файла БД (DBQ). Вот как выглядит создание источника данных dbFolks для БД: 

    SQLConfigDataSource(NULL, ODBC_ADD_DSN, "Microsoft Access Driver (*.mdb)", "DSN=dbFolks\0DBQ=c:\\folks.mdb");
     

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

    Как обойтись без источника данных

    Хотя источники данных являются удобной абстракцией, иногда хочется обойтись без них. Как это сделать? ODBC не предоставляет стандартной возможности подключаться напрямую к БД. Как мы помним, стандарт определяет только четыре параметра (DSN, UID, PWD и DRIVER) для строки подключения, передаваемой в CDatabase::OpenEx. Среди них нет параметра, в который можно было бы записать имя конкретной БД. Тем не менее, стандарт не запрещает драйверам ODBC распознавать и другие параметры. Используя их, вы жертвуете универсальностью вашей программы, но взамен получаете доступ к дополнительным возможностям драйвера. 

    В частности, многие драйверы от Microsoft используют параметр DBQ для задания имени файла БД. Вот как можно подключиться к БД , не создавая для неё источника данных. 

    CDatabase Db;

    Db.OpenEx("DRIVER={Microsoft Access Driver (*.mdb)};DBQ=f:\\folks.mdb", CDatabase::noOdbcDialog);
     

    Точно так же можно подключиться к БД, управляемой MS SQL Server, используя параметры SERVER и DATABASE. Вот пример подключения к демонстрационной БД , поставляемой вместе с SQL Server. 

    CDatabase Db;

    Db.OpenEx("DRIVER={SQL Server};SERVER=(local);DATABASE=pubs;UID=sa;PWD=", CDatabase::noOdbcDialog);
     

    Описание дополнительных параметров следует искать в документации на каждый конкретный драйвер. В частности, информация о драйверах фирмы Microsoft содержится в MSDN. 

    Начинаем с нуля

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

    Создание БД

    Хотя в ODBC нет стандартных средств для создания новых баз данных, некоторые драйвера предоставляют такую возможность. Например, драйвер MS Access распознаёт дополнительные параметры для уже знакомой нам функции SQLConfigDataSource. Один из них, CREATE_DB, как раз и служит для создания новых баз данных. Для примера рассмотрим создание БД. 

    SQLConfigDataSource(NULL, ODBC_ADD_DSN, "Microsoft Access Driver (*.mdb)", "CREATE_DB=c:\\folks.mdb");
     

    Обратите внимание, что никакого источника данных в этом случае не создаётся, хотя мы и обращаемся к SQLConfigDataSource.

    В некоторых случаях драйвер не поддерживает создания БД, но в диалекте SQL соответствующей СУБД есть необходимые для этого конструкции. В этом случае можно использовать функцию CDatabase::ExecuteSQL для выполнения требуемых операторов языка SQL. Для примера рассмотрим, как создаётся новая база данных в SQL Server.

    Db.OpenEx("DRIVER={SQL Server};SERVER=(local);UID=sa;PWD=", CDatabase::noOdbcDialog);

    Db.ExecuteSQL("CREATE DATABASE MyDB");
     

    Если ни один из перечисленных способов создания БД вам не доступен, всё, что вам остаётся – это распространять вместе с вашей программой пустую БД и копировать её каждый раз, когда требуется создать новую базу данных. 

    Создание таблиц

    Для создания таблиц в БД используется SQL-оператор CREATE TABLE, который выполняется с помощью функции CDatabase::ExecuteSQL. В простейшем случае CREATE TABLE имеет следующий формат. 

    CREATE TABLE table_name (

     {column_name column_type} [,:n]

    )

    Полное описание формата CREATE TABLE для каждой конкретной СУБД можно найти в документации. Рассмотрим пример создания таблицы tPeople, содержащей поля Name (строка из 50 символов) и DateOfBirth (дата). Запрос будет выглядеть так. 

    Db.ExecuteSQL("CREATE TABLE tPeople (Name char(50), DateOfBirth datetime)");

    Модификация полей (столбцов)

    Иногда приходится не создавать таблицу с нуля, а модифицировать уже существующую. Не останавливаясь на подробностях, скажу, что для этого используется SQL-оператор ALTER TABLE, с помощью которого можно как добавлять в таблицу новые поля, так и изменять или удалять существующие. 

    Рассмотрим несколько примеров. Сначала добавим в таблицу tPeople поле DateOfDeath (дата). 

    Db.ExecuteSQL("ALTER TABLE tPeople ADD DateOfDeath datetime");
     

    Теперь изменим ширину поля Name (с 50 до 100 символов). 

    Db.ExecuteSQL("ALTER TABLE tPeople ALTER COLUMN Name char(100)");
     

    А теперь удалим только что созданное поле DateOfDeath: 

    Db.ExecuteSQL("ALTER TABLE tPeople DROP COLUMN DateOfDeath");
     

    В заключение замечу, что SQL предоставляет вам практически неограниченные возможности по манипулированию БД. Если вам не удаётся найти функции ODBC, выполняющей требуемое действие, попробуйте найти подходящий SQL-оператор и выполнить его с помощью CDatabase::ExecuteSQL. 

    Работа в незнакомой обстановке

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

    В ODBC есть целый набор похожих функций, предназначенных для получения списка доступных драйверов и источников данных, таблиц в БД и столбцов в таблице. Они называются SQLDrivers, SQLDataSources, SQLTables и SQLColumns соответственно. Обратите внимание, что это функции ODBC API, для которых не существует обёртки в MFC. 

    Кроме того, в класс CRecordset встроены функции GetODBCFieldCount и GetODBCFieldInfo. Первая возвращает количество полей (столбцов) в наборе записей, а вторая заполняет структуру CODBCFieldInfo информацией о заданном поле. 

    Хранимые процедуры

    Хранимая процедура – это сценарий на языке SQL, который вызывается клиентом для выполнения некоторых операций и работает на стороне сервера. Хранимые процедуры могут получать входные параметры, а также сообщать о результатах своей работы, возвращая наборы записей или записывая некоторые значения в выходные параметры. Работе с хранимыми процедурами и посвящён данный раздел. 

    Вызов процедур

    Хранимая процедура вызывается при помощи SQL-оператора CALL. Обратите внимание, что использование этого оператора является обязательным требованием к ODBC-программе, даже если СУБД поддерживает другой оператор вызова процедур (например, EXEC[UTE] в SQL Server). 

    Выполнение оператора CALL осуществляется с помощью функции CDatabase::ExecuteSQL. Сам оператор заключается в фигурные скобки. Рассмотрим пример вызова процедуры spClear, не требующей параметров (она очищает таблицу tPeople, выполняя оператор DELETE * FROM tPeople). 

    Db.ExecuteSQL("{CALL spClear}");
     

    Вызов процедур с параметрами

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

    Модифицируем хранимую процедуру из предыдущего примера так, чтобы она принимала параметр paramName и удаляла из таблицы tPeople только людей с заданным именем (то есть выполняла оператор DELETE * FROM tPeople WHERE Name=paramName). Теперь мы можем удалить из таблицы всех Александров, выполнив: 

    Db.ExecuteSQL("{CALL spClear('Alexander')}");
     

    Это наиболее простой способ, но он имеет ряд ограничений. В частности, невозможно получить доступ к выходным параметрам функции. Чтобы снять эти ограничения, необходимо связать нужные нам параметры с переменными. Связывание производится в функции CDatabase::BindParameter, которую для этой цели нужно перегрузить. Это, в свою очередь, означает, что нам придётся порождать новый класс от CDatabase. Для связывания каждого параметра с переменной используется функция SQLBindParameters из ODBC API. Вместо каждого связанного с переменной параметра в вызов процедуры вставляется вопросительный знак.

    Рассмотрим пример вызова хранимой процедуры spCount, которая возвращает количество людей с заданным именем в таблице tPeople. В СУБД SQL Server такую процедуру можно создать, выполнив запрос: 

    CREATE PROC spCount(@paramName CHAR(50), @paramCount INT OUTPUT)
     

    AS SELECT @paramCount = COUNT(*) FROM tPeople WHERE Name=@paramName

    Теперь, чтобы посчитать количество Александров, необходимо написать следующий код. 

    // Порождаем новый класс от CDatabase

    class CMyDatabase : public CDatabase {

    public:

     char m_paramName[50];

     int m_paramCount;

     void BindParameters(HSTMT);

    };


    void CMyDatabase::BindParameters(HSTMT hStmt) {

     SQLBindParameter(  // Связываем @paramName с m_paramName

      hStmt, 1, SQL_PARAM_INPUT, SQL_C_CHAR,

      SQL_CHAR, 50, 0, m_paramName, 50, NULL);

     SQLBindParameter( // Связываем @paramCount с m_paramCount

      hStmt, 2, SQL_PARAM_OUTPUT, SQL_C_SLONG,

      SQL_INTEGER, 0, 4, &m_paramCount, 4, NULL);

    }

    :


    CMyDatabase Db;


    Db.OpenEx(

     "DRIVER={SQL Server};SERVER=(local);DATABASE=tPeople;UID=sa;PWD=",

     CDatabase::noOdbcDialog);

    strcpy(Db.m_paramName, "Alexander");

    Db.ExecuteSQL("{CALL spCount(?,?)}");

    // Db.m_paramCount содержит результат!

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

    Вызов процедур, возвращающих наборы записей

    Процедуры, возвращающие наборы записей, также вызываются с помощью оператора CALL, но в функции CRecordset::Open. Соответственно, полученное множество записей будет связано с объектом класса CRecordset. Если у хранимой процедуры есть параметры, можно передать их напрямую или связать с ними переменные. Связывание переменных, в отличие от предыдущего случая, происходит в функции CRecordset::DoFieldExchange при помощи макросов RFX_* (то есть практически ни чем не отличается от связывания переменных с полями результирующего набора записей). Нужно только вызвать CFieldExchange::SetFieldType с параметром CFieldExchange::inputParam, чтобы сообщить MFC, что мы связываем параметры, а не поля. Важно также записать количество связываемых параметров в переменную CRecordset::m_nParams. Обычно это делается в конструкторе класса. 

    Рассмотрим пример вызова хранимой процедуры spGetByName, которая находит в таблице tPeople всех людей с заданным именем. В СУБД SQL Server такую процедуру можно создать, выполнив запрос: 

    CREATE PROC spGetByName(@paramName CHAR(50))

    AS SELECT * FROM tPeople WHERE Name=@paramName 

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

    class CPeople : public CRecordset {

    public:

     CPeople(CDatabase *pDatabase = NULL) : CRecordset(pDatabase) {

      m_nFields = 2, m_nParams = 1;

     };

     CString m_Name;

     Time m_DateOfBirth;

     String m_paramName;

     void DoFieldExchange(CFieldExchange *pFX);

    };


    void CPeople::DoFieldExchange(CFieldExchange *pFX) {

     pFX->SetFieldType(CFieldExchange::outputColumn);

     RFX_Text(pFX, "Name", m_Name, 50);

     RFX_Date(pFX, "DateOfBirth", m_DateOfBirth);

     pFX->SetFieldType(CFieldExchange::inputParam);

     RFX_Text(pFX, "paramName", m_paramName);

    }

    :

    CPeople Rs(&Db);

    Rs.m_paramName = "Alexander";

    Rs.Open(CRecordset::snapshot, "{CALL spGetByName(?)}");

    О чём ещё полезно знать

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

    Транзакции

    Транзакция – это блок команд, которые выполняются как единое целое. Другими словами, они либо выполняются все, либо не выполняется ни одна. Транзакция начинается вызовом CDatabase::BeginTrans и завершается вызовом CDatabase::CommitTrans. Все операции по изменению, добавлению и удалению данных вступят в силу только после вызова CommitTrans, причём в любой момент до вызова этой функции транзакцию можно полностью отменить, вызвав функцию CDatabase::Rollback. Используйте CDatabase::CanTransact, чтобы определить, поддерживает ли используемый вами драйвер транзакции.

    CRecordset и его потомки

    В первой части статьи мы рассмотрели, как использовать CRecordset, порождая от него новые классы. Возникает вопрос: а можно ли использовать этот класс напрямую? Ответ на этот вопрос звучит так: CRecordset может использоваться для доступа к множеству записей, построенному только на основе запроса (а не имени таблицы), и только в режиме read only. Обратиться к значениям конкретных полей в этом случае можно, используя функцию CRecordset::GetFieldValue. Функции CRecordset::Move* используются, как и раньше.

    Следующий фрагмент выводит фамилии всех авторов из БД pubs. Так как нам требуется доступ к таблице authors в режиме , мы можем использовать класс CRecordset напрямую.

    CRecordset Rs(&Db);

    Rs.Open(CRecordset::forwardOnly, "SELECT aau_lname FROM authors");

    while (!Rs.IsEOF()) {

     CString lname;

     Rs.GetFieldValue((short)0, lname);

     printf ("%s\n", lname);

     Rs.MoveNext();

    }

    Как обмануть IntelliSense

    Мы уже умеем конструировать объекты класса CRecordset, передавая конструктору указатель на соединение: 

    CRecordset Rs(&Db);
     

    Существует ещё одна эквивалентная форма создания объекта CRecordset: 

    CRecordset Rs;

    Rs.m_pDatabase = &Db;

    Зачем она может понадобиться, спросите вы. Дело в том, что система Microsoft IntelliSense, которая услужливо выдаёт вам списки членов класса и параметров функции прямо, очень болезненно реагирует на конструкторы с параметром: в коде, который следует за вызовом такого конструктора, подсказки попросту перестают появляться. Если вы столкнулись с такой проблемой, смело используйте второй вариант конструирования объекта CRecordset. 

    Любые замечания по форме и содержанию статьи вы можете прислать мне по адресу rudankort@mail.ru.

    ВОПРОС-ОТВЕТ 

    Q. Есть у меня файлы с расширением .pdb (Microsoft C/C++ program database 2.00) (их MS VC++ делает в папке Debug проекта создаются), можно ли с их помощью восстановить исходники программы (размер у них такой, что туда не только прога влезет, но и комментарии к ней в HTML (FrontPage Style) формате :)

    (Andrey Shtukaturov )

    A. Восстановить исходники не удастся, так как их там нет. Зато есть имена классов и глобальных переменных.

    Этого вполне достаточно для того чтобы восстановить недокументированный интерфейс COM-объекта.

    Дело в том, что согласно реализации COM'а для C++, имена функций членов класса реализующиго интерфейс должны полностью совпадать с именами исходного интерфейса. Плюс свои какие-то матоды. Обычно они не виртуальные так что легко отсекаются.

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

    И собственные методы класса реализующего интерфейс тоже отсекаются.

    Для не COM-объектов .pdb файлы тоже могут быть полезны.

    Если, например, в перечне экспортируемых из ImgUtil.dll функций содержится скупое "DecodeImage", то в .pdb файле честно написано, что это "_DecodeImage@12", т.е. уже извесно количество параметров. Это для функций описанных как extern "C". Для функций C++ в .pdb файле будет полное задекорированное имя.

    Типа "?DecodeImage@@YAJPAVISniffStream@@PAVIMapMIMEToCLSID@@PAVIImageDecodeEventSink@@@Z"

    Что после пропускания через утилиту UndName из набора утилит поставляемого MS с PlatformSDK выглядит как "long cdecl DecodeImage(class ISniffStream *, class IMapMIMEToCLSID *, class IImageDecodeEventSink *)".

    Более чем достаточно для восстановления не целиком исходников, но хоть декларации функций.

    (Paul Bludov )
    В ПОИСКАХ ИСТИНЫ 

    Q. Насколько корректно будут работать методы контроля утечек памяти (в частности объект CMemoryState) в многопоточных приложениях? 

    У меня сложилось впечатление, что объект CMemoryState не делает различия в каком потоке вызывались операторы new с момента обращения к memState.Checkpoint() до обращения к memState.DumpAllObjectsSince(). 

    Видимо "моментальные снимки" распределённой памяти в данном случае не информативны, ведь несколько потоков работают в одном адресном пространстве?

    (Николай Турпитко )

    Это все на сегодня. Успехов! 

    (Алекс Jenter jenter@mail.ru) (Красноярск, 2001.)







    Главная | В избранное | Наш E-MAIL | Прислать материал | Нашёл ошибку | Наверх