Использование ODBC в Visual C++

ОГЛАВЛЕНИЕ

Класс CDatabase представляет собой класс, который обеспечивает связь с источником данных. Под источником данных может пониматься как непосредсвенно файл, в котором находится таблица, например dBase, так и файл с многими таблицами, например Microsoft Access или сервер баз данных Oracle, MS SQL Server и т.д. Для связи с источником данных используется интерфейс ODBC. У данного класса есть папа в виде класса CObject.

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

Для работы необходимо включить описание функций, которые находятся в файле afxdb.h:

// пример
#include "afxdb.h"

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

// Описание консруктора CDatabase();
// пример

CDatabase cdbMyDB();

У данного класса есть всего один член данных. Это m_hdbc. Он указывает на текущее соединение ODBC. Имеет смысл только если оно установленно. Эта переменная имеет тип HDBC

Для установки соединения Вам необходимо вызвать функцию Open. Вот её описание.

virtual BOOL Open( LPCTSTR lpszDSN, BOOL bExclusive = FALSE,
BOOL bReadOnly = FALSE, LPCTSTR lpszConnect = "ODBC;",
BOOL bUseCursorLib = TRUE );
throw( CDBException, CMemoryException );

Как видите все параметры кроме первого не устанавливать. Первый параметр lpszDSN указывает на имя DNS для связи с источником данных. Эти имена находятся в настройке ODBC в панели управления на вкладке File DNS. Это имя можно установить в NULL, и тогда при выполнении программы Вам предложат выбрать источник. Данная ситуация изображена на рисунке ниже:

//  Пример
void CDatebaseDlg::OnOpen()
{
CDatabase cdbMyDB;
cdbMyDB.Open(NULL);
}

Если вы укажите имя и оно будет ошибочно, то получите сообщение о ошибке, как на рисунке ниже:

/ Пример

void CDatebaseDlg::OnOpen() 
{
CDatabase cdbMyDB;
cdbMyDB.Open("Bad date source");
}

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

// Пример
CDatabase cdbMyDB;
try
{
cdbMyDB.Open("Bad date source");
}
catch(...)
{
AfxMessageBox("Error Open DNS");
}

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

// Пример
void CDatebaseDlg::OnOpen()
{
CDatabase cdbMyDB;
try
{
cdbMyDB.Open("MS Access 97 Database");
}
catch(...)
{
AfxMessageBox("Error Open DNS");
}
}

В случае, если с источником данных файл связан вы ничего не увидите. Но установку связи молжно проверить вызвав функцию IsOpen, которая вернет TRUE в случае успеха. Эта же функция поможет Вам при работе и ответит на вопрос - Связь установлена или нет. Например вы захотите испоьзовать один класс CDatabase для множества разных соединений.

// Описание BOOL IsOpen( ) const;
// Пример

void CDatebaseDlg::OnOpen()
{
CDatabase cdbMyDB;
try
{
cdbMyDB.Open("MS Access 97 Database");
if (cdbMyDB.IsOpen())
AfxMessageBox("Open Base");
else
AfxMessageBox("Not Open");
}
catch(...)
{
AfxMessageBox("Error Open DNS");
}
}

По завершению работы источник данных необходимо закрыть используя функцию Close.

// Описание virtual void Close( );
// Пример

void CDatebaseDlg::OnOpen()
{
CDatabase cdbMyDB;
try
{
cdbMyDB.Open("MS Access 97 Database");
if (cdbMyDB.IsOpen())
AfxMessageBox("Ok Open Base");
else
AfxMessageBox("Not Open");
cdbMyDB.Close();
}
catch(...)
{
AfxMessageBox("Error Open DNS");
}
}

Ну на сегодня всё.

Если вы хотите попробовать, то создайте с помощью AppWizard приложение диалоговое окно с одной кнокой и к этой кнопке привяжите событие нажатия. Я так и делал, когда писал пример.


 

Продолжаем устанавливать соединение

Для работы с данным примером нам необходимо установить связь ODBC драйвера с файлом. Это очень удобно. Если у Вас есть файл на сетевом диске, то при изменении его местоположения необходиом только изменить путь в ODBC и программе. Так же и при установке нового рабочего место настраиваете доступ и программа работает.

Настройка происходит по пути - Мой компьютер -> Панель управления -> ODBC.

Настройка связи драйвера с файлом происходит на вкладке File DNS. Здесь нам нужно выбрать пункт меню Configure.

Если файл выбран, то Вы увидите запись в разделе Database, если нет, выберите пунк Select и выберите файл. Дальнейшее описание подразумевает, что файл выбран.

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

Ниже процедура открытия с новым параметром и обработкой ошибок.

