Net-Base Maġazin

08.05.2026

Delphi RTTI għal mapping mingħajr maġija: bbażat fuq attributi, faċli għall-debug u kompatibbli ma' sistemi legacy

Mudell ta' mapping pragmatiku bi Delphi RTTI: attribwiti minflok kunvenzjonijiet, konverżjonijiet kontrollati, messaġġi ta' żball ċari u modalità ta' debug li fil-produzzjoni verament tgħin. B'frammenti ta' kodiċi sors għall-mapping minn Dataset jew Record għal oġġett, mingħajr maġija moħbija.

08.05.2026

Min jopera software tan-negozju li kibret f‘ Delphi jafu l-qasam ta‘ tensjoni: minn naħa wieħed trid oġġetti ta‘ dominju strutturati u saffijiet ċari, minn naħa oħra hemm Datasets, Variants, importazzjonijiet CSV, payloads ta‘ interfeys jew API REST li jridu jinżammu mapped fuq oġġetti b’xi mod. Hawn tibda l-bżonn għall- Delphi RTTI für Mapping ohne Magie: mapping permezz ta‘ Reflection (RTTI = Run-Time Type Information, informazzjoni tat-tip waqt il-ħin tal-eżekuzzjoni), iżda b’mod li jibqa‘ trasparenti, faċli biex tdebugġa u mhux moħbi fuq konvenzjonijiet jew manipulazzjonijiet tal-ismijiet.

Il-punt ewlieni: il-„maġija“ normalment ma toħroġx minn RTTI per se, iżda minn regoli implisi. Meta r-regoli tal-mapping jkunu espliċiti f’attributi, il-konverżjonijiet ikunu ċentralizzati u l-iżbalji jindikaw kawża ċara, RTTI jsir għodda minflok sorpriża.

Għaliex it-RTTI-Mapping f‘ Delphi spiss jaqa‘

It-mapping ibbażat fuq RTTI fl-iskejjel reali rari jfalli minħabba l-idea innifisha, imma spiss minħabba kundizzjonijiet limiti:

  • Formati ta‘ dejta legacy: Null/Empty/0 mhumiex separati b’mod nadif, it-tipi tal-kamp jinbidlu, strings fihom „N/A“.
  • Konvenzjonijiet li jmorru b’mod gradwali: „Il-kamp jissejjaħ bħall-Property“ jaħdem sa l-ewwel alias, join jew isem tal-Property rifattorizzat.
  • Diffiċli biex tdebugggja: Jekk mapper „semplicement ma jissettjax xejn“, mbagħad jonqos is-sors. Fil-operazzjoni dan ikun perikoluż.
  • Mitoloġiji dwar il-prestazzjoni: RTTI spiss jiġi stmat b’mod ġenerali bħala „bil-mod“, filwaqt li spiss in-nuqqas ta‘ caching hu l-problema.

Għalhekk approċċ sostenibbli għandu (1) metadati tal-mapping espliċiti, (2) jindirizza b’mod ċar il-konverżjoni u s-semantika tal-null, (3) jipprovdi rapporti ta‘ żbalji u output ta‘ debug, u (4) jaħżen l-informazzjoni RTTI fil-cache.

Delphi RTTI für Mapping ohne Magie: Prinċipji tad-disinn

Il-mudell hawn taħt hu intenzjonalment sempliċi fil-aħjar sens: ir-regoli huma viżibbli, l-effetti sekondarji huma limitati, u jista‘ jiġi integrat gradwalment fil-moduli eżistenti.

  • Attributi minflok konvenzjoni ta‘ isim: Property tingħata attribut li jsemmilha l-kolonna tas-sors.
  • Opt-in: Biss il-Properties immarkati jiġu msetta. L-ebda sorpriżi minn „il-proprjetajiet kollha ppubblikati“.
  • Konverżjoni f’post wieħed: Variant/String/Integer/Boolean/Enum/Nullable jiġu mappjati b’mod ċentrali.
  • Modalità Debug: Skond il-bżonn, jinżamm protokoll dwar liema kampjiet ġew imsetta jew injorati – u r-raġuni.
  • RTTI-Caching: L-iktar partijiet kostużi (lista tal-Property, valutazzjoni tal-attributi) jiġu ppreparati għal kull tip.

Snippett tal-kodiċi: Mapping bl-Attributi, RTTI, Caching u Debug

Is-snippett jimappja ringiela (per eżempju minn BDE-Ablosung mit nativer Anbindung via TDataSet) fuq oġġett. Minflok ma nkunu koppjati b’mod fissi mad-TField, nużaw interfazzja żgħira ta‘ reader. Dan huwa siewi fil-prattika għax tista‘ tuża l-istess loġika aktar tard għal JSON, INI, CSV jew risposti tal-API.

