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

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

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

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

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

Программная модель OLE DB.. 2

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

Инициализация источника данных. 2

Открытие сеанса. 3

Подготовка команды и параметров. 3

Выполнение команды.. 3

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

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

Объекты OLE DB.. 4

Объект SQLOLEDB.. 4

Создание объекта. 4

Подготовка параметров инициализации. 5

Установка свойств. 8

Инициализация объекта. 9

Объект Session.. 9

Объект Command.. 10

Создание объекта. 10

Определение команды.. 10

Выполнение команды.. 11

Интерфейс IRowset и набор записей. 11

Получение описания набора записей. 12

Подготовка информации для привязки данных. 15

Выполнение привязки данных. 17

Обработка набора записей. 18

Программа OLEDB.. 20

Листинг 5-1 Вы найдете в файле ch5\oledb\oledb.cpp на прилагаемом к книге компакт-диске. 20

Глобальные определения. 20

Функция main.. 21

Функция init.. 22

Функция startCommand.. 22

Функция get_records.. 23

Использование библиотеки шаблонов ATL. 24

Классы для работы с источником данных OLE DB.. 24

Класс CDataSource. 24

Класс CSession. 25

Класс CCommand. 25

Программа ATLOLEDB.. 27

Листинг 5-2 хранится в файле ch5\atloledb\atloledb.cpp на прилагаемом к книге компакт-диске. 27

Глобальные определения. 27

Функция main.. 28

В предыдущей главе мы рассмотрели практические приемы использования объектного интерфейса ADO в серверных сценариях ASP и в автономных приложениях Microsoft Windows, написанных на языке программирования C++. Как Вы смогли убедиться, и в том, и в другом случае применяются достаточно эффективные методы обращения к базам данных. Эти методы, не вызывают особых затруднений при реализации и отладке. Интерфейс автоматизации, определенный в рамках объектов ADO, позволяет обращаться к этим объектам из приложений, составленных практически на любом языке программирования, в том числе из языков сценария (таких, как JScript и VB Script).

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

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

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

В рамках одной главы невозможно рассказать об объектах OLE DB хоть сколько-нибудь подробно, поэтому мы изложим только основы. Вы сможете применить полученные знания на практике, создавая, например, расширения сервера Web, обращающиеся к базам данных через OLE DB, в виде приложений CGI или ISAPI.

Программная модель OLE DB

Так же как и в случае только что рассмотренной объектной модели ADO, базовыми элементами программной модели OLE DB является набор объектов. Эти объекты применяются для установки соединения с базами данных и сеансов, выполнения команд с параметрами, получения результата выполнения этих команд в виде переменных или наборов записей, обработки событий и ошибок.

Рассмотрим порядок обращения приложения к базе данных с применением программной модели OLE DB.

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

Работа OLE DB основана на модели компонентных объектов COM, поэтому сразу после начала своей работы приложение должно выполнить инициализацию системы COM. Как правило, обычные приложения выполняют эту инициализацию вызовом функции CoInitialize.

В результате становится возможным загрузка объектов провайдера OLE DB и работа c этими объектами.

Инициализация источника данных

Прежде чем обращаться к данным, приложения OLE DB должно установить соединение с источником данных. Это действие требуется и при использовании метода доступа ADO, однако установка соединения с применением объектов OLE DB выполняется по-другому.

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

Во-вторых, приложение должно вызвать метод SetProperties интерфейса IDBProperties, выполняющий инициализацию указанных выше свойств. Интерфейс IDBProperties становится доступным после инициализации OLE DB.

И наконец, в-третьих, приложению необходимо вызвать метод Initialize интерфейса IDBInitialize, что обязательно для инициализации источника данных.

После завершения работы с соединением его надо закрыть, вызвав метод Uninitialize интерфейса IDBInitialize.

Открытие сеанса

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

Для открытия сеанса приложение использует метод CreateSession интерфейса IDBCreateSession.

Подготовка команды и параметров

При создании сеанса методом CreateSession интерфейса IDBCreateSession программа получает указатель на интерфейс IDBCreateCommand, позволяющий создавать команды.

Устанавливая методом SetProperties интерфейса ICommandProperties различные атрибуты команды, программа влияет на ее исполнение.

Далее необходимо задать текст команды. Эта операция выполняется методом SetCommandText интерфейса ICommandText. Текст команды представляет собой строку языка Transact-SQL, имя хранимой процедуры SQL Server или имя таблицы.

При необходимости средствами метода Prepare интерфейса ICommandPrepare программа может выполнить предварительную подготовку команды. Эта операция имеет смысл, если команда представляет собой строку языка Transact-SQL (а не хранимую процедуру) и будет выполняться многократно.

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

Выполнение команды

Для выполнения команды программа должна вызвать метод Execute интерфейса ICommandText.

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

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

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

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

Прежде всего, Вам потребуется интерфейс IColumnsInfo. С его помощью программа получит информацию о столбцах набора данных. Интерфейс IRowsetInfo обеспечивает программу информацией о самом наборе записей. С помощью интерфейса IAccessor программа выполнит привязку данных полученной таблицы к переменным, определенным в программе. И наконец, интерфейс IRowset нужен для получения данных из строк набора записей.

Обычно при извлечении данных из набора записей приложение вначале вызывает метод CreateAccessor интерфейса IAccessor, выполняя привязку данных к переменным. Далее все записи набора извлекаются порциями в цикле с помощью метода GetNextRows интерфейса IRowset. А затем программа вызывает метод GetData интерфейса IRowset для получения данных из строк набора и записи этих данных в переменные, указанные в процессе привязки.

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

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

Ошибки могут возникать при создании многочисленных объектов OLE DB, поэтому Ваше приложение должно проверять код завершения соответствующих функций. Если метод какого-либо интерфейса завершился с ошибкой и вернул соответствующее значение, необходимо его проанализировать. При этом Вам потребуются интерфейсы ISupportErrorInfo, IErorInfo, IErrorLookup, IErrorRecords и ISQLErrorInfo. В таблице 5-1 кратко описаны перечисленные интерфейсы.

Таблица 5-1. Интерфейсы, связанные с обработкой ошибок

Интерфейс

Описание

ISupportErrorInfo

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

IErorInfo

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

IErrorLookup

Этот интерфейс обеспечивается провайдером OLE DB. Используется интерфейсами IErrorRecords и IErorInfo

IErrorRecords

Интерфейс для доступа к объектам, описывающим возникшие ошибки OLE DB

ISQLErrorInfo

Данный интерфейс нужен для получения значения SQLSTATE и естественного сообщения об ошибке. Обеспечивается провайдерами ODBC

Объекты OLE DB

Для работы с OLE DB Ваше приложение должно создать ряд объектов OLE DB, а затем обращаться к методам и свойствам этих объектов.

Ниже мы рассмотрим некоторые методы и свойства важнейших объектов OLE DB, необходимые для создания автономных приложений C++, выполняющих запросы к базам данных.

Объект SQLOLEDB

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

Создание объекта

Для создания этого объекта необходимо воспользоваться функцией CoCreateInstance, вызвав ее следующим образом:

IDBInitialize* pIDBInitialize = NULL;
HRESULT hr;
hr = CoCreateInstance(CLSID_MSDASQL, NULL, CLSCTX_INPROC_SERVER,
  IID_IDBInitialize, (void**)&pIDBInitialize);

Здесь через первый параметр мы передаем функции CoCreateInstance константу CLSID_MSDASQL, содержащую идентификатор класса SQLOLEDB.

Третий параметр функции CoCreateInstance, заданный с помощью макрокоманды CLSCTX_INPROC_SERVER, определяет, что источник данных SQLOLEDB работает как встраиваемый в процесс сервер (in-process server).

Четвертый и пятый параметры задают соответственно идентификатор интерфейса IDBInitialize и адрес переменной pIDBInitialize, в которую записывается указатель на интерфейс IDBInitialize.

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

После завершения работы с источником данных программа должна вызвать метод Uninitialize объекта SQLOLEDB, воспользовавшись для этого интерфейсом IDBInitialize:

pIDBInitialize->Uninitialize();

Далее программа освободит ненужный более указатель на интерфейс IDBInitialize, вызвав метод Release:

pIDBInitialize->Release();

Подготовка параметров инициализации

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

Структура DBPROP определена следующим образом:

typedef struct tagDBPROP
{
   DBPROPID        dwPropertyID;
   DBPROPOPTIONS     dwOptions;
   DBPROPSTATUS    dwStatus;
   DBID            colid;
   VARIANT         vValue;
} DBPROP;

Ее поля описаны в таблице 5-2.

Таблица 5-2. Поля структуры DBPROP

Поле

Описание

dwPropertyID

Идентификатор свойства, параметры которого описаны в данной структуре

dwOptions

Параметры свойства. В это поле Вы можете записать константы DBPROPOPTIONS_REQUIRED или DBPROPOPTIONS_OPTIONAL.

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

dwStatus

Результат записи или чтения свойства. Устанавливается провайдером и может использоваться для получения информации о том, успешно ли выполнилась эта операция

colid

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

vValue

Значение свойства (типа VARIANT).

Если прочитано свойство, значение которого не установлено и для которого не предусмотрено значения по умолчанию, в это поле записывается константа VT_EMPTY.

В том случае, когда Вы задали в этом поле константу VT_EMPTY

при установке свойства, для инициализации свойства используется значение по умолчанию

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

Таблица 5-3. Значения поля dwStatus

Значение

Описание

DBPROPSTATUS_OK

Значение свойства было установлено без ошибок

DBPROPSTATUS_BADCOLUMN

Было указано неправильное значение поля colid структуры DBPROP

DBPROPSTATUS_BADOPTION

Было указано неправильное значение поля dwOptions структуры DBPROP

DBPROPSTATUS_BADVALUE

Неправильное значение в поле vValue

DBPROPSTATUS_CONFLICTING

Значение свойства не может быть установлено, так как это приведет к конфликту с параметрами других свойств

DBPROPSTATUS_NOTALLSETTABLE

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

DBPROPSTATUS_NOTSET

Свойство не было установлено в указанное значение. Причина этого в том, что для параметра dwOptions указано значение в виде константы DBPROPOPTIONS_OPTIONAL, и при этом установка свойства в заданное значение невозможна

DBPROPSTATUS_NOTSETTABLE

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

DBPROPSTATUS_NOTSUPPORTED

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

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

В простейшем случае требуется задать четыре свойства:

·       уровень приглашения, определяющий, нужно ли выводить на экран приглашения для пользователя;

·       имя источника данных DSN;

·       имя пользователя;

·       пароль пользователя для доступа к источнику данных.

Для хранения этих свойств мы создаем массив rgInitProperties из четырех элементов:

DBPROP rgInitProperties[4];

Далее нужно выполнить инициализацию элементов массива rgInitProperties.

В поле dwPropertyID первого элемента массива (уровень приглашения) мы записываем константу DBPROP_INIT_PROMPT:

rgInitProperties[0].dwPropertyID = DBPROP_INIT_PROMPT;

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

Данное свойство имеет отношение ко всем столбцам, поэтому в поле colid мы указываем значение DB_NULLID:

rgInitProperties[0].colid = DB_NULLID;

Свойство, определяющее уровень приглашения, является обязательным (как и все остальные три свойства нашего массива). Поэтому в поле dwOptions указана константа DBPROPOPTIONS_REQUIRED:

rgInitProperties[0].dwOptions = DBPROPOPTIONS_REQUIRED;

Установка значения свойства выполняется в три приема:

VariantInit(&rgInitProperties[0].vValue);
rgInitProperties[0].vValue.vt = VT_I2;
rgInitProperties[0].vValue.iVal = DBPROMPT_NOPROMPT;    

Вначале инициализируется поле vValue, имеющее тип VARIANT, с помощью функции VariantInit. В результате такой инициализации в поле будет записано значение VT_EMPTY.

Далее мы указываем в поле vt тип данных как VT_I2 (двухбайтовое целое со знаком), а затем записываем в поле iVal константу DBPROMPT_NOPROMPT. Эта константа указывает, что в процессе инициализации пользователю не следует выводить на экран никаких приглашений.

Все возможные значения данного свойства перечислены в таблице 5-4.

Таблица 5-4. Значения свойства DBPROP_INIT_PROMPT

Значение

Описание

DBPROMPT_PROMPT

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

DBPROMPT_COMPLETE

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

DBPROMPT_COMPLETEREQUIRED

Аналогично предыдущему, но пользователю не разрешается вводить дополнительную информацию

DBPROMPT_NOPROMPT

Панель приглашения не отображается

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

rgInitProperties[1].dwPropertyID = DBPROP_INIT_DATASOURCE;

В поля dwOptions и colid этого и следующих двух элементов массива rgInitProperties мы записываем значения DBPROPOPTIONS_REQUIRED и DB_NULLID соответственно:

rgInitProperties[1].dwOptions = DBPROPOPTIONS_REQUIRED;
rgInitProperties[1].colid = DB_NULLID;

Что же касается собственно имени источника данных, то оно записывается в поле vValue как строка типа BSTR:

VariantInit(&rgInitProperties[1].vValue);
rgInitProperties[1].vValue.vt = VT_BSTR;
rgInitProperties[1].vValue.bstrVal =
  SysAllocString(OLESTR("BookStore"));

Аналогичным образом заполняются элементы массива rgInitProperties, задающие имя пользователя и его пароль:

rgInitProperties[2].dwPropertyID = DBPROP_AUTH_USERID;
VariantInit(&rgInitProperties[2].vValue);
rgInitProperties[2].vValue.vt = VT_BSTR;
rgInitProperties[2].vValue.bstrVal = SysAllocString(OLESTR("dbo"));
 
rgInitProperties[3].dwPropertyID = DBPROP_AUTH_PASSWORD;
VariantInit(&rgInitProperties[3].vValue);
rgInitProperties[3].vValue.vt = VT_BSTR;
rgInitProperties[3].vValue.bstrVal = SysAllocString(OLESTR(""));

Свойство, задающее имя пользователя, имеет идентификатор DBPROP_AUTH_USERID, а свойство, определяющее пароль пользователя, — идентификатор DBPROP_AUTH_PASSWORD.

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

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