CDatabase cdbMyDB;
try
{
cdbMyDB.Open("MS Access 97 Database",TRUE);
// Если TRUE не указать то по умолчанию FALSE
}
catch(CDBException cdbThrow)
{
AfxMessageBox(cdbThrow.m_strError);
}

Функция Open итмеет второй параметр который говорит и типе открытия со стороны доступа. Если здесь установлено TRUE то после открытия программой данной базы данных доступ к ней для других программ будет запрещен. А если другая программа открыла раньше ? Тогла перед вами извеняться в виде исключительной ситуации :-). 

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

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

Данный тип доступа применим часто. Например 1C бухгалтерия использует его для востановления работоспособности таблиц. Он нужен если с Вашей базой одновременно могут работать несколько человек. Так как базы данных должны быть нормализованны, и в них выделяются таблицы с классфикаторами, то в момент изменения таблиц классификаторов доступ к ним должне быть запрешен. Вот пример. У Вас поменялся материально отвественный на складе. Есть таблица со списком данных людей. Вы перед внесением изменений должны заблокировать данную таблицу, дабы фамилия несушествуюшего человека не попала в расходные ведомости. У других остановится работа, но что сделаешь - OnLine :-).

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

Открывать базу данных можно и так.

cdbMyDB.Open("MS Access 97 Database",TRUE,TRUE);
// Если TRUE не указать то по умолчанию FALSE

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

На базе данных может стоять пароль. Вот так это делается в ACCESS.

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

cdbMyDB.Open("MS Access 97 Database",TRUE,TRUE,"ODBC;UID=ADMIN;PWD=123");

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

ODBC;		// Тип соединения. Создан для дальнейшего использования,
// если будете пользоваться не ODBC

UID=ADMIN; // Имя пользователя
PWD=123 // Пароль

Этот набор может меняться от одной базы данных к другой. Например при связи с MS SQL есть дополнительные параметры, как HOST.

И последний возможный параметр:

cdbMyDB.Open("MS Access 97 Database",TRUE,TRUE,"ODBC;UID=ADMIN;PWD=123",TRUE);

Этот параметр указывает надо загружать или нет библиотеку курсоров. Библиотека кусоров улучшает функциональность ODBC, но нужна она не всегда.

 

Информация о соединении

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

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

// BOOL IsOpen( ) const;
//......

CDatabase cdbMyDB;
cdbMyDB.Open("MS Access 97 Database");
if (cdbMyDB.IsOpen())
{
//......
}
//......

При установки связи в прошлый раз мы с Вами передавали строку соединения, но использовали часть параметров. Функция GetConnect() возвращает строку CString, в которой перечислены все параметры используемые при установке соединения.

// const CString& GetConnect();
if (cdbMyDB.IsOpen())
{
AfxMessageBox(cdbMyDB.GetConnect());
}

У меня появляется вот такая строка. Чать параметров в строке соединения я не упоминал, а они присутсвуют так как заданы по умолчанию. И если Вы хотите посмотреть парамтры соединения с конкретным сервером, то используйте этот код для получения информации.

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

// CString GetDatabaseName();
cdbMyDB.Open("MS Access 97 Database");
if (cdbMyDB.IsOpen())
{
AfxMessageBox(cdbMyDB.GetDatabaseName());
}

Сейчас Вы должны возмутиться. Как так, зачем нам знать имя, мы же его задаем. А вот нет. Мы задаем универсальное имя, псевдоним по которому не известно, что и где раположенно. А вот именно эта функция позволяет нам на основе псевдонима узнать настоящее лицо - т.е. где и какая конкретно база данных открыта.

Проверим можно ли вносить в базу данных изменения. То есть убедимся, что открыли в режиме отличном от Read Only. Для этого воспользуемся функцией CanUpdate.

// BOOL CanUpdate();
cdbMyDB.Open("MS Access 97 Database");
if (cdbMyDB.IsOpen())
{
if (cdbMyDB.CanUpdate()) AfxMessageBox("Update Yes");
}

И проверим возможность использования транзакций используя функцию CanTransact(). Не все драйвера ODBC имеют данную возможность.

// BOOL CanTransact();
if (cdbMyDB.IsOpen())
{
if (cdbMyDB.CanTransact()) AfxMessageBox("Transact Yes");
}

 

CRecordset

Recordset - это источник строк. Этот обьект можно рассматривать как оболочку для строк или колонок таблицы. То есть этот обьект может включать, как всю таблицу, так и часть её на основе SQL запроса. В файле базы данных может храниться не одна таблица. Базы данных для этого и предусмотрены. Только старые версии персональных баз данных типа FoxPro хранят каждую таблицу в отдельном файле.

Итак для создания обьекта нам необходимо обьявить переменную. Обьект создается на основе конструктора. Вот описание конструктора.

CRecordset( CDatabase* pDatabase = NULL);

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

