Net-Base Časopis

08.05.2026

Delphi RTTI za mapiranje bez magije: temeljeno na atributima, pogodno za otklanjanje pogrešaka i kompatibilno s naslijeđenim sustavima

Pragmatičan obrazac mapiranja s Delphi RTTI: atributi umjesto konvencija, kontrolirane konverzije, jasni tekstovi o pogreškama i debug-mod koji u produkciji zaista pomaže. S isječcima izvornog koda za mapiranje iz Dataset-a ili Record-a u objekt bez skrivene magije.

08.05.2026

Tko upravlja naslijeđenim poslovnim softverom u Delphi zna koja je napetost: s jedne strane zahtijevaju se strukturirani domenski objekti i jasni slojevi, a s druge postoje dataseti, Variants, CSV-uvozi, payloadi sučelja ili REST-API koje se „nekako“ moraju mapirati na objekte. Upravo tu se brzo dođe do Delphi RTTI za mapiranje bez magije: odnosno mapiranja preko refleksije (RTTI = Run-Time Type Information, informacije o tipu u vrijeme izvođenja), ali tako da ostane razumljivo, dobro za debugiranje i da se ne oslanja skriveno na konvencije ili trikove s imenima.

Ključna poanta: „magija“ obično ne nastaje zbog same RTTI, nego zbog implicitnih pravila. Ako su pravila mapiranja eksplicitno u atributima, konverzije su centralizirane i greške imaju jasno imenovan uzrok, RTTI postaje alat umjesto iznenađenja.

Zašto RTTI-mapping u Delphi često zakaže

RTTI-bazirano mapiranje u realnim sustavima rijetko propada zbog ideje, već zbog ograničenja:

  • Naslijeđeni oblici podataka: Null/Empty/0 nisu jasno razdvojeni, tipovi polja se mijenjaju, stringovi sadrže „N/A“.
  • Postupno uvlačenje konvencija: „Polje se zove kao Property“ funkcionira dok se ne pojavi prvi alias, join ili refaktorirano ime propertyja.
  • Teško za debugiranje: Ako mapper „jednostavno ništa ne postavi“, kasnije nedostaje uzrok. U produkciji je to otrov.
  • Mitovi o performansama: RTTI se paušalno proglašava „sporim“, iako je najčešće nedostatak keširanja pravi problem.

Održiv pristup stoga treba (1) imati eksplicitne metapodatke mapiranja, (2) jasno rješavati konverzije i semantiku null-vrijednosti, (3) isporučivati greške i debug-izlaze te (4) keširati RTTI-informacije.

Delphi RTTI za mapiranje bez magije: principi dizajna

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

  • Atributi umjesto konvencije imenovanja: svojstvo se označi atributom koji imenuje izvorni stupac.
  • Opt-in: Samo označena svojstva se postavljaju. Nema iznenađenja zbog „sva publicirana svojstva“.
  • Konverzija na jednom mjestu: Variant/String/Integer/Boolean/Enum/Nullable se centralno mapiraju.
  • Debug-mode: Opcionalno se bilježi koja su polja postavljena/preskočena — s razlogom.
  • RTTI-keširanje: Najskuplji dijelovi (lista svojstava, evaluacija atributa) pripremaju se po tipu.

Izvorni isječak: mapiranje atributa s RTTI-jem, keširanjem i debugiranjem

Isječak preslikava jedan red (npr. iz BDE-zamjena s nativnom integracijom putem TDataSet) na objekt. Umjesto da mapper čvrsto povežemo s TField, koristimo malo Reader-sučelje. To je u praksi vrijedno jer kasnije istu logiku možete koristiti i za JSON, INI, CSV ili API-odgovore.

unit RttiMapping;

interface

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

type
// Eksplizitno mapiranje: Property <- naziv izvora
MapFromAttribute = class(TCustomAttribute)
private
FName: string;
public
constructor Create(const AName: string);
property Name: string read FName;
end;