Установка свойств

Задание свойств выполняется методом SetProperties интерфейса IDBProperties. Этому методу передается указатель на структуру DBPROPSET, который, в свою очередь, используется для ссылки на только что подготовленный нами массив структур DBPROP.

Структура DBPROPSET определена следующим образом:

typedef struct tagDBPROPSET
{
   DBPROP * rgProperties;
   ULONG    cProperties;
   GUID     guidPropertySet;
} DBPROPSET;

Ее поля описаны в таблице 5-5.

Таблица 5-5. Поля структуры DBPROPSET

Поле

Описание

rgProperties

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

cProperties

Количество свойств, для которых выполняется операция записи или чтения (размер массива структур DBPROP)

guidPropertySet

Глобальный уникальный идентификатор GUID набора свойств

Структура DBPROPSET создается и инициализируется очень просто:

DBPROPSET rgInitPropSet;

rgInitPropSet.guidPropertySet  = DBPROPSET_DBINIT;
rgInitPropSet.cProperties      = 4;
rgInitPropSet.rgProperties     = rgInitProperties;

В поле guidPropertySet мы записали константу DBPROPSET_DBINIT, так как наш набор свойств отвечает за инициализацию источника данных. Массив rgInitProperties состоит из четырех элементов, поэтому в поле cProperties записывается значение 4. Что же касается указателя на массив, то мы помещаем его в поле rgProperties.

Теперь у нас есть структура, описывающая массив свойств. Чтобы задать свойства, нам необходимо вызвать метод SetProperties интерфейса IDBProperties. А для этого, в свою очередь, нам потребуется указатель на интерфейс IDBProperties.

Необходимый указатель мы получаем с помощью функции QueryInterface и записываем в переменную pIDBProperties типа IDBProperties*:

IDBProperties*   pIDBProperties;
pIDBInitialize->QueryInterface(IID_IDBProperties,
  (void**)&pIDBProperties);

Через первый параметр мы передаем функции QueryInterface глобальный уникальный идентификатор интерфейса. В нашем случае идентификатором интерфейса IDBProperties служит значение константы IID_IDBProperties.

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

Теперь мы можем устанавливать свойства:

HRESULT hr;
hr = pIDBProperties->SetProperties(1, &rgInitPropSet);

Метод SetProperties интерфейса IDBProperties имеет два параметра. Через второй параметр передается указатель на массив структур типа DBPROPSET, а через первый — количество таких структур в массиве. В нашем случае подготовлена одна структура DBPROPSET, описывающая массив DBPROP, поэтому значение первого параметра равно 1.

Для наглядности мы показали на рис. 5-1 взаимосвязь структур DBPROPSET и DBPROP при установке свойств источника данных.

Рис. 5-1. Задание свойств источника данных

Здесь мы подготовили массив из трех структур DBPROPSET, поэтому первый параметр метода SetProperties интерфейса IDBProperties имеет значение, равное 3.

После того как программа установила свойства методом SetProperties, интерфейс IDBProperties станет нам не нужен. Поэтому мы освобождаем указатель на интерфейс методом Release:

pIDBProperties->Release();

Инициализация объекта

Последний этап в инициализации объекта провайдера источника данных — вызов метода Initialize интерфейса IDBInitialize:

if(FAILED((pIDBInitialize)->Initialize()))
{
  . . .
}

Напомним, что указатель на этот интерфейс мы получили на этапе создания объекта провайдера источника данных функцией CoCreateInstance:

hr = CoCreateInstance(CLSID_MSDASQL, NULL, CLSCTX_INPROC_SERVER,
  IID_IDBInitialize, (void**)&pIDBInitialize);

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

Объект Session

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

Для создания объекта Session нужно получить указатель на интерфейс IDBCreateSession. Для этого применяют метод QueryInterface интерфейса IDBInitialize, вызвав его следующим образом:

IDBCreateSession* pIDBCreateSession;
pIDBInitialize->QueryInterface(IID_IDBCreateSession,
  (void**)&pIDBCreateSession));

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

С помощью интерфейса IDBCreateSession и метода CreateSession мы создаем сеанс:

IDBCreateCommand* pIDBCreateCommand;
HRESULT hr;
hr = pIDBCreateSession->CreateSession(NULL, IID_IDBCreateCommand,
  (IUnknown**)&pIDBCreateCommand);

Если новый сеанс создается в рамках агрегированного объекта, через первый параметр методу CreateSession передается указатель на управляющий интерфейс IUnknown. Если же сеанс не является составной частью агрегированного объекта (как в нашем случае), Вы можете указать здесь значение NULL.

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

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

После получения интерфейса создания команд IDBCreateCommand мы освободим ненужный нам более указатель на интерфейс IDBCreateSession с помощью метода Release:

pIDBCreateSession->Release();

Объект Command

Объект Command необходим для выдачи команд. Он создается при помощи метода CreateCommand интерфейса IDBCreateCommand.

Создание объекта

Ниже показан фрагмент кода, создающего объект Command:

ICommandText* pICommandText;
hr = pIDBCreateCommand->CreateCommand(NULL, IID_ICommandText,
  (IUnknown**)&pICommandText);

Рассмотрим параметры использованного здесь метода CreateCommand.

Первый параметр предназначен для передачи управляющего интерфейса IUnknown при агрегации. Мы не применяем агрегацию, поэтому указываем здесь значение NULL.

Через второй параметр передается идентификатор интерфейса ICommandText, необходимого для определения команды.

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

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

pIDBCreateCommand->Release();

Эта операция выполняется как обычно, при помощи метода Release.

Определение команды

Для определения команды мы используем метод SetCommandText интерфейса ICommandText:

LPCTSTR wSQLString =
  OLESTR("SELECT ClientID, UserID, Password, RegisterDate, Email FROM clients");

pICommandText->SetCommandText(DBGUID_DBSQL, wSQLString);

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

Если первый параметр метода SetCommandText задан в виде константы DBGUID_SQL (как в нашем примере), то команда интерпретируется в соответствии с правилами языка SQL.

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

Выполнение команды

После определения команды можно отправить ее на выполнение при помощи метода Execute интерфейса ICommandText:

LONG cRowsCounter;
hr = pICommandText->Execute(NULL, IID_IRowset, NULL,
 &cRowsCounter, (IUnknown**)&pIRowset);

Первый параметр метода Execute используется для агрегирования. Мы задаем его в виде константы NULL.

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

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

Через четвертый параметр методу Execute передается указатель на переменную, куда после выполнения команды записывается счетчик строк, на которые повлияла команда. Например, при создании набора записей в результате выполнения команды SELECT в эту переменную будет записано количество строк в образованном наборе записей.

После выполнения команды мы должны освободить указатель на интерфейс ICommandText, вызвав метод Release:

pICommandText->Release();

Интерфейс IRowset и набор записей

Как мы только что сказали, в результате выполнения команды методом Execute мы получаем указатель на интерфейс IRowset. Этот интерфейс используется для извлечения результатов выполнения команды, то есть для извлечения данных из полей строк набора записей, созданного командой.