Работа с классом CDatabase расматривалась в предыдущих главах. Будем cчитать, что мы создали и открыли этот обьект.

Использование конструктора.

if (cdbMyDB.IsOpen()) 
{
CRecordset cr(&cdbMyDB);
}

Или вот так например:

if (cdbMyDB.IsOpen()) 
{
CRecordset cr;
cr.m_pDatabase=&cdbMyDB;
}

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

CDatabase* m_pDatabase;

Имея созданный объект CRecordset Вы можете теперь открыть таблицу на основе SQL запроса.

cr.Open(CRecordset::forwardOnly,"SELECT * FROM TABLE1;",CRecordset::readOnly); 

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

CDBException
CMemoryException;

Расшифровка этих исключительных ситуаций позволит Вам получить информацию о произошедшей ошибке. Только не забудьте при подобных испытаниях с исключительными ситуациями переключаться на тип проекта Release, дабы не нарываться на злобный ASSERT. Смотрите пример ниже. Таблицы TABLE21 у меня нет.

CRecordset cr;
cr.m_pDatabase=&cdbMyDB;
try
{
cr.Open(CRecordset::forwardOnly,"SELECT * FROM TABLE21;",
CRecordset::readOnly);
cr.Close();
}
catch(CDBException cdb)
{
AfxMessageBox(cdb.m_strStateNativeOrigin);
}

Класс CRecordset имеет функцию IsOpen она вам знакома из класса CDatabase:

if (cr.IsOpen()) ......;

 

Знакомство с SQL

Для построения объекта CRecordset необходим параметр в виде SQL запроса к БД.

SQL - Structured Query Language. По русски это язык построения запросов. Его появление вызвано переходом от настольных СУБД типа FoxPro, Paradox и т.д. к системам управления базами данных. Эти системы сформировались в понятие сервера баз данных. Язык SQL является стандартом для реляционных баз данных и его поддерживают практически все систему управления данными. Но к спецификации SQL очень часто производители конкретной БД добавляют свои расширения.

Если кратко, то системы управления данными стремятся к модели клиент - сервер. Сервер - это мощное программное обеспечение работающее обычно на неменее можной аппаратной части, а клиент это программное обеспечение, которое мы с вами разрабатываем. При этом подходы могут быть разные. Например каждому клиенту сервер, или один сервер (часто мультипроцессорный) для всех. Но самое интересное, что нас это не очень волнует. Использование ODBC и SQL позволяют создать приложения не особенно зависимые от сервера баз данных, где хранятся наши таблицы, запросы и т.д. Конечно иcпользуя только стандарты, наше приложение будет работать на многих платормах, но в замен мы не сможем получить максимум производительности. Так вот мое мнение такое. Чем оптимизировать программу, дешевле увеличить оперативную память в два раза на сервере и получить совместимость.

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

Запрос SQL состоит из одного или нескольких операторов SQL разделенных точкой с запятой. Самые важные операторы - описание стандартов ANSI/ISO SQL.

В прошлом шаге мы использовали оператор SQL для создания обьекта CRecordset.

cr.Open(CRecordset::forwardOnly,"SELECT * FROM TABLE1;",CRecordset::readOnly); 

Мы использовали оператор Select. Этот оператор позволяет получить данные. Ниже приведена расшифровка.

SELECT	// выбери
* // все колонки таблицы
FROM // для таблицы
TABLE1; // имя таблицы

Данный оператор выберет всю таблицу. Но мы с Вами можем этим оператором ограничить диапазон выбора. Например:

SELECT TABLE1.Family FROM TABLE1;

Выбирает только столбец Family в таблице TABLE1.

Если необходимо выбрать несколько столбцов, то они должны быть разделены запятой:

SELECT TABLE1.Family, TABLE1.ID FROM TABLE1;

Два столбца Family и ID.

Используя WHERE и логические операции AND,OR можно составить довольно сложное условие.

SELECT TABLE1.Family, TABLE1.ID FROM TABLE1 WHERE (((TABLE1.Family)="Petrov") AND ((TABLE1.ID)=1));

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

SELECT TABLE1.Family, TABLE1.ID, TABLE2.ID_PROF FROM TABLE1, TABLE2;

Пришло время выводов. Для создания обьекта CRecordse на основе запроса SQL только те данные, с которыми вы будете работать. Тем самым перенеся тяжесть обработки данных на сервер и съэкономив системные ресурсы на клиенте.

Описание оператора SELECT далеко не полное :-)

 

Продолжаем открывать Recordset

Вот полное описание функции Open:

virtual BOOL Open( UINT nOpenType = AFX_DB_USE_DEFAULT_TYPE,
LPCTSTR lpszSQL = NULL, DWORD dwOptions = none );
throw( CDBException, CMemoryException );

