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.
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:
tkClassitkSetsu namjerno neimplementirani. Za ugniježdene objekte trebate ili rekurzivno mapirati (s jasnom policom) ili svjesno ručno dodijeliti. - Locale-Fallen:
varDoubleprekoVarAsTypeje 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).
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:
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:
- Postepena modernizacija: Želite uvesti domenske objekte bez potpune pregradnje pristupa podacima odjednom (klasično kod Delphi modernizacije u postojećim aplikacijama).
- Rubne tačke interfejsa: CSV-/Excel-uvozi, REST-payloadi ili „miješani“ izvori podataka zahtijevaju robusnu konverziju i jasne poruke o greškama.
- 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.