Помимо этого интерфейса, нам потребуются интерфейсы IColumnsInfo, IRowsetInfo, и IAccessor.

Для извлечения данных из набора записей нам предстоит выполнить следующие процедуры:

·       получить описание столбов набора записей при помощи метода GetColumnInfo интерфейса IColumnsInfo;

·       выполнить привязку данных набора к переменным программы с помощью метода CreateAccessor интерфейса IAccessor;

·       извлечь идентификаторы строк набора методом GetNextRows интерфейса IRowset;

·       извлечь данные из полей строк методом GetData интерфейса IRowset.

Рассмотрим поэтапное выполнение этих процедур.

Получение описания набора записей

На первом этапе обработки набора записей нам нужно получить описание столбцов набора, вызвав метод GetColumnInfo интерфейса IColumnsInfo.

Для этого мы вначале получим указатель на интерфейс IColumnsInfo, воспользовавшись для этого указателем на интерфейс IRowset, ставший доступным после выполнения команды:

IColumnsInfo* pIColumnsInfo;
pIRowset->QueryInterface(IID_IColumnsInfo, (void**)&pIColumnsInfo));

Указатель на интерфейс IColumnsInfo извлекается с помощью метода QueryInterface. Через первый параметр мы передаем этому методу идентификатор интерфейса IColumnsInfo в виде константы IID_IColumnsInfo, а через второй параметр — указатель на переменную, в которую будет записан указатель на интерфейс IColumnsInfo.

Далее мы вызовем метод GetColumnInfo интерфейса IColumnsInfo:

HRESULT       hr;
ULONG         nColsCount;
DBCOLUMNINFO* pColInfo = NULL;
OLECHAR*      pColStringsBuffer = NULL;

hr = pIColumnsInfo->GetColumnInfo(&nColsCount, &pColInfo,
  &pColStringsBuffer);

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

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

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

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

Структура DBCOLUMNINFO определена так:

typedef struct tagDBCOLUMNINFO
{
  LPOLESTR      pwszName;
  ITypeInfo *   pTypeInfo;
  ULONG         iOrdinal;
  DBCOLUMNFLAGS dwFlags;
  ULONG         ulColumnSize;
  DBTYPE        wType;
  BYTE          bPrecision;
  BYTE          bScale;
  DBID          columnid;
} DBCOLUMNINFO;

Поля этой структуры описаны в таблице 5-6.

Таблица 5-6. Поля структуры DBPROPSET

Поле

Описание

pwszName

Указатель на имя столбца или NULL, если имя столбца определить не удалось

pTypeInfo

Зарезервировано

iOrdinal

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

dwFlags

Флаги характеристик столбца. Набор флагов определен в рамках перечисления DBCOLUMNFLAGS

ulColumnSize

Максимальная длина значения в столбце

wType

Тип данных, расположенных в столбце

bPrecision

Максимальная точность численных данных или длина строки, в которой представлены данные других типов

bScale

Количество цифр справа от десятичной точки для данных типа DBTYPE_DECIMAL или DBTYPE_NUMERIC

columnid

Идентификатор столбца

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

pIColumnsInfo->Release();

Сведения, полученные на этом этапе, потребуются нам для выполнения привязки полей из строк набора записей к переменным, определенным в нашем приложении. Особый интерес вызывает информация, полученная в полях dwFlags (характеристики столбца) и wType (тип данных).

Поле dwFlags может содержать константы, объединенные логической операцией ИЛИ (таблица 5-7).

Таблица 5-7. Константы для заполнения поля dwFlags

Константа

Описание

DBCOLUMNFLAGS_CACHEDEFERRED

Отсроченное чтение с использованием механизма кэширования

DBCOLUMNFLAGS_ISBOOKMARK

Столбец содержит закладку bookmark

DBCOLUMNFLAGS_ISCHAPTER

Столбец содержит значение оглавления (chapter value)

DBCOLUMNFLAGS_ISFIXEDLENGTH

Все данные в столбце имеют одну и ту же длину

DBCOLUMNFLAGS_ISLONG

Столбец содержит данные типа BLOB

DBCOLUMNFLAGS_ISNULLABLE

Этот флажок устанавливается в том случае, если столбец может содержать значения NULL или если провайдер источника данных не способен определить, может ли значение полей столбца быть равно NULL

DBCOLUMNFLAGS_ISROWID

Столбец содержит идентификатор сохраняемой строки

DBCOLUMNFLAGS_ISROWVER

Столбец содержит отметку о времени (timestamp) или другое значение, используемое для отслеживания версий записей, которое нельзя обновлять при помощи операций прямой записи

DBCOLUMNFLAGS_MAYBENULL

Поля столбца могут содержать значения NULL

DBCOLUMNFLAGS_MAYDEFER

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

DBCOLUMNFLAGS_WRITE

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

Этот флажок не используется совместно с DBCOLUMNFLAGS_WRITEUNKNOWN

DBCOLUMNFLAGS_WRITEUNKNOWN

Данные в столбце обновляются как посредством метода SetData интерфейса IrowsetChange, так и с применением других механизмов.

Этот флажок не используется совместно с DBCOLUMNFLAGS_WRITE

Что же касается типа данных, указанного в поле wType, то в таблице 5-8 мы перечислили некоторые возможные значения для провайдера сервера Microsoft SQL Server.

Таблица 5-8. Типы данных

Тип данных OLE DB

Тип в Microsoft SQL Server

Описание

DBTYPE_STR

char
varchar
text

Строка символов ANSI, закрытая двоичным нулем

DBTYPE_BYTES

binary
varbinary
timestamp
image

Двоичные данные

DBTYPE_NUMERIC

numeric
decimal

Численное значение, имеющее фиксированную точность и масштаб. Описывается структурой DB_NUMERIC:

typedef struct   tagDB_NUMERIC
{
  BYTE precision;
  BYTE scale;
  BYTE sign;
  BYTE val[16];
} DB_NUMERIC;

Здесь precision определяет количество десятичных цифр, scale — количество цифр справа от десятичной точки, sign — знак (1 для положительных чисел и 0 для отрицательных), val — число, занимающее в памяти 16 байт

DBTYPE_UI1

tinyint

Целое без знака, занимающее в памяти 1 байт

DBTYPE_I2

smallint

Двухбайтовое целое со знаком

DBTYPE_I4

int

Целое со знаком размером в 4 байта

DBTYPE_R4

real

Значение с плавающей десятичной точкой одинарной точности

DBTYPE_R8

float

Аналогично предыдущему, но двойной точности

DBTYPE_DATE

smalldatetime
datetime

Значение типа double. Целая часть этого значения содержит количество дней, прошедших с 30 декабря 1899 года, а дробная — прошедшую часть текущего дня

DBTYPE_DBTIMESTAMP

smalldatetime
datetime

Структура типа DBTIMESTAMP:

typedef struct tagDBTIMESTAMP
{
  SHORT  year;
  USHORT month;
  USHORT day;
  USHORT hour;
  USHORT minute;
  USHORT second;
  ULONG  fraction;
} DBTIMESTAMP;

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

DBTYPE_CY

smallmoney
money