Как видите при открытии набора записей есть несколько типов:

CRecordset::dynaset   
CRecordset::snapshot
CRecordset::dynamic
CRecordset::forwardOnly

snapshot - набор записей типа моментального снимка (Snapshot-type Recordset). Набор записей статического типа или, другими словами, типа моментального снимка (Snapshot-type Recordset) содержит копию данных, которую нельзя изменять. Этот тип набора записей удобно использовать для поиска записи, удовлетворяющей какому-либо критерию, или при генерации отчетов. Следует помнить, что при использовании этого набора записей содержимое всех полей заносится в память, что может потребовать значительных ресурсов памяти.

Смотрим как это сделать:

void CDatebaseDlg::OnOpen() 
{
CDatabase cdbMyDB;
cdbMyDB.OpenEx("DSN=123");
if (cdbMyDB.IsOpen())
{
CRecordset cr(&cdbMyDB);
try
{
cr.Open(CRecordset::snapshot,
"SELECT Family FROM TABLE1", CRecordset::readOnly );

if (cr.IsOpen()) AfxMessageBox("Open");
cr.Close();
}
catch(CDBException cdb)
{
AfxMessageBox(cdb.m_strStateNativeOrigin);
}
}
else AfxMessageBox("Not Open");
cdbMyDB.Close();
}

Для связи с базой данных мы применили новую функцию OpenEx. Она сразу открывает и инициализирует связь с базой данных и рекомендована к использованию. В ней я указал драйвер в формате DNS=name.

И выделенное жирным открытие набора строк. Этот набор строк можно проверить открытием небезызвестной IsOpеn. Этот код защищен от ошибок. 

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

Этот код можно сократить и он будет работать !!!

void CDatebaseDlg::OnOpen() 
{
CRecordset cr(NULL);
try
{
cr.Open(CRecordset::snapshot,
"SELECT Family FROM TABLE1", CRecordset::readOnly );
if (cr.IsOpen()) AfxMessageBox("Open");
cr.Close();
}
catch(CDBException cdb)
{
AfxMessageBox(cdb.m_strStateNativeOrigin);
}
}

Как видите, в указание драйвера я поставил NULL.

 

Изучаем CRecordset

Итак, мы можем получить обьект CRecordset, например, как ниже. Теперь пора изучить его свойства. Открытие сильно упрощено.

void CDatebaseDlg::OnOpen() 
{
CRecordset cr(NULL);
try
{
cr.Open(CRecordset::snapshot,
"SELECT Family,Count FROM TABLE1", CRecordset::readOnly );
//........
cr.Close();
}
catch(CDBException cdb)
{
AfxMessageBox(cdb.m_strStateNativeOrigin);
}
}

Нам может понадобиться SQL, на основе которого создан данный источник строк:

// Описание  const CString& GetSQL( ) const;
//......

cr.Open(CRecordset::snapshot,"SELECT Family,Count FROM TABLE1",CRecordset::readOnly);
AfxMessageBox(cr.GetSQL());
//......

Теперь надо проанализировать сколько столбцов нам вернулось. Вы можете сказать: "ведь мы знаем, что два из запроса SQL", но запрос можно построить и так, что это будет не факт. Смотрите SELECT. Получаем количество столбцов:

// Описание short GetODBCFieldCount( ) const;
short nFields = cr.GetODBCFieldCount();

Используя количество столбцов можно получить о них информацию с помощью GetODBCFieldInfo():

// Описание void GetODBCFieldInfo( short nIndex,
// CODBCFieldInfo& fieldinfo ); throw( CDBException );


short nFields = cr.GetODBCFieldCount();
for (short x=0;x < nFields;x++)
{
CODBCFieldInfo fieldinfo;
short pos=x;
cr.GetODBCFieldInfo(pos,fieldinfo );
AfxMessageBox(fieldinfo.m_strName);
}

Информация о типе полей находится в струтуре CODBCFieldInfo():

struct CODBCFieldInfo
{
CString m_strName;
SWORD m_nSQLType;
UDWORD m_nPrecision;
SWORD m_nScale;
SWORD m_nNullability;
};

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

Второе поле m_nSQLType говорит нам о типе данных в данной колонке. Вот описание типов. Вы, наверно, заметили, что я перечислил не все возможные типы, например, нет OLE.

#define	SQL_UNKNOWN_TYPE 0
#define SQL_CHAR 1
#define SQL_NUMERIC 2
#define SQL_DECIMAL 3
#define SQL_INTEGER 4
#define SQL_SMALLINT 5
#define SQL_FLOAT 6
#define SQL_REAL 7
#define SQL_DOUBLE 8
#define SQL_DATETIME 9
#define SQL_VARCHAR 12

