Создание приложений с базами данных для Интернета и интрасетей: практическое руководство (С) Александр Вячеславович Фролов, Григорий Вячеславович Фролов, 2000 5. Связь приложений с базами данных через OLE DB 5. Связь приложений с базами данных через OLE DB Инициализация среды выполнения Инициализация источника данных Подготовка команды и параметров Обработка результатов выполнения команды Подготовка параметров инициализации Интерфейс IRowset и набор записей Получение описания набора записей Подготовка информации для привязки данных Листинг 5-1 Вы найдете в файле ch5\oledb\oledb.cpp на прилагаемом к книге компакт-диске. Использование библиотеки шаблонов ATL Классы для работы с источником данных OLE DB Листинг 5-2 хранится в файле ch5\atloledb\atloledb.cpp на прилагаемом к книге компакт-диске. В предыдущей главе мы рассмотрели практические приемы использования объектного интерфейса 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. Интерфейсы, связанные с обработкой ошибок
Объекты OLE DB Для работы с OLE DB Ваше приложение должно создать ряд объектов OLE DB, а затем обращаться к методам и свойствам этих объектов. Ниже мы рассмотрим некоторые методы и свойства важнейших объектов OLE DB, необходимые для создания автономных приложений C++, выполняющих запросы к базам данных. Объект SQLOLEDB Для установки соединения с источником данных Ваша программа должна создать объект SQLOLEDB, предоставляющий интерфейсы источника данных, а затем выполнить его инициализацию. Для создания этого объекта необходимо воспользоваться функцией CoCreateInstance, вызвав ее следующим образом: IDBInitialize* pIDBInitialize = NULL; Здесь через первый параметр мы передаем функции 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 Ее поля описаны в таблице 5-2. Таблица 5-2. Поля структуры DBPROP
Возможные значения, устанавливаемые провайдером источника данных в поле dwStatus, перечислены в таблице 5-3. Таблица 5-3. Значения поля dwStatus
Итак, для инициализации соединения с источником данных нам нужен массив структур 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); Вначале инициализируется поле vValue, имеющее тип VARIANT, с помощью функции VariantInit. В результате такой инициализации в поле будет записано значение VT_EMPTY. Далее мы указываем в поле vt тип данных как VT_I2 (двухбайтовое целое со знаком), а затем записываем в поле iVal константу DBPROMPT_NOPROMPT. Эта константа указывает, что в процессе инициализации пользователю не следует выводить на экран никаких приглашений. Все возможные значения данного свойства перечислены в таблице 5-4. Таблица 5-4. Значения свойства DBPROP_INIT_PROMPT
Второй элемент массива rgInitProperties определяет имя источника данных. Идентификатор соответствующего свойства задается в поле dwPropertyID с помощью константы DBPROP_INIT_DATASOURCE: rgInitProperties[1].dwPropertyID = DBPROP_INIT_DATASOURCE; В поля dwOptions и colid этого и следующих двух элементов массива rgInitProperties мы записываем значения DBPROPOPTIONS_REQUIRED и DB_NULLID соответственно: rgInitProperties[1].dwOptions =
DBPROPOPTIONS_REQUIRED; Что же касается собственно имени источника данных, то оно записывается в поле vValue как строка типа BSTR: VariantInit(&rgInitProperties[1].vValue); Аналогичным образом заполняются элементы массива rgInitProperties, задающие имя пользователя и его пароль: rgInitProperties[2].dwPropertyID = DBPROP_AUTH_USERID; Свойство, задающее имя пользователя, имеет идентификатор DBPROP_AUTH_USERID, а свойство, определяющее пароль пользователя, — идентификатор DBPROP_AUTH_PASSWORD. Здесь для простоты мы опустили инициализацию полей dwOptions и colid. Она выполняется аналогично тому, как это делается для элементов массива, определяющих уровень приглашения и имя источника данных. На данном этапе мы подготовили массив rgInitProperties структур DBPROP, содержащий параметры для установки свойств объекта источника данных. Теперь нужно выполнить зададим свойства. Задание свойств выполняется методом SetProperties интерфейса IDBProperties. Этому методу передается указатель на структуру DBPROPSET, который, в свою очередь, используется для ссылки на только что подготовленный нами массив структур DBPROP. Структура DBPROPSET определена следующим образом: typedef struct
tagDBPROPSET Ее поля описаны в таблице 5-5. Таблица 5-5. Поля структуры DBPROPSET
Структура DBPROPSET создается и инициализируется очень просто: DBPROPSET rgInitPropSet; В поле guidPropertySet мы записали константу DBPROPSET_DBINIT, так как наш набор свойств отвечает за инициализацию источника данных. Массив rgInitProperties состоит из четырех элементов, поэтому в поле cProperties записывается значение 4. Что же касается указателя на массив, то мы помещаем его в поле rgProperties. Теперь у нас есть структура, описывающая массив свойств. Чтобы задать свойства, нам необходимо вызвать метод SetProperties интерфейса IDBProperties. А для этого, в свою очередь, нам потребуется указатель на интерфейс IDBProperties. Необходимый указатель мы получаем с помощью функции QueryInterface и записываем в переменную pIDBProperties типа IDBProperties*: IDBProperties* pIDBProperties; Через первый параметр мы передаем функции QueryInterface глобальный уникальный идентификатор интерфейса. В нашем случае идентификатором интерфейса IDBProperties служит значение константы IID_IDBProperties. Второй параметр служит для передачи указателя на переменную, в которую будет записан полученный указатель на интерфейс. Теперь мы можем устанавливать свойства: HRESULT hr; Метод 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, В том случае, если значения свойств указаны правильно, инициализация пройдет успешно. Теперь можно переходить к созданию сеанса и выдаче команд. Для этого нам опять потребуется интерфейс IDBInitialize. Объект Session Прежде чем выдавать команды, нам необходимо создать сеанс как объект Session. Этот объект обеспечивает методы для создания команд, наборов записей, для создания и изменения таблиц и индексов. Он также применяется для построения объектов транзакций, однако в этой книге мы этот случай не рассматриваем. Для создания объекта Session нужно получить указатель на интерфейс IDBCreateSession. Для этого применяют метод QueryInterface интерфейса IDBInitialize, вызвав его следующим образом: IDBCreateSession* pIDBCreateSession; В качестве первого параметра мы передаем методу QueryInterface идентификатор интерфейса IDBCreateSession в виде константы IID_IDBCreateSession. В случае успеха метод QueryInterface записывает указатель на интерфейс IDBCreateSession в переменную, расположенную по адресу, заданному вторым параметром. В нашем случае указатель на интерфейс IDBCreateSession будет храниться в переменной pIDBCreateSession типа IDBCreateSession*. С помощью интерфейса IDBCreateSession и метода CreateSession мы создаем сеанс: IDBCreateCommand* pIDBCreateCommand; Если новый сеанс создается в рамках агрегированного объекта, через первый параметр методу CreateSession передается указатель на управляющий интерфейс IUnknown. Если же сеанс не является составной частью агрегированного объекта (как в нашем случае), Вы можете указать здесь значение NULL. Второй параметр метода CreateSession предназначен для передачи идентификатора интерфейса. При создании сеанса мы получаем интерфейс, средствами которого можно создавать команды. Идентификатор этого интерфейса задается константой IID_IDBCreateCommand. И наконец, через третий параметр методу CreateSession передается адрес переменной, в которой будет сохранен указатель на интерфейс создания команд. После получения интерфейса создания команд IDBCreateCommand мы освободим ненужный нам более указатель на интерфейс IDBCreateSession с помощью метода Release: pIDBCreateSession->Release(); Объект Command Объект Command необходим для выдачи команд. Он создается при помощи метода CreateCommand интерфейса IDBCreateCommand. Ниже показан фрагмент кода, создающего объект Command: ICommandText* pICommandText; Рассмотрим параметры использованного здесь метода CreateCommand. Первый параметр предназначен для передачи управляющего интерфейса IUnknown при агрегации. Мы не применяем агрегацию, поэтому указываем здесь значение NULL. Через второй параметр передается идентификатор интерфейса ICommandText, необходимого для определения команды. Третий параметр передает указатель на переменную, в которую будет записан указатель на только что упомянутый интерфейс ICommandText. После успешного получения указатель на интерфейс ICommandText мы можем освободить указатель на интерфейс IDBCreateCommand, который нам больше не потребуется: pIDBCreateCommand->Release(); Эта операция выполняется как обычно, при помощи метода Release. Для определения команды мы используем метод SetCommandText интерфейса ICommandText: LPCTSTR wSQLString = Первый параметр метода SetCommandText указывает синтаксис команды и общие правила, которые должен использовать провайдер источника данных в процессе разбора строки команды. Текст команды задается вторым параметром. Если первый параметр метода SetCommandText задан в виде константы DBGUID_SQL (как в нашем примере), то команда интерпретируется в соответствии с правилами языка SQL. В том случае, когда этот параметр задан как DBGUID_DEFAULT, интерпретация выполняется способом, заданным для провайдера источника данных по умолчанию. В частности, провайдер OLE DB может по умолчанию выполнять команды, не имеющие отношения к SQL. После определения команды можно отправить ее на выполнение при помощи метода Execute интерфейса ICommandText: LONG cRowsCounter; Первый параметр метода 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; Указатель на интерфейс IColumnsInfo извлекается с помощью метода QueryInterface. Через первый параметр мы передаем этому методу идентификатор интерфейса IColumnsInfo в виде константы IID_IColumnsInfo, а через второй параметр — указатель на переменную, в которую будет записан указатель на интерфейс IColumnsInfo. Далее мы вызовем метод GetColumnInfo интерфейса IColumnsInfo: HRESULT hr; Методу GetColumnInfo через параметры передаются три указателя на переменные, в которые будет записана информация о столбцах набора записей. В переменную nColsCount, указатель на которую передается через первый параметр, метод GetColumnInfo запишет количество столбцов, имеющихся в наборе записей, или нулевое значение, если в результате выполнения команды набор записей не был создан. Через второй параметр методу GetColumnInfo передается адрес переменной, в которую будет записан адрес массива структур DBCOLUMNINFO, описывающих столбцы набора. И наконец, через третий параметр передается указатель на переменную, в которую будет записан указатель на память для всех строковых значений. Буфер может содержать одну или более строк, закрытых двоичным нулем. Структура DBCOLUMNINFO определена так: typedef struct tagDBCOLUMNINFO Поля этой структуры описаны в таблице 5-6. Таблица 5-6. Поля структуры DBPROPSET
После получения информации о столбцах набора мы должны освободить ненужный в дальнейшей работе указатель на интерфейс IColumnsInfo: pIColumnsInfo->Release(); Сведения, полученные на этом этапе, потребуются нам для выполнения привязки полей из строк набора записей к переменным, определенным в нашем приложении. Особый интерес вызывает информация, полученная в полях dwFlags (характеристики столбца) и wType (тип данных). Поле dwFlags может содержать константы, объединенные логической операцией ИЛИ (таблица 5-7). Таблица 5-7. Константы для заполнения поля dwFlags
Что же касается типа данных, указанного в поле wType, то в таблице 5-8 мы перечислили некоторые возможные значения для провайдера сервера Microsoft SQL Server. Таблица 5-8. Типы данных
Продолжим процесс извлечения значений из полей набора записей, образованного в результате выполнения команды. На данный момент мы извлекли характеристики столбцов набора данных и готовы выполнить привязку данных. Подготовка информации для привязки данных Для выполнения привязки данных мы должны создать массив структур DBBINDING, содержащий информацию о привязке для всех столбцов набора. Массив создается следующим образом: DBBINDING* pDBBind = NULL; Размер массива равен количеству строк в полученном наборе записей, которое мы определили на предыдущем этапе при помощи метода GetColumnInfo интерфейса IColumnsInfo. Определение структуры DBBINDING показано ниже: typedef struct tagDBBINDING Как видите, некоторые поля этой структуры называются также как и поля структуры DBCOLUMNINFO. Они имеют аналогичное назначение. Полное описание полей данной структуры Вы найдете в таблице 5-9. Таблица 5-9. Поля структуры DBCOLUMNINFO
Заполнение массива структур DBBINDING удобно выполнять в цикле. Перед запуском цикла нам нужно определить переменную nCurrentCol, в которой будет храниться номер текущего столбца, а также переменную cbRow для хранения текущего смещения поля в буфере строки: ULONG nCurrentCol; Чтобы обнулить неиспользуемые поля структуры DBBINDING, мы применяем функцию memset: memset(pDBBind, 0, sizeof(DBBINDING) * nColsCount); Цикл выглядит следующим образом: for(ULONG nCurrentCol = 0; nCurrentCol
< nColsCount; nCurrentCol++) На каждой итерации цикла помимо увеличения на единицу номера текущей строки nCurrentCol мы извлекаем длину данных текущего столбца из соответствующего элемента массива с информацией о наборе данных pColInfo. Эта информация записывается в поле cbMaxLen структуры pDBBind и используется для увеличения содержимого переменной cbRow, определяющей смещение данных текущего столбца в буфере. В теле этого цикла выполняется инициализация других полей массива структур привязки: pDBBind[nCurrentCol].iOrdinal = nCurrentCol + 1; В поле iOrdinal записывается номер текущего столбца, увеличенный на единицу. Это увеличение необходимо потому, что столбцы нумеруются, начиная с единицы, а начальное значение переменной цикла nCurrentCol равно нулю. В поле obValue записывается текущее смещение данных столбца в буфере потребителя данных, вычисляемое на каждой итерации цикла с учетом размера данных, хранящихся в столбце. Чтобы использовать поле obValue подобным образом, мы записали в поле dwPart константу DBPART_VALUE. Для того чтобы возложить задачу управления памятью на потребителя данных, мы записываем в поле dwMemOwner константу DBMEMOWNER_CLIENTOWNED. Так как наша привязка предназначена для работы с набором данных, а не для передачи параметров команде, в поле eParamIO необходимо записать значение DBPARAMIO_NOTPARAM. Инициализация полей bPrecision, bScale и wType массива pDBBind выполняется путем переписывания соответствующих значений из массива pColInfo, содержащего информацию о столбцах набора записей, полученного в результате выполнения команды. После подготовки массива с информацией о привязке мы должны выполнить привязку, создав объект Assessor. Для этого мы вначале получаем указатель на интерфейс IAccessor и сохраняем его в переменной pIAccessor: IAccessor* pIAccessor; Далее мы создаем массив структур DBBINDSTATUS, размер которого равен количеству столбцов в полученном наборе данных: DBBINDSTATUS* pDBBindStatus = NULL; Далее мы передаем количество столбцов в наборе данных, указатель на массив pDBBind с информацией о привязке данных, адрес переменной для хранения идентификатора привязки hAccessor и указатель методу CreateAccessor интерфейса IAccessor на массив структур DBBINDSTATUS: HACCESSOR hAccessor; Через первый параметр методу CreateAccessor передаются флажки свойств объекта привязки, которые определяют назначение этого объекта. Возможные значения флажков мы перечислены в таблице 5-10. Таблица 5-10. Поля структуры DBCOLUMNINFO
Третий параметр метода CreateAccessor задает количество байт в наборе параметров и не используется при работе с наборами записей. Поэтому для него указано нулевое значение. Массив структур DBBINDSTATUS, указатель на который передается методу CreateAccessor через последний параметр, позволяет отследить результат привязки для каждого столбца. Структура DBBINDSTATUS определена как двойное слово, в которое записываются флаги результата привязки: typedef DWORD DBBINDSTATUS; Эти флажки перечислены в таблице 5-11. Таблица 5-11. Поля структуры DBCOLUMNINFO
Теперь, когда привязка данных выполнена, мы можем приступить к извлечению данных из набора, созданного в результате выполнения команды. Эта операция выполняется в тройном вложенном цикле. Внешний цикл вызывает метод GetNextRows интерфейса IRowset, как это показано ниже: HROW rghRows[30]; Первый параметр метода GetNextRows определяет идентификатор оглавления набора записей и в нашем случае равен нулю. Второй параметр задает начальное смещение при обработке набора записей. Мы обрабатываем набор с самого начала и определяем для этого параметра нулевое значение. Через третий параметр мы передаем количество строк, извлекаемых для обработки из набора записей за один прием (мы задали произвольное значение, равное 30). Если указать для этого параметра отрицательное значение, выборка записей будет выполняться в обратном направлении (от конца набора записей к его началу). С помощью четвертого параметра мы предаем методу GetNextRows адрес переменной, в которую метод запишет количество строк, извлеченных из набора записей. И наконец, пятый параметр задает адрес массива для записи идентификаторов извлеченных строк. После обработки извлеченных строк мы освобождаем ресурсы, связанные с извлеченными строками, с помощью метода ReleaseRows. Обработка извлеченных строк выполняется в цикле второго уровня вложенности: char* pRowValues; Здесь мы получаем отдельные строки из блока строк, извлеченных только что рассмотренным методом GetNextRows, используя для этого массив идентификаторов строк rghRows и метод GetData. В качестве первого параметра методу GetData передается идентификатор извлекаемой строки. Второй параметр предназначен для передачи идентификатора объекта привязки. Данные строки записываются методом GetData в область памяти pRowValues, передаваемой методу GetData через третий параметр. Размер этой области хранится в переменной cbRow. Он был вычислен на этапе привязки данных. Теперь мы зададим обработку значений отдельных полей текущей записи, извлеченной из набора. Она выполняется в цикле третьего уровня вложенности: ULONG nCurrentCol; В этом цикле мы можем ссылаться как на массив pColInfo, содержащий полную информацию о столбцах (имя столбца, тип данных и т. д.), а также на массив значений pRowValues. По завершении обработки набора записей Ваша программа должна освободить объект привязки и указатель на интерфейс IAccessor: pIAccessor->ReleaseAccessor(hAccessor, NULL); Первая операция выполняется с помощью метода ReleaseAccessor, а вторая — методом Release. В качестве примера программы, написанной на языке C++ и обращающейся к базе данных средствами OLE DB, приведем исходные тексты простой утилиты OLEDB, отображающей на консольном экране информацию из таблицы посетителей clients нашего Интернет-магазина. Программа получает из базы данных и выводит на экран идентификатор записи покупателя, его имя, пароль, дату регистрации и электронный почтовый адрес E-Mail: 1 frolov 123 01.12.1999 20:23:42
frolov@glasnet.ru Полные исходные тексты утилиты OLEDB вы найдете в листинге 5-1. Листинг 5-1 Вы найдете в файле ch5\oledb\oledb.cpp на прилагаемом к книге компакт-диске. Рассмотрим эти исходные тексты в деталях. В самом начале файла исходных текстов oledb.cpp мы определили несколько макросов и включили некоторые include-файлы. Для того чтобы все строки и символы представлялись в кодировке 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> Файлы oledb.h и oledberr.h предназначены для определений объектов и интерфейсов OLE DB, а файлы msdaguid.h и msdasql.h относятся к провайдеру ODBC, использованному нами для создания источника данных. Для инициализации системы COM перед началом работы программы и для освобождения ресурсов COM перед завершением программы мы определили в области глобальных переменных уже знакомую Вам по предыдущей главе переменную com_init класса ComInit: struct ComInit До начала работы выполняется инициализация COM методом CoInitialize, а до ее завершения — освобождение ресурсов COM методом CoUninitialize, В области глобальных переменных мы также определили три указателя на интерфейсы IMalloc, IDBInitialize и IRowset: IMalloc* pIMalloc = NULL; Первый из них используется для управления памятью, второй — для инициализации объекта провайдера источника данных, а третий — для работы с извлеченным набором записей. Функция main Исходный текст этой функции, получающей управление при запуске программы, представлен ниже: int main(int argc, TCHAR* argv[],
TCHAR* envp[]) В начале своей работы функция main выполняет инициализацию источника данных, вызывая для этого функцию init, определенную в нашей программе. Если инициализация выполнилась успешно, функция main запускает команду, извлекающую из таблицы clients информацию о покупателях. В том случае, если команда выполнена без ошибок, вызывается функция get_records, извлекающая или отображающая в консольном окне строки таблицы clients. При возникновении каких-либо ошибок в работе программы функция main освобождает указатели на интерфейсы IMalloc, IDBInitialize и IRowset. Предварительно она убеждается в том, что указатели не содержат нулевые значения. Перед освобождением указателя на интерфейс IDBInitialize мы вызываем метод Uninitialize, освобождающий ресурсы, полученные программой при инициализации источника данных. Функция init Перед тем как приступить к инициализации источника данных, функция init получает память для системы COM, вызывая для этого функцию CoGetMalloc: if(FAILED(CoGetMalloc(MEMCTX_TASK, &pIMalloc))) В результате в глобальную переменную pIMalloc записывается указатель на интерфейс системы управления памятью COM. Приложение может использовать этот указатель для вызова методов интерфейса IMalloc, таких, как Alloc, Realloc, Free и т. д. Память, полученная таким образом, применяется в многопоточной среде. Наша программа заказывает память неявно, обращаясь к интерфейсам OLE DB, однако, прежде чем завершить работу, функция get_records вызывает метод Free интерфейса IMalloc для освобождения памяти, заказанной для описания столбцов набора записей. Дальнейшие действия, выполняемые функцией init, мы подробно описали в начале этой главы. Вначале функция создает объект IDBInitialize и получает указатель на соответствующий интерфейс: CoCreateInstance(CLSID_MSDASQL, NULL,
CLSCTX_INPROC_SERVER, Далее функция init готовит массив свойств rgInitProperties и, пользуясь указателем на интерфейс IDBInitialize, выполняет инициализацию источника данных: VariantInit(&rgInitProperties[0].vValue); Здесь мы привели только фрагмент кода, выполняющий установку свойств, необходимых для инициализации. Обратите внимание, что перед вызовом метода инициализации наша программа освобождает память, заказанную для переменных типа BSTR: SysFreeString(rgInitProperties[1].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++) Программа выводит на консоль данные в текстовом виде, однако извлекаемые данные могут иметь различный тип в зависимости от столбца. Прежде чем выполнять преобразование, мы определяем тип данных в текущем столбце, анализируя соответствующий элемент массива описания столбцов pColInfo. Тип данных записан в поле wType. Для разработки кода, выполняющего преобразование, Вам помогут данные из таблицы 5-8. Анализируя содержимое поля wType, Вы сможете выбрать подходящий способ преобразования. Для столбцов таблицы, содержащих текстовые значения, тип данных будет DBTYPE_STR. В этом случае наша программа передает функции printf, выполняющей преобразование данных и вывод результата на консоль, указатель на данные. Если же столбец таблицы содержит числовое значение DBTYPE_I4, мы используем в функции printf другой спецификатор формата и передаем данные не через указатель, а непосредственно. Сложнее дело обстоит с данными типа DBTYPE_DBTIMESTAMP, содержащими отметку о времени. Для того чтобы получить отдельные составляющие такой отметки, мы определили в своей программе указатель ts типа DBTIMESTAMP. Далее мы записываем в него значение адреса поля данных с временной отметкой и обращаемся к полям структуры DBTIMESTAMP для отображения даты регистрации посетителя в форматированном виде. Прежде чем завершить работу, функция get_records удаляет заказанные массивы и освобождает память, полученную для буферов: delete [] pDBBind; Использование библиотеки шаблонов 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; Все, что Вам нужно сделать для создания соединения, — вызвать метод Open класса CDataSource. В данном классе имеется несколько перегруженных определений метода Open, позволяющих указывать все или только некоторые параметры. В нашем случае через первый параметр мы передаем методу Open идентификатор провайдера данных в виде текстовой строки. Другие перегруженные определения этого метода позволяют ссылаться на глобальный уникальный идентификатор провайдера CLSID. Через второй, третий и четвертый параметры методу Open передаются имя источника данных, имя пользователя и пароль пользователя соответственно. При необходимости Вы сможете передать эти параметры и через структуру DBPROPSET, применяя другой вариант определения метода Open. Когда работа с источником данных закончена, его нужно закрыть, вызвав метод Close класса CDataSource: dsDSN.Close(); В классе CDataSource определено еще несколько методов, которые мы не используем в нашей книге. К ним относятся методы GetProperties и GetProperty, предназначенные для определения свойств соединения с провайдером, метод GetInitializationString, позволяющий получить строку инициализации источника данных (включая пароль) и другие методы, предназначенные для соединения с источником данных. Класс CSession Для создания сеанса, необходимого для работы с командами и наборами записей, Вы должны использовать класс CSession. Это очень просто: CSession sSession; Вам необходимо вызвать метод 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> Здесь класс TAccessor представляет собой класс объекта привязки Accessor, с которым мы уже имели дело в этой главе, TRowset — класс набора записей, создаваемого при выполнении команды, и Tmultiple, который используется с командами, возвращающими одновременно несколько результатов. Несмотря на устрашающий вид определения класса CСommand, пользоваться им достаточно просто. Вот как мы создаем объект cmd этого класса: CCommand <CAccessor<tabClients> > cmd; Здесь мы ссылаемся на класс tabClients, определенный в нашем приложении для выполнения привязки данных: class tabClients Этот класс определяет всю информацию, необходимую для выполнения привязки. Фактически он содержит описание полей набора данных, который будет образован после выполнения запроса к базе данных. В нашем случае это таблица 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[] = Этот метод очень прост в применении. Достаточно передать ему в качестве первого параметра ссылку на открытый сеанс, а в качестве второго — адрес текстовой строки команды, подлежащей выполнению. В результате выполнения команды создается набор записей, доступ к которому осуществляется с помощью все того же класса CCommand. Для этого в программе необходимо организовать цикл: while(cmd.MoveNext() == S_OK) Здесь мы перебираем записи образованного набора с помощью метода MoveNext, определенного в классе CCommand. Для доступа к значениям полей достаточно просто сослаться на соответствующие поля класса CCommand. Это возможно благодаря применению библиотеки шаблонов ATL. По завершении цикла обработки записей приложение должно закрыть объект класса CCommand, сеанс и соединение с источником данных. Все это делается методами Close соответствующих классов: cmd.Close(); Для демонстрации простоты использования объектного интерфейса 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> Кроме того, для применения ATL необходимо подключить пространство имен std: using namespace std; В области глобальных определений мы расположили определение класса привязки переменных к набору записей tabClients: class tabClients О назначении и внутреннем устройстве этого класса мы рассказывали ранее Помимо этого, в области глобальных определений мы разместили три объекта классов CDataSource, CSession и CCommand: CDataSource dsDSN; Объект dsDSN используется для создания соединения с источником данных, объект sSession нужен для образования сеанса, а объект cmd — для выдачи команды и обработки результата ее выполнения. Функция main Наша программа настолько проста, что все свои действия она выполняет в рамках единственной функции main. В начале своей работы программа выполняет инициализацию COM, вызывая для этого функцию CoInitialize: CoInitialize(NULL); Заметим, что освобождение ресурсов выполняется автоматически, поэтому при использовании шаблонов ATL функцию CoUninitialize вызывать не надо (и нельзя). После инициализации наша программа открывает источник данных и сеанс: HRESULT hr; В случае возникновения ошибок программа завершает свою работу с кодом возврата 1. Далее программа выполняет команду: TCHAR mySQL[] = "SELECT ClientID, UserID, Password, RegisterDate, Email FROM clients"; В качестве команды мы используем строку SQL, выполняющую запрос к таблице покупателей clients. Записи набора, образованного в результате выполнения этой команды, обрабатываются в цикле: while(cmd.MoveNext() == S_OK) Здесь мы просто выводим извлеченные значения в выходной поток cout, выполняя форматирование манипуляторами ввода/вывода (установку ширины колонки и выравнивание). Что же касается форматирования поля даты регистрации, то здесь мы использовали уже знакомый Вам трюк, связанный с использованием структуры DBTIMESTAMP. Создав переменную ts типа DBTIMESTAMP, мы записали в нее значение, полученное из поля cmd.m_RegisterDate. Далее мы выполняем преобразование формата, обращаясь к полям переменной ts. Перед завершением своей работы программа закрывает объект команды, сеанс и соединение с источником данных: cmd.Close(); |