Значение типа LARGE_INTEGER. Это число с фиксированной десятичной точкой, причем справа от точки указаны четыре цифры. Хранится как знаковое целое, занимающее в памяти 8 байт, с масштабом 10 000

DBTYPE_UDT

Тип данных, определенный пользователем

Тип данных пользователя может иметь переменную длину

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

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

Подготовка информации для привязки данных

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

Массив создается следующим образом:

DBBINDING*  pDBBind = NULL;
pDBBind = new DBBINDING[nColsCount];

Размер массива равен количеству строк в полученном наборе записей, которое мы определили на предыдущем этапе при помощи метода GetColumnInfo интерфейса IColumnsInfo.

Определение структуры DBBINDING показано ниже:

typedef struct tagDBBINDING
{
  ULONG       iOrdinal;
  ULONG       obValue;
  ULONG       obLength;
  ULONG       obStatus;
  ItypeInfo * pTypeInfo;
  DBOBJECT  * pObject;
  DBBINDEXT * pBindExt;
  DBPART      dwPart;
  DBMEMOWNER  dwMemOwner;
  DBPARAMIO   eParamIO;
  ULONG       cbMaxLen;
  DWORD       dwFlags;
  DBTYPE      wType;
  BYTE        bPrecision;
  BYTE        bScale;
} DBBINDING;

Как видите, некоторые поля этой структуры называются также как и поля структуры DBCOLUMNINFO. Они имеют аналогичное назначение. Полное описание полей данной структуры Вы найдете в таблице 5-9.

Таблица 5-9. Поля структуры DBCOLUMNINFO

Поле

Описание

iOrdinal

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

obValue

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

Применяется только в том случае, если в поле dwPart установлен флажок DBPART_VALUE

obLength

Смещение значения размера поля в буфере потребителя данных.

Используется только в том случае, если в поле dwPart установлен флажок DBPART_LENGTH

obStatus

Смещение значения слова состояния поля в буфере потребителя данных.

Используется только в том случае, если в поле dwPart установлен флажок DBPART_STATUS

pTypeInfo

Зарезервировано

pObject

Указатель на структуру типа DBOBJECT. Применяется при работе с полями типа BLOB или с полями, содержащими объекты COM. В нашей книге не рассматривается

pBindExt

Указатель на структуру DBBINDEXT, которую можно использовать для дальнейших расширений структуры привязки данных

dwPart

Набор флагов, объединяемых логической операцией ИЛИ и определяющих, какая область буфера пользователя привязана к столбцам или параметрам. Может принимать значения DBPART_VALUE, DBPART_LENGTH и DBPART_STATUS

dwMemOwner

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

eParamIO

Используется для привязки данных параметров. Определяет направление передачи данных и может принимать следующие значения: DBPARAMIO_INPUT (входной параметр), DBPARAMIO_OUTPUT (выходной параметр), DBPARAMIO_NOTPARAM (привязка используется не для параметров). Константы DBPARAMIO_INPUT и DBPARAMIO_OUTPUT могут объединяться логической операцией ИЛИ, если параметр является одновременно и входным, и выходным

cbMaxLen

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

dwFlags

Флаги характеристик столбца. Набор флажков определен в рамках перечисления DBCOLUMNFLAGS

wType

Тип данных, расположенных в столбце

bPrecision

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

bScale

Количество цифр справа от десятичной точки для данных типа DBTYPE_DECIMAL или DBTYPE_NUMERIC

Заполнение массива структур DBBINDING удобно выполнять в цикле.

Перед запуском цикла нам нужно определить переменную nCurrentCol, в которой будет храниться номер текущего столбца, а также переменную cbRow для хранения текущего смещения поля в буфере строки:

ULONG  nCurrentCol;
ULONG  cbRow = 0;

Чтобы обнулить неиспользуемые поля структуры DBBINDING, мы применяем функцию memset:

memset(pDBBind, 0, sizeof(DBBINDING) * nColsCount);

Цикл выглядит следующим образом:

for(ULONG nCurrentCol = 0; nCurrentCol < nColsCount; nCurrentCol++)
{
  . . .
  pDBBind[nCurrentCol].cbMaxLen =
     pColInfo[nCurrentCol].ulColumnSize;
  cbRow += pDBBind[nCurrentCol].cbMaxLen;
}

На каждой итерации цикла помимо увеличения на единицу номера текущей строки nCurrentCol мы извлекаем длину данных текущего столбца из соответствующего элемента массива с информацией о наборе данных pColInfo. Эта информация записывается в поле cbMaxLen структуры pDBBind и используется для увеличения содержимого переменной cbRow, определяющей смещение данных текущего столбца в буфере.

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

pDBBind[nCurrentCol].iOrdinal    = nCurrentCol + 1;
pDBBind[nCurrentCol].obValue     = cbRow;
pDBBind[nCurrentCol].dwPart      = DBPART_VALUE;
pDBBind[nCurrentCol].dwMemOwner  = DBMEMOWNER_CLIENTOWNED;
pDBBind[nCurrentCol].eParamIO  = DBPARAMIO_NOTPARAM;
pDBBind[nCurrentCol].bPrecision = pColInfo[nCurrentCol].bPrecision;
pDBBind[nCurrentCol].bScale    = pColInfo[nCurrentCol].bScale;
pDBBind[nCurrentCol].wType       = pColInfo[nCurrentCol].wType;

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

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

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

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

Инициализация полей bPrecision, bScale и wType массива pDBBind выполняется путем переписывания соответствующих значений из массива pColInfo, содержащего информацию о столбцах набора записей, полученного в результате выполнения команды.

Выполнение привязки данных

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

Для этого мы вначале получаем указатель на интерфейс IAccessor и сохраняем его в переменной pIAccessor:

IAccessor* pIAccessor;
pIRowset->QueryInterface(IID_IAccessor, (void**)&pIAccessor);

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

DBBINDSTATUS* pDBBindStatus = NULL;
pDBBindStatus = new DBBINDSTATUS[nColsCount];

Далее мы передаем количество столбцов в наборе данных, указатель на массив pDBBind с информацией о привязке данных, адрес переменной для хранения идентификатора привязки hAccessor и указатель методу CreateAccessor интерфейса IAccessor на массив структур DBBINDSTATUS:

HACCESSOR hAccessor;
pIAccessor->CreateAccessor(DBACCESSOR_ROWDATA, nColsCount,
  pDBBind, 0, &hAccessor, pDBBindStatus);

Через первый параметр методу CreateAccessor передаются флажки свойств объекта привязки, которые определяют назначение этого объекта. Возможные значения флажков мы перечислены в таблице 5-10.

Таблица 5-10. Поля структуры DBCOLUMNINFO

Флажок

Описание

DBACCESSOR_INVALID

Используется методом GetBindings для того чтобы сообщить программе о произошедшей ошибке

DBACCESSOR_PASSBYREF

Объект привязки передает значения по ссылке

DBACCESSOR_ROWDATA

Объект привязки описывает привязку столбцов набора данных

DBACCESSOR_PARAMETERDATA

Объект предназначен для привязки значений параметров команды

DBACCESSOR_OPTIMIZED

Создается оптимизированный объект привязки, которая касается использования внутренних буферов

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

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