Delphi
unit RttiMapping;

interface

uses
  System.SysUtils, System.Rtti, System.TypInfo, System.Generics.Collections,
  System.Variants;

type
  // Mappar esplicit: Property <- isem tas-sors
  MapFromAttribute = class(TCustomAttribute)
  private
    FName: string;
  public
    constructor Create(const AName: string);
    property Name: string read FName;
  end;

  // Astrazzjoni żgħira: tipprovdi valur + iddifferenzja eżistenza/NULL
  IValueReader = interface
    ['{7D1E5864-7D3A-4D30-BD1C-0A94F7E6C0EF}']
    function HasValue(const AName: string): Boolean;
    function IsNull(const AName: string): Boolean;
    function GetValue(const AName: string): Variant;
  end;

  TRttiMapOptions = set of (moDebug, moIgnoreMissing, moIgnoreNull);

  ERttiMappingError = class(Exception);

  TRttiMapper = class
  private
    type
      TPropMap = record
        Prop: TRttiProperty;
        SourceName: string;
      end;
      TTypeCache = class
        Props: TArray<TPropMap>;
      end;
  private
    class var FCache: TObjectDictionary<PTypeInfo, TTypeCache>;
    class var FCacheLock: TObject;

    class function GetOrBuildCache(ATypeInfo: PTypeInfo): TTypeCache; static;
    class function FindMapFromAttr(const AProp: TRttiProperty): string; static;
    class procedure SetPropertyValue(const AInstance: TObject; const AProp: TRttiProperty;
      const AValue: Variant); static;
    class function VariantToBoolean(const V: Variant): Boolean; static;
    class function VariantToEnumOrdinal(AEnumType: TRttiType; const V: Variant): Integer; static;
  public
    class constructor Create;
    class destructor Destroy;

    class procedure MapToObject(const AReader: IValueReader; const ATarget: TObject;
      const AOptions: TRttiMapOptions = [moIgnoreMissing]); static;
  end;

implementation

{ MapFromAttribute }

constructor MapFromAttribute.Create(const AName: string);
begin
  inherited Create;
  FName := AName;
end;

{ TRttiMapper }

class constructor TRttiMapper.Create;
begin
  FCache := TObjectDictionary<PTypeInfo, TTypeCache>.Create([doOwnsValues]);
  FCacheLock := TObject.Create;
end;

class destructor TRttiMapper.Destroy;
begin
  FCache.Free;
  FCacheLock.Free;
end;

class function TRttiMapper.FindMapFromAttr(const AProp: TRttiProperty): string;
var
  Attr: TCustomAttribute;
begin
  Result := '';
  for Attr in AProp.GetAttributes do
    if Attr is MapFromAttribute then
      Exit(MapFromAttribute(Attr).Name);
end;

class function TRttiMapper.GetOrBuildCache(ATypeInfo: PTypeInfo): TTypeCache;
var
  Ctx: TRttiContext;
  RType: TRttiType;
  P: TRttiProperty;
  L: TList<TPropMap>;
  Src: string;
  M: TPropMap;
begin
  TMonitor.Enter(FCacheLock);
  try
    if FCache.TryGetValue(ATypeInfo, Result) then
      Exit;

    Result := TTypeCache.Create;

    Ctx := TRttiContext.Create;
    RType := Ctx.GetType(ATypeInfo);

    L := TList<TPropMap>.Create;
    try
      for P in RType.GetProperties do
      begin
        if not P.IsWritable then
          Continue;

        // Opt-in: biss Properties b'Attribbut
        Src := FindMapFromAttr(P);
        if Src = '' then
          Continue;

        M.Prop := P;
        M.SourceName := Src;
        L.Add(M);
      end;

      Result.Props := L.ToArray;
    finally
      L.Free;
    end;

    FCache.Add(ATypeInfo, Result);
  finally
    TMonitor.Exit(FCacheLock);
  end;
end;

class function TRttiMapper.VariantToBoolean(const V: Variant): Boolean;
var
  S: string;
begin
  if VarIsBool(V) then
    Exit(V);

  if VarIsNumeric(V) then
    Exit(V <> 0);

  S := Trim(VarToStr(V)).ToLower;
  if (S = '1') or (S = 'true') or (S = 't') or (S = 'y') or (S = 'yes') then
    Exit(True);
  if (S = '0') or (S = 'false') or (S = 'f') or (S = 'n') or (S = 'no') then
    Exit(False);

  raise ERttiMappingError.CreateFmt('Konverżjoni għal Boolean falliet: "%s"', [VarToStr(V)]);
