Net-Base Časopis

08.05.2026

Delphi RTTI za mapiranje bez magije: zasnovano na atributima, pogodno za otklanjanje grešaka i kompatibilno sa naslijeđenim sistemima

Pragmatičan obrazac mapiranja s Delphi RTTI: atributi umjesto konvencija, kontrolirane konverzije, jasne poruke o greškama i način za otklanjanje grešaka koji u radu zaista pomaže. Sa isječcima izvornog koda za mapiranje Dataset-a ili Record-a u objekte bez skrivene magije.

08.05.2026

Ko upravlja rastućim poslovnim softverom u Delphi, zna to napetostno polje: s jedne strane želi se strukturirani domenski objekti i jasni slojevi, a s druge postoje Datasets, Variants, CSV-importi, payloadi sučelja ili jedna REST-API koja se „na neki način“ mora mapirati na objekte. Upravo tu se brzo dođe do Delphi RTTI für Mapping ohne Magie: dakle mapiranje preko Reflection (RTTI = Run-Time Type Information, informacije o tipu pri izvođenju), ali tako da ostane razumljivo, dobro za debugiranje i da se ne oslanja tajno na konvencije ili igre s imenima.

Suština: „magija“ obično ne nastaje zbog samog RTTI, nego zbog implicitnih pravila. Ako su pravila mapiranja umjesto toga eksplicitno u atributima, konverzije su centralizirane i greške navode jasan uzrok, RTTI postaje alat umjesto iznenađenja.

Warum RTTI-Mapping in Delphi oft kippt

RTTI-bazirano mapiranje u realnim sistemima rijetko propada zbog same ideje, već zbog ograničavajućih okolnosti:

  • Naslijeđeni oblici podataka: Null/Empty/0 nisu jasno odvojeni, tipovi polja se mijenjaju, stringovi sadrže „N/A“.
  • Postepene konvencije: „Feld heißt wie Property“ funkcionira dok se ne pojavi prvi alias, join ili refaktorisano ime Property-ja.
  • Teško za debugiranje: Ako Mapper „jednostavno ništa ne postavlja“, kasnije nedostaje uzrok. U produkciji je to neprihvatljivo.
  • Mitovi o performansama: RTTI se paušalno označava kao „sporo“, iako je najčešće nedostatak keširanja pravi problem.

Održiv pristup bi zato trebao (1) imati eksplicitne metapodatke mapiranja, (2) jasno tretirati konverziju i null-semantiku, (3) pružati greške i debug-izlaze i (4) keširati RTTI-informacije.

Delphi RTTI für Mapping ohne Magie: Designprinzipien

Sljedeći obrazac je namjerno „dosadan“ u najboljem smislu: pravila su vidljiva, nuspojave su ograničene i može se korak po korak uvesti u postojeće module.

  • Atributi umjesto konvencije imena: Property dobije atribut koji navodi izvorni stupac.
  • Opt-in: Postavljaju se samo označena Property-ja. Nema iznenađenja kroz „sva publicirana Property-ja“.
  • Konverzija na jednom mjestu: Variant/String/Integer/Boolean/Enum/Nullable se centralno mapiraju.
  • Debug-Mode: Opcionalno se evidentira koja polja su postavljena/preskočena – s obrazloženjem.
  • RTTI-Caching: Najskuplji dijelovi (lista Property-ja, evaluacija atributa) se pripremaju po tipu.

Source-Schnipsel: Attribut-Mapping mit RTTI, Caching und Debug

Snippet preslikava jedan red (npr. iz BDE-Ablosung mit nativer Anbindung via TDataSet) na objekt. Umjesto da Mapper čvrsto povezujemo s TField, koristimo malu Reader-sučelje. To je u praksi vrijedno, jer kasnije istu logiku možete koristiti i za JSON, INI, CSV ili API-odgovore.

Delphi
unit RttiMapping;

interface

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