// Mala apstrakcija: dostaviti vrijednost + razlikovati postojanje/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: samo Properties s atributom
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(‚Konverzija u Boolean nije uspjela: „%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 izvan raspona: %d‘, [Ord]);
Exit(Ord);
end;

Name := VarToStr(V);
Ord := GetEnumValue(AEnumType.Handle, Name);
if Ord < 0 then
raise ERttiMappingError.CreateFmt(‚Nepoznato ime enum-a: „%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;

// Pretvorba svjesno selektivna: radije jasno propasti nego tiho ’nekako‘.
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(‚Mapiranje tipa set nije implementirano za %s‘, [AProp.Name]);

tkClass:
raise ERttiMappingError.CreateFmt(‚Mapiranje svojstva klase nije implementirano za %s‘, [AProp.Name]);
else
raise ERttiMappingError.CreateFmt(‚TypeKind nije podržan (%s) za %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 ili 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(‚Izvor nedostaje: „%s“ za Property %s‘,
[M.SourceName, M.Prop.Name]);
Continue;
end;

if AReader.IsNull(M.SourceName) then
begin
if moIgnoreNull in AOptions then
Continue;
// Bez Nullable/Optional mehanike NULL se ne može smisleno postaviti.
Continue;
end;

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

try
SetPropertyValue(ATarget, M.Prop, V);
if moDebug in AOptions then
begin
Msg := Format(‚Mapirano %s <- %s (%s)‘, [M.Prop.Name, M.SourceName, VarTypeAsText(VarType(V))]);
OutputDebugString(PChar(Msg));
end;
except
on E: Exception do
raise ERttiMappingError.CreateFmt(‚Greška mapiranja kod %s <- %s: %s‘,
[M.Prop.Name, M.SourceName, E.Message]);
end;
end;
end;

end.

Čemu to služi

Dobivate mapiranje koje se u pregledima koda može jasno ocijeniti:

  • Svako mapirano Property je vizualno označeno (atribut).
  • Konverzija je centralizirana, što osigurava konzistentnost i testabilnost.
  • Tekstovi pogrešaka navode, koje Property i koji izvor su pogođeni.
  • Debug-mod omogućuje vam, u slučaju sumnje, lanac dokaza bez potrebe za breakpointima u produkcijskom procesu.

Okvirni uvjeti i tipične zamke

  • NULL-Semantik: Bez vlastitog Nullable-koncepta (npr. Nullable<T> ili Option-Types) postavljanje na NULL nije jednoznačno. U primjeru se NULL standardno preskače. To je konzervativno i sprječava tiho prepisivanje.
  • TRttiContext-Lebensdauer: Gradimo cache jednom po tipu i zatim odbacimo Context. To je uobičajeno. Važno je: Ne stvarajte novi RTTI-Context za svako dodjeljivanje polja.
  • Threading: Cache je zaštićen putem Monitora. U visoko paralelnim mapiranjima (npr. REST-Server) trebali biste dodatno provjeriti hoćete li cache izgraditi već pri pokretanju („warm“ build / Preload), kako biste smanjili lock-contention.
  • PropertyType Kind: tkClass i tkSet namjerno nisu implementirani. Za ugniježdene objekte trebali biste ili rekurzivno mapirati (s jasnom politikom) ili namjerno dodijeliti ručno.
  • Locale-Fallen: varDouble preko VarAsType je relativno robustan, ali stringovi poput „1,23“ nasuprot „1.23“ i dalje predstavljaju problem. Ako vaši izvori vraćaju stringove, vlastiti parser (s definiranim Culture) često je bolji.

Varijanta za FireDAC i TDataSet: Reader-Adapter umjesto Mapper-Kopplung

U BDE-Ablosung mit nativer Anbindung- ili klasičnim VCL/Win32-aplikacijama izvor je često TDataSet. Umjesto da vežete Mapper za TField, napišite adapter koji implementira sučelje IValueReader. Prednost: Mapper ostaje neovisan o pristupu podacima (važno ako kasnije želite pristup podacima premjestiti 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 ovako:

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;

Wo sich der Ansatz lohnt – und wo nicht

Dieses Muster lohnt sich typischerweise in drei Situationen:

  1. Schrittweise Modernisierung: Sie wollen Domänenobjekte einführen, ohne den Datenzugriff sofort komplett umzubauen (klassisch bei Delphi modernizaciji u postojećim aplikacijama).
  2. Schnittstellenkanten: CSV-/Excel-Importe, REST-Payloads oder „gemischte“ Datenquellen brauchen robuste Konvertierung und gute Fehlermeldungen.
  3. Wartbarkeit im Team: Attribute machen Mapping-Regeln sichtbar und reviewbar, was in größeren Codebasen Gold wert ist.

Einsatzgrenzen gibt es ebenfalls klar:

  • Komplexe Objektgraphen (podkolekcije, cikličke Referenzen) sollten Sie nicht „automagisch“ mappen. Hier ist expliziter Code oder ein getrenntes Assembler/Factory-Muster meist stabiler.
  • High-Throughput-Hotpaths (z. B. Massendaten-ETL) profitieren eher von codegenerierten Mappern oder handoptimiertem Mapping, selbst wenn RTTI gecacht ist.
  • Nullable/Optional ist ein eigenes Thema. Wenn Sie wirklich zwischen „nicht vorhanden“, „NULL“ und „Default“ unterscheiden müssen, sollten Sie das im Domänenmodell ausdrücken, nicht im Mapper verstecken.

Einordnung in Architektur und Betrieb

Aus Architekturperspektive ist dieser Mapper eine Infrastruktur-Komponente an der Grenze zwischen Datenrepräsentation und Domäne. Er ersetzt keine saubere Schichtung, kann sie aber ermöglichen: Der Datenzugriff (FireDAC, SQL, Views) darf weiterhin pragmatisch sein, während die Domäne konsistent bleibt. In mehrschichtigen Systemen (oft als Layer-3 arhitektura bezeichnet: UI, Domain/Services, Infrastruktur) gehört der Mapper in die Infrastruktur und wird von Services genutzt, nicht von UI-Formularen.

Betrieblich wichtig: Aktivieren Sie moDebug nicht dauerhaft in produktiven Services, sondern gezielt. Für schwer reproduzierbare Datenprobleme ist es sinnvoll, einen schaltbaren Diagnosepfad zu haben (Konfiguration, Feature-Flag). Sonst drohen Log-Volumen und Nebenwirkungen.

Fazit: RTTI ja, aber nur mit klaren Leitplanken

Delphi RTTI za mapiranje bez magije dobro funkcionira kada koristite RTTI kao alat za deklarativne metapodatke — a ne kao poziv za implicitne heuristike. Atributi kao opt-in, centralizirana konverzija, Cache po tipu i razumljivi tekstovi pogrešaka premještaju temu iz „neprozirno“ u „operativno“. Pristup je namjerno ne-univerzalan: za ugniježdene grafove, strogu semantiku null vrijednosti ili maksimalne performanse trebate dodatne komponente. Kao robusni most između Dataset/Legacy-Strukturen i modernijih objekata domene, on je u mnogim Delphi codebasama upravo pragmatičan korak koji modernizaciju čini mogućom.

Ako u rastućoj Delphi-aplikaciji 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 kontekstu također Delphi Rtti Mapping i Attribute Mapping Delphi igraju važnu ulogu kada integracije, tokovi podataka i daljnji razvoj moraju usklađeno funkcionirati.

Razgovarajte o projektu ili modernizacijskom pothvatu s Net-Base.

Podijeli objavu

Izravno proslijedite ovu objavu

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

E-pošta

Instagram se otvara u novoj kartici. Link i kratki tekst se prethodno kopiraju u međuspremnik.