end;

class function TRttiMapper.VariantToEnumOrdinal(AEnumType: TRttiType; const V: Variant): Integer;
var
  Ord: Integer;
  Name: string;
begin
  if VarIsNumeric(V) then
  begin
    Ord := Integer(V);
    if (Ord < GetTypeData(AEnumType.Handle)^.MinValue) or
       (Ord > GetTypeData(AEnumType.Handle)^.MaxValue) then
      raise ERttiMappingError.CreateFmt('Ordinal tal-enum barra mill-firxa: %d', [Ord]);
    Exit(Ord);
  end;

  Name := VarToStr(V);
  Ord := GetEnumValue(AEnumType.Handle, Name);
  if Ord < 0 then
    raise ERttiMappingError.CreateFmt('Isem tal-enum mhux magħruf: "%s"', [Name]);
  Result := Ord;
end;

class procedure TRttiMapper.SetPropertyValue(const AInstance: TObject;
  const AProp: TRttiProperty; const AValue: Variant);
var
  V: TValue;
  T: TRttiType;
  Ord: Integer;
begin
  T := AProp.PropertyType;

  // Konverżjoni intenzjonalment selettiva: aħjar li tfalli b'mod ċar milli tikkonverti xi ħaġa b'mod mhux determinat.
  case T.TypeKind of
    tkUString, tkString, tkLString, tkWString:
      V := TValue.From<string>(VarToStr(AValue));

    tkInteger, tkInt64:
      V := TValue.From<Int64>(VarAsType(AValue, varInt64));

    tkFloat:
      V := TValue.From<Double>(VarAsType(AValue, varDouble));

    tkEnumeration:
      begin
        if T.Handle = TypeInfo(Boolean) then
          V := TValue.From<Boolean>(VariantToBoolean(AValue))
        else
        begin
          Ord := VariantToEnumOrdinal(T, AValue);
          V := TValue.FromOrdinal(T.Handle, Ord);
        end;
      end;

    tkSet:
      raise ERttiMappingError.CreateFmt('Set-Mapping mhux implementat għal %s', [AProp.Name]);

    tkClass:
      raise ERttiMappingError.CreateFmt('Class-Property Mapping mhux implementat għal %s', [AProp.Name]);
  else
    raise ERttiMappingError.CreateFmt('TypeKind mhux appoġġjat (%s) għal %s',
      [GetEnumName(TypeInfo(TTypeKind), Ord(T.TypeKind)), AProp.Name]);
  end;

  AProp.SetValue(AInstance, V);
end;

class procedure TRttiMapper.MapToObject(const AReader: IValueReader;
  const ATarget: TObject; const AOptions: TRttiMapOptions);
var
  Cache: TTypeCache;
  M: TPropMap;
  V: Variant;
  Msg: string;
begin
  if (ATarget = nil) or (AReader = nil) then
    raise ERttiMappingError.Create('MapToObject: Reader jew Target huwa nil');

  Cache := GetOrBuildCache(ATarget.ClassInfo);

  for M in Cache.Props do
  begin
    if not AReader.HasValue(M.SourceName) then
    begin
      if not (moIgnoreMissing in AOptions) then
        raise ERttiMappingError.CreateFmt('Sors nieqes: "%s" għal Property %s',
          [M.SourceName, M.Prop.Name]);
      Continue;
    end;

    if AReader.IsNull(M.SourceName) then
    begin
      if moIgnoreNull in AOptions then
        Continue;
      // Mingħajr mekanika Nullable/Optional, ma jistax jitqiegħed NULL b'mod sensat.
      Continue;
    end;

    V := AReader.GetValue(M.SourceName);

    try
      SetPropertyValue(ATarget, M.Prop, V);
      if moDebug in AOptions then
      begin
        Msg := Format('Mapped %s <- %s (%s)', [M.Prop.Name, M.SourceName, VarTypeAsText(VarType(V))]);
        OutputDebugString(PChar(Msg));
      end;
    except
      on E: Exception do
        raise ERttiMappingError.CreateFmt('Żball tal-mapping għal %s <- %s: %s',
          [M.Prop.Name, M.SourceName, E.Message]);
    end;
  end;
end;

end.

Għalxiex dan iservi

