Автор: Дмитрий Померанцев
Проблема обнаружена под операционной системой 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. |