Pregunta

Given the following

{------------------------------------------------------------------------------}
function TTestClass.NewQuery(const ASql : String) : TSqlQuery;
begin
  Result := TSqlQuery.Create(FConn);
  Result.SQLConnection := FConn;
  Result.Sql.Text := ASql;
  Result.Prepared := True;
end;

{------------------------------------------------------------------------------}
procedure TTestClass.ExecuteSql(const ASql : String);
begin
  with NewQuery(ASql) do
  try
    ExecSql();
  finally
    Free;
  end;
end;

How might I create an ExecSql method that will populate the parameters of the query?

I tried this overloaded method:

{------------------------------------------------------------------------------}
procedure TTestClass.ExecuteSql(const ASql : String; const ParamVals : Array Of Variant);
var
  i : integer;
  Qry : TSqlQuery;
begin
  Qry := NewQuery(ASql);
  with Qry do
  try
    for i := Low(ParamVals) to High(ParamVals) do
      Qry.Params[i].Value := ParamVals[i];
    ExecSql();
  finally
    Free;
  end;
end;

But I get the error message :

Project MyProj.exe raised exception class EDatabaseError with message 'No value for parameter 'SomeParam''.

Putting a watch on Parameter[0] apparently shows the value is set - and the parameter name is as I would expect. Can anyone suggest what I am doing wrong ?

And I have been criticised in the past for using 'Array Of Variant' - I wonder if there is a better way .

Thanks, all.

I found something interesting :

ParamByName('SomeParam').Value := 1234567;

Gives rise to the same Error Message whereas

ParamByName('SomeParam').AsInteger := 1234567;

Does not.

It is many years since I have used DBExpress - have I forgotten something ?

Edit

I have come up with a method that works, but am not happy with it; By examining the Variant Type of the VALUE I have managed to get some results

{------------------------------------------------------------------------------}
procedure TMyTestCase.SetParamValues(const AQuery : TSqlQuery; const ParamVals : Array Of Variant);
var
  i : Integer;
begin
  for i := 0 to AQuery.Params.Count - 1 do begin
    case VarType(ParamVals[i]) of
      varEmpty  :    AQuery.Params[i].AsInteger  := VarNull;       //The variant is Unassigned.
      varNull  :     AQuery.Params[i].AsInteger  := VarNull;       //The variant is Null.
      varAny  :      AQuery.Params[i].AsInteger  := VarNull;       //Represents a Variant that can hold any value.
      varSmallint  : AQuery.Params[i].AsInteger  := ParamVals[i];  //16-bit signed integer (type Smallint in Delphi, short in C++).
      varInteger  :  AQuery.Params[i].AsInteger  := ParamVals[i];  //32-bit signed integer (type Integer in Delphi, int in C++).
      varSingle  :   AQuery.Params[i].AsFloat    := ParamVals[i];  //Single-precision floating-point value (type Single in Delphi, float in C++).
      varDouble  :   AQuery.Params[i].AsFloat    := ParamVals[i];  //Double-precision floating-point value (type double).
      varCurrency  : AQuery.Params[i].AsFloat    := ParamVals[i];  //Currency floating-point value (type Currency).
      varDate  :     AQuery.Params[i].AsDateTime := ParamVals[i];  //Date and time value (type TDateTime).
      varOleStr  :   AQuery.Params[i].AsString   := ParamVals[i];  //Reference to a dynamically allocated UNICODE string.
      varDispatch  : AQuery.Params[i].AsInteger  := VarNull;       //Reference to an Automation object (an IDispatch interface pointer).
      varError  :    AQuery.Params[i].AsInteger  := VarNull;       //Operating system error code.
      varBoolean  :  AQuery.Params[i].AsBoolean  := ParamVals[i];  //16-bit Boolean (type WordBool).
      varVariant  :  AQuery.Params[i].AsInteger  := VarNull;       //Indicates another variant.
      varUnknown  :  AQuery.Params[i].AsInteger  := VarNull;       //Reference to an unknown object (an IInterface or IUnknown interface pointer).
      varShortInt  : AQuery.Params[i].AsInteger  := ParamVals[i];  //8-bit signed integer (type ShortInt in Delphi or signed char in C++).
      varByte  :     AQuery.Params[i].AsInteger  := ParamVals[i];  //A Byte.
      varWord  :     AQuery.Params[i].AsInteger  := ParamVals[i];  //Unsigned 16-bit value (Word).
      varLongWord  : AQuery.Params[i].AsInteger  := ParamVals[i];  //Unsigned 32-bit value (type LongWord in Delphi or unsigned long in C++).
      varInt64  :    AQuery.Params[i].AsInteger  := ParamVals[i];  //64-bit signed integer (Int64 in Delphi or __int64 in C++).
      varStrArg  :   AQuery.Params[i].AsString   := ParamVals[i];  //COM-compatible string.
      varString  :   AQuery.Params[i].AsString   := ParamVals[i];  //Reference to a dynamically allocated string (not COM-compatible).
      varArray  :    AQuery.Params[i].AsInteger  := VarNull;       //Indicates a Variant array.
      varByRef  :    AQuery.Params[i].AsInteger  := VarNull;       //Indicates that the variant contains a reference as opposed to a value.
      varTypeMask:   AQuery.Params[i].AsInteger  := VarNull;       //
    end;
  end;
