Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Pass Generic Record in an array of const

Is it possible, in any way, to pass a Generic Record in an array of const argument to a function call ?

I would like to use the Nullable record from Allen Bauer in a kind of home made ORM using "Plain Old Delphi Objects" to map database rows :

type
  Nullable<T> = record
    ...
  end;

  TMyTableObject = class(TDbOject)
  private
    FId: Integer;
    FOptionalField: Nullable<string>;
  protected
    procedure InternalSave; override;
  public
    property Id: Integer read FId write SetId;
    property OptionalField: Nullable<string> read FOptionalField write SetOptionalField;
  end;

  ...

  implementation

  procedure TMyTableObject.InternalSave;
  begin
    { FDbController is declared and managed in TDbObject, it contains fonction to run queries
      on the database }

    FDbController.Execute(
      'update or insert into MY_TABLE(TABLE_ID, OPTIONAL_FIELD) ' +
      'values (?, ?) ' + 
      'matching(TABLE_ID) returning TABLE_ID', [FId, FOptionalField],
      procedure (Fields: TSQLResult)
      begin
        FId := Fields.AsInteger[0];
      end;
    end;      
  end;

The above code results in an error : "E2250 : no overriden version of the "Execute" function can be called with these arguments" (translated from French : E2250 Aucune version surchargée de 'Execute' ne peut être appelée avec ces arguments)

I could explicitly convert FOptionalField to string or anything else since Nullable overrides ad-hoc operators but I really have to know if the Nullable has a value or not until I map the array of const into the Params field of the database query object :

procedure TDbContext.MapParams(Q: TUIBQuery; Params: TConstArray);
const
  BOOL_STR: array[Boolean] of string = ('F', 'V');
var
  i: Integer;
begin
  for i := 0 to High(Params) do
    case Params[i].VType of
      vtInteger :      Q.Params.AsInteger[i]       := Params[i].VInteger;
      vtInt64   :      Q.Params.AsInt64[i]         := Params[i].VInt64^;
      vtBoolean :      Q.Params.AsString[i]        := BOOL_STR[Params[i].VBoolean];
      vtChar    :      Q.Params.AsAnsiString[i]    := Params[i].VChar;
      vtWideChar:      Q.Params.AsString[i]        := Params[i].VWideChar;
      vtExtended:      Q.Params.AsDouble[i]        := Params[i].VExtended^;
      vtCurrency:      Q.Params.AsCurrency[i]      := Params[i].VCurrency^;
      vtString  :      Q.Params.AsString[i]        := string(Params[i].VString^);
      vtPChar   :      Q.Params.AsAnsiString[i]    := Params[i].VPChar^;
      vtAnsiString:    Q.Params.AsAnsiString[i]    := AnsiString(Params[i].VAnsiString);
      vtWideString:    Q.Params.AsUnicodeString[i] := PWideChar(Params[i].VWideString);
      vtVariant   :    Q.Params.AsVariant[i]       := Params[i].VVariant^;
      vtUnicodeString: Q.Params.AsUnicodeString[i] := string(Params[i].VUnicodeString);
      vtPointer :
      begin
        if Params[i].VPointer = nil then
          Q.Params.IsNull[i] := True
        else
          Assert(False, 'not nil pointer is not supported');
      end
    else
      Assert(False, 'not supported');
    end;
end;

Do you have any idea on how to make this kind of construct possible ? I find a way by adding an explicit cast operator override to Variant in Nullable using RTTI but that's a bit tricky because it needs an explicit cast to Variant in the array of const call :

class operator Nullable<T>.Explicit(Value: Nullable<T>): Variant;
begin
  if Value.HasValue then
    Result := TValue.From<T>(Value.Value).AsVariant
  else
    Result := Null;
end;

...

    FDbController.Execute(
      'update or insert into MY_TABLE(TABLE_ID, OPTIONAL_FIELD) ' +
      'values (?, ?) ' + 
      'matching(TABLE_ID) returning TABLE_ID', [FId, Variant(FOptionalField)],
      procedure (Fields: TSQLResult)
      begin
        FId := Fields.AsInteger[0];
      end;
    end;  
like image 716
ZeDalaye Avatar asked May 10 '11 08:05

ZeDalaye


2 Answers

As you stated, there is no record allowed in the array of const parameters, in the current state of the compiler.

In fact, TVarRec.VType values doesn't have any vtRecord.

And the Variant type itself doesn't have a varRecord type handled by Delphi. There is such a variant type in the Windows world (for example, DotNet structs are mapped into vt_Record type in COM), but this kind of variant is not handled by Delphi.

What is possible is to pass a pointer to the typeinfo of the record, then to the record:

case Params[i].VType of
  vtPointer:
  if (i<high(Params)) and (Params[i+1].VType=vtPointer) then
    if Params[i].VPointer = TypeInfo(MyRecord) then begin
      ...
    end;

Perhaps a not wonderful answer since you are using generics... but at least a start... In all cases, that's how I could use this with pre-generic Delphi compilers. The TypeInfo() keyword was already quite powerful, and faster than the "new" RTTI implementation.

Another possibility (compatible with generics) should be to add some record type ID in the record content. Then pass the record as pointer, read the ID, and act with it as expected:

case Params[i].VType of
  vtPointer:
  if Params[i].VPointer <> nil then
  case PRecordType(Params[i].VPointer)^ of
  aRecordType: ...
      ...
  end;

This could be made with simple extending of the initial the Nullable<T> definition.

Post-Scriptum:

Using records in an ORM is not perhaps the best solution...

The class model is more advanced than the record/object model in Delphi, and since an ORM is about object modeling, you should use the best OOP model available, IMHO. By relying on classes instead of records, you can have inheritance, embedded type information (via the Class method or just via PPointer(aObject)^), nullable values, and virtual methods (which is very handy for an ORM). Even the array of const parameters problem will allow direct handling of such vtObject types, and a dedicated class virtual method. An ORM based on records will be only a way of serializing table rows, not designing your application using OOP best practice. About performance, memory allocation of class instances is not a problem, when compared to potential isssues of copying records (the _CopyRecord RTL function - called in an hidden manner by the compiler e.g. for function parameters - can be very slow).

For instance, in our ORM, we handle dynamic array properties, even dynamic arrays of records in properties (Delphi doesn't produce RTTI for published properties of records, that's another inconsistent behavior of the current class implementation). We serialize records and dynamic arrays into JSON or binary with the "old" RTTI, and it works very well. Best of both object worlds at hand.

like image 143
Arnaud Bouchez Avatar answered Nov 08 '22 20:11

Arnaud Bouchez


Thanks to Robert Love idea, I added Implcit Cast to TValue on Nullable and replaced all my Params: array of const by const Params: array of TValue.

I can now pass params without Variant casting :

procedure TMyTableObject.InternalSave;
begin
  { FDbController is declared and managed in TDbObject, it contains fonction to run queries
    on the database }

  FDbController.Execute(
    'update or insert into MY_TABLE(TABLE_ID, OPTIONAL_FIELD) ' +
    'values (?, ?) ' + 
    'matching(TABLE_ID) returning TABLE_ID', [FId, FOptionalField],
    procedure (Fields: TSQLResult)
    begin
      FId := Fields.AsInteger[0];
    end;
  end;      
end;

The MapParams method has become :

procedure TDbContext.MapParams(Q: TUIBQuery; const Params: array of TValue);
const
  { maps to CREATE DOMAIN BOOLEAN AS CHAR(1) DEFAULT 'F' NOT NULL CHECK (VALUE IN ('V', 'F')) }
  BOOL_STR: array[Boolean] of string = ('F', 'V'); 
var
  I: Integer;
begin
  Q.Prepare(True);
  for I := 0 to Q.Params.ParamCount - 1 do
  begin
    if Params[I].IsEmpty then
      Q.Params.IsNull[I] := True
    else
      case Q.Params.FieldType[I] of
      uftChar,
      uftVarchar,
      uftCstring:
      begin
        { Delphi Booleans are tkEnumeration in TValue }
        if Params[I].Kind = tkEnumeration then
          Q.Params.AsString[I] := BOOL_STR[Params[I].AsBoolean]
        else
          Q.Params.AsString[I] := Params[I].ToString;
      end;
      uftSmallint,
      uftInteger:          Q.Params.AsSmallint[I] := Params[I].AsInteger;
      uftFloat,
      uftDoublePrecision : Q.Params.AsDouble[I]   := Params[I].AsExtended;
      uftNumeric:          Q.Params.AsCurrency[I] := Params[I].AsCurrency;
      uftTimestamp:        Q.Params.AsDateTime[I] := Params[I].AsExtended;
      uftDate:             Q.Params.AsDate[I]     := Params[I].AsInteger;
      uftTime:             Q.Params.AsDateTime[I] := Params[I].AsInteger;
      uftInt64:            Q.Params.AsInt64[I]    := Params[I].AsInt64;

      uftUnKnown, uftQuad, uftBlob, uftBlobId, uftArray
    {$IFDEF IB7_UP}
     ,uftBoolean
    {$ENDIF}
    {$IFDEF FB25_UP}
     ,uftNull
    {$ENDIF}:       Assert(False, 'type de données non supporté');
     end;
  end;
end;

Note that I enforced type checking by asking the database to describe and force type of input params and I reversed the way params are visited giving priority to query params so it will raise exceptions when conversions are not possible : there is far less datatypes available in the database World than in the Delphi World.

like image 28
ZeDalaye Avatar answered Nov 08 '22 19:11

ZeDalaye