Пример ниже показывает как этим можно воспользоваться. Он просматривает все колонки в поисках поля типа Integer и при нахождении выдает о нем информацию:

for (short x=0;x < nFields;x++)
{
CODBCFieldInfo fieldinfo;
short pos=x;
cr.GetODBCFieldInfo(pos,fieldinfo );
if (fieldinfo.m_nSQLType==SQL_INTEGER)
AfxMessageBox("integer " + fieldinfo.m_strName);
}

Поле структуры m_nPrecision соотвествует в ACCESS ширине поля. 

Поле Scale говорит о том, сколько знаков после запятой у числового поля, а m_nNullability отвечает за то, может ли это поле принимать NULL. И для этого данное поле необходимо сравнить с SQL_NULLABLE или с SQL_NO_NULLS.

 

Записи в CRecordset

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

void CDatebaseDlg::OnOpen() 
{
CRecordset cr(NULL);
try
{
cr.Open(CRecordset::snapshot, "SELECT Family,Count FROM TABLE1",
CRecordset::readOnly );
CDBVariant var;
short index=0;
cr.Move(0);
while (!cr.IsEOF())
{
cr.GetFieldValue(index,var);
AfxMessageBox(*var.m_pstring);
cr.MoveNext();
}
cr.Close();
}
catch(CDBException cdb)
{
AfxMessageBox(cdb.m_strStateNativeOrigin);
}
}

Переменная типа CDBVariant принимает значение поля и может принять любое значение. Описание этой переменной. Только это класс, а не переменная :-)

// Указатели на данные в этом классе
m_dwType
m_boolVal
m_chVal
m_iVal
m_lVal
m_fltVal
m_dblVal
m_pdate
m_pstring
m_pbinary

Конструктор этого класса не имеет параметров, и поэтому в коде я так спокойно объявил объект класса. Команда Move перемещает указатель на заданную позицию. Я ставлю 0 и перемещаю в первую позицию. После этого задаю цикл, проверяя с помощью IsEOF достижение конца выборки (источника строк).

Функция GetFieldValue данные из указанного столбца в переменную. Эта функция перегруженная и может иметь несколько возможных параметров. Я применил этот вариант.

void GetFieldValue( short nIndex, CDBVariant& varValue,
short nFieldType = DEFAULT_FIELD_TYPE );
throw( CDBException, CMemoryException );

Зная, что в этом столбце находится строка, я взял соотвествующий ей указатель m_pstring. Проверить это можно запустив в режиме отладки до строки AfxMessageBox.

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

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

При использовании обьекта CRecordset возникает необходимость подсчитать сумму в поле. Возникает желание пробежаться по всем полям с помощью MoveNext и произвести подсчет. Это можно. На всякий случай задумайтесь о другом варианте. В примере ниже я подсчитаю сумму на основе оператора SQL SELECT:

void int_AfxMessageBox(int i)
{
char t[10];
itoa(i,t,10);
AfxMessageBox(t);
}

void CDatebaseDlg::OnOpen()
{
CRecordset cr(NULL);
try
{
// Внимание тестировалось на ACCESS 97
cr.Open(CRecordset::snapshot,
"SELECT Sum(TABLE1.Count) AS SumCount FROM TABLE1",
CRecordset::readOnly );
cr.Move(0);
CDBVariant var;
short index=0;
cr.GetFieldValue(index,var);
double count=var.m_dblVal;
int_AfxMessageBox((int)count);
cr.Close();
}
catch(CDBException cdb)
{
AfxMessageBox(cdb.m_strStateNativeOrigin);
}
}

И простите меня за столь лобовое приведение типа, дабы показать результат. Неохото мне было использовать перевод double - char, да и данные в моей таблице в сумме дают 332 :-). Если отвлечься от этих мелочей, то я построил SQL оператор, который считает сумму в колонке. Ну и что здесь такого ? Конечно ничего, если записей мало. А вот если их много (скажем тысячи) и эту операцию сделает сервер с парочкой процессоров Pentium II и передаст это на Ваш клиент на базе Pentium 133 эффект будет потрясающий.

 

Фильтры, сортировка CRecordset

Если тщательно разобраться с SQL оператором SELECT, то с помощью него можно получить практически любые виды от исходной таблицы. Но этот способ не единственный. Даже если с помощью данного оператора Вы отсортировали записи, всегда возникает необходимость допустим их отсортировать по полю или наложить фильтр. Это можно сделать и на основе доступных переменных класса CRecordset.

Для накладывания фильтра в классе CRecordset есть переменная m_strFilter. В эту переменную можно поместить фильтр и вызвать функцию обновления источника строк.