Структура DBBINDSTATUS определена как двойное слово, в которое записываются флаги результата привязки:

typedef DWORD DBBINDSTATUS;

Эти флажки перечислены в таблице 5-11.

Таблица 5-11. Поля структуры DBCOLUMNINFO

Флаг

Описание

DBBINDSTATUS_OK

Успешное выполнение привязки

DBBINDSTATUS_BADORDINAL

Неправильно указан номер столбца

DBBINDSTATUS_UNSUPPORTEDCONVERSION

Провайдер не может выполнить затребованное преобразование данных

DBBINDSTATUS_BADBINDINFO

Неправильное значение параметров в поле dwPart структуры параметров привязки

DBBINDSTATUS_BADSTORAGEFLAGS

Ошибка при установке значения в поле dwFlags структуры параметров привязки

DBBINDSTATUS_NOINTERFACE

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

Обработка набора записей

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

Эта операция выполняется в тройном вложенном цикле.

Внешний цикл вызывает метод GetNextRows интерфейса IRowset, как это показано ниже:

HROW rghRows[30];
HROW* pRows = &rghRows[0];
ULONG cObtainedRows;
while(TRUE)
{
  pIRowset->GetNextRows(0, 0, 30, &cObtainedRows, &pRows );
  if(cObtainedRows == 0)
     break;

     . . .
  //
Обработка строк
     . . .

  pIRowset->ReleaseRows(cObtainedRows, rghRows, NULL, NULL, NULL);
}

Первый параметр метода GetNextRows определяет идентификатор оглавления набора записей и в нашем случае равен нулю. Второй параметр задает начальное смещение при обработке набора записей. Мы обрабатываем набор с самого начала и определяем для этого параметра нулевое значение. Через третий параметр мы передаем количество строк, извлекаемых для обработки из набора записей за один прием (мы задали произвольное значение, равное 30). Если указать для этого параметра отрицательное значение, выборка записей будет выполняться в обратном направлении (от конца набора записей к его началу).

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

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

Обработка извлеченных строк выполняется в цикле второго уровня вложенности:

char* pRowValues;
pRowValues = new char[cbRow];
ULONG iRowCounter;
 
for (iRowCounter=0; iRowCounter < cObtainedRows; iRowCounter++)
{
  pIRowset->GetData(rghRows[iRowCounter], hAccessor, pRowValues);

  . . .
 
// Обработка полей строки
  . . .
}

Здесь мы получаем отдельные строки из блока строк, извлеченных только что рассмотренным методом GetNextRows, используя для этого массив идентификаторов строк rghRows и метод GetData.

В качестве первого параметра методу GetData передается идентификатор извлекаемой строки. Второй параметр предназначен для передачи идентификатора объекта привязки.

Данные строки записываются методом GetData в область памяти pRowValues, передаваемой методу GetData через третий параметр. Размер этой области хранится в переменной cbRow. Он был вычислен на этапе привязки данных.

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

ULONG nCurrentCol;
for (nCurrentCol = 0; nCurrentCol < nColsCount; nCurrentCol++)
{
  // Тип данных, хранящихся в поле:
  // pColInfo[nCurrentCol].wType

  // Данные из текущего поля
  // pRowValues[pDBBind[nCurrentCol].obValue]
     . . .
  }
}

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

По завершении обработки набора записей Ваша программа должна освободить объект привязки и указатель на интерфейс IAccessor:

pIAccessor->ReleaseAccessor(hAccessor, NULL);
pIAccessor->Release();

Первая операция выполняется с помощью метода ReleaseAccessor, а вторая — методом Release.

Программа OLEDB

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

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

1 frolov     123 01.12.1999 20:23:42 frolov@glasnet.ru
10 petrov     111 05.12.1999 12:02:12
petrov@some_mail.ru
12 sidorov  1aa 06.12.1999 13:14:44 sidorov@some_mail.ru

Полные исходные тексты утилиты OLEDB вы найдете в листинге 5-1.

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

Рассмотрим эти исходные тексты в деталях.

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

В самом начале файла исходных текстов oledb.cpp мы определили несколько макросов и включили некоторые include-файлы.

Для того чтобы все строки и символы представлялись в кодировке UNICODE, мы ввели следующие определения:

#define UNICODE
#define _UNICODE

Инициализация констант OLE DB выполняется при помощи определения макроса DBINITCONSTANTS:

#define DBINITCONSTANTS

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

#define INITGUID

Помимо обычных для консольных приложений Windows файлов windows.h и stdio.h, мы включили в исходный текст файлы oledb.h, oledberr.h, msdaguid.h и msdasql.h:

#include <oledb.h>
#include <oledberr.h>
#include <msdaguid.h>
#include <msdasql.h>

Файлы oledb.h и oledberr.h предназначены для определений объектов и интерфейсов OLE DB, а файлы msdaguid.h и msdasql.h относятся к провайдеру ODBC, использованному нами для создания источника данных.

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

struct ComInit
{
  ComInit()
  {
     ::CoInitialize(NULL);
  }

  ~ComInit()
  {
     ::CoUninitialize();
  }
} com_init;

До начала работы выполняется инициализация COM методом CoInitialize, а до ее завершения — освобождение ресурсов COM методом CoUninitialize,

В области глобальных переменных мы также определили три указателя на интерфейсы IMalloc, IDBInitialize и IRowset:

IMalloc*      pIMalloc = NULL;
IDBInitialize* pIDBInitialize = NULL;
IRowset*      pIRowset = NULL;

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

Функция main

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

int main(int argc, TCHAR* argv[], TCHAR* envp[])
{
  if(init())
  {
     if(startCommand())
       get_records();
  }
  else
  {
     if(pIRowset != NULL)
        pIRowset->Release();

     if(pIDBInitialize != NULL)
     {
       pIDBInitialize->Uninitialize();
       pIDBInitialize->Release();
     }

     if(pIMalloc !=
NULL)
       pIMalloc->Release();
  }
  return 0;
}

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

При возникновении каких-либо ошибок в работе программы функция main освобождает указатели на интерфейсы IMalloc, IDBInitialize и IRowset. Предварительно она убеждается в том, что указатели не содержат нулевые значения. Перед освобождением указателя на интерфейс IDBInitialize мы вызываем метод Uninitialize, освобождающий ресурсы, полученные программой при инициализации источника данных.

Функция init

Перед тем как приступить к инициализации источника данных, функция init получает память для системы COM, вызывая для этого функцию CoGetMalloc:

if(FAILED(CoGetMalloc(MEMCTX_TASK, &pIMalloc)))
  return false;

В результате в глобальную переменную pIMalloc записывается указатель на интерфейс системы управления памятью COM. Приложение может использовать этот указатель для вызова методов интерфейса IMalloc, таких, как Alloc, Realloc, Free и т. д. Память, полученная таким образом, применяется в многопоточной среде.

Наша программа заказывает память неявно, обращаясь к интерфейсам OLE DB, однако, прежде чем завершить работу, функция get_records вызывает метод Free интерфейса IMalloc для освобождения памяти, заказанной для описания столбцов набора записей.

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

Вначале функция создает объект IDBInitialize и получает указатель на соответствующий интерфейс:

CoCreateInstance(CLSID_MSDASQL, NULL, CLSCTX_INPROC_SERVER,
  IID_IDBInitialize, (void**)&pIDBInitialize);
if (pIDBInitialize == NULL)
  return false;

Далее функция init готовит массив свойств rgInitProperties и, пользуясь указателем на интерфейс IDBInitialize, выполняет инициализацию источника данных:

VariantInit(&rgInitProperties[0].vValue);
rgInitProperties[0].vValue.vt = VT_I2;
rgInitProperties[0].vValue.iVal = DBPROMPT_NOPROMPT;    
 rgInitProperties[0].dwPropertyID = DBPROP_INIT_PROMPT;
. . .
VariantInit(&rgInitProperties[3].vValue);
rgInitProperties[3].vValue.vt = VT_BSTR;
rgInitProperties[3].vValue.bstrVal = SysAllocString(OLESTR(""));
rgInitProperties[3].dwPropertyID = DBPROP_AUTH_PASSWORD;
. . .
rgInitPropSet.guidPropertySet = DBPROPSET_DBINIT;
rgInitPropSet.cProperties = 4;
rgInitPropSet.rgProperties = rgInitProperties;
. . .
pIDBInitialize->QueryInterface(IID_IDBProperties,
  (void**)&pIDBProperties);
. . .
hr = pIDBProperties->SetProperties(1, &rgInitPropSet);
. . .
if(FAILED((pIDBInitialize)->Initialize()))
  return false;

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

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

SysFreeString(rgInitProperties[1].vValue.bstrVal);
SysFreeString(rgInitProperties[2].vValue.bstrVal);
SysFreeString(rgInitProperties[3].vValue.bstrVal);

После установки значений свойств эти переменные уже не нужны.

Функция startCommand

Функция startCommand предназначена для создания сеанса, а также создания и запуска команды.

Так как мы уже достаточно подробно описали этот процесс, то не будем повторяться. Отметим только, что данная функция запускает команду SELECT, выбирающую из таблицы покупателей clients поля с именами ClientID, UserID, Password, RegisterDate и Email:

LPCTSTR wSQLString = OLESTR("SELECT ClientID, UserID, Password, RegisterDate, Email FROM clients");

Функция get_records

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

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

Обработка полей текущей строки выполняется в цикле:

for(nCurrentCol = 0; nCurrentCol < nColsCount; nCurrentCol++)
{
  if(pColInfo[nCurrentCol].wType == DBTYPE_STR)
  {
     printf("%10s ", &pRowValues[pDBBind[nCurrentCol].obValue]);
  }
  else if(pColInfo[nCurrentCol].wType == DBTYPE_I4)
  {
     printf("%5d ", pRowValues[pDBBind[nCurrentCol].obValue]);
  }
  else if(pColInfo[nCurrentCol].wType == DBTYPE_DBTIMESTAMP)
  {
     DBTIMESTAMP* ts = (DBTIMESTAMP*)
       (&pRowValues[pDBBind[nCurrentCol].obValue]);

     printf("%02d.%02d.%04d %02d:%02d:%02d ",
       ts->day, ts->month, ts->year,
       ts->hour, ts->minute, ts->second);
  }
}
printf("\n");

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

Для разработки кода, выполняющего преобразование, Вам помогут данные из таблицы 5-8. Анализируя содержимое поля wType, Вы сможете выбрать подходящий способ преобразования.

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

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

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

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

delete [] pDBBind;
delete [] pRowValues;
delete [] pDBBindStatus;

pIMalloc->Free(pColStringsBuffer);
pIMalloc->Free(pColInfo);

Использование библиотеки шаблонов ATL

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

Однако те из Вас, кто пользуется для создания приложений системой программирования Microsoft Visual C++ версии 6.0 или более новой, получают возможность заметно сократить объем кода, не имеющего непосредственного отношения к выполнению операций с базой данных, а значит, сконцентрироваться на решении своей прикладной задачи. Такую возможность предоставляет библиотека шаблонов ActiveX Template Library (ATL).

Она существенно упрощает как создание новых элементов управления ActiveX, так и использование готовых элементов управления ActiveX, к которым можно отнести объекты OLE DB. Шаблоны этой библиотеки обеспечивают простой доступ к возможностям OLE DB, упрощают процесс привязки данных наборов записей и параметров процедур, а также допускают использование естественных типов данных, привычных для разработчиков программ C++.

Классы для работы с источником данных OLE DB

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

Для использования этих классов в исходные тексты Вашего приложения необходимо включить оператором #include файл atldbcli.h:

#include <atldbcli.h>

Класс CDataSource

Этот класс инкапсулирует в себе соединение с объектом источника данных OLE DB. В рамках этого соединения приложение может создавать один или несколько сеансов.

С применением класса CDataSource инициализация источника данных выполняется так же легко, как и в серверных сценариях, обращающихся к объектам ADO:

CDataSource dsDSN;
HRESULT     hr;
hr = dsDSN.Open(_T("MSDASQL"), "BookStore", "dbo", "");
if(FAILED(hr))
{
  // Обработка ошибки
}

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

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

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

Когда работа с источником данных закончена, его нужно закрыть, вызвав метод Close класса CDataSource:

dsDSN.Close();

В классе CDataSource определено еще несколько методов, которые мы не используем в нашей книге. К ним относятся методы GetProperties и GetProperty, предназначенные для определения свойств соединения с провайдером, метод GetInitializationString, позволяющий получить строку инициализации источника данных (включая пароль) и другие методы, предназначенные для соединения с источником данных.

Класс CSession

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

Это очень просто:

CSession  sSession;
hr = sSession.Open(dsDSN);
if(FAILED(hr))
{
  dsDSN.Close();
  // Обработка ошибки
}

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

По завершении работы с источником данных приложение должно закрыть все открытые сеансы методом Close, как это показано ниже:

sSession.Close();

Помимо методов Open и Close, в классе CSession определено несколько методов для работы с транзакциями. Это StartTransaction (начало транзакции), Commit (фиксация транзакции), Abort (отмена транзакции) и GetTransactionInfo (получение информации о транзакции).

Класс CCommand

Класс CСommand обеспечивает методы для выполнения команд. Он определен следующим образом:

template <class TAccessor = CNoAccessor, class TRowset = CRowset, class TMultiple = CNoMultipleResults>
class CCommand :
  public CAccessorRowset<TAccessor, TRowset>,
  public CCommandBase,
  public TMultiple

Здесь класс TAccessor представляет собой класс объекта привязки Accessor, с которым мы уже имели дело в этой главе, TRowset — класс набора записей, создаваемого при выполнении команды, и Tmultiple, который используется с командами, возвращающими одновременно несколько результатов.

Несмотря на устрашающий вид определения класса CСommand, пользоваться им достаточно просто. Вот как мы создаем объект cmd этого класса:

CCommand <CAccessor<tabClients> > cmd;

Здесь мы ссылаемся на класс tabClients, определенный в нашем приложении для выполнения привязки данных:

class tabClients
{
public:
  TCHAR         m_ClientID[6];
  TCHAR         m_UserID[50];
  TCHAR         m_Password[50];
  DBTIMESTAMP   m_RegisterDate;
  TCHAR         m_Email[80];

