Net-Base Magazín

08.05.2026

Delphi RTTI pre mapovanie bez mágie: atribútmi riadené, laditeľné a kompatibilné so starším kódom

Pragmatický mapovací vzor s Delphi RTTI: atribúty namiesto konvencií, kontrolované konverzie, jasné chybové hlásenia a debugovací režim, ktorý v prevádzke skutočne pomáha. S ukážkami zdrojového kódu pre mapovanie z Datasetu alebo Recordu na objekt bez skrytej mágie.

08.05.2026

Kto prevádzkuje existujúci obchodný softvér v Delphi, pozná toto napätie: na jednej strane chcete štruktúrované doménové objekty a jasné vrstvy, na druhej strane sú tu Datasets, Variants, CSV-importy, payloady rozhraní alebo REST-API, ktoré sa „nejako“ musia namapovať na objekty. Práve tu rýchlo narazíte na Delphi RTTI für Mapping ohne Magie: teda mapovanie pomocou Reflection (RTTI = Run-Time Type Information, typové informácie za behu), ale tak, aby to zostalo zrozumiteľné, dobre ladiť a aby sa nespoliehalo potajomky na konvencie alebo hru s názvami.

Jadro veci: „mágia“ vzniká väčšinou nie samotným RTTI, ale implicitnými pravidlami. Ak sú pravidlá mapovania explicitne uvedené v atribútoch, konverzie sú centralizované a chyby uvádzajú jasnú príčinu, stane sa RTTI nástrojom namiesto prekvapenia.

Prečo RTTI-mapovanie v Delphi často zlyhá

Mapovanie založené na RTTI v reálnych systémoch zriedka zlyháva kvôli myšlienke, skôr kvôli okrajovým podmienkam:

  • Legacy formáty dát: Null/Empty/0 nie sú jasne oddelené, typy polí sa menia, reťazce obsahujú „N/A“.
  • Postupné konvencie: „Pole sa volá ako Property“ funguje až do prvého aliasu, joinu alebo refaktorizovaného názvu Property.
  • Ťažké na ladiť: Keď mapper „jednoducho nič nenastaví“, neskôr chýba dôvod. V prevádzke je to jed.
  • Mýty o výkonnosti: RTTI je všeobecne označované za „pomalé“, hoci problémom býva zvyčajne chýbajúce cachovanie.

Udržateľný prístup by mal preto (1) mať explicitné metadáta mapovania, (2) jasne riešiť konverziu a nulovú sémantiku, (3) poskytovať chybové a debug výstupy a (4) cachovať RTTI-informácie.

Delphi RTTI pre mapovanie bez mágie: návrhové princípy

Nasledujúci vzor je zámerne „nudný“ v tom najlepšom zmysle: pravidlá sú viditeľné, vedľajšie účinky obmedzené a dá sa ho postupne zaviesť do existujúcich modulov.

  • Atribúty namiesto menných konvencií: Property dostane atribút, ktorý pomenováva zdrojový stĺpec.
  • Opt-in: Nastavujú sa len označené Properties. Žiadne prekvapenia z „všetkých zverejnených Properties“.
  • Konverzia na jednom mieste: Variant/String/Integer/Boolean/Enum/Nullable sa mapujú centrálne.
  • Debug režim: Voliteľne sa protokoluje, ktoré polia boli nastavené/preskočené – s uvedením dôvodu.
  • Cachovanie RTTI: Najnákladnejšie časti (zoznam Properties, vyhodnocovanie atribútov) sa pripravujú pre každý typ.

Ukážka zdrojového kódu: Mapovanie atribútov s RTTI, cachovaním a debugom

Úryvok mapuje riadok (napr. z BDE-náhrada s natívnym prepojením cez TDataSet) na objekt. Namiesto pevného viazania mappera na TField používame malé rozhranie pre čítanie. To je v praxi cenné, pretože neskôr môžete tú istú logiku použiť aj pre JSON, INI, CSV alebo API-odpovede.

Delphi
unit RttiMapping;

interface

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