Tikseb mapping li jista‘ jiġi vvalutat b’mod ċar fil-Code-Reviews:

  • Kull Property mappjata hija mmarkata viżwalment (atribut).
  • Il-konverżjoni hija ċentrali, u b’hekk konsistenti u ttestjabbli.
  • Il-messaġġi ta‘ żball jindikaw liema Property u liema sors huma affettwati.
  • Modalità debug tipprovdulek, fil-każ ta‘ dubju, il-katina ta‘ evidenza mingħajr ma jkollok bżonn breakpoints fil-proċess ta‘ produzzjoni.

Kundizzjonijiet limitu u ostakli tipici

  • NULL-Semantik: Mingħajr kunċett proprju ta‘ Nullable (eż. Nullable<T> jew Option-Types) l-issettjar ta‘ NULL mhuwiex uniku. Fil-snippet NULL tiġi skippjata b’mod default. Dan hu konservattiv u jipprevjeni sovrascrizzjonijiet silentjużi.
  • TRttiContext-Lebensdauer: Nabnu l-cache darba għal kull tip u nħassru l-Context wara. Dan hu prassi komuni. Importanti: Tħallux tkun qed toħloq RT TI-Context ġdid għal kull assenja ta‘ field.
  • Threading: Il-cache huwa protett permezz ta‘ Monitor. F’mappings b’paralleli għoljin (eż. REST-Server) għandek tivverifika wkoll jekk tagħmel ‚warm‘ il-cache waqt l-avvio (Preload), biex tnaqqas il-lock-contention.
  • PropertyType Kind: tkClass u tkSet huma intenzjonalment mhux implementati. Għal oġġetti inkapsulati għandek tmapja rekurżivament (bi policy ċara) jew tassenja b’mod manwali u intenzjonat.
  • Locale-Fallen: varDouble permezz ta‘ VarAsType huwa relattivament robust, imma strings bħal „1,23“ vs. „1.23“ għadhom problema. Jekk is-sorsi tiegħek jirritornaw strings, parser proprju (bi Culture definit) spiss ikun aħjar.

Varjant għal FireDAC u TDataSet: Reader-Adapter minflok koppja mal-Mapper

F’BDE-Ablosung mit nativer Anbindung jew f’applikazzjonijiet klassiċi VCL/Win32 is-sors spiss huwa TDataSet. Minflok tgħaqqad il-Mapper ma‘ TField, ikteb adapter li jimplimenta l-interface IValueReader. Il-vantaġġ: il-Mapper jibqa‘ indipendenti mill-aċċess tad-dejta (importanti meta tirtira l-aċċess tad-dejta aktar tard f’services jew f‘REST-Server).

Delphi
uses Data.DB, System.Variants, RttiMapping;

type
  TDataSetValueReader = class(TInterfacedObject, IValueReader)
  private
    FDS: TDataSet;
  public
    constructor Create(ADS: TDataSet);
    function HasValue(const AName: string): Boolean;
    function IsNull(const AName: string): Boolean;
    function GetValue(const AName: string): Variant;
  end;

constructor TDataSetValueReader.Create(ADS: TDataSet);
begin
  inherited Create;
  FDS := ADS;
end;

function TDataSetValueReader.HasValue(const AName: string): Boolean;
begin
  Result := (FDS <> nil) and (FDS.FindField(AName) <> nil);
end;

function TDataSetValueReader.IsNull(const AName: string): Boolean;
var
  F: TField;
begin
  F := FDS.FindField(AName);
  Result := (F = nil) or F.IsNull;
end;

function TDataSetValueReader.GetValue(const AName: string): Variant;
begin
  Result := FDS.FieldByName(AName).Value;
end;

B’dan il-mod mapping konkreta tidher hekk:

Delphi
type
  TOrderRow = class
  private
    FId: Int64;
    FCustomerNo: string;
    FIsClosed: Boolean;
  public
    [MapFrom('order_id')]
    property Id: Int64 read FId write FId;

    [MapFrom('customer_no')]
    property CustomerNo: string read FCustomerNo write FCustomerNo;

    [MapFrom('is_closed')]
    property IsClosed: Boolean read FIsClosed write FIsClosed;
  end;

// ...
var
  Row: TOrderRow;
  Reader: IValueReader;
begin
  Row := TOrderRow.Create;
  try
    Reader := TDataSetValueReader.Create(MyQuery);
    TRttiMapper.MapToObject(Reader, Row, [moIgnoreMissing, moDebug, moIgnoreNull]);
  finally
    Row.Free;
  end;
end;

Fejn dan l-approċċ jiswa — u fejn le