  BEGIN_COLUMN_MAP(tabClients)
     COLUMN_ENTRY(1, m_ClientID)
     COLUMN_ENTRY(2, m_UserID)
     COLUMN_ENTRY(3, m_Password)
     COLUMN_ENTRY(4, m_RegisterDate)
     COLUMN_ENTRY(5, m_Email)
  END_COLUMN_MAP()
};

Этот класс определяет всю информацию, необходимую для выполнения привязки. Фактически он содержит описание полей набора данных, который будет образован после выполнения запроса к базе данных. В нашем случае это таблица clients, входящая в состав базы данных Интернет-магазина, о которой мы уже рассказывали ранее (поэтому, кстати, мы и выбрали для класса привязки имя tabClients). Заметьте, мы не создаем объектов класса tabClients, нам нужно только его определение.

В классе tabClients мы расположили определения полей, сделанные с применением обычных типов данных C++. Кроме этого, в этом классе есть описание столбцов набора записей, сделанное при помощи макрокоманд BEGIN_COLUMN_MAP, COLUMN_ENTRY и END_COLUMN_MAP.

Через единственный параметр макрокоманде BEGIN_COLUMN_MAP нужно указать имя класса привязки. В нашем случае это tabClients.

Макрокоманда COLUMN_ENTRY, предназначенная для выполнения привязки данных, имеет два параметра — номер столбца и имя поля данных записи.

И наконец, макрокоманда END_COLUMN_MAP, закрывающая определение столбцов набора, не имеет параметров.

Для того чтобы запустить команду на выполнение, Вы должны воспользоваться методом Open, определенным в классе CCommand:

TCHAR mySQL[] =
"SELECT ClientID, UserID, Password, RegisterDate, Email FROM clients";

hr = cmd.Open(sSession, mySQL);
if(FAILED(hr))
{
  sSession.Close();
  dsDSN.Close();
  //
Обработка ошибки
}

Этот метод очень прост в применении. Достаточно передать ему в качестве первого параметра ссылку на открытый сеанс, а в качестве второго — адрес текстовой строки команды, подлежащей выполнению.

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

while(cmd.MoveNext() == S_OK)
{
  // cmd.m_ClientID;
  // cmd.m_UserID;
  // cmd.m_Password;
  // cmd.m_Email;
  // cmd.m_RegisterDate;
}

Здесь мы перебираем записи образованного набора с помощью метода MoveNext, определенного в классе CCommand.

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

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

cmd.Close();
sSession.Close();
dsDSN.Close();

Программа ATLOLEDB

Для демонстрации простоты использования объектного интерфейса OLE DB с применением библиотеки шаблонов ATL мы подготовили консольную программу ATLOLEDB. Она решает ту же задачу, что и предыдущая, рассмотренная в этой главе — отображает содержимое нескольких полей таблицы регистрации посетителей Интернет-магазина clients.

Полный исходный текст программы ATLOLEDB Вы найдете в листинге 5-2.

Листинг 5-2 хранится в файле ch5\atloledb\atloledb.cpp на прилагаемом к книге компакт-диске.

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

Для того чтобы использовать шаблоны ATL, предназначенные для работы с объектами OLE DB, мы включили в исходный текст нашего приложения файл atldbcli.h:

#include <atldbcli.h>

Так как наша программа выводит результаты на консоль и при этом пользуется манипуляторами ввода/вывода, мы включили файлы iostream и iomanip:

#include <iostream>
#include <iomanip>

Кроме того, для применения ATL необходимо подключить пространство имен std:

using namespace std;

В области глобальных определений мы расположили определение класса привязки переменных к набору записей tabClients:

class tabClients
{
public:
  TCHAR       m_ClientID[6];
  TCHAR       m_UserID[50];
  TCHAR       m_Password[50];
  DBTIMESTAMP m_RegisterDate;
  TCHAR       m_Email[80];

  BEGIN_COLUMN_MAP(tabClients)
     COLUMN_ENTRY(1, m_ClientID)
     COLUMN_ENTRY(2, m_UserID)
     COLUMN_ENTRY(3, m_Password)
     COLUMN_ENTRY(4, m_RegisterDate)
     COLUMN_ENTRY(5, m_Email)
  END_COLUMN_MAP()
};

О назначении и внутреннем устройстве этого класса мы рассказывали ранее

Помимо этого, в области глобальных определений мы разместили три объекта классов CDataSource, CSession и CCommand:

CDataSource dsDSN;
CSession  sSession;
CCommand <CAccessor<tabClients> > cmd;

Объект dsDSN используется для создания соединения с источником данных, объект sSession нужен для образования сеанса, а объект cmd — для выдачи команды и обработки результата ее выполнения.

Функция main

Наша программа настолько проста, что все свои действия она выполняет в рамках единственной функции main.

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

CoInitialize(NULL);

Заметим, что освобождение ресурсов выполняется автоматически, поэтому при использовании шаблонов ATL функцию CoUninitialize вызывать не надо (и нельзя).

После инициализации наша программа открывает источник данных и сеанс:

HRESULT hr;
hr = dsDSN.Open(_T("MSDASQL"), "BookStore", "dbo", "");
if(FAILED(hr))
  return 1;

hr = sSession.Open(dsDSN);
if(FAILED(hr))
{
  dsDSN.Close();
  return 1;
}

В случае возникновения ошибок программа завершает свою работу с кодом возврата 1.

Далее программа выполняет команду:

TCHAR mySQL[] = "SELECT ClientID, UserID, Password, RegisterDate, Email FROM clients";

hr = cmd.Open(sSession, mySQL);
if(FAILED(hr))
{
  sSession.Close();
  dsDSN.Close();
  return 1;
}

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

Записи набора, образованного в результате выполнения этой команды, обрабатываются в цикле:

while(cmd.MoveNext() == S_OK)
{
  cout << setw(4) << setiosflags(ios::left) << cmd.m_ClientID;
  cout << setw(10) << cmd.m_UserID;
  cout << setw(10) << cmd.m_Password;
  cout << setw(20) << cmd.m_Email;

  DBTIMESTAMP ts = cmd.m_RegisterDate;
  char szBuf[256];
  wsprintf(szBuf, "%02d.%02d.%04d %02d:%02d:%02d ",
     ts.day, ts.month, ts.year, ts.hour, ts.minute, ts.second);

  cout << setw(30) << szBuf << endl;
}

Здесь мы просто выводим извлеченные значения в выходной поток cout, выполняя форматирование манипуляторами ввода/вывода (установку ширины колонки и выравнивание).

Что же касается форматирования поля даты регистрации, то здесь мы использовали уже знакомый Вам трюк, связанный с использованием структуры DBTIMESTAMP. Создав переменную ts типа DBTIMESTAMP, мы записали в нее значение, полученное из поля cmd.m_RegisterDate. Далее мы выполняем преобразование формата, обращаясь к полям переменной ts.

Перед завершением своей работы программа закрывает объект команды, сеанс и соединение с источником данных:

cmd.Close();
sSession.Close();
dsDSN.Close();

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