type
  // Explizites Mapping: Property <- Quellname
  MapFromAttribute = class(TCustomAttribute)
  private
    FName: string;
  public
    constructor Create(const AName: string);
    property Name: string read FName;
  end;

  // Kleine Abstraktion: Wert liefern + Existenz/NULL unterscheiden
  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: Nur Properties mit Attribut
        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('Boolean-Konvertierung fehlgeschlagen: "%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 außerhalb des Bereichs: %d', [Ord]);
    Exit(Ord);
  end;

  Name := VarToStr(V);
  Ord := GetEnumValue(AEnumType.Handle, Name);
  if Ord < 0 then
    raise ERttiMappingError.CreateFmt('Enum-Name unbekannt: "%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;

  // Konvertierung bewusst selektiv: lieber klar scheitern als still "irgendwie".
  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 nicht implementiert für %s', [AProp.Name]);

    tkClass:
      raise ERttiMappingError.CreateFmt('Class-Property Mapping nicht implementiert für %s', [AProp.Name]);
  else
    raise ERttiMappingError.CreateFmt('TypeKind nicht unterstützt (%s) für %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 oder Target ist 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('Quelle fehlt: "%s" für Property %s',
          [M.SourceName, M.Prop.Name]);
      Continue;
    end;

    if AReader.IsNull(M.SourceName) then
    begin
      if moIgnoreNull in AOptions then
        Continue;
      // Ohne Nullable/Optional-Mechanik kann man NULL nicht sinnvoll setzen.
      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('Mapping-Fehler bei %s <- %s: %s',
          [M.Prop.Name, M.SourceName, E.Message]);
    end;
  end;
end;

end.

Čemu to služi

Dobijate mapiranje koje se može jasno procijeniti u pregledima koda:

  • Svako mapirano svojstvo je vizuelno označeno (atribut).
  • Konverzija je centralizovana, zbog čega je konzistentna i testabilna.
  • Tekstovi grešaka navode koje svojstvo i koji izvor su pogođeni.
  • Debug-modus vam, u slučaju sumnje, daje lanac dokaza, bez potrebe za Breakpoints u proizvodnom procesu.

Ograničenja i tipične zamke

  • NULL-Semantik: Bez vlastitog Nullable-koncepta (npr. Nullable<T> ili Option-Types) „postavljanje NULL-a“ nije jednoznačno. U primjeru se NULL standardno preskače. To je konzervativno i sprječava tiha prepisivanja.
  • TRttiContext-Lebensdauer: Gradimo cache jednom po tipu i potom odbacimo Context. To je uobičajeno. Važno je: Ne graditi novi RTTI-Context za svako dodjeljivanje polja.
  • Threading: Cache je zaštićen putem Monitora. U visoko paralelnim mapiranjima (npr. REST-Server) trebate dodatno provjeriti da li cache već pri startu gradite „toplo“ (Preload), kako biste smanjili Lock-Contention.
  • PropertyType Kind: tkClass i tkSet su namjerno neimplementirani. Za ugniježdene objekte trebate ili rekurzivno mapirati (s jasnom policom) ili svjesno ručno dodijeliti.
  • Locale-Fallen: varDouble preko VarAsType je relativno robustan, ali stringovi poput „1,23“ vs. „1.23“ i dalje su problem. Ako vaši izvori vraćaju stringove, vlastiti parser (s definiranim Culture) je često bolji.

Varijanta za FireDAC i TDataSet: Reader-Adapter umjesto vezivanja Mappera

U BDE-Ablosung mit nativer Anbindung- ili klasičnim VCL/Win32-aplikacijama je izvor često TDataSet. Umjesto da Mapper vezujete za TField, napišite adapter koji implementira sučelje IValueReader. Prednost: Mapper ostaje nezavisan od pristupa podacima (važno ako kasnije pristup podacima premještate u servise ili 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;

Tako konkretno mapiranje izgleda:

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;

Gdje se pristup isplati — i gdje ne

Ovaj pristup se tipično isplati u tri situacije:

  1. Postepena modernizacija: Želite uvesti domenske objekte bez potpune pregradnje pristupa podacima odjednom (klasično kod Delphi modernizacije u postojećim aplikacijama).
  2. Rubne tačke interfejsa: CSV-/Excel-uvozi, REST-payloadi ili „miješani“ izvori podataka zahtijevaju robusnu konverziju i jasne poruke o greškama.
  3. Održavanje u timu: Atributi čine pravila mapiranja vidljivim i preglednim, što u većim kodnim bazama ima veliku vrijednost.

Postoje i jasna ograničenja primjene:

  • Složeni grafovi objekata (Child-Collections, ciklične reference) ne biste trebali „automagično“ mapirati. Ovdje je eksplicitan kod ili odvojeni Assembler/Factory-obrazac obično stabilniji.
  • High-Throughput-Hotpaths (npr. Massendaten-ETL) više profitiraju od mappera generisanih iz koda ili ručno optimiziranog mapiranja, čak i ako je RTTI keširan.
  • Nullable/Optional je zasebna tema. Ako zaista morate razlikovati „nepostojeće“, „NULL“ i „Default“, trebate to izraziti u domenskom modelu, a ne skrivati u mapperu.

Pozicioniranje u arhitekturi i operacijama

Iz perspektive arhitekture, ovaj mapper je infrastrukturna komponenta na granici između reprezentacije podataka i domene. Ne zamjenjuje jasnu separaciju slojeva, ali je može omogućiti: pristup podacima (FireDAC, SQL, Views) može ostati pragmatičan, dok domena ostaje konzistentna. U višeslojnih sistema (često nazivanih Layer-3 arhitektura: UI, Domain/Services, infrastruktura) mapper pripada infrastrukturi i koriste ga servisi, ne UI-formulari.

Operativno važno: Ne aktivirajte moDebug trajno u produkcijskim servisima, već ciljano. Za teško reproducibilne probleme s podacima korisno je imati preklopivi dijagnostički put (konfiguracija, Feature-Flag). Inače prijeti veliki volumen logova i neželjeni učinci.

Zaključak: RTTI da, ali samo uz jasne smjernice

Delphi RTTI za mapiranje bez magije djeluje dobro kada RTTI koristite kao alat za deklarativne metapodatke – ne kao poziv na prikrivene heuristike. Atributi kao opt-in, centralizirana konverzija, cache po tipu i razumljive poruke o greškama premještaju temu iz „neprozirno“ u „operativno“. Pristup je namjerno nije univerzalan: za ugnježdene grafove, strogu null-semantiku ili maksimalne performanse potrebni su dodatni moduli. Kao robusni most između dataset/legacy-struktura i modernijih objekata domene, on je u mnogim Delphi codebasama upravo onaj pragmatičan korak koji modernizaciju čini mogućom.

Ako u već postojećoj Delphi aplikaciji trenutno zapinjete na rubovima mapiranja, kvaliteti podataka ili postupnoj modernizaciji, možemo to zajedno uredno postaviti i uklopiti u vašu arhitekturu: Kontaktirajte nas.

U stručnom okruženju također Delphi Rtti Mapping i Attribute Mapping Delphi igraju važnu ulogu kada integracije, tokovi podataka i dalji razvoj moraju skladno funkcionirati.

Razgovarajte o projektu ili planu modernizacije sa Net-Base.

Podijeli objavu

Ovu objavu direktno proslijediti

LinkedIn, X, XING, Facebook, WhatsApp i e-pošta su odmah dostupni. Za Instagram pripremamo link i kratak tekst.

E-pošta

Instagram se otvara u novom tabu. Link i kratak tekst se prethodno kopiraju u međuspremnik.