Dan il-mudell ġeneralment jiswa fi tliet sitwazzjonijiet:

  1. Modernizzazzjoni gradwali: Tixtiequ tinroduċu oġġetti tad-dominju mingħajr ma tbiddlu kompletament l-aċċess tad-dejta minnufih (tipikament fil-Delphi Modernizzazzjoni f’applikazzjonijiet eżistenti).
  2. Punti tal-interface: Importazzjonijiet CSV/Excel, REST-Payloads jew sorsi ta‘ dejta „miksti“ jeħtieġu konverżjoni robusta u messaġġi ta‘ żball ċari.
  3. Mantenibbiltà fit-tim: L-attributi jagħmlu r-regoli tal-mapping viżibbli u reviżjabbli, li għandu valur kbir f’kodiċi kbar.

Hemm ukoll limiti ċari tal-użu:

  • Grafi ta‘ oġġetti kumplessi (Child-Collections, referenzi ċikliċi) m’għandhomx jiġu mappjati b’mod „automagiku“. Hawnhekk kodiċi esplicitu jew mudell Assembler/Factory separati huwa spiss aktar stabbli.
  • Hotpaths ta‘ throughput għoli (pereżempju ETL ta‘ dejta massiva) jiffavorixxu mapperi ġenerati mill-kodiċi jew mapping ottimizzat b’mod manwali, anki jekk RTTI tkun cached.
  • Nullable/Optional huwa suġġett separat. Jekk verament għandek bżonn tiddistingwi bejn „mhux preżenti“, „NULL“ u „Default“, għandek tesprimi dan fil-mudell tad-dominju, mhux taħbih fil-mapper.

Pożizzjonament fl-arkitettura u fl-operat

Minn perspettiva arkitettonika, dan il-mapper huwa komponent ta‘ infrastruttura fil-fruntiera bejn ir-repreżentazzjoni tad-dejta u d-dominju. Mhuwiex sostitut ta‘ stratifikazzjoni nadifa, iżda jista‘ jippermettiha: l-aċċess tad-dejta (FireDAC, SQL, Views) jista‘ jibqa‘ pragmatiku, filwaqt li d-dominju jibqa‘ konsistenti. Fis-sistemi b’ħafna saffar (spiss imsejħa Layer-3 Arkitettura: UI, Domain/Services, Infrastruktur) il-mapper għandu jkun fin-infrastruttura u jintuża mis-servizzi, mhux mill-formoli tal-UI.

Operattivament importanti: Ma tħallix moDebug attivat b’mod permanenti fis-servizzi produttivi; uża dan b’mod mirqum. Għal problemi tad-dejta diffiċli biex jirreprodukku, huwa sensat li jkollok triq dijanjostika li tista‘ tinbidel (konfigurazzjoni, Feature-Flag). Inkella hemm riskju ta‘ volum kbir ta‘ log u effetti sekondarji.

Konklużjoni: RTTI iva, iżda biss bi linji gwida ċari

Delphi RTTI għal Mapping mingħajr maġija jaħdem tajjeb meta tuża RTTI bħala għodda għal metadati deklarattivi — mhux bħala sejħa għal euristiċi moħbija. Attribwiti bħala opt-in, konverżjoni ċentralizzata, cache għal kull tip u testi ta‘ żball ċari jġibu s-suġġett minn „mhux trasparenti“ għal „operabbli“. L-approċċ hu b’mod konxju mhux universali: għal grafi imwaħħla, semantika stretta tal-null jew prestazzjoni massima għandek bżonn komponenti addizzjonali. Bħala pont robust bejn Dataset/Legacy-Strukturen u oġġetti ta‘ dominju aktar moderni, huwa f’ħafna Delphi-codebasen eżatt il-pass pragmatiku li jagħmel il-modernizzazzjoni possibbli.

Jekk f’applikazzjoni Delphi li kibret issa qed tiffaċċja limiti tal-mapping, problemi fil-kwalità tad-dejta jew modernizzazzjoni gradwali, nistgħu nissettjaw dan flimkien b’mod nadif u nintegrawh fl-arkitettura tiegħek: Ikkuntattjana.

Fil-kuntest professjonali, Delphi Rtti Mapping u Attribute Mapping Delphi jilagħbu rwol importanti meta l-integrazzjonijiet, il-flussi tad-dejta u l-iżvilupp kontinwu jridu jaħdmu flimkien b’mod nadif.

Niddiskutu proġett jew inizjattiva ta‘ modernizzazzjoni ma‘ Net-Base.

Aqsam il-post

Aqsam dan il-post direttament

LinkedIn, X, XING, Facebook, WhatsApp u E-Mail huma immedjatament disponibbli. Għal Instagram nippreparaw il-link u t-test qasir direttament.

Imejl

Instagram jiftaħ f'tab ġdid. Il-link u t-test qasir jiġu kkopjati qabel fil-clipboard.