Опыт использования ADO для доступа к базам данных форматов MS Access, xBase и Paradox

{codecitation class=»brush: pascal; gutter: false;» width=»600px»}

Оформил: DeeCo

Автор: Иванов Денис Михайлович

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

Подвигло меня на написание этой статьи то обстоятельство, что когда я приступал к этой работе (я имею в виду использование ADO), я размещал свои вопросы во многих конференциях, а ответов на них не получено до сих пор и, более того, эти же вопросы стали задаваться по новой, а ответов на них как не было, так и нет. На некоторые из них я отвечал, а потом подумал, что не все будут просматривать конференцию целиком, да и когда все сведено в одном месте оно и лучше. Кроме того, толковой литературы по использованию ADO практически нет никакой. Например, мне не удалось найти в солидных по объему книгах г-на Архангельского необходимую мне информацию. Или еще пример — Microsoft Press ‘Справочник по OLE DB’. Здесь другой уклон — информации много, слишком много, а примеров никаких (но это вообще проблема справок от Microsoft — написано много, а примеров использования почти нет).

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

Причины перехода от BDE к ADO

Итак, чтобы было понятно что к чему, сначала поясню, зачем же понадобился переход к ADO. Я работаю программистом в компании, которая занимается написанием оболочки для создания геоинформационных систем (ГИС). То есть имеется некая красивая карта и необходимо получение каких-то атрибутивных данных по объектам на этой карте размещенным. При этом атрибутивные таблицы не имеют заранее установленной структуры — только некие предустановленные поля, которых пользователь не видит, но которые используются для связи объектов на карте и записей в базе данных.

Итак, для хранения атрибутивной информации был выбран формат MS Access, который имеет то обстоятельство, что все таблицы хранятся в одном файле (в отличие от Paradox и Dbase) и не требует при этом запущенного сервера, как, к примеру, Interbase. Необходима также связь с файлами форматов dbf и db для загрузки/выгрузки данных в/из БД. Для написания программы мы используем Delphi 4, а для подключения к файлам БД использовалась BDE. И все это было отлично, но вот появились два важных обстоятельства:

Вышел MS Access 2000. BDE отказывается работать с этим форматом. Как мне удалось найти ответ после долгих поисков на сайте Inprise — Inprise не знает как производить коннект к этому формату. Цитата: ‘Для доступа к данным MS Access мы используем DAO 2.5, который не может работать с форматом MS Access 2000. Если Вам необходим доступ к БД формата MS Access 2000, используйте, пожалуйста, компоненты ADO Delphi 5. По нашей (возможно неверной) информации причина здесь в отсутствии официальной документации от Microsoft.

2. Была найдена интересная особенность поведения BLOB потоков под управлением Windows NT 4. Допустим, нам необходим доступ к BLOB полям таблиц в БД формата MS Access 97. Как произвести подключение через BDE к MS Access 97 я говорить не буду, т.к. многие знают, а кто не знает, тот легко найдет эту информацию. Итак, подключение произведено. Отлично. Вот фрагмент программы:

var

AStream: TBLOBStream;

Data: Integer;

begin

// Открываем таблицу (обычный TTable)

ATable.Open;

// Создаем поток.

AStream := TBLOBStream(ATable.CreateBLOBStream(ATable.FieldByName(‘Поле’)));

// Что-либо читаем из него.

AStream.Read(Data, SizeOf(Data));

// Освобождаем поток и закрываем таблицу.

AStream.Free;

ATable.Close;

end;

Казалось бы — абсолютно тривиальный код. НО! Строка, где производится чтение из потока, вызывает исключительную ситуацию — ‘External error — EEFFACE’. И в исходных текстах VCL от Delphi 5 мы находим потрясающее объяснение — это, оказывается, ‘C Exception’. Интересно, а при чем тут C ? Единственный ответ, какой я знаю, — Delphi написана на C .

Плюс ко всему, если вы запускаете эту программу из-под Delphi — ошибка не возникает, а если запускаете ее прямо в Windows — ошибка будет непременно. Я дошел в своих поисках до вызовов прямых вызовов BDE API — вот там-то ошибка и возникает, так что я думаю тут очередная ошибка BDE, хотя я использовал на тот момент самую последнюю версию с сайта Inprise — BDE 5.11.

Так что, господа, если Вы используете нечто подобное в своих программах, то знайте, что под Windows NT 4.0/Windows 2000 Ваши программы работать не будут. Самое интересное, что компоненты из библиотеки VCL, которые используют подобный метод для получения данных (к примеру, TDBRichEdit) тоже не работают!

Итак, этих двух причин оказалось достаточно для нашей фирмы, чтобы начать переход от BDE к ADO.

ADO и файлы формата MS Access

— Учитель, почему ты обманул меня? Ты сказал, что Вейдер предал и убил моего отца, а теперь оказалось, что он и есть мой отец!

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

— С определенной точки зрения?

— Люк… ты вот увидишь сам… что очень многие истины зависят от нашей точки зрения.

(Звездные войны. Эпизод 6.)

