Запросы и параметры, или как избавится от многих проблем….

=== Запросы и параметры, или как избавится от многих проблем…. ===
=== Где я? О чём вы? ===
Компонент 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;
Author: Автор: Vit (www.delphist.com, www.drkb.ru, www.unihighlighter.com, www.nevzorov.org)
ID: 02414
----
Примечание от 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;
не нужна, но может использоваться.
Что здесь сделано?
# Создан явный параметр
# ему дано имя
# явно указан тип
# явно указано направление, что этот параметр входной
# и присвоено ему значение, эквивалент ParamByName
что это дает:
Это снимает все разночтения, которые могут возникнуть при автоматическом разборе выражения с типом и присвоением ему значения.
ID: 02415
----
Примечание от Петровича
Внесу и свою лепту
К параметрам запроса можно обращаться и по индексу. Например так:
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);
 // Возвращает закрытый TADOQuery с заполненными SQL.Text и Parameters.
 // Ответственность за уничтожение возвращенного TADOQuery лежит на вызывающем!
 function CreateQuery(SQLText: String; fParams: array of const; qParams: array of Variant): TADOQuery;
 // Выполняет SQL-оператор, и возвращает RowAffected
 function ExecSql(SQLText: String; fParams: array of const; qParams: array of Variant): Integer;
 // Возвращает значение первого поля первой записи результата SQL-запроса
 function GetField(SQLText: String; fParams: array of const; qParams: array of Variant): Variant;
 // Возвращает открытый TADOQuery с результатом SQL-запроса.
 // Ответственность за уничтожение возвращенного TADOQuery лежит на вызывающем!
 function CreateSelect(SQLText: String; fParams: array of const; qParams: array of Variant): TADOQuery;
 // Возвращает число записей в таблице TableName
 function GetRecCount(const TableName: string): Integer; overload;
 // Возвращает число записей в таблице TableName у которых значение поля FieldName равно Value
 function GetRecCount(const TableName, FieldName: string; Value: Variant): Integer; overload;
 // TADOConnection с которым соединен данный TDbAdo
 property Connection: TADOConnection read FConnection;
 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;
// Возвращает объект последнего исключения или Nil (если вызвана вне except .. end)
function LastExcept: Exception;
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;
ID: 02416
Category:Базы данных