Net-Base Revistă

08.05.2026

Delphi RTTI pentru mapare fără magie: bazat pe atribute, depanabil și compatibil cu sisteme legacy

Un model pragmatic de mapare cu Delphi RTTI: atribute în loc de convenții, conversii controlate, mesaje de eroare clare și un mod de depanare care ajută efectiv în producție. Cu fragmente de cod sursă pentru maparea dataset-urilor sau a record-urilor către obiecte, fără magie ascunsă.

08.05.2026

Cei care administrează software de business evoluat în Delphi cunosc tensiunea: pe de o parte își doresc obiecte de domeniu structurate și straturi clare, pe de altă parte există Datasets, Variants, importuri CSV, payload-uri de interfață sau o REST-API care trebuie „cumva“ mapate pe obiecte. Tocmai aici ajungi rapid la Delphi RTTI pentru mapare fără magie: adică mapare prin Reflection (RTTI = Run-Time Type Information, informații de tip în timpul rulării), dar astfel încât să rămână urmăribilă, ușor de depanat și să nu depindă în mod ascuns de convenții sau jocuri de nume.

Punctul central: „magia” apare de regulă nu din RTTI în sine, ci din reguli implicite. Dacă regulile de mapare sunt în schimb explicite în atribute, conversiile sunt centralizate și erorile indică o cauză clară, RTTI devine un instrument și nu o surpriză.

De ce eșuează adesea maparea RTTI în Delphi

Maparea bazată pe RTTI eșuează în sisteme reale rar din cauză ideii, mai degrabă din cauza condițiilor limită:

  • Formate de date legacy: Null/Empty/0 nu sunt clar separate, tipurile de câmp se schimbă, stringurile conțin „N/A”.
  • Convenții insidioase: „Câmpul se numește ca proprietatea” funcționează până la primul alias, join sau nume de proprietate refactorizat.
  • Dificil de depanat: Dacă un mapper „pur și simplu nu setează nimic”, mai târziu lipsește cauza. În producție este otravă.
  • Mitoze de performanță: RTTI este etichetat în bloc ca „lent”, deși de obicei problema este lipsa unei strategii de caching.

O abordare viabilă ar trebui deci să (1) aibă metadate de mapare explicite, (2) trateze clar conversia și semantica null, (3) furnizeze erori și ieșiri de debug și (4) păstreze în cache informațiile RTTI.

Delphi RTTI pentru mapare fără magie: principii de proiectare

Modelul următor este în mod intenționat „plictisitor” în cel mai bun sens: regulile sunt vizibile, efectele secundare sunt limitate și se poate integra treptat în modulele existente.

  • Atribute în locul convențiilor de nume: Proprietatea primește un atribut care denumește coloana sursă.
  • Opt-in: Sunt setate doar proprietățile marcate. Nicio surpriză cauzată de „toate proprietățile publicate”.
  • Conversie într-un singur loc: Variant/String/Integer/Boolean/Enum/Nullable sunt mapate centralizat.
  • Mod debug: Opțional se înregistrează ce câmpuri au fost setate/sărite — cu motivul.
  • RTTI-Caching: Părțile cele mai costisitoare (lista de proprietăți, evaluarea atributelor) sunt pregătite per tip.

Fragment de cod: mapare prin atribute cu RTTI, caching și debug

Snippetul mapează un rând (de ex. din BDE-înlocuire cu conectare nativă via TDataSet) pe un obiect. În loc să legăm mapperul strict de TField, folosim o mică interfață Reader. În practică aceasta are valoare, pentru că ulterior puteți folosi aceeași logică și pentru JSON, INI, CSV sau răspunsuri API.

unit RttiMapping;

interface

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

type
// Mapare explicită: Property <- nume sursă
MapFromAttribute = class(TCustomAttribute)
private
FName: string;
public
constructor Create(const AName: string);
property Name: string read FName;
end;

// Abstracție mică: furnizează valoarea + diferențiază existența/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: doar proprietăți cu atribut
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(‚Conversie la boolean eșuată: „%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 enum în afara intervalului: %d‘, [Ord]);
Exit(Ord);
end;

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

// Conversia intenționat selectivă: mai bine eșuează clar decât să eșueze în tăcere „cumva“.
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(‚Mapare pentru set neimplementată pentru %s‘, [AProp.Name]);

