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;
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With