type
  // Explicitné mapovanie: vlastnosť <- názov zdroja
  MapFromAttribute = class(TCustomAttribute)
  private
    FName: string;
  public
    constructor Create(const AName: string);
    property Name: string read FName;
  end;

  // Malá abstrakcia: poskytnúť hodnotu + rozlíšiť existenciu/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: len vlastnosti s atribútom
        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('Konverzia na boolean zlyhala: "%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('Enum ordinal mimo rozsahu: %d', [Ord]);
    Exit(Ord);
  end;

  Name := VarToStr(V);
  Ord := GetEnumValue(AEnumType.Handle, Name);
  if Ord < 0 then
    raise ERttiMappingError.CreateFmt('Názov enumu neznámy: "%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;

  // Konverzia zámerne selektívna: radšej jasne zlyhať než ticho "nejako".
  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('Mapovanie typu set nie je implementované pre %s', [AProp.Name]);

    tkClass:
      raise ERttiMappingError.CreateFmt('Mapovanie Class-Property nie je implementované pre %s', [AProp.Name]);
  else
    raise ERttiMappingError.CreateFmt('TypeKind nie je podporovaný (%s) pre %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 alebo Target je 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('Zdroj chýba: "%s" pre vlastnosť %s',
          [M.SourceName, M.Prop.Name]);
      Continue;
    end;

    if AReader.IsNull(M.SourceName) then
    begin
      if moIgnoreNull in AOptions then
        Continue;
      // Bez mechaniky Nullable/Optional nie je možné rozumne nastaviť NULL.
      Continue;
    end;

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

    try
      SetPropertyValue(ATarget, M.Prop, V);
      if moDebug in AOptions then
      begin
        Msg := Format('Namapované %s <- %s (%s)', [M.Prop.Name, M.SourceName, VarTypeAsText(VarType(V))]);
        OutputDebugString(PChar(Msg));
      end;
    except
      on E: Exception do
        raise ERttiMappingError.CreateFmt('Chyba mapovania pri %s <- %s: %s',
          [M.Prop.Name, M.SourceName, E.Message]);
    end;
  end;
end;

end.

Na čo to slúži

Dostanete mapovanie, ktoré sa dá v Code-Reviewoch spoľahlivo posúdiť:

  • Každá mapovaná Property je vizuálne označená (atribút).
  • Konverzia je centralizovaná, v dôsledku čoho konzistentná a testovateľná.
  • Chybové hlásenia uvádzajú, ktorá Property a ktorý zdroj sú postihnuté.
  • Debugovací režim vám v prípade potreby poskytne reťazec dôkazov, bez nutnosti nastavovať breakpoints v produkčnom procese.

Predpoklady a typické úskalia

  • NULL-Semantik: Bez vlastného Nullable-konceptu (napr. Nullable<T> alebo Option-Types) nie je „NULL nastaviť“ jednoznačné. V ukážke sa NULL štandardne preskočí. To je konzervatívne a zabraňuje tichým prepísaniam.
  • TRttiContext-Lebensdauer: Cache vytvárame raz pre typ a Context potom zahodíme. To je bežné. Dôležité je: netvorte nový RTTI-Context pri každom priradení poľa.
  • Threading: Cache je chránený cez Monitor. Pri vysoko paralelných mapovaniach (napr. REST-Server) by ste mali zvážiť, či cache už pri štarte „warm“ nevytvoriť (preload), aby ste znížili lock-contention.
  • PropertyType Kind: tkClass a tkSet sú zámerne neimplementované. Pre vnorené objekty mapujte rekurzívne (s jasnou politikou) alebo priraďte vedome manuálne.
  • Úskalia lokality (Locale): varDouble cez VarAsType je relatívne robustné, ale reťazce ako „1,23″ vs. „1.23″ sú stále problém. Ak vaše zdroje vracajú stringy, je často lepší vlastný parser (s definovanou Culture).

Varianta pre FireDAC a TDataSet: Reader-Adapter namiesto prepojenia s mapperom

V BDE-Ablosung mit nativer Anbindung- alebo klasických VCL/Win32 aplikáciách je zdroj často TDataSet. Namiesto viazania Mapperu na TField napíšte adaptér, ktorý implementuje rozhranie IValueReader. Výhoda: Mapper zostáva nezávislý od prístupu k dátam (dôležité, ak prístup k dátam neskôr presuniete do služieb alebo na 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;

Konkrétne mapovanie potom vyzerá takto:

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;

Kde sa prístup oplatí — a kde nie

Tento vzor sa obvykle oplatí v troch situáciách:

  1. Kroková modernizácia: Chcete zaviesť doménové objekty, bez toho aby ste okamžite kompletne prerábali prístup k dátam (klasicky pri Delphi modernizácia v existujúcich aplikáciách).
  2. Spojnicové body: CSV-/Excel-importy, REST-payloady alebo „zmiešané“ zdroje dát potrebujú robustnú konverziu a kvalitné chybové hlásenia.
  3. Udržiavateľnosť v tíme: Atribúty robia pravidlá mapovania viditeľnými a prehľadnými pri revízii, čo je v rozsiahlych kódových bárach veľmi cenné.

Existujú aj jasné hranice použitia:

  • Komplexné grafy objektov (podriadené kolekcie, cyklické referencie) by ste nemali „automagicky“ mapovať. Tu je explicitný kód alebo oddelený Assembler/Factory-vzor zvyčajne stabilnejší.
  • Hotpaths s vysokou priepustnosťou (napr. ETL pre hromadné dáta) majú skôr úžitok z kódom generovaných mapperov alebo ručne optimalizovaného mapovania, aj keď je RTTI cachované.
  • Nullable/Optional je samostatná téma. Ak potrebujete skutočne rozlíšiť medzi „neprítomné“, „NULL“ a „predvolená hodnota“, mali by ste to vyjadriť v doménovom modeli, nie skrývať v mapperi.

Zaradenie do architektúry a prevádzky

Z architektonického hľadiska je tento mapper infraštruktúrnou komponentou na rozhraní medzi reprezentáciou dát a doménou. Nenahrádza čisté vrstvenie, môže ho však umožniť: Prístup k dátam (FireDAC, SQL, Views) môže zostať pragmatický, zatiaľ čo doména zostáva konzistentná. Vo viacvrstvových systémoch (často označovaných ako Layer-3 architektúra: UI, Domain/Services, Infrastruktur) patrí mapper do infraštruktúry a používajú ho služby, nie UI formuláre.

Prevádzková dôležitosť: Neaktivujte moDebug trvale v produkčných službách, ale cielene. Pre ťažko reprodukovateľné dátové problémy je vhodné mať prepínateľnú diagnostickú cestu (konfigurácia, Feature-Flag). Inak hrozí objem logov a nežiaduce vedľajšie účinky.

Záver: RTTI áno, ale len s jasnými vodiacimi zásadami

Delphi RTTI pre mapovanie bez mágie funguje dobre, keď používate RTTI ako nástroj pre deklaratívne metadáta – nie ako pozvánku k tichým heuristikám. Atribúty ako opt-in, centralizovaná konverzia, cache pre typ a zrozumiteľné chybové hlásenia posúvajú tému z „neprehľadnej“ na „prevádzkyschopnú“. Prístup nie je úmyselne univerzálny: pre vnorené grafy, prísnu nulovú sémantiku alebo maximálny výkon potrebujete ďalšie komponenty. Ako robustný most medzi dataset/legacy štruktúrami a modernejšími doménovými objektami je však v mnohých Delphi kódbázach práve ten pragmatický krok, ktorý modernizáciu vôbec umožní.

Ak v existujúcej Delphi aplikácii práve narazíte na hranách mapovania, problémy s kvalitou dát alebo pri postupnej modernizácii, môžeme to spoločne čisto nastaviť a zapracovať do vašej architektúry: Kontaktujte nás.

V odbornom kontexte zohrávajú dôležitú úlohu aj Delphi Rtti Mapping a Attribute Mapping Delphi, keď musia integrácie, dátové toky a ďalší vývoj hladko spolupracovať.

Prediskutovať projekt alebo modernizačný zámer s Net-Base.

Zdieľať príspevok

Tento príspevok priamo zdieľať

LinkedIn, X, XING, Facebook, WhatsApp a e-mail sú ihneď k dispozícii. Pre Instagram pripravujeme priamo odkaz a krátky text.

E-mail

Instagram sa otvorí v novej karte. Odkaz a krátky text sa predtým skopírujú do schránky.