void CDatebaseDlg::OnOpen() 
{
CRecordset cr(NULL);
try
{
cr.Open(CRecordset::dynaset, "SELECT * FROM TABLE1");
cr.m_strFilter ="Count<100";
cr.Requery();
CDBVariant var;
short index=1;
cr.Move(0);
cr.GetFieldValue(index,var);
AfxMessageBox(*var.m_pstring);
cr.Close();
}
catch(CDBException cdb)
{
AfxMessageBox(cdb.m_strStateNativeOrigin);
}
}

Обратите внимание, что я обьявил таблицу как dynaset, что позволяет проводить подобные операции, в будущем эта опция разрешит добавлять, редактировать и удалять записи.

В фильтр я поместил строку, которая указывает, что поле должно быть меньше 100. Если вы запустите, то увидите фамилию Vasilev, так как он единственный, который имеет это поле меньше 100. После задания фильтра я вызвал функцию Requery, которая обновила источник строк на основе фильтра. Не забывайте её использовать.

Для сортировки тоже есть переменная и она носит имя m_strSort, в ней молжно указать поле ,по которому будет произведена сортировка.

void CDatebaseDlg::OnOpen() 
{
CRecordset cr(NULL);
try
{
cr.Open(CRecordset::dynaset, "SELECT * FROM TABLE1");
cr.m_strSort="Family";
cr.Requery();
CDBVariant var;
short index=1;
cr.Move(0);
cr.GetFieldValue(index,var);
AfxMessageBox(*var.m_pstring);
cr.Close();
}
catch(CDBException cdb)
{
AfxMessageBox(cdb.m_strStateNativeOrigin);
}
}

При работе этого кода первым будет Artem, хотя в исходной таблице он последний :-(. Логика точно такая же, как и выше в примере. Установка поля сортировки и обновление.

 

Понимание RFX

RFX - Record Field Exchange. Это механизм обмена данными между классом потомком от CRecordset и самой базой данных. Работа этого механизма по смыслу аналогична работе DDX. Данный механизм применяет ClassWizard при автоматическом создании класса CRecordset.

Для работы с RFX нам необходимо создать сына от класса CRecordset на основе известной структуры базы данных. Я создал в файле ACCESS новую таблицу TABLE3 с одним текстовых полем, и поместил туда две записи. Итак нам известна структура - одна текстовая колонка (не являющаяся ключевым полем). Эта текстовая колонка.

class MyCrec:public CRecordset
{
public:
MyCrec( CDatabase* pDatabase = NULL);
virtual void DoFieldExchange(CFieldExchange* pFX);
CString m_Fam;
};

Как видите ничего необычного нет. Простое наследование, перегрузка конструктора, обьявление функции DoFieldExchange для организации механизма обмена и переменной типа CString для соответствия формату поля колонки из таблицы базы данных. Я не сделал эту переменную private, но Вы можете это делать спокойно.

Реализация конструктора. Просто и ясно :-).

MyCrec::MyCrec( CDatabase* pDatabase)
:CRecordset(pDatabase)
{
}

Функция обмена:

void MyCrec::DoFieldExchange(CFieldExchange* pFX)
{
pFX->SetFieldType(CFieldExchange::outputColumn);
RFX_Text(pFX,_T("Famili"),m_Fam);
}

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

Опять сильно упрошено. Эта функция позволяет Вам произвести обмен между переменной типа CString и текущей строкой в конкретной колонке базы данных. У меня колонка называется Famili. Обратите внимание на то, что строка помещена в конструкцию _T(...), это сделано для гарантии создания объекта типа CString. А вот ниже полное описание этой функции.

void RFX_Text( CFieldExchange* pFX, const char* szName,
CString& value, int nMaxLength = 255, int nColumnType = SQL_VARCHAR,
short nScale = 0 );

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

void CDatebaseDlg::OnOpen() 
{
MyCrec cr(NULL);
try
{
cr.m_nFields=1;
cr.Open(CRecordset::dynaset, "SELECT * FROM TABLE3");
if(cr.CanAppend())
{
cr.AddNew();
}
cr.m_Fam="Kaev";
cr.Update();
cr.Close();
}
catch(CDBException cdb)
{
AfxMessageBox(cdb.m_strStateNativeOrigin);
}
}

Итак, объявляем объект от нашего класса. Устанавливаем количество колонок в m_nFields. Открываем набор записей, как динамический (т.е. в который можно вносить измения) - dynaset. CanAppend проверяет возможность добавления записей. Запись добавляется пустая. Вводим в переменную содержание и заносим данные непосредственно в базу данных Update.

 

Как Колю сделать Васей?

Добалять записи мы научились. Но как оказывается, кто бы мог подумать, их необходимо еще и редактировать. Это являение вызвано несовершенством нашего мира. Например жена может поменяться :-)

Смотрим код:

void CDatebaseDlg::OnOpen() 
{
MyCrec cr(NULL);
try
{
cr.m_nFields=1;
cr.Open(CRecordset::dynaset, "SELECT * FROM TABLE3");
cr.Move(0);
cr.Edit();
AfxMessageBox(cr.m_Fam);
cr.m_Fam="Yaci";
cr.Update();
cr.Close();
}
catch(CDBException cdb)
{
AfxMessageBox(cdb.m_strStateNativeOrigin);
}
}

Первой камандой мы перемещаемся к первой записи (0). Это всегда возмущало меня. Если запись первая, то и должна начинаться с 1, а не 0. Отсюда столько ошибок !!! Но факт.

Функцией Edit мы разрешаем начать редактировать запись. Эта функция вызовет механизм RFX дабы уведомить о том, что мы будет редактировать. Это нормально. И хорошо, а не так как некоторые начальники, правят еще не прочитав. Соответственно мы можем увидеть, что изменим. Для этого и используется функция AfxMessageBox.

Теперь мы присваиваем новое значение переменной и вызываем Update, дабы закрепить изменение.

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

void CDatebaseDlg::OnOpen() 
{
MyCrec cr(NULL);
try
{
cr.m_nFields=1;
cr.Open(CRecordset::dynaset, "SELECT * FROM TABLE3");
cr.Move(1);
cr.Delete();
cr.Close();
}
catch(CDBException cdb)
{
AfxMessageBox(cdb.m_strStateNativeOrigin);
}
}

Просто до ужаса. Открываем CRecordset, переходим на запись, удаляем, закрываем. Что здесь можно написать еще?

 

Общий обзор ODBC

ODBC - Open Database Connectivity это интерфейс доступа к базам данных в среде Windows. Для доступа используются специальные драйвера подобно тому как используются драйвера принтера. Какую бы внутри сложно не был устроен принтер команды обращения к нему и печати одинаковы и каждая программа может печатать на принтере. Разработчик любой базы данных может создать свой драйвер ODBC.

ODBC реализует интерфейс доступа к разным SQL совместимым базам данных.

    Клиент
|
ODBC
|
Диспечер драйверов ODCB
|
драйвер БД
|
БД

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

API функции одинаковые и не зависят от поставщика

SQL операторы могут быть сгенерированы на любой стадии при компиляции или выполнении.

Данные принимаются в программу в едином формате.

Для работы с источниками данных используют имена DNS - Data Source Name - именованный источник данных ODBC. Диспетчер использует информацию связанную с именем для доступа к БД. С именем связана следующая информация:

  • Местонахождение
  • Тип драйвера
  • Другие обязательные параметры

Существует три типа имен DNS:

  • Пользовательский
  • Системный
  • Файловый

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

Драйвера могут поддерживать три уровня грамматики SQL минимальная грамматика поддерживается всеми драйверами:

  • Минимальная - Create Table, Drop Dtable Select, Insert,Update Dearch, Delete Search Char,VarChar или Long VarChar
  • Основная - Минимальная + Alter Table, Create Index, Drop Index, Create View, DropView, Grant, Revoke Select (полная) Decimal,Numeric,SmallInt,Integer,Real,Float,Double Precision
  • Расширенная - Основная + внешние соединения, поддержка позицирования, Bit,TinyInt, BigInt, Binary, VarVariant, Long, VarBinary, Date, Time, TimeStamp пакетные операторы SQL, вызов процедур

 

Где хранятся настройки об источнике данных ODBC?

В предыдущих шагах "Установка соединения" и "Продолжаем устанавливать соединение" мы с Вами пользовались утилитой Администратор ODBC для настройки соединения. Мы его находили в панели управления. На самом деле это обычная программа Odbcad32.exe, которая находится по определенному пути. Тo есть ее можно напрямую запускать из программы и требовать, чтобы пользователь установил данные. Но есть и другой выход. Данные о на строке источников ODBC хранятся в реестре в разделе SOFTWARE/ODBC.

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

HKEY_CURRENT_USER\SOFTWARE\ODBC\ODBC.INI\

Еще один способ программно воздействовать на настройки драйверов ODBC - это использовать функцию SQLConfigDataSource().

Итак мы может настраивать ODBC следующими методами:

  • Утилита администрирования.
  • Реестр.
  • Функция SQLConfigDataSource().
 

Функция SQLConfigDataSource()

Эта функция имеет следующий вид:

BOOL SQLConfigDataSource
(
HWND hwndParent, // Указатель на окно вызвавшее функцию
WORD fRequest, // Тип запроса
LPCSTR lpszDriver, // Пользовательское имя драйвера
LPCSTR lpszAttributes // атрибуты
);