К чему я привел эту цитату — в результате всей этой работы я пришел к выводу, что у нас, программистов, и у Microsoft разный взгляд на фразу ‘Обеспечивается доступ к данным’. Мы (ну или, по крайней мере, я) в этой фразе видим следующее содержание ‘обеспечивается доступ к данным для их просмотра и РЕДАКТИРОВАНИЯ (т.е. редактирование, удаление и добавление новых данных)’. Что имеет в виду Microsoft можно только догадываться, но явно, что без особых проблем достигается только просмотр данных. Кроме того, практически все примеры в литературе ограничиваются получением данных именно для просмотра, после чего следует несколько бодрых фраз и все заканчивается. Как говорится выше — разные точки зрения…

Итак, прежде всего, работа была ограничена условием разработки в Delphi 4. Причин этому много, но к этой статье это отношения не имеет. Просто — программа, разработанная в Delphi 4 должна работать через ADO. Поэтому приступили к поиску компонент, обеспечивающих такую работу. Нашли их довольно много, как платных, так и бесплатных. Все, что будет написано, одинаково и для всех вариантов и даже для Delphi5. Исключение составляет только работа с закладками в Delphi 5.

ADO была взята на тот момент самая последняя версия с сайта Microsoft — это ADO 2.6.

Итак, возьмем файл mdb формата MS Access 97. Его можно сделать с помощью хотя бы самого Access. И создадим там небольшую таблицу, к примеру, такую:

Object_ID Integer — идентификатор объекта на карте

Object_Description Text (50) — описание объекта на карте

Введем туда какие-либо данные (абсолютно все равно какие). Только надо учесть, что в силу специфики работы у нас могут быть описания, которым пока объекты не соответствуют. Такая связка будет выполнена позже пользователем. Ну, попробуем вывести содержимое таблицы в DBGrid. Ага, получилось. Например, как на картинке: Вроде как все нормально и доступ к данным мы получили.

А теперь давайте, вообразим себя пользователями и попробуем что-нибудь исправить или добавить. Например, добавим несколько пустых записей и попробуем внести туда данные. Добавляем. Нормально. Теперь внесем данные и нажмем POST. И что мы видим?

Ага. Интересно, а при чем тут ключ, если у нас на таблицу ключ не наложен? Пробуем добавить новую запись, удалить запись без Object_ID. Результат одинаков — все то же сообщение об ошибке. И что же делать? Запускаем MS Access, пробуем там, и видим, что там все отлично. Стало быть, что-то не так мы делаем с ADO. И тут мы вспоминаем, что когда мы создавали таблицу в MS Access, он предлагал создать ключевые поля для этой таблицы. А после долгих поисков в ADO SDK я нашел этому такое объяснение: ADO предполагает, что таблица будет в первой нормальной форме. Если кто не помнит главное требование первой формы — отсутствие повторяющихся записей.

В данном случае мы не можем создать ключ на то, что есть. Что же делать? И тут приходит на ум простое решение: добавим еще одно поле, чтобы каждая запись была однозначно определена (т.е. некий внутренний идентификатор). Чтобы не думать о содержимом этого нового поля, делаем совсем просто — пусть это будет автоинкрементное поле, и создадим на него первичный ключ. Отлично! Делаем — все работает. Пока мы не добавляем больше одной записи. Если мы их добавим подряд несколько, мы увидим очень интересную ситуацию как на картинке.

Что здесь интересного? А то, что содержимое Internal_ID для всех этих записей равно нулю, хотя это автоинкрементное поле! И Table.Refresh здесь не помогает! Только закрытие и последующее открытие таблицы приводит к тому, что мы видим то, что и ожидалось.

А пока мы не имеем правильных идентификаторов, наличие такого поля не дает ничего. Выше приведенные ошибки будут продолжать сыпаться как из рога изобилия. Но вот только закрывать — открывать таблицу каждый раз после добавления новой записи для того, чтобы автоинкрементное поле принимало правильные значения — это сильно. Так не пойдет. Вот так ADO, подумал я, а давай-ка попробуем MS Access 2000. И тут оказалось, что там все нормально работает: добавляем запись, делаем сохранение (Post) автоинкрементное поле тут же принимает правильное значение.

В результате я могу сделать только один вывод — Microsoft активно, всеми доступными средствами, пытается заставить пользователей переходить к своим новым продуктам.

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

Ну а чтобы пользователь не видел этого внутреннего идентификатора (он ведь нужен только нам) делаем это поле невидимым. Надеюсь, что все знают, что это делается через TField.Visible := FALSE.

Кто-нибудь может возразить: а зачем нам такой идентификатор, мы можем записи идентифицировать по каким-нибудь своим полям. Ради Бога! Но тут есть еще одна проблема и эта проблема называется закладки.

Проблемы закладок нет в Delphi 5, потому что там вокруг Bookmark сделан класс ими управляющий, а я имею в виду работу с закладками через ADO. Смотрим опять же в ADO SDK и видим там такое описание:

‘Recordset.Bookmark: Устанавливает или возвращает закладку, которая однозначно определяет текущую запись в Recordset. При создании или открытии объекта Recordset каждая из его записей получает уникальную закладку. Для того чтобы запомнить положение текущей записи, следует присвоить текущее значение свойства Bookmark переменной. Для быстрого возвращения к сохраненному в переменной указателю текущей записи в любое время после перехода на другую запись следует указать в значении свойства Bookmark объекта Recordset значение этой переменной’.