tkClass:
raise ERttiMappingError.CreateFmt(‚Maparea proprietății de clasă neimplementată pentru %s‘, [AProp.Name]);
else
raise ERttiMappingError.CreateFmt(‚TypeKind nesuportat (%s) pentru %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 sau Target este 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(‚Sursa lipsește: „%s“ pentru proprietatea %s‘,
[M.SourceName, M.Prop.Name]);
Continue;
end;

if AReader.IsNull(M.SourceName) then
begin
if moIgnoreNull in AOptions then
Continue;
// Fără mecanism Nullable/Optional, NULL nu poate fi setat în mod util.
Continue;
end;

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

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

end.

De ce este util

Primiți un mapping care poate fi evaluat clar în code reviews:

  • Fiecare Property mapată este marcată vizual (atribut).
  • Conversia este centralizată, deci consistentă și testabilă.
  • Mesajele de eroare indică, care Property și care sursă sunt afectate.
  • Un mod debug vă oferă, la nevoie, lanțul dovezilor, fără a fi nevoie de breakpoints în fluxul de producție.

Condiții cadru și capcane tipice

  • Semantica NULL: Fără un concept propriu de nullable (de ex. Nullable<T> sau Option-Types) atribuirea lui „NULL” nu este univocă. În fragmentul de cod NULL este omis implicit. Aceasta este o alegere conservatoare și previne suprascrierile tacite.
  • Durata de viață a TRttiContext: Construim cache-ul o singură dată per tip și apoi aruncăm Contextul. Acest lucru este obișnuit. Important: nu creați câte un RTTI-Context nou pentru fiecare atribuire de câmp.
  • Threading: Cache-ul este protejat prin Monitor. În mapări foarte paralele (de ex. REST-Server) ar trebui să verificați suplimentar dacă construiți cache-ul „warm” la pornire (Preload), pentru a reduce lock contention.
  • PropertyType Kind: tkClass și tkSet nu sunt implementate intenționat. Pentru obiecte încluse ar trebui fie să le mapați recursiv (cu o politică clară), fie să le atribuiți manual în mod deliberat.
  • Capcane legate de locale: varDouble prin VarAsType este relativ robust, dar stringuri precum „1,23” vs. „1.23” rămân problematic. Dacă sursele dvs. furnizează stringuri, un parser propriu (cu Culture definită) este adesea mai bun.

Variantă pentru FireDAC și TDataSet: Reader-Adapter în locul cuplării cu mapperul

În aplicațiile BDE-Ablosung mit nativer Anbindung sau în aplicațiile clasice VCL/Win32, sursa este adesea un TDataSet. În loc să legați mapper-ul de TField, scrieți un adaptor care implementează interfața IValueReader. Avantajul: mapper-ul rămâne independent de accesul la date (important dacă externalizați mai târziu accesul la date în servicii sau într-un 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;

Astfel, un mapping concret arată astfel:

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;

Unde merită această abordare — și unde nu

Acest tipar este de obicei util în trei situații:

  1. Modernizare treptată: Doriți să introduceți obiecte de domeniu fără a reface imediat în întregime accesul la date (clasic în cadrul Delphi Modernisierung în aplicații existente).
  2. Puncte de integrare: Importuri CSV/Excel, REST-payloads sau surse de date „mixte” necesită conversie robustă și mesaje de eroare clare.
  3. Mentenabilitate în echipă: Atributele fac regulile de mapare vizibile și ușor de revizuit, ceea ce într-o bază de cod mai mare este deosebit de valoros.

Există și limite clare de utilizare:

  • Grafuri de obiecte complexe (colecții child, referințe ciclice) nu ar trebui mapate „automagically”. Aici codul explicit sau un șablon Assembler/Factory separat este de obicei mai stabil.
  • Secțiuni critice cu throughput ridicat (de ex. ETL pentru volume mari de date) beneficiază mai degrabă de mapperi generați prin cod sau de mapări optimizate manual, chiar dacă RTTI este pus în cache.
  • Nullable/Optional este o problemă separată. Dacă trebuie să faceți real distincția între „nu este prezent”, „NULL” și „valoare implicită”, ar trebui să o exprimați în modelul de domeniu, nu să o ascundeți în mapper.

Încadrare în arhitectură și operare

Din perspectiva arhitecturală, acest mapper este o componentă de infrastructură la limita dintre reprezentarea datelor și domeniu. Nu înlocuiește o separare clară a straturilor, dar o poate facilita: Accesul la date (FireDAC, SQL, Views) poate rămâne pragmatic, în timp ce domeniul rămâne coerent. În sisteme stratificate (adesea denumite Layer-3 Arhitectură: UI, Domeniu/Servicii, Infrastructură) mapperul aparține infrastructurii și este utilizat de servicii, nu de formularele UI.

Important din punct de vedere operațional: Nu activați moDebug permanent în servicii productive; folosiți-l țintit. Pentru probleme de date greu de reprodus, este util să există o cale de diagnostic activabilă (configurație, feature-flag). Altfel riscați volum mare de loguri și efecte secundare.

Concluzie: RTTI da, dar doar cu reguli clare

Delphi RTTI pentru mapare fără magie funcționează bine atunci când folosiți RTTI ca un instrument pentru metadate declarative – nu ca o invitație la euristici tacite. Atribute ca opt-in, conversie centralizată, cache pe tip și mesaje de eroare explicite mută tema din „opac” în „operabil”. Abordarea nu este intenționat universală: pentru grafuri îmbinate, semantică strictă a valorilor nule sau performanță maximă aveți nevoie de componente suplimentare. Ca punte robustă între structuri Dataset/Legacy și obiecte de domeniu mai moderne, însă, în multe Delphi-baze de cod este exact pasul pragmatic care face posibilă modernizarea.

Dacă într-o aplicație Delphi existentă vă blocați tocmai la muchiile de mapare, calitatea datelor sau modernizare incrementală, putem configura asta împreună în mod curat și integra în arhitectura dumneavoastră: Contactați-ne.

În contextul profesional, și Delphi Rtti Mapping și Attribute Mapping Delphi joacă un rol important dacă integrațiile, fluxurile de date și dezvoltarea ulterioară trebuie să funcționeze în mod coerent.

Discutați un proiect sau o inițiativă de modernizare cu Net-Base.

Partajează postarea

Distribuiți această postare direct

LinkedIn, X, XING, Facebook, WhatsApp și e-mail sunt disponibile imediat. Pentru Instagram pregătim linkul și textul scurt imediat.

E-mail

Instagram se deschide într-o filă nouă. Linkul și textul scurt se copiază în prealabil în clipboard.