Функция ворачивает TRUE, если выполнена удачно и FALSE если не удачно. Атрибут описывает тип команды и может быть следующим:

  • ODBC_ADD_DSN - Добавляем новый источник данных пользователя.
  • ODBC_CONFIG_DSN - Выбор конфигурации существующего источника данных пользователя.
  • ODBC_REMOVE_DSN - Удаляем существующий источник данных пользователя.
  • ODBC_ADD_SYS_DSN - Добавляем новый источник данных системы.
  • ODBC_CONFIG_SYS_DSN - Изменяем существующий источник данных системы.
  • ODBC_REMOVE_SYS_DSN - Удаляем существующий источник данных системы.
  • ODBC_REMOVE_DEFAULT_DSN - Удаляем заданный по умолчанию раздел спецификации источника данных из системы

Давайте попробуем ???

// TestConfig.cpp : Defines the entry point for the console application.
//


#include "stdafx.h"
#include "windows.h"
#include "odbcinst.h"
#include "iostream.h"

void main()
{
if (!SQLConfigDataSource(NULL,ODBC_ADD_DSN,
"Microsoft Excel Driver (*.xls)",
"DSN=New Excel Data Source\0"
"Description=New Excel Data Source\0"
"FileType=Excel\0"
"DataDirectory=C:\\ExcelData\0"
"MaxScanRows=20\0"))
{
cout << "Error config ODBC" << endl;
}
}

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

 

Пишем в таблицы Excel

Давайте создадим консольное приложение WIN32 Console. Незабудьте поставить использование классов MFC:
// TestConfig.cpp : Defines the entry point for the console application.
//


#include "stdafx.h"
#include "afxwin.h"
#include "iostream.h"
#include "afxdb.h"

void main()
{
CDatabase cdbMyDB;
cdbMyDB.Open("New Excel Data Source");
cdbMyDB.ExecuteSQL("CREATE TABLE mydata (FirstName TEXT,LastName TEXT)");
cdbMyDB.ExecuteSQL("INSERT INTO mydata (FirstName,LastName) VALUES('Kaev','Artem');");
cdbMyDB.Close();
}

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

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

if (!SQLConfigDataSource(NULL,ODBC_ADD_DSN,
"Microsoft Excel Driver (*.xls)",
"DSN=New Excel Data Source\0"
"Description=New Excel Data Source\0"
"DriverId=278\0"
"DefaultDir=C:\\ExcelData\0"
"MaxScanRows=8\0"))
{
cout << "Error config ODBC" << endl;
}

Параметр DefaultDir устанавливает текущий каталог. DriverID - устанавливает версию драйвера. 


 

Выполнение функций ODBC API из класса CDatabase

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

CDatabase::m_hdbc

В этой переменной хранится дескриптор соединения. Этот дескриптор автоматически появляется после установки соединения воспользовавшись функцией Open() или OpenEx(). А вот уберется он только после удаления объекта CDatabase. ВНИМАНИЕ !!! Close() его не уберет. Итак, если функций MFC для работы с ODBC не хватает мы можем воспользоваться дескриптором соединения. А вот и пример из MSDN:

nRetcode = ::SQLGetInfo
(
m_db.m_hdbc,
SQL_ODBC_SQL_CONFORMANCE,
&nValue, sizeof( nValue ),
&cbValue
);

 

Поддержка транзакций

Класс CDatabase поддерживает использование транзакций. 

Драйвер ODBC может использовать транзакции. Это легко проверить. Функция:

BOOL CanTransact( ) const;

Позволяет Вам проверить поддерживает ли драйвер транзакции. Вам вернется 0, если да. Если транзакции поддерживаются, то перед операциями нам нужно вызвать функцию:

BOOL BeginTrans( );

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

BOOL CommitTrans( );

Либо отменить:

BOOL Rollback( );

Все операции по управлению транзакциями основываются на объекте СRecordset, который использует это соединение.

 

Настройка ODBC и текущий каталог

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

// TestMyDir.cpp : Defines the entry point for the console application.
//


#include "stdafx.h"
#include "afxwin.h"
#include "odbcinst.h"
#include "iostream.h"

#pragma comment(lib,"odbc32.lib")
#pragma comment(lib,"odbccp32.lib")

void main()
{
char buffer_path[100];
GetCurrentDirectory(sizeof(buffer_path),buffer_path);
CString path=buffer_path;
CString temp;
temp="DSN=New Excel Data Source;";
temp+="Description=New Excel Data Source;";
temp+="DriverId=278;";
temp+="DefaultDir=";
temp+= path;
temp+=";MaxScanRows=8;\0\0";
char *szAttributes=temp.GetBuffer(temp.GetLength());

if (!SQLConfigDataSource(NULL,ODBC_ADD_DSN,
"Microsoft Excel Driver (*.xls)",szAttributes))
{
cout << "Error config ODBC" << endl;
}
}