FireDAC mapping rules do not apply to parameters?

2020-04-17 07:52发布

I have a TFDConnection to a FireBird database for which I apply Data type mapping for backward compatibility with a previous data access technology (SQLDirect):

with FormatOptions.MapRules.Add do     // TIMESTAMP will be ftDateTime instead of ftTimeStamp
begin
   SourceDataType := dtDateTimeStamp;
   TargetDataType := dtDateTime;
end;
with FormatOptions.MapRules.Add do     // FLOAT will be ftFloat instead of ftSingle
begin
   SourceDataType := dtSingle;
   TargetDataType := dtDouble;
end;
FormatOptions.OwnMapRules := true;

At runtime I create a TFDQuery that I link to that TFDConnection.
I can see that it inherits the mapping rules: FormatOptions.MapRules.count=2

I assign an INSERT query to its SQL.Text:

insert into TT_ACT (TT_ACT_ID,TT_PARENT_ID,TT_FROMDATE,TT_TODATE,TT_NAME,TT_NR,TT_CODE,TT_GROUP...)
values (:TT_ACT_ID,:TT_PARENT_ID,:TT_FROMDATE,:TT_TODATE,:TT_NAME,:TT_NR,:TT_CODE,:TT_GROUP,...)

This gives me params.count=42 with parameters with datatype ftUnknown (of course).

I then call Prepare for the query.

If I now inspect a known datetime parameter, I see params[x].datatype = ftTimeStamp, not ftDateTime. So when the query goes back to the database to look at the fields, it does not seem to listen to the data mapping rules when setting up the parameters.

Is this a bug?

In a later stage in my code this got me into trouble, resulting in the famous 338 error:

[FireDac][Phys][IB]-338 Param [TT_FROMDATE] type changed from [ftSQLTimeStamp] to [ftDateTime]. Query must be reprepared. 

I managed to work around that error, so that is not part of the question. But I would expect the Params to follow data type mapping rules as well, that would have made all this easier.

1条回答
太酷不给撩
2楼-- · 2020-04-17 08:06

You just misdefined the mapping rule definitions. For parameters, it is transformation of target into source. The Data Type Mapping topic says that as well:

In case of a command parameter, the rule defines a transformation of a target data type, specified by an application, into a source data type, supported by a driver.

So to map command parameters from TIMESTAMP to dtDateTime and FLOAT to dtDouble just swap source with target in your definition:

{ FLOAT → dtDouble in parameters }
with FormatOptions.MapRules.Add do
begin
  SourceDataType := dtDouble; { TFDParam.DataType }
  TargetDataType := dtSingle; { Firebird FLOAT }
end;
{ TIMESTAMP → dtDateTime in parameters }
with FormatOptions.MapRules.Add do
begin
  SourceDataType := dtDateTime; { TFDParam.DataType }
  TargetDataType := dtDateTimeStamp; { Firebird TIMESTAMP }
end;
{ enable custom map rules }
FormatOptions.OwnMapRules := True;

It's worth adding that mapping rules for parameters do the only thing. They only map data types for parameters when command is being prepared (data types must be determinable for them). They're not converting parameter values as they are passed to the driver. Consider this code:

{ Firebird FLOAT equals to dtSingle data type, map it to dtDouble }
with FDQuery1.FormatOptions.MapRules.Add do
begin
  SourceDataType := dtDouble;
  TargetDataType := dtSingle;
end;
FDQuery1.FormatOptions.OwnMapRules := True;
{ setup the command; MyFloat field is Firebird FLOAT }
FDQuery1.SQL.Text := 'INSERT INTO MyTable (MyFloat) VALUES (:MyFloat)';
{ rules are applied when preparing command, so let's prepare it }
FDQuery1.Prepare;
{ now the parameter data type should be dtDouble instead of dtSingle }
if FDQuery1.ParamByName('MyFloat').DataType = dtDouble then
  ShowMessage('Parameter is of dtDouble data type');
{ but you can easily change the parameter data type to another, e.g. by mistake;
  this will change data type to dtSingle, so the whole mapping effort is lost }
FDQuery1.ParamByName('MyFloat').AsSingle := 1.2345;
{ if this would execute (which does not because the parameter data type has been
  changed since the command preparation), parameter map rules would still not be
  involved in converting parameter value for the driver }
FDQuery1.ExecSQL;

So as you can see, it's quite a lot of effort for almost nothing (changing determined parameter data type to another only). Parameter values are converted automatically regardless mapping rules. So, even if your parameter data type won't match DBMS data type but will be convertible, FireDAC will simply convert it for you no matter what (this magic is inside ConvertRawData method):

{ assume MyFloat FLOAT, MyTimeStamp TIMESTAMP in Firebird }
FDQuery1.SQL.Text := 'INSERT INTO MyTable (MyFloat, MyTimeStamp) VALUES (:MyFloat, :MyTimeStamp)';
{ setup parameter data types badly to be dtDouble and dtDateTime }
FDQuery1.ParamByName('MyFloat').AsFloat := 1.2345;
FDQuery1.ParamByName('MyTimeStamp').AsDateTime := Now;
{ and execute; parameter values will be converted automatically to DBMS data types
  dtDouble → dtSingle and dtDateTime → dtDateTimeStamp }
FDQuery1.ExecSQL;

So even here I would repeat, that parameter collections should be defined manually rather than by the DBMS from a prepared command (developer must know what values fill into which fields).

查看更多
登录 后发表回答