在 const 数组中传递通用记录

发布于 2024-11-05 23:38:35 字数 3512 浏览 0 评论 0原文

是否可以以任何方式将 const 参数数组中的通用记录传递给函数调用?

我想在一种自制的 ORM 中使用 Allen Bauer 的 Nullable 记录,使用“普通旧 Delphi 对象”来映射数据库行:

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;

上面的代码会导致错误:“E2250:无法使用这些参数调用“执行”函数的重写版本”(翻译自法语) :E2250 Aucune 版本 surchargée de 'Execute' ne peut être appelée avec ces argument)

我可以显式地将 FOptionalField 转换为字符串或其他任何内容,因为 Nullable 覆盖临时运算符,但我确实必须知道 Nullable 是否有值,直到我将 const 数组映射到数据库查询对象的 Params 字段:

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;

您知道如何使这种构造成为可能吗?我找到了一种方法,通过使用 RTTI 在 Nullable 中向 Variant 添加显式强制转换运算符覆盖,但这有点棘手,因为它需要在 const 调用数组中显式强制转换为 Variant :

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;  

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;  

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

小矜持 2024-11-12 23:38:35

正如您所说,在编译器的当前状态下,const 参数数组中不允许有记录

事实上,TVarRec.VType 值没有任何 vtRecord。

而且 Variant 类型本身没有 Delphi 处理的 varRecord 类型。 Windows 世界中存在这样一种变体类型(例如,DotNet 结构体在 COM 中映射为 vt_Record 类型),但 Delphi 不处理这种变体。

可能的方法是将指针传递给记录的类型信息,然后传递给记录:

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;

也许这不是一个很好的答案,因为您使用的是泛型......但至少是一个开始......在所有情况下,这就是我可以使用的方式这与预通用 Delphi 编译器有关。 TypeInfo() 关键字已经非常强大,并且比“新”RTTI 实现更快。

另一种可能性(与泛型兼容)应该是在记录内容中添加一些记录类型 ID。然后将记录作为指针传递,读取 ID,并按预期操作:

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

这可以通过初始 Nullable 定义的简单扩展来实现。

Post-Scriptum:

在 ORM 中使用记录可能不是最好的解决方案...

class 模型比 record/object 更先进Delphi 中的模型,并且由于 ORM 是关于对象建模的,因此您应该使用可用的最佳 OOP 模型,恕我直言。通过依赖类而不是记录,您可以拥有继承、嵌入类型信息(通过 Class 方法或仅通过 PPointer(aObject)^ )、可为 null 的值和虚拟方法(这对于 ORM 来说非常方便)。即使是 const 参数数组问题也将允许直接处理此类 vtObject 类型和专用的类虚拟方法。基于记录的 ORM 只是序列化表行的一种方式,而不是使用 OOP 最佳实践来设计应用程序。关于性能,与复制记录的潜在问题(_CopyRecord RTL 函数 - 由编译器以隐藏方式调用,例如对于函数参数 - 可能会非常慢)。

例如,在我们的 ORM 中,我们处理动态数组属性,甚至是属性中记录的动态数组< /a> (Delphi 不会为记录的已发布属性生成 RTTI,这是当前类实现的另一个不一致行为)。我们使用“旧”RTTI 将记录和动态数组序列化为 JSON 或二进制文件,而且效果很好。两个对象世界中最好的。

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.

心的位置 2024-11-12 23:38:35

感谢 Robert Love 的想法,我在 Nullable 上向 TValue 添加了隐式转换,并将所有 Params: array of const 替换为 const Params: array of TValue

我现在可以在不进行变体转换的情况下传递参数:

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;

MapParams 方法已变为:

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;

请注意,我通过要求数据库描述并强制输入参数的类型来强制执行类型检查,并且我反转了访问参数的方式,优先考虑查询参数,因此它会引发无法进行转换时的例外:数据库 World 中可用的数据类型比 Delphi World 中的可用数据类型少得多。

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.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文