end;

Surely I have missed out a step - Why doesnt the query parameter have a type ?

Edit Again

My current 'Best' solution is to rely on the programmer supplying the correct types and number of values. I have posted the full SetParamValues() method above. This is by NO MEANS thoroughly tested, but hopefully it will assist someone.

¿Fue útil?

Solución

There is no easy solution for this


All tests performed with Delphi XE3 and MySQL


Behavior Explanation

At designtime you will get the correct parameter data types as long as the parameters depend on a table field from the FROM table without aliases

SELECT id FROM items WHERE id = :id

but it will also fail if not

SELECT id FROM items WHERE id/2 = :id

and this will also fail

SELECT i.* FROM items i WHERE i.id = :id

At runtime both will result in parameters with datatype ftUnknown.

Parameters are setup in private method

// Delphi XE3
Data.SqlExpr.TCustomSQLDataSet.SetParameterFromSQL

Inside this method the tablename is extracted and only

if csDesigning in ComponentState then

a temporary DataSet is created with

SELECT * FROM <tablename> WHERE 0 = 1

Every parameter is checked against the fieldnames from this dataset and if names match the parameter datatype is set.

Thats the reason why you get ftUnknown for your parameters at runtime.

To solve that Delphi does the same you tried with your solution, but dbexpress fails sometime. The Setter for parameters is located at

Data.DB.TParam.SetAsVariant

and value 1234567 has the variant type varLongword and parameter datatype will be set to ftLongword and causes this error.


Workaround

As a workaround you can set parameter datatype to ftString/ftWideString, as this will work in most cases.

procedure TTestClass.ExecuteSql(const ASql : String; const ParamVals : Array Of Variant);
var
  i : integer;
  Qry : TSqlQuery;
begin
  Qry := NewQuery(ASql);
  with Qry do
  try
    for i := Low(ParamVals) to High(ParamVals) do
    begin
      Qry.Params[i].DataType := ftWideString;
      Qry.Params[i].Value := ParamVals[i];
    end;
    ExecSql();
  finally
    Free;
  end;
end;

To get a better solution you get a procedure to set the parameter datatype to ftString/ftWidestring only for critical variant types (like your method SetParamValues but more general)

procedure SetParamValues( const AParams : TParams; const AValues : array of Variant );
var
  LIdx : Integer;
  LParam : TParam;
  LValue : Variant;
begin
  for LIdx := 0 to Pred( AParams.Count ) do
  begin
    LParam := AParams[LIdx];
    LValue := AValues[LIdx];
    // only handle the critical parts
    case VarType( LValue ) of
      varByte, varLongword : LParam.DataType := ftWideString;
    end;
    // all other will be set here
    LParam.Value := LValue;
  end;
end;

Solution: the hard way

As i first stated there is no easy solution for this. For a full functional solution you have to parse the whole WHERE statement

SELECT a.*, b*
FROM table1 a
JOIN table2 b ON a.id = b.id
WHERE a.id = :id AND b.count / 2 = :halfcount

and build a query from this

SELECT a.id as Param1, b.count / 2 as Param2
FROM table1 a
JOIN table2 b ON a.id = b.id
WHERE 0 = 1

to get the expected datatype.


Solution: the long way

IMHO this is a bug and should be reported to EMBA ...


Solution: the expensive way

I made a test with UniDAC and everything is the same as dbExpress. Parameter datatype is ftUnknown and setting parameter value will set the datatype to ftLongword.

But there is one special case: You will not get an error and your query is processed as you expect.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top