ГЛАВА 8 Доступ к базам данных с помощью dbGO Теперь, когда мы подробно изучили клиентские наборы данных и компоненты графического интерфейса приложений БД, мы наконец-то расстаемся с технологией доступа к данным dbExpress и переходим к освоению других технологий, предоставляемых компанией Borland. Эта и следующая глава будут посвящены технологии dbGO, которая основана на технологии ADO. Введение в ADO Технология Microsoft ADO (ActiveX Data Objects, объекты данных ActiveX) является одной из стандартных технологий Microsoft, предназначенных для доступа к источникам данных. Технология ADO представляет собой надстройку над другой технологией доступа к данным — Microsoft OLE DB. Вдаваться в подробности работы с OLE DB и детали реализации ADO мы не будем. В конце концов, продукты Borland существуют именно для того, чтобы избавить нас от лишних сложностей. Интерфейс ADO основан на семи объектах (трех главных и четырех вспомогательных): Connection — управляет соединением программы с БД. В случае возникновения ошибки где-то за пределами приложения (ошибка на стороне сервера или ошибка уровня ADO), этот объект использует объект Error для передачи данных об ошибке; Command — предназначен для выполнения команд. При работе с реляционными СУБД объект Command может выполнять команды SQL. Этот объект позволяет определить параметры и установить порядок выполнения запросов. Коллекция объектов Parameter предоставляет доступ к параметрам; Глава 8. Доступ к базам данных с помощью dbGO 169 Recordset — представляет собой интерфейс доступа к данным, которые могут быть получены в результате обработки запроса или каким-либо другим способом. Объект Recordset предоставляет доступ к коллекции объектов Field, которые содержат описания полей набора записей и их значения. Объекты Recordset очень похожи на объекты-наборы данных BDS. Каждый высокоуровневый объект ADO содержит коллекцию объектов Property. Эти объекты, как следует из их названия, предоставляют данные о свойствах объектов ADO. Важной особенностью объектов Property является то, что данные предоставляются ими динамически. Это важно потому, что разные объекты ADO, поставляемые разными поставщиками, поддерживают разные свойства. Компоненты Delphi и C++ Builder, предназначенные для работы с ADO (они расположены на странице dbGO Палитры инструментов), служат своего рода мостом между моделью ADO и моделью приложений БД BDS, основанной на наборах данных и компонентах-провайдерах. Читатель вправе задать вопрос о том, что предпочтительнее, dbExpress или ADO. Однозначного ответа нет, ведь у каждой из этих технологий есть свои достоинства и недостатки. Если бы одна технология была однозначно лучше другой, зачем было бы писать про обе? Интерфейс ADO и лежащий в его основе интерфейс OLE DB являются стандартными механизмами Microsoft для доступа к данным. Это значит, что практически все приложения, работающие с данными на платформе Microsoft, поддерживают эти механизмы. С помощью ADO можно получить доступ к самым разным приложениям, начиная с Oracle Database Server и заканчивая Microsoft Excel. Более того, мы можем быть уверены, что и будущие приложения, связанные с хранением данных на платформе Windows, обеспечат поддержку ADO. Набор драйверов dbExpress, конечно, не может похвастаться такой всеохватностью. С другой стороны, механизмы dbExpress, по понятным причинам, лучше интегрированы с продуктами Borland. Короче говоря, выбор технологии доступа к данным должен зависеть от конкретных условий, и критерием истины здесь будет сравнение возможностей технологий при решении поставленной перед вами задачи. Впрочем, модель приложений баз данных BDS устроена так, что переход с одной технологии доступа к БД на другую не составит особого труда. 170 Часть II. Программирование приложений БД для Win32 Базовые компоненты для работы с ADO В этом разделе мы рассмотрим два компонента, без которых не обходится ни одно приложение ADO-dbGO. В конце главы будет дан краткий обзор других компонентов. Компонент TADOConnection Компонент TADOConnection инкапсулирует объект ADO Connection и осуществляет соединение с хранилищами данных. Компонент TADOConnection во многом подобен компоненту TSQLConnection, так что у вас не должно возникнуть проблем с его освоением. При работе с компонентом TSQLConnection мы выбрали драйвер и параметры соединения с помощью свойств ConnectionName, DriverName и Params. При работе с компонентом TADOConnection аналогичную роль выполняет свойство ConnectionString. Строка ConnectionString позволяет указать практически всю информацию, необходимую для связи с хранилищем данных. Мы сейчас не будем подробно останавливаться на формате строки ConnectionString. Некоторые детали этого формата будут рассмотрены далее на конкретных примерах. Вообще говоря, вам не нужно разбираться в тонкостях формата ConnectionString, поскольку, как вы увидите, есть прекрасный визуальный инструмент, который позволяет создать необходимую строку путем выбора из предлагаемого списка опций. Соединение с хранилищем данных открывается и закрывается при помощи свойства Connected или одного из двух перегруженных методов Open. Первый вариант метода Open не имеет параметров, у второго варианта два параметра — UserID и Password, в которых можно передать соответственно имя и пароль пользователя БД (они могут быть переданы так же, как часть строки ConnectionString). У компонента TADOCnnection есть и хорошо знакомое нам свойство LoginPrompt, которое определяет, будет ли выводиться окно с запросом имени и пароля при каждой попытке соединиться с сервером БД. Соединение закрывается вызовом метода Close. Свойство ConnectOptions определяет тип соединения. Можно создавать синхронное и асинхронное соединения. По умолчанию создаются синхронные соединения, но если сервер БД работает очень медленно, вы можете выбрать асинхронное соединение, которое избавит компоненты вашего приложения от лишних "зависаний". Свойство IsolationLevel определяет уровень изоляции транзакции — уровень видимости изменений, выполняемых транзакциями при одновременном обращении нескольких транзакций к одной таблице. Например, назначенное свойству IsolationLevel значение ilReadUncommitted указывает, что изменения, сделанные транзакцией, могут быть видимы для других транзакций до того, как данная транзакция вызовет COMMIT. Значение ilReadCommitted, наоборот, ука- Глава 8. Доступ к базам данных с помощью dbGO 171 зывает на то, что изменения, сделанные данной транзакцией, могут быть видимы для других транзакций только после их закрепления в БД. Описания других возможных значений свойства IsolationLevel вы найдете в справочной системе BDS. Свойство Provider содержит имя провайдера (поставщика интерфейса ADO), который в данный момент используется объектом ADO Connection, лежащим в основе данного объекта TADOConnection. Информация о провайдере на данный момент также содержится в строке ConnectionString. Права доступа к БД, которыми обладает соединение, определяются значением свойства Mode: cmUnknown — указывает на отсутствие определенных ограничений доступа; cmRead — указывает, что соответствующее соединение может читать данные, но не может их модифицировать; cmWrite — позволяет соединению только модифицировать данные, но не читать их; cmReadWrite — указывает на то, что у соединения есть права на чтение и запись данных; cmShareDenyRead — указывает, что пока данное соединение активно, другие пользователи не могут открыть соединение, имеющее право на чтение; cmShareDenyWrite — указывает, что пользователи не могут открыть соединение с разрешением на запись; cmShareExclusive — запрещает другим пользователям открывать соеди- нение; указывает на то, что пока данное соединение активно, другие пользователи не могут открывать соединения с какими-либо правами доступа. Естественно, что любые ограничения прав доступа для других соединений распространяются только на тот источник данных, которым пользуется текущее соединение. cmShareDenyNone Свойство KeepConnection определяет, может ли данное приложение поддерживать связь с базой данных, если у него нет открытых наборов данных. Если свойству присвоено значение True, соединение будет удерживаться в открытом состоянии. Назначение свойству KeepConnection значения True значительно уменьшает сетевой трафик и увеличивает скорость работы тех приложений, которые работают с удаленными СУБД, а также приложений, которые часто открывают и закрывают наборы данных. Для получения прямого доступа к объекту ошибок ADO Errors следует обратиться к свойству Errors. 172 Часть II. Программирование приложений БД для Win32 Свойство DataSets содержит массив активных наборов данных, связанных с компонентом, реализующим соединение. Компонент TADODataSet Этот компонент реализует базовый набор данных для технологии ADO и его можно по праву рассматривать как некий аналог компонента TClientDataSet. Важное отличие компонента TADODataSet от компонента TClientDataSet заключается в том, что компонент TADODataSet может быть напрямую связан с компонентами, реализующими доступ к базам данных. Возможности компонента TADODataSet чрезвычайно широки и... не очень определенны. Причина этого кроется в том, что компонент TADODataSet представляет собой надстройку над объектами ADO Recordset, а возможности этих объектов существенно зависят от поставщиков соответствующих сервисов. Компонент TADODataSet наследует многие важные свойства и методы от своего предка — класса TCustomADODataSet. Свойство Connection должно указывать на объект класса TADOConnection, реализующий соединение с сервером БД. Свойство ConnectionString аналогично по смыслу одноименному свойству компонента TADOConnection. Как и при работе с компонентом TSQLDataSet, в свойстве CommandText можно задать текст команды, при помощи которой будут получены данные. Значением свойства CommandText может быть строка SQL-запроса, название таблицы или хранимой процедуры. В свойстве CommandType указывается тип исполняемой команды. В отличие от компонента TSQLDataSet, компонент TADODataSet не может выполнять команды SQL, не возвращающие наборы записей (такие как операторы DELETE, INSERT и UPDATE). Свойство LockType, унаследованное от базового класса TCustomADODataSet, позволяет определить блокировки, налагаемые на набор данных при его открытии (блокировки нужно устанавливать до открытия соединения). Значение ltUnspecified указывает, что тип блокировки не определен, так что будут действовать только блокировки, установленные источником данных. Значение ltReadOnly указывает блокировку "только для чтения". С помощью значения ltPessimistic вы можете указать, что другие наборы данных не могут редактировать запись до тех пор, пока она не будет записана в хранилище данных. Значение ltOptimistiс (заданное по умолчанию) указывает, что блокировка налагается на запись только в момент внесения изменений. Значение ltBatchOptimistic налагает блокировку на пакет записей во время записи в хранилище данных. Глава 8. Доступ к базам данных с помощью dbGO 173 Для того чтобы получить сведения о состоянии набора данных, следует обратиться к свойству RecordsetState. Если свойство RecordsetState содержит значение stOpen, значит, набор записей, лежащий в основе набора данных, готов к выполнению запросов. Значение stExecuting сигнализирует о том, что набор записей в данный момент выполняет команду, а значение stFetching указывает нам, что набор записей занят извлечением данных из таблицы. Свойство CursorLocation определяет, поддерживается ли курсор клиентом (значение clUseClient) или сервером (значение clUseServer). От значения этого свойства зависит, в частности, будет ли поддерживаться кэширование данных (кэширование поддерживается, если свойству CursorLocation присвоено значение clUseServer). Свойство CursorType определяет тип курсора набора данных ADO. Тип курсора набора данных указывает направление, в котором будет производиться перемещение по набору данных и, в соответствии с этим, отображение видимых записей. Тип курсора, который должен быть установлен перед активацией компонента набора данных, определяется одним из следующих значений: ctUnspecified — указывает, что тип курсора не определен и будет выбран в соответствии с возможностями источника данных; ctOpenForwardOnlу — задает использование курсора, допускающего перемещение по записям только в одном направлении — от первой к последней; ctKeyset — позволяет использовать двунаправленный курсор, обеспечивающий возможность просмотра записей, добавленных и удаленных другими пользователями; ctDynamic — указывает, что данный тип курсора является двунаправленным, и обеспечивает просмотр всех изменений, внесенных в набор данных; ctStatic — позволяет использовать двунаправленный курсор, получающий копию набора данных и, соответственно, игнорирующий изменения, внесенные другими пользователями. Используется в основном при создании отчетов. Значение свойства булевого типа EnableBCD определяет, должны ли числовые значения интерпретироваться как числа с плавающей точкой или как числа в формате BCD. Если сервер БД отказывается принимать значения числовых полей, попробуйте переключить значение свойства EnableBCD. Целочисленное свойство CacheSize определяет размер кэша набора данных. После передачи клиенту пакета записей они размещаются в буфере локальной памяти. Когда приложение перемещается по набору данных, ему пересылаются строки, расположенные в буфере. Свойство RecordStatus определяет 174 Часть II. Программирование приложений БД для Win32 статус текущей записи (была ли запись добавлена, изменена, удалена или вообще не подвергалась изменениям). Данное свойство может также предоставить информацию о том, почему строка не была сохранена после ее модификации, удаления или добавления. Значением, которое возвращает это свойство, является подмножество набора констант типа TRecordStatus. В наборе определены следующие константы: rsOK — запись была успешно сохранена; rsNew — новая запись в наборе; rsModified — rsDeleted — запись была изменена; запись была удалена; rsUnmodified — rsInvalid — запись не изменялась с момента обновления набора; запись не сохранена из-за ошибки; rsMultipleChanges — запись не может быть сохранена, т. к. это повлияет сразу на несколько записей; rsPendingChanges — запись не может быть сохранена из-за ссылки на несохраненные изменения; rsCanceled — операция с записью была отменена; rsCantRelease — запись была заблокирована; rsConcurrencyViolation — запись не была сохранена из-за того, что использовалась оптимистическая блокировка; rsIntegrityViolation — нарушение ссылочной целостности; rsMaxChangesExceeded — все изменения не могли быть сохранены из-за слишком большого их количества; rsObjectOpen — конфликт с открытым объектом хранилища данных; rsOutOfMemory — не хватает оперативной памяти (да, такое иногда случа- ется); rsPermissionDenied — данный пользователь не имеет прав на совершение данной операции; rsSchemaViolation — rsDBDeieted нарушение структуры базы данных; — запись была ранее удалена. Помимо возможностей фильтрации, унаследованных от класса TDataSet, компонент TADODataSet поддерживает унаследованную от класса TCustomADODataSet фильтрацию записей на основе их состояния. Свойство FilterGroup задает условие фильтрации записей, основываясь на их состоянии. Характер свойства FilterGroup предполагает, что его значения будут устанавливаться во время выполнения программы, поэтому данное свойство Глава 8. Доступ к базам данных с помощью dbGO 175 не отображается в Инспекторе объектов. Перечислим допустимые значения свойства FilterGroup: fgUnassigned — условия фильтрации не заданы; fgNone — нет фильтрации. Указывает, что условия фильтрации снимаются. Использование этого значения равноценно присваиванию свойству Filtered значения False; fgPendingRecords — должны отображаться те записи, которые были изменены, но не были сохранены в хранилище данных, или те, для которых изменения были отменены; fgAffectedRecords — должны отображаться только записи, затронутые последним обновлением набора данных; fgFetchedRecords — должны отображаться записи, находящиеся в кэше. Речь идет о записях, полученных в результате последнего обращения к хранилищу данных; fgPredicate — отображать только удаленные записи; fgConflictingRecords — отображать записи, которые не удалось сохранить в хранилище данных вследствие ошибок. Для того чтобы фильтрация могла быть выполнена, необходимо присвоить свойству LockType значение HBatchOptimistic, а свойству Filter — значение True. Рассмотрим теперь некоторые методы компонента TADODataSet. Метод UpdateStatus позволяет определить текущее состояние записи. Метод SaveToFile позволяет сохранить содержимое набора записей ADO в файле на диске. Первым параметром этого метода является имя файла, а вторым параметром — формат сохранения данных. По умолчанию данные сохраняются в формате pfADTG. Метод LoadFromFile позволяет загрузить набор записей ADO из файла. Поскольку компонент TADODataSet поддерживает двунаправленные курсоры, вполне естественно ожидать наличия у него методов, осуществляющих поиск в наборе данных. Методы Lookup, Locate и Seek работают аналогично одноименным методам клиентских наборов данных. Важную роль в работе компонента играет метод Supports, который позволяет определить, поддерживает ли текущий набор записей определенные операции с курсором. Вызывая этот метод, вы передаете ему одно из значений типа TCursorOption. Если данная операция с курсором поддерживается, метод возвращает True, в противном случае — False. Например, для того чтобы узнать, допускает ли набор записей модификацию уже существующих записей, мы вызываем метод Supports с аргументом coUpdate. Если мы хотим узнать, 176 Часть II. Программирование приложений БД для Win32 можно ли использовать метод Seek для поиска в наборе, мы вызываем метод Supports с аргументом coSeek. Перечень остальных значений типа TCursorOption вы найдете в справочной системе. Метод Requery позволяет обновить набор данных. Каждый раз после обновления набора записей вызывается метод-обработчик события OnFetchComplete. Параметр Error метода-обработчика является указателем на одноименный объект ADO Error. Этот объект позволяет получить информацию об ошибках, ежели таковые возникли в процессе обновления набора данных. Через параметр EventStatus обработчику передается сообщение об успешном или неуспешном выполнении данной операции. В режиме асинхронного соединения с сервером БД периодически вызывается событие OnFetchProgress, задача которого — информировать приложение о том, как продвигается процесс обновления набора записей. Перед перемещением курсора с одной записи на другую (а также перед добавлением новой записи и закрытием набора данных) вызывается метод-обработчик события OnWillMove. Параметр этого обработчика Reason содержит константу, указывающую на метод, который осуществляет перевод курсора. Параметр EventStatus позволяет обработчику узнать, успешно ли выполняется операция. После завершения перемещения курсора вызывается метод-обработчик события OnMoveComplete. У метода обработчика OnMoveComplete тот же список аргументов, что и у OnWillMove, плюс указатель на объект Error, который содержит информацию об ошибках, которые могли возникнуть на стороне сервера или в системе ADO. Мы рассмотрели далеко не все свойства, методы и события компонента TADODataSet. Помимо многочисленных элементов, унаследованных от своего непосредственного предка — класса TCustomADODataSet, компонент TADODataSet наследует свойства, методы и события класса TDataSet, о котором мы уже неоднократно упоминали. Простое приложение dbGO В качестве примера мы напишем приложение, работающее с таблицей, хранящей графические данные. Прежде всего, нам понадобится сама таблица. На прилагаемом компакт-диске вы найдете файлы сценариев для SQL Server и Oracle. Имена файлов сценариев — CreatePicturesTableSServ.sql для SQL Server и CreatePicturesTableOracle.sql для Oracle. Вариант сценария для Oracle приводится в листинге 8.1. Листинг 8.1. Сценарий создания таблицы для Oracle Глава 8. Доступ к базам данных с помощью dbGO 177 CREATE TABLE "DELPHIUSER"."DPICTS" ( "PICTID" NUMBER(38,0) NOT NULL ENABLE, "USERNAME" VARCHAR2(32 BYTE) NOT NULL ENABLE, "TITLE" VARCHAR2(32 BYTE), "USERCOMMENT" VARCHAR2(1024 BYTE), "PICTURE" CLOB, CONSTRAINT "DPIKT_PK" PRIMARY KEY ("PICTID") ENABLE, UNIQUE ("TITLE") ENABLE ) TABLESPACE DELPHI_PICTURES; Вариант сценария для SQL Server показан в листинге 8.2. Листинг 8.2. Сценарий создания таблицы для Oracle USE [DelphiDemo] GO CREATE TABLE [DPICTS] ( [PICTID] INTEGER IDENTITY(1, 1) PRIMARY KEY, [USERNAME] NVARCHAR(32) NOT NULL, [PICTURE] IMAGE NOT NULL, [TITLE] NVARCHAR(32) UNIQUE, [USERCOMMENT] NVARCHAR(1024), ) GO Таблица, хранящая изображения, носит имя DPICTS. Поле таблицы PICTID представляет собой первичный ключ таблицы и содержит числовые идентификаторы записей. В поле USERNAME должно храниться имя пользователя, загрузившего изображение, в поле TITLE — название изображения, а в поле USERCOMMENT — комментарий к изображению. Обратите внимание, что на поле TITLE наложено ограничение уникальности. Само изображение должно храниться в поле PICTURE типа CLOB в варианте для Oracle и типа IMAGE в варианте для SQL Server. Таблица, хранящая изображения, будет занимать на диске довольно много места, поэтому на сервере Oracle мы создаем для нее специальную табличную область (tablespace) DELPHI_PICTURES. Сценарий, создающий табличную область, вы найдете в файле CreateTablespaceOracle.sql. Этот сценарий нужно запускать до запуска сценария создания таблицы от имени пользователя system. Я выделил под табличную область DELPHI_PICTURES 100 Мбайт, вы можете выделить больше (или меньше), если сочтете нужным. Если вы собираетесь работать с таблицей DPICTS под Oracle, то вам нужно выполнить еще один сценарий. В варианте сценария для SQL Server мы по- 178 Часть II. Программирование приложений БД для Win32 мечаем поле PICTID как IDENTITY. Это значит, как вы уже знаете, что значения этого поля будут задаваться автоматически, и при добавлении новой записи в таблицу нам даже не нужно будет обращаться к этому полю. Нечто подобное мы можем сделать и для Oracle. Мы знаем, что для заполнения значений полей-идентификаторов можно использовать последовательности. Однако по умолчанию значение поля, для которого создана последовательность, все равно нужно указывать явным образом при добавлении записи. Мы можем изменить это, создав триггер, который будет автоматически заполнять поле PICTID значениями последовательности при добавлении записи в таблицу. Последовательность и триггер создаются сценарием, который хранится в файле CreateTriggerOracle.sql. Текст сценария приводится в листинге 8.3. Листинг 8.3. Сценарий создания таблицы для Oracle CREATE SEQUENCE "DELPHIUSER"."PICTID_SEQ" MINVALUE 1 MAXVALUE 10000000 INCREMENT BY 1 START WITH 1 CACHE 20 NOORDER NOCYCLE ; CREATE TRIGGER PICTID_TRIGGER BEFORE INSERT ON DPICTS FOR EACH ROW BEGIN SELECT PICTID_SEQ.NEXTVAL INTO :NEW.PICTID FROM DUAL; END; Для того чтобы пользователь DelphiUser мог выполнить этот сценарий, у него должны быть права на создание триггеров (соответствующее право дается пользователю в сценарии CreateTablespaceOracle.sql). Теперь, когда таблица готова, мы можем приступить к созданию приложения-редактора таблицы. На компакт-диске, в каталоге ADODemo, вы найдете полные исходные тексты этого приложения (на языке C++). В форме приложения мы, первым делом, размещаем компонент TADOConnection. В Инспекторе объектов выделите свойство ConnectionString и щелкните по кнопке с многоточием. Откроется окно редактора строки связи (рис. 8.1). Глава 8. Доступ к базам данных с помощью dbGO 179 Рис. 8.1. Редактор строки соединения Для создания соединения мы могли бы использовать файл Data Link, если бы он у нас был. Поскольку подходящего файла в нашем распоряжении нет, мы выбираем опцию Use Connection String (Использовать строку связи) и щелкаем кнопку Build... (Создать…). Откроется окно настроек Windows с несколько корявым названием Свойства связи с данными (рис. 8.2). На первой вкладке этого окна нам нужно выбрать поставщика данных. Если вы работаете с SQL Server, выбирайте пункт Microsoft OLE DB Provider for SQL Server. Если же вы используете Oracle, у вас появляется возможность выбора. В каждой системе установлен поставщик OLE DB для Oracle, разработанный Microsoft (пункт Microsoft OLE DB Provider for Oracle). В системе, где установлен Oracle Server или Oracle Client, нам так же доступен провайдер OLE DB, разработанный Oracle (в списке поставщиков ему соответствует пункт Oracle Provider for OLE DB). Выбор провайдера остается за вами. По моим наблюдениям, провайдер, разработанный Oracle, лучше взаимодействует с этой СУБД. Однако он установлен не в каждой системе, так что вам придется распространять его вместе со своей программой, написанной для Oracle. Далее мы переходим на вкладку Подключение (рис. 8.3). 180 Часть II. Программирование приложений БД для Win32 Рис. 8.2. Редактор связи Внешний вид этой вкладки будет зависеть от выбранного поставщика OLE DB. В любом случае на ней необходимо указать адрес сервера БД, параметры авторизации на сервере, а также проверить созданное подключение с помощью одноименной кнопки. Если вы сколько-нибудь знакомы с той СУБД, с которой собираетесь работать, заполнение полей ввода этого окна не вызовет у вас проблем. Если созданное подключение работает, вы можете закрыть окно, нажав кнопку OK. Глава 8. Доступ к базам данных с помощью dbGO 181 Рис. 8.3. Вкладка Подключение В окне Свойства связи с данными есть еще несколько вкладок, но они нам сейчас не нужны. Теперь мы можем посмотреть на созданную строку связи. Она должна выглядеть примерно так: Provider=SQLOLEDB.1;Password=letmein; Persist Security \\ Info=True;UserID=delphiuser;Initial Catalog=DelphiDemo;\\ Data Source=sda\sqlexpress Разобраться в формате строки должно быть нетрудно. Помимо прочего, она содержит имя пользователя и пароль (поскольку при ее создании была выбрана опция Разрешить сохранение пароля). Мы можем убрать из строки связи не только пароль, но и имя пользователя. В этом случае нам придется указывать их при каждом соединении с БД. Закроем окно редактора строки связи, щелкнув кнопку OK, и вернемся к редактору форм. Наш компонент TADOConection (объект ADOConection1) настроен на связь c БД. Добавим в форму компонент TADODataSet (объект ADODataSet1). Свойству Connection этого 182 Часть II. Программирование приложений БД для Win32 объекта назначим ссылку на объект ADOConection1. Теперь выделим в Инспекторе объектов свойство CommandText и щелкнем по кнопке с многоточием. Откроется окно редактирования SQL-запроса, очень похожее на окно редактирования SQL-запроса компонента TSQLDataSet. С помощью этого окна или просто в поле свойства CommandText мы введем текст нашего запроса: select USERNAME, TITLE, USERCOMMENT, PICTURE from DPICTS Мы не запрашиваем значение поля PICTID, потому что и в случае SQL Server, и в случае Oracle значение этому полю присваивается автоматически, и нас это значение пока что не интересует. Свойству EnableBCD присвоим значение False. Значения остальных свойств оставим такими, какими они заданы по умолчанию. Присвоив свойству Active значение True, мы можем проверить, работает ли созданное соединение. Если все в порядке, мы приступаем к программированию графического интерфейса нашего приложения. Прежде всего, добавим в форму компонент TDataSource и назначим его свойству DataSet ссылку на объект ADODataSet1. Затем добавим в форму компоненты TDBNavigator и TDBCtrlGrid (для удобства их можно расположить на отдельных панелях TPanel). Свойствам DataSource обоих компонентов назначим ссылки на объект DataSource1. Свойству RowCount объекта DBCtrlGrid1 присвоим значение 1. Мы делаем это для того, чтобы в каждый данный момент в форме отображалась только одна запись набора данных ADODataSet1. Поскольку записи набора данных будут содержать изображения, размер панели объекта DBCtrlGrid1, выделенной под одну запись, может быть очень большим. На панели-шаблоне объекта DBCtrlGrid1 мы разместим два компонента TDBEdit, один компонент TDBMemo и компонент TDBImage. Свойствам DataSource всех этих компонентов уже присвоена ссылка на объект DataSource1. Осталось заполнить свойства DataField. Свойству DataField первого объекта TDBEdit мы присваиваем значение USERNAME. Соответствующее поле ввода будет предназначено для имени автора загружаемого изображения. Свойству DataField второго объекта TDBEdit присвоим значение TITLE. Это поле ввода будет служить для указания названия изображения. Свойству DataField объекта DBMemo1 присвоим значение USERCOMMENT. Таким образом, многострочное поле ввода будет предназначено для комментария к изображению. Наконец, свойству DataField объекта DBImage1 мы присвоим значение PICTURE. Наше приложение практически готово, осталось добавить немного кода. Наше приложение предназначено, кроме прочего, для добавления новых записей в таблицу. Это значит, что мы должны иметь возможность заполнять связанный с полем PICTURE объект DBImage1. Как заполнять объект TDBImage изображениями? Самый простой способ — загружать их из файла на диске. Добавим в форму приложения компонент TOpenDialog. Добавим кнопку, предназначенную для загрузки изображения. Текст обработчика события OnClick этой кнопки приводится в листинге 8.4. Глава 8. Доступ к базам данных с помощью dbGO 183 Листинг 8.4. Загрузка изображения из файла void __fastcall TForm1::BitBtn1Click(TObject *Sender) { if (OpenDialog1->Execute()) { TPicture * pic = new TPicture(); pic->LoadFromFile(OpenDialog1->FileName); DBImage1->Picture->Bitmap->Assign(pic->Graphic); delete pic; } } На первый взгляд может показаться, что наша задача проста. У компонента TDBImage есть свойство Picture, ссылающееся на объект класса TPicture, у которого есть метод LoadFromFile. С помощью этого метода можно загрузить изображение в компонент TDBImage, однако есть одно "но". Компонент TDBImage способен обрабатывать данные только в формате BMP (то есть в виде простой битовой карты). Для того чтобы компонент TDBImage мог передать данные на сервер БД, изображение должно быть преобразовано в формат BMP. Для этого мы сначала загружаем изображение в объект pic класса TPicture, а затем с помощью метода Assign объекта DBImage1->Picture-> Bitmap превращаем изображение в битовую карту. К сожалению, наш метод позволяет загружать изображения только из файлов BMP (что мы могли бы сделать и непосредственно, используя метод DBImage1->Picture->Bitmap-> LoadFromFile), и из файлов JPEG. Но для начала этого не так уж и мало. Нам осталось только добавить кнопку, устанавливающую связь с сервером БД. Как и в случае с dbExpress, мы можем связать наше приложение с базой данных на этапе разработки. Для этого нужно присвоить значение Тrue свойству Active объекта ADODataSet. Однако делать этого не стоит. Не забывайте, что наше приложение работает с изображениями, хранимыми в таблице БД, а это связано с пересылкой больших объемов данных. Обработчик события OnClick кнопки, устанавливающей соединение (листинг 8.5), вряд ли нуждается в комментариях. Листинг 8.5. Команда соединения с базой данных void __fastcall TForm1::BitBtn3Click(TObject *Sender) { ADODataSet1->Active = true; } 184 Часть II. Программирование приложений БД для Win32 Теперь мы можем скомпилировать и запустить наше приложение. Редактирование таблицы, содержащей изображения, мало чем отличается от редактирования обычных таблиц (рис. 8.4). Для заполнения поля, предназначенного для изображений, необходимо загрузить графический файл BMP или JPG. Помните, что все изменения, которые вы делаете, автоматически фиксируются на сервере БД. Рис. 8.4. Редактор таблицы с изображениями Конструируем строку связи во время выполнения программы В рассмотренном варианте приложения все параметры связи с базой данных жестко зашиты в текст программы. В программах из "реальной жизни" такие параметры, как адрес сервера, пароль и имя пользователя, должны быть настраиваемыми во время выполнения программы. Программа может считывать их из файла конфигурации или каждый раз заставлять пользователя вводить их заново, но ее текст не должен содержать данных, которые могут измениться. В программе RuntimeAuth (полный исходный текст программы вы найдете в одноименном каталоге) мы (точнее, пользователь) задаем параметры соединения во время выполнения программы. В процессе запуска программа выводит диалоговое окно (рис. 8.5), в котором пользователь должен ввести имя сервера, свое и пароль. Глава 8. Доступ к базам данных с помощью dbGO 185 Рис. 8.5. Окно авторизации пользователя Окно авторизации реализовано в приложении в виде отдельной формы TAuthDlg в модуле ADlg.cpp. Окно авторизации создается в обработчике события OnShow главной формы приложения (листинг 8.6). Листинг 8.6. Обработчик OnShow главной формы void __fastcall TForm1::FormShow(TObject *Sender) { AuthDlg = new TAuthDlg(this); if (AuthDlg->ShowModal() == 1) { ADOConnection1->ConnectionString = "Data Source=" + AuthDlg->ServerNameEdit->Text + ";"; ADOConnection1->Open(AuthDlg->UserNameEdit->Text, AuthDlg->PasswordEdit->Text); ADODataSet1->Active = true; } else Application->Terminate(); delete AuthDlg; } Мы создаем диалоговое окно авторизации и делаем его видимым с помощью метода ShowModal (если метод возвращает значение 1, значит, была нажата кнопка OK). Имя сервера, введенное пользователем, возвращается в свойстве Text строки ввода ServerNameEdit. На основе этого значения мы формируем строку связи для объекта TADOConnection, которая в нашем случае имеет вид: "Data Source=имя_сервера;" Имя сервера — единственный параметр, который мы указываем явным образом в строке связи. Имя провайдера указано в свойстве Provider объекта ADOConnection1. В окне Инспектора объектов это свойство представляет собой 186 Часть II. Программирование приложений БД для Win32 раскрывающийся список, в котором можно выбрать один из провайдеров, установленных в системе. Я выбрал значение "OraOLEDB.Oracle". Для установки связи с SQL Server можно воспользоваться провайдером SQLOLEDB. После того как строка связи сформирована, мы вызываем метод Open объекта и передаем ему имя и пароль, введенные пользователем. Для подключения приложения к базе данных осталось присвоить значение Тrue свойству ADODataSet1->Active, что мы и делаем. Если пользователь щелкнул кнопку Отмена диалогового окна авторизации (т. е. если метод ShowModal вернул не 1, а другое значение), мы просто завершаем работу нашей программы. ADOConnection1 Оптимизация передачи данных В рассмотренном примере работы с таблицей DPicts набор данных TADODataSet загружал в память машины все записи, полученные в результате запроса. Это может быть неудобно, если записей много, тем более, что записи содержат большие объемы двоичных данных. Предположим, что размер таблицы DPICTS достиг 500 Мбайт. Допустим, вы запускаете программуредактор таблицы, которая соединяется с сервером по локальной сети. В момент открытия набор данных загружает все записи таблицы — все 500 Мбайт данных. Даже в хорошо отлаженной локальной сети передача 500 Мбайт может занять заметный промежуток времени. Более того, после загрузки эти 500 Мбайт будут "висеть" в оперативной памяти вашей системы до тех пор, пока вы не закроете набор данных. Думаю, я убедил вас, что было бы гораздо разумнее, если бы данные передавались по мере надобности, а не все сразу. В этом случае вы загрузили бы только те изображения, которые хотели бы, а процесс загрузки был бы растянут во времени и меньше действовал бы вам на нервы. Мы можем добиться этих целей, если программа, работающая с таблицей DPicts, будет запрашивать по одной записи из таблицы по мере того, как пользователь будет переходить от одной записи к другой. На прилагаемом компакт-диске вы найдете проект ImageViewer. Программа ImageViewer предназначена для просмотра данных из таблицы DPicts. Программа включает в себя стандартную цепочку компонентов TADOConnection, TADODataSet, TDataSource. Компонент TADOConnection настроен на соединение с таблицей DPicts. Свойству CommandText компонента TADODataSet присвоено значение: 'select PICTID, USERNAME, PICTURE, TITLE, USERCOMMENT from DPICTS' К объекту DataSource1 подключены три объекта класса TDBText, предназначенных для отображения значений полей USERNAME, TITLE и USERCOMMENT, и объект класса TDBImage для отображения содержимого поля PICTURE. Кроме Глава 8. Доступ к базам данных с помощью dbGO 187 того, в программе есть три кнопки. Кнопка ConnectButton предназначена для подключения к БД. Кнопки ForwardButton и BackwardButton используются для перехода к следующей и предыдущей записям таблицы соответственно. Вся "магия" работы приложения сосредоточена в настройках объекта ADODataSet1. У объекта есть свойство CacheSize, которое определяет, сколько записей набор хранит в локальной оперативной памяти. По умолчанию значение этого свойства равно 1, что означает, что в каждый данный момент времени в локальной памяти набора данных должна храниться только одна запись. При переходе к следующей или предыдущей записи набор данных затребует соответствующую запись у сервера. Как видите, это именно то, что нам нужно, однако в рассмотренном примере кэширование почему-то не работало. Все дело в значении свойства CursorLocation, компонента TADODataSet, которому по умолчанию присвоено значение clUseClient. Если курсором управляет клиентская сторона, набор данных вынужден загружать все записи, несмотря на параметры кэширования. В этом примере мы сменим значение свойства CursorLocation на clUseServer. Тем самым мы указываем, что курсор должен поддерживаться сервером. Свойству CursorType объекта ADODataSet1 мы присваиваем значение ctKeyset (двунаправленный курсор, позволяющий видеть изменения, внесенные другими пользователями). Весь код, который нам необходимо "вручную" добавить в наше приложение, сосредоточен в обработчиках событий OnClick трех кнопок (листинг 8.7). Рис. 8.6. Приложение просмотра таблицы DPicts 188 Часть II. Программирование приложений БД для Win32 Листинг 8.7. Обработчики OnClick трех кнопок procedure TForm1.ConnectButtonClick(Sender: TObject); begin ADODataSet1.Active := True; end; procedure TForm1.ForwardButtonClick(Sender: TObject); begin ADODataSet1.Next; end; procedure TForm1.BackwardButtonClick(Sender: TObject); begin ADODataSet1.Prior; end; Как видите, кода совсем немного. В результате мы получаем приложение просмотра таблицы (рис. 8.6), загружающее данные по мере необходимости. Сжатие данных Компонент TDBImage работает с данными в формате BMP, самом "многословном" формате хранения изображений. Именно в этом формате данные записываются в таблицу DPicts. Очевидно, что даже если записать в таблицу DPicts лишь несколько десятков изображений, таблица займет на диске очень много места (и выделенная для нее квота табличного пространства очень быстро закончится). Было бы гораздо разумнее сохранять данные в формате JPEG. Это тем более разумно, что большая часть изображений, которые вы захотите сохранить в таблице, скорее всего, будет распакована из файлов JPEG. Проблема заключается в том, что, как мы уже знаем, компонент TDBImage не умеет работать с форматом JPEG. Если мы хотим сохранять данные в формате JPEG, мы должны использовать другой компонент, например, TImage. Но TImage не умеет работать с данными. Это значит, что мы должны выполнить явным образом те действия, которые компонент, предназначенный для работы с данными, выполняет неявно, а именно, мы сами должны записывать и считывать графические данные из таблицы. В папке с примерами для Oracle вы найдете проект JPEGStorage, который сохраняет графические данные в формате JPEG. Приложение JPEGStorage должно взаимодействовать с таблицей, полностью аналогичной DPicts, но если в таблицу DPicts уже внесены какие-то данные в формате BMP, то при- Глава 8. Доступ к базам данных с помощью dbGO 189 ложение не сможет взаимодействовать с таблицей корректно. Вы можете либо очистить таблицу DPicts, либо создать ее копию (включая последовательность и триггер) под другим именем. За основу приложения JPEGStorage взято простое приложение dbGO, рассмотренное ранее. Вместо компонента TDBImage мы теперь используем компонент TImage. Остальные компоненты, работающие с данными, остались на своих местах. Свойству CursorLocation объекта ADODataSet1 присвоено значение clUseServer, а свойству CursorType — значение ctDynamic. Поскольку компонент TImage нельзя разместить на панели компонента TDBCtrlGrid, нам придется немного подредактировать расположение панелей. Мы модифицируем простое приложение dbGO таким образом, чтобы в режиме редактирования оно заполняло поле PICTURE данными в формате JPEG, а в режиме просмотра загружало бы эти данные в объект класса TImage. Рассмотрим сначала обработчик OnClick кнопки, которая загружает изображение из файла (листинг 8.8). Листинг 8.8. Обработчик OnClick, загружающий графические данные void __fastcall TForm1::Button1Click(TObject *Sender) { TJPEGImage * jimg = new TJPEGImage(); if (OpenDialog1->Execute()) { String FN = AnsiLowerCase(OpenDialog1->FileName); String ext = ExtractFileExt(OpenDialog1->FileName); if (ext == ".bmp") { Image1->Picture->Bitmap->LoadFromFile(FN); jimg->Assign(Image1->Picture->Bitmap); jimg->Compress(); } if ((ext == ".jpg") || (ext == ".jpeg")) { jimg->LoadFromFile(FN); Image1->Picture->Bitmap->Assign(jimg); } if (ADODataSet1->Active) { if ((ADODataSet1->State != dsEdit) && (ADODataSet1->State != dsInsert)) ADODataSet1->Edit(); ADODataSet1->FieldByName("PICTURE")->Assign(jimg); } } delete jimg; } 190 Часть II. Программирование приложений БД для Win32 Как и другие приложения из этой главы, приложение JPEGStorage может загружать изображения в форматах BMP и JPEG. В обработчике Button1Click мы проверяем, к какому формату принадлежит загружаемое изображение. Если это формат BMP, мы загружаем его в битовую карту объекта Image1 (в результате чего изображение отображается в окне программы). Далее мы конвертируем полученную битовую карту в формат JPEG с помощью объекта jimg класса TJPEGImage. В результате вызова метода jimg->Compress() мы получаем сжатое изображение в формате JPEG, хранящееся в объекте jimg. Допустим теперь, что открываемый файл имеет формат JPEG. В этом случае мы сначала загружаем его в объект jimg, а затем, с помощью метода Image1-> Picture->Bitmap->Assign создаем битовую карту для отображения в объекте Image1. Далее мы переходим к внесению изменений в базу данных. Помните, что в каком бы формате ни было загружено изображение, на данном этапе у нас есть объект jimg, который содержит это изображение в формате JPEG. Мы проверяем, находится ли набор данных ADODataSet1 в состоянии редактирования или добавления записей, и если набор не находится ни в том, ни в другом состоянии, переводим его в состояние редактирования. Если вы загружаете изображение, значит, вы хотите редактировать записи набора данных, верно? Далее, с помощью вызова метода: ADODataSet1->FieldByName("PICTURE")->Assign(jimg); мы загружаем данные в формате JPEG в поле PICTURE текущей записи (а значит, и в текущую строку таблицы). На этом часть работы, связанная с загрузкой в таблицу данных в формате JPEG, готова. Осталось организовать загрузку данных в объект Image1 в процессе просмотра таблицы. Преобразование данных мы выполняем в обработчике события AfterScroll объекта ADODataSet1 (листинг 8.9). Этот обработчик вызывается всякий раз, когда набор делает активной новую запись в результате вызова методов First, Last, MoveBy, Next, Prior, FindKey, FindFirst, FindNext, FindLast, FindPrior, и Locate. Листинг 8.9. Обработчик OnClick, загружающий графические данные void __fastcall TForm1::ADODataSet1AfterScroll( TDataSet *DataSet) { if (!(DataSet->FieldByName("PICTURE")->IsNull)) { TJPEGImage * jimg = new TJPEGImage(); Глава 8. Доступ к базам данных с помощью dbGO 191 TStream * stream = DataSet->CreateBlobStream( DataSet->FieldByName("PICTURE"), bmRead); jimg->LoadFromStream(stream); Image1->Picture->Bitmap->Assign(jimg); delete jimg; delete stream; } } Для распаковки данных мы опять используем объект класса TJPEGImage. Для того чтобы загрузить двоичные данные из поля записи набора данных, мы пользуемся методом CreateBlobStream. Этот метод возвращает объект класса TStream, предназначенный для чтения или для записи данных в поле типа BLOB. Первым аргументом метода CreateBlobStream является указатель на объект TField, представляющий поле, с данными которого мы собираемся работать. Вторым аргументом метода должна быть константа, указывающая, для чего открывается поток — для чтения, для записи или для того и для другого. Мы создаем поток, открытый для чтения, после чего загружаем данные в объект jimg класса TJPEGImage с помощью метода LoadFromStream. Далее нам остается только загрузить изображение в компонент TImage. Другие компоненты dbGo В заключение этой главы мы бегло рассмотрим еще несколько компонентов с панели инструментов dbGo. Компонент TADOTable Компонент TADOTable используется для доступа к хранилищам данных ADO и представления информации из них в виде таблиц. Этот компонент происходит от класса TCustomADODataSet, и располагает набором свойств и методов, сходным с набором элементов компонента TADODataSet. Можно сказать, что возможности компонента TADOTable представляют собой подмножество возможностей компонента TADODataSet. Компонент связывается с базой данных с помощью свойства Connection или ConnectionString. Имя таблицы указывается в свойстве ТаbleName. Во время разработки свойство ТаbleName превращается в раскрывающийся список, в котором можно выбрать таблицу базы данных (провайдер MS SQL Server поддерживает эту возможность, а провайдер Oracle — нет). Свойство ТаbleDirect указывает, каким образом набор данных связывается с хранилищем данных. Если свойству присвоено значение True, 192 Часть II. Программирование приложений БД для Win32 компонент использует фоновые операторы SQL для получения данных, если же свойству присвоено значение False, компонент сам создает оператор SELECT для выборки данных из таблицы. Используя свойство Readonly, можно установить для таблицы ограничение "только для чтения", запретив таким образом изменение данных. В свойстве MasterSource указывается компонент TDataSource, используемый для создания отношения Master-Detail. Метод GetlndexNames возвращает список индексов, доступных компоненту в качестве списка. Компонент TADOQuery Этот компонент, предназначенный для передачи SQL-команд серверу баз данных, также является потомком класса TCustomADODataSet. Компонент TADOQuery можно рассматривать как аналог компонента TSQLQuery для механизма ADO. Компонент связывается с базой данных с помощью свойства Connection или ConnectionString. Текст запроса записывается в свойство SQL. Запросы могут быть параметризованными, и в этом случае параметры запроса должны содержаться в свойстве-коллекции Parameters. Если заданный в свойстве SQL запрос возвращает набор данных, его следует открывать с помощью метода Open (или присвоив свойству Active значение True). Если запрос не должен возвращать набор данных, его следует выполнять с помощью метода ExecSQL. Метод ExecSQL возвращает число записей, затронутых выполнением запроса. Это же значение содержится в свойстве RowsAffected. Компонент TADOStoredProc Подобно компоненту TSQLStoredProc, компонент TADOStoredProc позволяет обращаться к хранимым процедурам, содержащимся в базах данных. Как и все компоненты, рассмотренные ранее, этот компонент происходит от класса TCustomADODataSet. Связь компонента с хранилищем данных осуществляется так же, как и в случае двух рассмотренных уже компонентов. Имя хранимой процедуры задается свойством ProcedureName. На этапе разработки имя хранимой процедуры можно выбрать из раскрывающегося списка (если провайдер OLE DB поддерживает эту возможность). Для определения входных и выходных параметров процедуры используется свойство Parameters. Через параметры хранимая процедура получает аргументы и возвращает результаты своей работы. В том случае, если хранимая процедура будет вызываться множество раз с одними и теми же аргументами, ее выполнение можно ускорить, назначив свойству Prepared значение True.