You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

类映射中用于简化SELECT查询的内存指针有效性问题

问题背景与疑问

我正在通过类映射并将相关信息存储到记录中来改进应用的SELECT功能,相关实现细节如下:

类属性示例

[Field('ID'), PK, AutoInc]
property ID: Int64 read FID write FID;
[Field('NOTA_ORIGINAL')]
property NotaOriginal: string read FNotaOriginal write FNotaOriginal;
[Field('NOTA_TROCA')]
property NotaTroca: string read FNotaTroca write FNotaTroca;

映射函数代码

for lProperty in lType.GetProperties do
begin
  if (lProperty.HasField<Field>) then
  begin
    lMappedField.Field := lProperty.SearchAttribute<Field>.FieldName;
    lMappedField.PK := False;

    if (lProperty.HasAttribute<PK>) then
    begin
      lMappedField.PK := True;
      FPKKey := lProperty.SearchAttribute<Field>.FieldName;
      FPropertyPK := lProperty;
    end;

    lMappedField.AutoInc := (lProperty.HasAttribute<AutoInc>);

    if lProperty is TRttiInstanceProperty then
    begin
      PropInstance := TRttiInstanceProperty(lProperty);
      MemAddress := nil;

      if PropInstance.IsReadable then
      begin
        // Check if lProperty is directly tied to a field
        if (IntPtr(PropInstance.PropInfo^.GetProc) and PROPSLOT_MASK) = PROPSLOT_FIELD then
        begin
          // Obtain the memory address of the directly associated field
          MemAddress := PByte(FObjeto) + (IntPtr(PropInstance.PropInfo^.GetProc) and (not PROPSLOT_MASK));
          lMappedField.Mem := MemAddress;
        end;
      end;
    end;

    FMapping.Add(lProperty, lMappedField);
  end;
end;

记录结构

TMappedField = record
  Field: string;
  PK: Boolean;
  AutoInc: Boolean;
  Mem: Pointer;
end;

映射存储与关键变量

FMapping: TDictionary<TRttiProperty, TMappedField>; // 映射存储
FSQLTable: string; // 类属性
FPKKey: string; 
FPropertyPK: TRttiProperty;

SELECT查询相关代码

function TMCRTTILibORM.Load: TMCRTTILibORM;
begin
  FSelectText.Clear;
  FSelectText.Add(' SELECT FIRST 1 ' + FPKKey + ' FROM ' + FSQLTable + ' ');
  Result := Self;
end;

function TMCRTTILibORM.Where(const pProperty: Pointer; const pValue: Variant): TMCRTTILibORM;
var
  lPropertyField: TPair<TRttiProperty, TMappedField>;
begin
  for lPropertyField in FMapping do
  begin
    if lPropertyField.Value.Mem = pProperty then
    begin
      // Add WHERE or AND to SQL query
      if FSelectText.Count > 1 then
        FSelectText.Add(' AND ' + lPropertyField.Value.Field + ' = ')
      else
        FSelectText.Add(' WHERE ' + lPropertyField.Value.Field + ' = ');

      // Handle different types (Integer, Float, String, Enumeration, etc.)
      // Example for tkInteger:
      FSelectText.Add(VarToStr(pValue)); 

      Result := Self;
      Exit;
    end;
  end;
end;

function TMCRTTILibORM.Open: Boolean;
var
  lQuery: TCustomQuery;
begin
  lQuery := TCustomQuery.Create(nil);
  try
    lQuery.SQL := FSelectText;
    lQuery.Open;

    // Update object with primary key value
    if lQuery.RecordCount > 0 then
    begin
      FPropertyPK.SetValue(FObjeto, lQuery.FieldByName(FPKKey).AsInteger); // Example for tkInteger
      Result := LoadObject; // Populate the object
    end;
  finally
    lQuery.Free;
  end;
end;

调用方式

Result := ItemObject.Mapping.Load
  .Where(@ItemObject.ID, pID)
  .Open;

核心疑问

