Проблема обнаружена под операционной системой Windows 2000 SP3, в среде Delphi6,
Delphi7 (скорее всего не зависит от версии Delphi) с использованием Microsoft
Jet DB Engine версия 4, SP3.
Некоторый, вполне типичный, код заполнения запроса в процессе выполнения
вызывает Access Violation, притом, что согласно документации все должно работать
корректно.
Пример кода:
Допустим, есть база данных в MS Access 2000, имеющая таблицу main и в ней
целочисленное (INT) поле id в качестве главного ключа. Так же есть компонент
ADOQuery1: TADOQuery, для доступа к базе данных. Максимальное значение поля id
может быть получено следующим кодом:
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;
|
Как было показано в комментарии, исключение возникает в процессе добавления
текста в запрос, но при этом в сообщении об ошибке указывалось, что исключение
произошло внутри библиотеки 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. Передавать запрос целиком — одной строкой. Пример:
ADOQuery1.Active := false; ADOQuery1.SQL.Text := 'SELECT max(id) AS idmax FROM main;'; ADOQuery1.Active := true;
|
2. Отключить галочку Tools->Debugger Options->Language
Exceptions->Stop on Delphi Exceptions
3. Просто игнорировать это исключение (в этом случае в процессе разработки
придется периодически несколько раз нажимать OK, что, конечно, менее удобно)
Напоследок: Небольшое исследование исходного кода компонент данных BDE и
dbExpress показало, что в них передача SQL-запроса происходит через
промежуточное текстовое поле, что, на мой взгляд, исключает в них возможность
появления аналогичной ошибки.
КОММЕНТАРИЙ:
Компонент TADOQuery от Delphi 5 содержит аналогичный код (метод
QueryChanged), приводящий к ошибке.
Еще один вариант решения - использовать стандартные возможности TStrings по
управлению обновлением:
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;
|
В этом случае событие OnChange произойдет только при выполнении EndUpdate.