Казалось бы, какие проблемы? А вот какие: возвращаемое значение всегда одно и тоже для любой записи. И когда мы устанавливаем этот, с позволения сказать, Bookmark, ничего не происходит. И только наш внутренний идентификатор поможет в такой ситуации, кроме того, его значение всегда имеет смысл, даже после закрытия и повторного открытия таблицы, что, в общем-то, удобно.

После того как все заработало, я решил проверить скорость работы ADO. У нас может быть ситуации, когда в таблицу добавляется сразу большое количество записей, к примеру, 50-60 тысяч записей за раз. Так вот, когда использовалась BDE, такая операция занимала максимум 10 минут. Угадайте, чему стало равно это время при использовании ADO? Минимум 25 минут на той же самой машине. Если после этого мне будут говорить, что ADO быстрее BDE чуть ли не в 2 раза — позвольте мне с Вами не согласиться.

Итак, для нормальной работы мы должны иметь таблицы в первой нормальной форме, для этого делаем автоинкрементное поле с уникальным индексом. Кроме того, если мы можем добавлять больше одной записи за один раз и потом сразу возможно будем их редактировать, нам надо использовать файлы MS Access 2000.

ADO и файлы xBASE и Paradox

Итак, мы смогли наладить работу через ADO к файлам формата MS Access. Но ведь мы можем и должны использовать файлы xBase и Paradox в качестве обменных файлов.

Попробуем это сделать. Все примеры какие я видел в книгах работают одинаково — через ‘Microsoft OLE DB provider for ODBC’. А все редакторы, которые делают строку подключения, всегда показывают только mdb файлы в диалоге, в котором задается путь к файлу БД. Что-то тут нечисто, подумал я — а как же тот же самый Access это делает? Ведь явно не через ODBC, стало быть, есть какая-то хитрость.

После примерно недельных поисков в Интернете решение было найдено. Да, действительно можно использовать ‘Microsoft Jet 4.0 OLE DB Provider’. Чтобы не рассказывать долго, представим, что у нас на диске D в корне лежит файл Test.dbf формата dBase 5.0.

Строка коннекта для этого случая будет выглядеть так:

{‘Provider=Microsoft.Jet.OLEDB.4.0;Data Source=D:\;

Extended Properties = dBase 5.0;

Mode = Read|Write|Share Deny None;

Persist Security Info = True’;}

И это все. Самое интересное во всей это строке — секция ‘Extended Properties’.

Чтобы знать, что конкретно для разных форматов надо писать в Extended properties, загляните в реестр Windows на следующую ветку:

HKEY_LOCAL_MACHINE\Software\Microsoft\Jet\4.0\ISAM Formats

Там перечислены все поддерживаемые в данном случае форматы.

После опытов над форматом dbf оказалось, что все выше сказанное для формата mdb совершенно не относится к этому формату — и все требования про первую форму можно и не соблюдать! В общем, загадка природы.

А вот формат Paradox — это оказалась песня на меньшая, чем mdb. И вот почему — здесь все требования о первой форме таблицы в действии, но ведь мы не можем создавать таблицу, потом говорить пользователю ‘Слышь, мужик, а теперь метнулся, запустил Paradox и создал первичный ключ на эту таблицу. А потом нажмешь на ОК и мы продолжим’. Это несерьезно. Стало быть, этот ключ надо создавать нам самим.

Хорошо, запускаем справку по MS Jet SQL и ищем раздел создания индексов или первичных ключей. Находим следующее:

CREATE INDEX имя_индекса on название_таблицы(название_поля)with PRIMARY.

ALTER TABLE название_таблицы ADD CONSTRAINT имя_ограничения PRIMARY

KEY(название_поля)

Все далее сказанное абсолютно одинаково для обоих вариантов.

Предположим, что наша таблица называется ExpTbl.db и поле, на которое мы хотим наложить первичный ключ, называется IntrernalID. Хорошо, подключаемся к таблице и задаем такую строку SQL для исполнения:

CREATE INDEX My_Index ON ExpTable (InternalID) WITH PRIMARY

Запустим на выполнение. Ого, а что это мы видим? Вот те на — очередное сообщение об ошибке. При этом сообщение как всегда очень содержательное применительно к нашему случаю.

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

Вывод один — опять очередное требование ADO, которое сразу не поймешь. Ладно, запускаем он-лайн MS MSDN и делаем запрос на PARADOX. Видим что-то около 50 документов. И где-то в 35-36 документе я нашел ответ маленькими буковками внизу экрана! Сейчас я вам скажу в чем проблема — держитесь крепче: имя первичного ключа должно совпадать с названием таблицы, а имена индексов с именами полей. Неслабо.

Исправляем SQL:

CREATE INDEX ExpTable ON ExpTable (InternalID) WITH PRIMARY

Запускаем, смотрим — все отлично.

Чтобы никто больше мучился с этим делом, я хотел бы привести самые значащие ограничения для драйвера PARADOX, которые я нашел в MSDN:

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

Первичный ключ должен быть определен для первых ‘n’ полей таблицы.

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

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

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

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

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

