Запросы и параметры, или как избавится от многих проблем
Где я? О чём вы?
Компонент TQuery имеет свойство Params:TParams, а компонент TADOQuery имеет свойство Parameters:TParameters. Эти объекты позволяют в уже готовом запросе подставить какие-либо значения. Делается это таким незамысловатым способом:
ADOQuery1.Active:=False; ADOQuery1.SQL.text:='Select * From MyTable Where MyField=:prm'; ADOQuery1.Parameters.ParseSQL(ADOQuery1.SQL.text, true); ADOQuery1.Parameters.ParamByName('prm').Value:='чего-то там'; ADOQuery1.Active:=True;
Для BDE код будет собственно таким:
Query1.Active:=False; Query1.SQL.text:='Select * From MyTable Where MyField=:prm'; Query1.Params.ParseSQL(Query1.SQL.text, true); Query1.Params.ParamByName('prm').Value:= 'чего-то там'; Query1.Active:=True;
А зачем это нужно?
1) Если запрос один и тот же но в зависимости от условий надо его задать с другими значениями. Параметры здесь работают немного быстрее и с ними удобнее работать чем с динамическим посторением SQL каждый раз:
...подготавливаем запрос один раз...
ADOQuery1.Active:=False; ADOQuery1.SQL.text:='Select * From MyTable Where MyField=:prm'; ADOQuery1.Parameters.ParseSQL(ADOQuery1.SQL.text, true);
... всё запрос готов ...
procedure TForm1.Button1Click(Sender: TObject); begin ADOQuery1.Active:=False; ADOQuery1.Parameters.ParamByName('prm').Value:=Edit1.text; ADOQuery1.Active:=True; end;
2)При передаче даты и времени. Тут прикол вот в чём. Допустим стоит у вас SQL Server в США и использует формат времени и даты как "01/20/04 1PM", а клиент у вас работает один в Канаде где формат даты "20/01/04 01:00 PM", а другой в России где формат даты "20/01/04 13:00". При отсылке запроса с датой в виде строки надо обязательно согласовывать форматы даты времени клиента и сервера, а при передаче параметров не надо! Дело в том что преобразование параметра происходит на сервере, и сервер его сам преобразует в тот формат который ему нужен!
ADOQuery1.Active:=False; ADOQuery1.SQL.text:='Select * From MyTable Where LastDate>:prm'; ADOQuery1.Parameters.ParseSQL(ADOQuery1.SQL.text, true); ADOQuery1.Parameters.ParamByName('prm').Value:=now; ADOQuery1.Active:=True;
3) При необходимости использовать в запросе двоичные данные или Memo поля использование параметров - это единственный путь
ADOQuery1.Active:=False; ADOQuery1.SQL.text:='Insert Into MyTable (id, MyPicture)'; ADOQuery1.SQL.Add('Values (:Id, :pic)'); ADOQuery1.Parameters.ParseSQL(ADOQuery1.SQL.text, true); ADOQuery1.Parameters.ParamByName('id').Value:=now; ADOQuery1.Parameters.ParamByName('pic').LoadFromFile('c:\mypic.bmp', ftGraphic); ADOQuery1.ExecSQL;
Примечание от Anatoly Podgoretsky
Для ран тайм можно указать про добавление параметров, определение их типов, поскольку у людей часто возникают проблемы с типами параметров, с присвоением значений через AsФункции и через Value.
with ADOQuery1.Parameters.AddParameter do begin Name := 'prm'; DataType := ftDateTime; Direction := pdInput; Value := Now; end;
Есть и другие полезные свойства, например Attributes
строка ADOQuery1.Parameters.ParamByName('prm').Value:=now;
не нужна, но может использоваться.
Что здесь сделано?
1. Создан явный параметр
2. ему дано имя
3. явно указан тип
4. явно указано направление, что этот параметр входной
5. и присвоено ему значение, эквивалент ParamByName
что это дает:
Это снимает все разночтения, которые могут возникнуть при автоматическом разборе выражения с типом и присвоением ему значения.
Примечание от Петровича
Внесу и свою лепту
К параметрам запроса можно обращаться и по индексу. Например так:
with ADOQuery1 do begin SQL.text:='UPDATE Customers SET Addr1=:p WHERE CustNo=:c'; Parameters[0].Value := 'г.Арбатов'; Parameters[1].Value := 753; ExecSQL; end;
Это полностью эквивалентно:
with ADOQuery1 do begin SQL.text:='UPDATE Customers SET Addr1=:p WHERE CustNo=:c'; Parameters.ParamByName('p').Value := 'г.Арбатов'; Parameters.ParamByName('c').Value := 753; ExecSQL; end;
Кстати, обращаться по именам можно и чуть короче:
with ADOQuery1 do begin SQL.text:='UPDATE Customers SET Addr1=:p WHERE CustNo=:c'; Parameters.ParamValues['p'] := 'г.Арбатов'; Parameters.ParamValues['c'] := 753; ExecSQL; end;
Но, вернусь к обращению по индексу.
Так вот, по индексу, к Parameters лучше никогда не обращаться. Или, делать это с особой осторожностью.
Например, если усложнить предыдущий запрос:
with ADOQuery1 do begin SQL.text:='UPDATE Customers SET Addr1=:p , Addr2=:p WHERE CustNo=:c'; Parameters[0].Value := 'г.Арбатов'; Parameters[1].Value := 753; ExecSQL; end;
то результат выполнения будет отличаться от варианта:
with ADOQuery1 do begin SQL.text:='UPDATE Customers SET Addr1=:p , Addr2=:p WHERE CustNo=:c'; Parameters.ParamValues['p'] := 'г.Арбатов'; Parameters.ParamValues['c'] := 753; ExecSQL; end;
Все дело в том, что разбор SQL-оператора на уровне компонет Delphi весьма прост, и на мой взгляд делается не совсем корректно.
А именно, не смотря на то, что во втором варианте SQL-оператора упоминаются лишь два параметра: p и c (параметр p упоминается дважды), Delphi создаст ТРИ параметра в массиве Params. Поэтому, оператор Parameters[1].Value := 753; выполнит присваивание значения не параметру с именем c, а второму экземпляру параметра p. А значение параметра c останется равным NULL. Соответственно, при выполнении ExecSQL ошибки возникнуть не должно, но результат будет неправильным. А вот при обращении по имени, все будет хорошо! Наверное, при передаче параметров запроса на SQL-сервер (или соответствующий движок БД), сами компоненты "вытаскивают" значения параметров тоже по имени. Поэтому, не смотря на то что после:
Parameters.ParamValues['p'] := 'г.Арбатов';
Parameters.ParamValues['c'] := 753;
заполненными будут лишь параметры с индексами 0 и 2, на правильной работе это не скажется, поскольку до параметра с индексом 1 по имени вообще нельзя "достучаться".
Казалось-бы, зачем так много говорить об этой проблеме? Можно ведь вообще не использовать доступ по индексу, как я и советовал ранее. Но, лично я все-таки использую его. Вот например небольшой объектик которым я люблю пользоваться. Может кому и пригодится. Приведу пример для ADO, но подобное существует и для других методов доступа к БД.
type tDbAdo = class private fConnection :TADOConnection; public constructor Create (aADOConnection :TADOConnection); function CreateQuery (SQLText :String; fParams :array of const; qParams :array of Variant) :TADOQuery; // Возвращает закрытый TADOQuery с заполнеными SQL.Text и Parameters. // Ответственность за уничтожение возвращенного TADOQuery лежит на вызывающем! function ExecSql (SQLText :String; fParams :array of const; qParams :array of Variant) :Integer; // Выполняет SQL-оператор, и возвращает RowAffected function GetField (SQLText :String; fParams :array of const; qParams :array of Variant) :Variant; // Возвращает значение первого поля первой записи результата SQL-запроса function CreateSelect (SQLText :String; fParams :array of const; qParams :array of Variant) :TADOQuery; // Возвращает открытый TADOQuery с результатом SQL-запроса. // Ответственность за уничтожение возвращенного TADOQuery лежит на вызывающем! function GetRecCount (const TableName :string) :Integer; overload; // Возвращает число записей в таблице TableName function GetRecCount (const TableName, FieldName :string; Value :Variant) :Integer; overload; // Возвращает число записей в таблице TableName у которых значение поля FieldName равно Value property Connection :TADOConnection read fConnection; // TADOConnection с которым соеденен данный tDbAdo end; function SQLInfo (SQLText :string; fParams :array of const; qParams :array of Variant) :String; function VarToSqlConstStr (v :Variant) :String; begin case VarType(v) of varEmpty, varNull : Result := 'NULL'; varSmallint, varInteger, varSingle, varDouble, varCurrency, varShortInt, varByte, varWord, varLongWord, varInt64 : Result := v; varDate, varOLEStr, varStrArg, varString : begin Result := Trim(v); if Result='' then Result:='NULL' else Result:=QuotedStr(Result); end; varBoolean : Result := 'NY'[Ord(Boolean(v))]; else raise EConvertError.Create('VarToSqlConstStr: значение неизвестного типа '+IntToStr(Ord(VarType(v)))); end; end; var i :Integer; begin try Result := Format(SQLText,fParams)+'|'; for i := 0 to High(qParams) do Result := Result + VarToSqlConstStr(qParams[i]) +','; SetLength(Result,Length(Result)-1); except Result := ExMsg(SQLText); end; end; constructor tDbAdo.Create (aADOConnection :TADOConnection); begin inherited Create(); fConnection := aADOConnection; end; function tDbAdo.CreateQuery (SQLText :String; fParams :array of const; qParams :array of Variant) :TADOQuery; var i : Integer; begin Result := TADOQuery.Create(Self.Connection); with Result do try Connection := Self.Connection; SQL.Text := Format(SQLText,fParams); if High(qParams)+1 < Parameters.Count then raise Exception.CreateFmt('CreateADOQuery: Передано параметров (%d) меньше чем требует SQL-запрос (%d)',[High(qParams)+1,Parameters.Count]); for i := 0 to Parameters.Count-1 do Parameters[i].Value := qParams[i]; // <- вот доступ к параметрам по индексу except Result.Free; raise; end; end; function tDbAdo.ExecSql (SQLText :String; fParams :array of const; qParams :array of Variant) : Integer; begin try with CreateQuery(SQLText,fParams,qParams) do try Result := ExecSQL; finally Free end; except ExMsg('ExecSql: Ошибка выполнения SQL-оператора:'^M^J^I+SQLInfo(SQLText,fParams,qParams)); raise; end; end; function tDbAdo.CreateSelect (SQLText :String; fParams :array of const; qParams :array of Variant) :TADOQuery; begin Result := CreateQuery(SQLText,fParams,qParams); try Result.Open; except Result.Free; ExMsg('CreateSelect: Ошибка выполнения SQL-оператора:'^M^J^I+SQLInfo(SQLText,fParams,qParams)); raise; end; end; function tDbAdo.GetField (SQLText :String; fParams :array of const; qParams :array of Variant) :Variant; begin with CreateSelect(SQLText,fParams,qParams) do try Result := Fields[0].Value; finally Free; end; end; function tDbAdo.GetRecCount (const TableName :string) :Integer; begin Result := GetField('SELECT Count(1) FROM %s',[TableName],[]); end; function tDbAdo.GetRecCount (const TableName, FieldName :string; Value :Variant) :Integer; begin Result := GetField('SELECT Count(1) FROM %s WHERE %s=:v',[TableName,FieldName],[Value]); end;
Тут правда я использую некоторые функции из моей библиотеки. Нужный модуль ( awString.pas ) я уже выкладывал то-ли здесь, то-ли на Vingrade. Ну а кому лень искать, все нужные функции я добавлю в конец поста.
Теперь, как енто пользовать.
Например, кладем на форму (я обычно делаю жто в Data-модуле) ADOConnection1: TADOConnection.
Описываем соединение с поставляемой Borland демо-базой данных:
ADOConnection1.ConnectionString := 'FILE NAME=C:\Program Files\Common Files\System\OLE DB\Data Links\DBDEMOS.udl'
Затем, в OnCreate формы пишем:
ADOConnection1.Open;
MyDB := tDbAdo.Create(ADOConnection1);
Переменную MyDB описываем например как глобал:
var
MyDB :tDbAdo;
Ну а далее все просто. Например:
procedure TForm3.Button1Click(Sender: TObject); begin ShowMessage( 'Всего стран: '+IntToStr( MyDB.GetRecCount('country') )); ShowMessage( 'Всего стран в северной америке: '+IntToStr( MyDB.GetRecCount('Country','Continent','North America') )); ShowMessage( 'Всего стран в южной америке: '+IntToStr( MyDB.GetRecCount('Country','Continent','South America') )); ShowMessage( 'Площадь Канады: '+IntToStr( MyDB.GetField('SELECT Area FROM Country WHERE Name = :p0',[],['Canada']) )); with TStringList.Create do try // получение списка стран южной америки с численностью населения менее 10 000 000 with MyDB.CreateSelect('SELECT Name, Population FROM Country' +' WHERE (Continent = :Continent)' + ' AND (Population < :Population)' ,[],['South America',10000000]) do try while not Eof do begin Add(Fields[0].AsString+' '+Fields[1].AsString); Next; end; finally Free; end; ShowMessage( 'Список стран южной америки с численностью населения менее 10 000 000:'^M^J +Text ); finally Free; end; end;
Вот и все.
Ах, да. Как обещал, необходимые функции:
var GetLocationInfoStrFunc :function (Addr :Pointer) :String =Nil; // Процедура испльзуемая для получения информации об адресе Addr (имя модуля, // процедуры, и пр.). // Если определена, то используется функцией GetExText. function LocationInfoStr (Addr :Pointer) :String; begin if Assigned(GetLocationInfoStrFunc) then Result := GetLocationInfoStrFunc(Addr) else Result := '['+SysUtils.IntToHex(Cardinal(Addr),8)+']'; end; function LastExcept :Exception; // Возвращает объект последнего исключения или Nil (если вызвана вне except .. end) var c :TClass; o :tObject; s1,s2 :String; begin o := ExceptObject; if o = nil then Result := Nil else if o is Exception then Result := Exception(o) else begin // Исключение возникло за пределами нашего EXE-файла, например в Dll // или ExceptObject вообще не имеет в предках класса Exception! // Возвращать его нельзя, поскольку полноценная работа с ним невозможна. // В частности, если это Exception из Dll, то его поле Message, имеющее // тип LongString нельзя будет переприсваивать - память под текущую // строку выделена не нашим распределителем памяти. // Можно было-бы заменить его собственным экземпляром, освободив текущий // экземпляр, но нам недоступна переменная System.RaiseListPtr. И кроме // того возможны ситуации когда обработка текущего исключения возобновится // в Dll, тогда опять возникнет проблемма. // Не нахожу ничего лучшего, как возбудить исключение с соответствующим // текстом ошибки. // Если в предках ExceptObject есть Exception, то включить его Message // в текст возбуждаемонго исключения. c := o.ClassType; while (c <> nil) and (not c.ClassNameIs('Exception')) do c := c.ClassParent; if c = nil then begin // в иерархии o нет 'Exception' - в принципе это нонсенс s1 := 'В предках ExceptObject отсутствует Exception.'; s2 := ''; end else begin // Есть. Перенесем его Message s1 := 'ExceptObject не принадлежащий модулю.'; s2 := ^M^J^I' Message = "'+Exception(o).Message+'"'; end; raise EInvalidCast.CreateFmt('Ошибка LastExcept: %s'^M^J^I' ExceptObject = %s at %s%s' ,[s1,o.ClassName,LocationInfoStr(ExceptAddr),s2]); end; end; function GetExMsg (e :Exception) :String; begin if e = Nil then Result := '' else with e do begin Result := Message; // //if ClassName = 'EInOutError' then // Result := 'Ошибка в/в ('+IntToStr(EInOutError(e).ErrorCode)+')' // +^M^J^I+Result; end; end; function GetExText (e :Exception; ExAddr :Pointer =Nil) :String; var s :String; begin if e = Nil then Result := '' else with e do begin Result := GetExMsg(e); s := ClassName; if s = 'Exception' then Exit; if ExAddr<>nil then s := s + ' at '+LocationInfoStr(ExAddr); if e.ClassNameIs('EInOutError') then s := s + ' (Ошибка в/в '+IntToStr(EInOutError(e).ErrorCode)+')'; if Result<>'' then s := s + ^M^J^I + Result; Result := s; end; end; function ExMsg (e :Exception; const Msg :String ='') :String; overload; begin if e = Nil then Result := 'No exception' else begin Result := GetExMsg(e); if Msg <> '' then Result := Msg + ^M^J^I + Result; e.Message := Result; end; end; function ExMsg ( const Msg :String ='') :String; overload; begin Result := ExMsg(LastExcept,Msg); end; function ExMsg (const Fmt:String; const Args:array of const) :String; overload; begin Result := ExMsg(Format(Fmt,Args)); end;