资深开发者指出,记录中存储的Mem指针可能因Windows内存管理或重定位而失效,导致Where函数中的比较失败。我当前仅映射对象一次并按需获取属性,目前测试均成功,但如果该内存管理问题确实存在,此方案将不可靠。

具体疑问:

  1. 此场景下对Mem指针的担忧是否成立?Windows内存管理是否真会改变指针?
  2. 若担忧成立,有什么更好的方式维护该映射以支持SELECT查询?

解答

一、关于Mem指针的担忧是否成立?

这个担忧完全成立,核心原因有三点:

  1. 对象内存地址可能变动:当容器(比如TObjectList)扩容、调用ReallocMem,或者Windows堆内存自动整理时,对象实例的内存地址会发生移动,你存储的Mem是基于原对象地址计算的偏移,此时会直接失效。
  2. 实例绑定问题:你当前的Mem是绑定到单个FObjeto实例的,一旦换用其他对象实例,同属性的内存地址必然不同,Where方法的指针匹配会直接失败。
  3. 依赖内部实现细节:通过PropInfo.GetProc的位掩码判断属性是否绑定字段,属于Delphi未公开的内部实现,不同版本的Delphi可能修改逻辑,存在兼容性风险。

二、替代方案

方案1:基于RTTI属性的直接匹配

利用你已经存储的TRttiProperty作为映射键,直接通过属性对象而非内存地址匹配,这是最稳定的方案:

修改Where方法,接收TRttiProperty作为参数:

function TMCRTTILibORM.Where(const AProperty: TRttiProperty; const AValue: Variant): TMCRTTILibORM;
var
  LMappedField: TMappedField;
begin
  if FMapping.TryGetValue(AProperty, LMappedField) then
  begin
    // 添加WHERE/AND子句,同时使用参数化查询避免注入
    if FSelectText.Count > 1 then
      FSelectText.Add(' AND ' + LMappedField.Field + ' = :' + LMappedField.Field)
    else
      FSelectText.Add(' WHERE ' + LMappedField.Field + ' = :' + LMappedField.Field);
    // 绑定参数
    FQuery.ParamByName(LMappedField.Field).Value := AValue;
  end;
  Result := Self;
end;

调用时通过RTTI获取属性对象:

var
  LContext: TRttiContext;
  LType: TRttiType;
  LIDProp: TRttiProperty;
begin
  LContext := TRttiContext.Create;
  try
    LType := LContext.GetType(ItemObject.ClassType);
    LIDProp := LType.GetProperty('ID');
    Result := ItemObject.Mapping.Load
      .Where(LIDProp, pID)
      .Open;
  finally
    LContext.Free;
  end;
end;

如果觉得每次获取RTTI属性麻烦,可以进一步封装,直接接收属性名称字符串:

function TMCRTTILibORM.Where(const APropName: string; const AValue: Variant): TMCRTTILibORM;
var
  LProp: TRttiProperty;
  LMappedField: TMappedField;
begin
  for LProp in FMapping.Keys do
  begin
    if LProp.Name = APropName then
    begin
      LMappedField := FMapping[LProp];
      // 执行上述SQL拼接与参数绑定逻辑
      Break;
    end;
  end;
  Result := Self;
end;

// 简化调用
Result := ItemObject.Mapping.Load
  .Where('ID', pID)
  .Open;

方案2:基于自定义属性元数据匹配

利用你已经定义的Field特性,直接存储属性与数据库字段的映射关系,完全脱离内存地址:

  1. 映射阶段确保每个属性的Field特性已正确注册
  2. Where方法通过属性的Field特性值直接关联数据库字段,或通过属性名称查找映射

额外优化建议

  • 务必使用参数化查询,避免直接拼接VarToStr(pValue)到SQL中,既防止SQL注入,又能自动处理不同数据类型的转换。
  • 放弃依赖PROPSLOT_MASK这类内部细节,改用Delphi公开的RTTI API(比如TRttiField)获取属性对应的字段信息,提升兼容性。

内容的提问来源于stack exchange,提问作者missingNO

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 04:26:00