Итак, для работы через ADO с файлами xBase или Paradox, нам необходимо указывать нужный драйвер в секции Extended Properties и в секции Data Source только путь до файла. Для xBase на этом все трудности закончены, а вот для Paradox необходимо задание первичного ключа как для формата MS Access, при этом есть определенные ограничения при задании названий ключей, так же как и возможных индексов.

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

Для добавления данных в эти таблицы, мы можем вставлять их по одной (Table.Append (Insert); Table.Post), а можем воспользоваться вариантом SELECT … INTO, INSERT … INTO. Поговорим теперь именно о втором варианте работы.

Смотрим файл справки MS Jet SQL.

SELECT поле_1[, поле_2[, …]]INTO новаяТаблица[in внешняяБазаДанных]

FROM источник

Ладно, пробуем. Пусть мы имеем в качестве источника данных mdb файл и хотим сохранить данные из таблицы SourceTable в таблицу формата Paradox 7.0 TestTable.db, расположенную в корне диска D:. Казалось бы:

SELECT * INTO[TestTable.DB] in ‘D:\’ FROM SourceTable

Нет, очередная ошибка. Вот, что мы видим.

Ага, хорошо, давайте попробуем указать таблицу в пути:

SELECT * INTO[TestTable] in ‘D:\ TestTable.DB’ FROM SourceTable

Получим очередное сообщение об ошибке.

Ага, стало быть, файл для экспорта должен уже существовать? Ладно, не проблема, давайте создадим его и попробуем еще раз.

Ну, в общем, желающие могут еще поэкспериментировать, а для остальных я скажу как делается:

SELECT * INTO[Paradox 7. x; DATABASE = D: \]. [TestTable#0DB]FROMSourceTable

Создавать таблицу до операции экспорта нет надобности — таблица будет создана автоматически, все поля будут созданы правильного типа. В получившейся таблице будут все данные из SourceTable.

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

Самое потрясающее это название раздела MSDN, где я нашел этот ответ — ‘Как, используя ADO, открыть таблицу Paradox, защищенную паролем’. Как ЭТО имеет отношение к этому синтаксису SQL, я так и не понял, честно говоря.

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

При написании статьи использовались следующие материалы:

Материалы Королевства Delphi.

Справочные файлы Delphi 4 и Delphi 5.

Исходные коды VCL Delphi 4 и Delphi 5.

MS ADO SDK и примеры MS ADO SDK.

MS MSDN.

А.Я. Архангельский ‘Язык SQL в Delphi 5’.

{/codecitation}

Компонент TADOUpdateSQL

{codecitation class=»brush: pascal; gutter: false;» width=»600px»}

Оформил: DeeCo

Автор: Горкуша Алексей

Здесь представлены работающие компоненты обновления данных, полученных запросом через TADOQuery, аналогичные компонентам BDE TQuery,TUpdateSQL

Компоненты TADOUpdateQuery, TADOUpdateSQL выполняют в точности те же функции что и компоненты BDE TQuery,TUpdateSQL.

Это может способствовать быстрому переводу программ с BDE на ADO. Компоненты работающие (в исходных текстах есть комментарии), но до полной совместимости необходимы доработки, например: отсутствуют события onUpdateRecord, onUpdateError.

Предлагаю всем подключится и довести дело до конца.

procedure TForm1.Button1Click(Sender: TObject);

begin

PowerControl1.Action := actCDEject; // Или…

actLogOFF, actShutDown…PowerControl1.Execute;

end;

= = = = = = = = = = = = = = = = = = = = = = = = = Component Code: unit

PowerControl;

interface uses WinTypes, WinProcs, Messages,

SysUtils, Classes, Controls, Forms, Graphics,

MMSystem;

type

TAction =

(actLogOFF, actShutDown, actReBoot, actForce, actPowerOFF,

actForceIfHung, actMonitorOFF, actMonitorON, actCDEject, actCDUnEject);

type

TPowerControl = class(TComponent)

private

FAction: TAction;

procedure

SetAction(Value: TAction);

protected

public

function Execute:

Boolean;

published

property Action:

TAction read FAction write SetAction;

end;

procedure Register;

implementation

procedure register;

begin

RegisterComponents(‘K2’, [TPowerControl]);

end;

procedure TPowerControl.SetAction(Value: TAction);

begin

FAction := Value;

end;

function TPowerControl.Execute: Boolean;

begin

with (Owner as TForm) do

case FAction of

actLogOff:

ExitWindowsEx(EWX_LOGOFF, 1);

actShutDown:

ExitWindowsEx(EWX_SHUTDOWN, 1);

actReBoot:

ExitWindowsEx(EWX_REBOOT, 1);

actForce:

ExitWindowsEx(EWX_FORCE, 1);

actPowerOff:

ExitWindowsEx(EWX_POWEROFF, 1);

actForceIfHung:

ExitWindowsEx(EWX_FORCEIFHUNG, 1);

actMonitorOFF:

SendMessage(Application.Handle,

WM_SYSCOMMAND,

SC_MONITORPOWER, 0);

actMonitorON: SendMessage(Application.Handle, WM_SYSCOMMAND,

SC_MONITORPOWER, -1);

actCDEject: mciSendstring(‘SET CDAUDIO DOOR OPEN WAIT’, nil, 0, Handle);

actCDUnEject: mciSendstring(‘SET CDAUDIO DOOR CLOSED WAIT’, nil, 0,

Handle);

end; {Case}

Result := True;

end;

end.

Скачать ADOComponent.zip (5.7 K)

{/codecitation}

Как сделать ADO-connection

{codecitation class=»brush: pascal; gutter: false;» width=»600px»}

Автор: http://www.swissdelphicenter.ch

uses

ComObj;

function OpenConnection(ConnectionString: AnsiString): Integer;

var

ADODBConnection: OleVariant;

begin

ADODBConnection := CreateOleObject(‘ADODB.Connection’);

ADODBConnection.CursorLocation := 3; // User client

ADODBConnection.ConnectionString := ConnectionString;

Result := 0;

try

ADODBConnection.Open;

except

Result := -1;

end;

end;

function DataBaseConnection_Test(bMessage: Boolean): AnsiString;

var

asTimeout, asUserName, asPassword, asDataSource, ConnectionString: AnsiString;

iReturn: Integer;

OldCursor: TCursor;

begin

OldCursor := Screen.Cursor;

Screen.Cursor := crHourGlass;

asTimeout := ‘150’;

asUserName := ‘NT_Server’;

asPassword := ‘SA’;

asDataSource := ‘SQL Server — My DataBase’;

ConnectionString := ‘Data Source = ‘ asDataSource

‘User ID = ‘ asUserName

‘Password = ‘ asPassword

‘Mode = Read|Write;Connect Timeout = ‘ asTimeout;

try

iReturn := OpenConnection(ConnectionString);

if (bMessage) then

begin

if (iReturn = 0) then

Application.MessageBox(‘Connection OK!’, ‘Information’, MB_OK)

else if (iReturn = -1) then

Application.MessageBox(‘Connection Error!’, ‘Error’, MB_ICONERROR

MB_OK);

end;

if (iReturn = 0) then

Result := ConnectionString

else if (iReturn = -1) then

Result := »;

finally

Screen.Cursor := OldCursor;

end;

end;

procedure TForm1.Button1Click(Sender: TObject);

begin

DataBaseConnection_Test(True);

end;

{/codecitation}

Как работать с ADO компонентами в DLL

{codecitation class=»brush: pascal; gutter: false;» width=»600px»}

Автор: Vit

WEB-сайт: http://forum.vingrad.ru

В оконных приложениях инициализацию COM берет на себя строка в файле проекта:

Application.Initialize;

А вот в DLL и консольных программах обэекта Application нет, и при попытке работать с любыми ActiveX, включая широко используемые ADO компоненты генерится ошибка, которую исправить очень просто: достаточно в секцию Uses в DPR файле добавить модуль oleauto

{/codecitation}

Как работать из Delphi напрямую с ADO

{codecitation class=»brush: pascal; gutter: false;» width=»600px»}

Автор: Nomadic

Итак, хочу поделиться некоторыми достижениями… так на всякий случай. Если у вас вдруг потребуется сделать в своей программке доступ к базе данных, а BDE использовать будет неохота (или невозможно) — то есть довольно приятный вариант: использовать ActiveX Data Objects. Однако с их использованием есть некоторые проблемы, и одна из них это как передавать Optional параметры, которые вроде как можно не указывать. Однако, если вы работаете с ADO по-человечески, а не через тормозной IDispatch.Invoke то это превращается в головную боль. Вот как от нее избавляться:

var

OptionalParam: OleVariant;

VarData: PVarData;

begin

OptionalParam := DISP_E_PARAMNOTFOUND;

VarData := @OptionalParam;

VarData^.VType := varError;

после этого переменную OptionalParam можно передавать вместо неиспользуемого аргумента.

Далее, самый приятный способ получения Result sets:

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

var

MyConn: _Connection;

MyComm: _Command;

MyRecSet: _Recordset;

prm1: _Parameter;

begin

MyConn := CoConnection.Create;

MyConn.ConnectionString := ‘DSN=pubs;uid=sa;pwd=;’; MyConn.Open( », », », -1 );

MyCommand := CoCommand.Create;

MyCommand.ActiveConnection := MyConn;

MyCommand.CommandText := ‘SELECT * FROM blahblah WHERE BlahID=?’

Prm1 := MyCommand.CreateParameter( ‘Id’, adInteger.adParamInput, -1, );

MyCommand.AppendParameter( Prm1 );

MyRecSet := CoRecordSet.Create;

MyRecSet.Open( MyCommand, OptionalParam, adOpenDynamic, adLockReadOnly, adCmdText );

… теперь можно фетчить записи. Работает шустро и классно. Меня радует. Особенно радуют серверные курсоры.

Проверялось на Delphi 3.02 ADO 1.5 MS SQL 6.5 sp4. Пашет как зверь.

Из вкусностей ADO — их легко можно использовать во всяких многопоточных приложениях, где BDE порой сбоит, если, конечно, ODBC драйвер грамотно сделан…

Ну и еще можно использовать для доступа к данным всяких там «нестандартных» баз типа MS Index Server или MS Active Directory Services.

В Delphi (как минимум в 4 версии) существует «константа» EmptyParam, которую можно подставлять в качестве пустого параметра.

{/codecitation}

Быстрый доступ к ADO

{codecitation class=»brush: pascal; gutter: false;» width=»600px»}

unit ADO;

{This unit provides a quick access into ADO

It handles all it’s own exceptions

It assumes it is working with SQL Server, on a PLC Database

If an exception is thrown with a [PLCErr] suffix:

the suffix is removed, and ErrMsg is set to the remaining string

otherwise

the whole exception is reported in ErrMsg

Either way, the function call fails.

Globals: adocn — connection which all other ADO objects use

adors — Recordset

adocmd — Command Object

adocmdprm — Command Object set aside for Parametric querying

ConnectionString

— Connection String used for connecting

ErrMsg — Last Error Message

ADOActive — Indicator as to whether ADO has been started yet

Functions:

General ADO

ADOStart:Boolean;

ADOReset:Boolean;

ADOStop:Boolean;

Recordsets

RSOpen(SQL:string;adRSType,adLockType,adCmdType:integer;UseServer:Boolean):Boolean;

RSClose:Boolean;

Normal Command Procedures

CMDExec(SQL:string;adCmdType:integer):Boolean;

Parametric Procedures

PRMClear:Boolean;

PRMSetSP(StoredProcedure:string;WithClear:Boolean):Boolean;

PRMAdd(ParamName:string;ParamType,ParamIO,ParamSize:integer;Val:variant):Boolean;

PRMSetParamVal(ParamName:string;val:variant):Boolean;

PRMGetParamVal(ParamName:string;var val:variant):Boolean;

Field Operations

function SQLStr(str:string;SQLStrType:TSQLStrType);

function SentenceCase(str:string):string;

—to convert from ‘FIELD_NAME’ -> ‘Field Name’ call

SQLStr(SentenceCase(txt),ssFromSQL);

}

interface

uses OLEAuto, sysutils;

const

{Param Data Types}

adInteger = 3;

adSingle = 4;

adDate = 7;

adBoolean = 11;

adTinyInt = 16;

adUnsignedTinyInt = 17;

adDateTime = 135;

advarChar = 200;

{Param Directions}

adParamInput = 1;

adParamOutput = 2;

adParamReturnValue = 4;

{Command Types}

adCmdText = 1;

adCmdTable = 2;

adCmdStoredProc = 4;

adCmdTableDirect = 512;

adCmdFile = 256;

{Cursor/RS Types}

adOpenForwardOnly = 0;

adOpenKeyset = 1;

adOpenDynamic = 2;

adOpenStatic = 3;

{Lock Types}

adLockReadOnly = 1;

adLockOptimistic = 3;

{Cursor Locations}

adUseServer = 2;

adUseClient = 3;

function ADOReset: Boolean;

function ADOStop: Boolean;

function RSOpen(SQL: string; adRSType, adLockType, adCmdType: integer;

UseServer: Boolean): Boolean;

function RSClose: Boolean;

function CMDExec(SQL: string; adCmdType: integer): Boolean;

function PRMClear: Boolean;

function PRMSetSP(StoredProcedure: string; WithClear: Boolean): Boolean;

function PRMAdd(ParamName: string; ParamType, ParamIO, ParamSize: integer; Val:

variant): Boolean;

function PRMSetParamVal(ParamName: string; val: variant): Boolean;

function PRMGetParamVal(ParamName: string; var val: variant): Boolean;

type

TSQLStrType = (ssToSQL, ssFromSQL);

function SQLStr(str: string; SQLStrType: TSQLStrType): string;

function SentenceCase(str: string): string;

var

adocn, adors, adocmd, adocmdPrm: variant;

ConnectionString, ErrMsg: string;

ADOActive: boolean = false;

implementation

var

UsingConnection: Boolean;

function ADOStart: Boolean;

begin

//Get the Object References

try

adocn := CreateOLEObject(‘ADODB.Connection’);

adors := CreateOLEObject(‘ADODB.Recordset’);

adocmd := CreateOLEObject(‘ADODB.Command’);

adocmdprm := CreateOLEObject(‘ADODB.Command’);

result := true;

except

on E: Exception do

begin

ErrMsg := e.message;

Result := false;

end;

end;

ADOActive := result;

end;

function ADOReset: Boolean;

begin

Result := false;

//Ensure a clean slate…

if not (ADOStop) then

exit;

//Restart all the ADO References

if not (ADOStart) then

exit;

//Wire up the Connections

//If the ADOconnetion fails, all objects will use the connection string

// directly — poorer performance, but it works!!

try

adocn.ConnectionString := ConnectionString;

adocn.open;

adors.activeconnection := adocn;

adocmd.activeconnection := adocn;

adocmdprm.activeconnection := adocn;

UsingConnection := true;

except

try

adocn := unassigned;

UsingConnection := false;

adocmd.activeconnection := ConnectionString;

adocmdprm.activeconnection := ConnectionString;

except

on e: exception do

begin

ErrMsg := e.message;

exit;

end;

end;

end;

Result := true;

end;

function ADOStop: Boolean;

begin

try

if not (varisempty(adocn)) then

begin

adocn.close;

adocn := unassigned;

end;

adors := unassigned;

adocmd := unassigned;

adocmdprm := unassigned;

result := true;

except

on E: Exception do

begin

ErrMsg := e.message;

Result := false;

end;

end;

ADOActive := false;

end;

function RSOpen(SQL: string; adRSType, adLockType, adCmdType: integer;

UseServer: Boolean): Boolean;

begin

result := false;

//Have two attempts at getting the required Recordset

if UsingConnection then

begin

try

if UseServer then

adors.CursorLocation := adUseServer

else

adors.CursorLocation := adUseClient;

adors.open(SQL, , adRSType, adLockType, adCmdType);

except

if not (ADOReset) then

exit;

try

if UseServer then

adors.CursorLocation := adUseServer

else

adors.CursorLocation := adUseClient;

adors.open(SQL, , adRSType, adLockType, adCmdType);

except

on E: Exception do

begin

ErrMsg := e.message;

exit;

end;

end;

end;

end

else

begin

//Use the Connetcion String to establish a link

try

adors.open(SQL, ConnectionString, adRSType, adLockType, adCmdType);

except

if not (ADOReset) then

exit;

try

adors.open(SQL, ConnectionString, adRSType, adLockType, adCmdType);

except

on E: Exception do

begin

ErrMsg := e.message;

exit;

end;

end;

end;

end;

Result := true;

end;

function RSClose: Boolean;

begin

try

adors.Close;

result := true;

except

on E: Exception do

begin

ErrMsg := e.message;

result := false;

end;

end;

end;

function CMDExec(SQL: string; adCmdType: integer): Boolean;

begin

result := false;

//Have two attempts at the execution..

try

adocmd.commandtext := SQL;

adocmd.commandtype := adCmdType;

adocmd.execute;

except

try

if not (ADOReset) then

exit;

adocmd.commandtext := SQL;

adocmd.commandtype := adCmdType;

adocmd.execute;

except

on e: exception do

begin

ErrMsg := e.message;

exit;

end;

end;

end;

result := true;

end;

function PRMClear: Boolean;

var

i: integer;

begin

try

for i := 0 to (adocmdprm.parameters.count) — 1 do

begin

adocmdprm.parameters.delete(0);

end;

result := true;

except

on e: exception do

begin

ErrMsg := e.message;

result := false;

end;

end;

end;

function PRMSetSP(StoredProcedure: string; WithClear: Boolean): Boolean;

begin

result := false;

//Have two attempts at setting the Stored Procedure…

try

adocmdprm.commandtype := adcmdStoredProc;

adocmdprm.commandtext := StoredProcedure;

if WithClear then

if not (PRMClear) then

exit;

result := true;

except

try

if not (ADOReset) then

exit;

adocmdprm.commandtype := adcmdStoredProc;

adocmdprm.commandtext := StoredProcedure;

//NB: No need to clear the parameters, as an ADOReset will have done this..

result := true;

except

on e: exception do

begin

ErrMsg := e.message;

end;

end;

end;

end;

function PRMAdd(ParamName: string; ParamType, ParamIO, ParamSize: integer; Val:

variant): Boolean;

var

DerivedParamSize: integer;

begin

//Only try once to add the parameter (a call to ADOReset would reset EVERYTHING!!)

try

case ParamType of

adInteger: DerivedParamSize := 4;

adSingle: DerivedParamSize := 4;

adDate: DerivedParamSize := 8;

adBoolean: DerivedParamSize := 1;

adTinyInt: DerivedParamSize := 1;

adUnsignedTinyInt: DerivedParamSize := 1;

adDateTime: DerivedParamSize := 8;

advarChar: DerivedParamSize := ParamSize;

end;

adocmdprm.parameters.append(adoCmdPrm.createparameter(ParamName, ParamType,

ParamIO, DerivedParamSize, Val));

except

on e: exception do

begin

ErrMsg := e.message;

end;

end;

end;

function PRMSetParamVal(ParamName: string; val: variant): Boolean;

begin

//Only try once to set the parameter (a call to ADOReset would reset EVERYTHING!!)

try

adocmdprm.Parameters[ParamName].Value := val;

result := true;

except

on e: exception do

begin

ErrMsg := e.message;

result := false;

end;

end;

end;

function PRMGetParamVal(ParamName: string; var val: variant): Boolean;

begin

//Only try once to read the parameter (a call to ADOReset would reset EVERYTHING!!)

try

val := adocmdprm.Parameters[ParamName].Value;

result := true;

except

on e: exception do

begin

ErrMsg := e.message;

result := false;

end;

end;

end;

function SQLStr(str: string; SQLStrType: TSQLStrType): string;

var

FindChar, ReplaceChar: char;

begin

{Convert ‘ ‘->’_’ for ssToSQL (remove spaces)

Convert ‘_’->’ ‘ for ssFromSQL (remove underscores)}

case SQLStrType of

ssToSQL:

begin

FindChar := ‘ ‘;

ReplaceChar := ‘_’;

end;

ssFromSQL:

begin

FindChar := ‘_’;

ReplaceChar := ‘ ‘;

end;

end;

result := str;

while Pos(FindChar, result) > 0 do

Result[Pos(FindChar, result)] := ReplaceChar;

end;

function SentenceCase(str: string): string;

var

tmp: char;

i {,len}: integer;

NewWord: boolean;

begin

NewWord := true;

result := str;

for i := 1 to Length(str) do

begin

if (result[i] = ‘ ‘) or (result[i] = ‘_’) then

NewWord := true

else

begin

tmp := result[i];

if NewWord then

begin

NewWord := false;

result[i] := chr(ord(result[i]) or 64); //Set bit 6 — makes uppercase

end

else

result[i] := chr(ord(result[i]) and 191); //reset bit 6 — makes lowercase

end;

end;

{This was the original way of doing it, but I wanted to look for spaces or ‘_’s,

and it all seemed problematic — if I find a better way another day, I’ll alter the above…

if str» then

begin

tmp:=LowerCase(str);

len:=length(tmp);

tmp:=Uppercase(copy(tmp,1,1)) copy(tmp,2,len);

i:=pos(‘_’,tmp);

while i0 do

begin

tmp:=copy(tmp,1,i-1) ‘ ‘ Uppercase(copy(tmp,i 1,1)) copy(tmp,i 2,len-i);

i:=pos(‘_’,tmp);

end;

end;

result:=tmp;}

end;

end.

{/codecitation}

ADO.OLEDB.JET — Access Violation при передаче неполного параметра

Автор: Дмитрий Померанцев

Проблема обнаружена под операционной системой Windows 2000 SP3, в среде Delphi6, Delphi7 (скорее всего не зависит от версии Delphi) с использованием Microsoft Jet DB Engine версия 4, SP3.

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

Пример кода:

Допустим, есть база данных в MS Access 2000, имеющая таблицу main и в ней целочисленное (INT) поле id в качестве главного ключа. Так же есть компонент ADOQuery1: TADOQuery, для доступа к базе данных. Максимальное значение поля id может быть получено следующим кодом:
{codecitation class=»brush: pascal; gutter: false;» width=»600px»}
ADOQuery1.Active := false;
ADOQuery1.SQL.Clear;
ADOQuery1.SQL.Add(‘SELECT max(id)’); // — Сбой здесь !!!
ADOQuery1.SQL.Add(‘AS idmax’);
ADOQuery1.SQL.Add(‘FROM main’);
ADOQuery1.Active := true;
{/codecitation}

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

Исследование исходных текстов компонента TADOQuery показало следущее: свойство SQL, типа TStrings связано с полем FSQL: TStrings, создаваемого как экземляр класса TStringList, при этом объекту FSQL назначается обработчик события OnChange — метод QueryChanged (protected, статический), что исключает его возможную перегрузку.

Этот метод устанавливает свойство Active в False и присваивает содержимое FSQL.Text полю CommandText объекта ADO.

За отсутствием исходных текстов библиотеки Jet, дальнейшее исследование пришлось прекратить, но можно сделать несколько выводов:

Корни проблемы в невполне корректном поведении как кода от Borland, так и от Microsoft. Компонент TADOQuery передает в ADO неоконченный SQL-запрос, а Jet начинает анализировать этот запрос до того, как он полностью поступит. Возможно, Microsoft пытался реализовать упреждающее выполнение запросов, чтобы снизить время обработки запроса после получения команды на выполнение.

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

При дополнительном исследовании были выяснены интересные подробности:

Данный код не прерывает выполнения при возникновении exception, т.е. теоретически даже try..except не нужен. Похоже, это происходит из-за того, что jet является COM-объектом, а их методы вызываются как safecall. Дальнейшие тесты подтвердили это предположение — при снятии галочки Stop on Delphi Exceptions и в варианте exe-файла ошибка не проявлялась. Таким образом, ситуация несколько меняется — исключение возникает только в среде разработки, что, правда, является слабым утешением, т.к. многие програмисты работают с настройками по-умолчанию, и в случае его возникновения могут долго ломать голову, ища свою ошибку там где ее нет.

ТИПОВЫЕ РЕШЕНИЯ

1. Передавать запрос целиком — одной строкой. Пример:
{codecitation class=»brush: pascal; gutter: false;» width=»600px»}
ADOQuery1.Active := false;
ADOQuery1.SQL.Text := ‘SELECT max(id) AS idmax FROM main;’;
ADOQuery1.Active := true;

{/codecitation}
2. Отключить галочку Tools->Debugger Options->Language Exceptions->Stop on Delphi Exceptions

3. Просто игнорировать это исключение (в этом случае в процессе разработки придется периодически несколько раз нажимать OK, что, конечно, менее удобно)

Напоследок: Небольшое исследование исходного кода компонент данных BDE и dbExpress показало, что в них передача SQL-запроса происходит через промежуточное текстовое поле, что, на мой взгляд, исключает в них возможность появления аналогичной ошибки.

КОММЕНТАРИЙ:

Компонент TADOQuery от Delphi 5 содержит аналогичный код (метод QueryChanged), приводящий к ошибке.

Еще один вариант решения — использовать стандартные возможности TStrings по управлению обновлением:
{codecitation class=»brush: pascal; gutter: false;» width=»600px»}
ADOQuery1.SQL.BeginUpdate;
try
ADOQuery1.SQL.Clear;
ADOQuery1.SQL.Add(‘SELECT max(id)’);
ADOQuery1.SQL.Add(‘AS idmax’);
ADOQuery1.SQL.Add(‘FROM main’);
finally
ADOQuery1.SQL.EndUpdate;
end;
{/codecitation}

В этом случае событие OnChange произойдет только при выполнении EndUpdate.