Kdo provozuje vzniklý businessový software v Delphi, zná toto napětí: na jedné straně chcete strukturované doménové objekty a jasné vrstvy, na druhé straně existují Datasets, Variants, CSV importy, payloady rozhraní nebo REST-API, které je třeba „nějak“ namapovat na objekty. Právě zde se rychle dostanete k Delphi RTTI für Mapping ohne Magie: tedy mapování pomocí reflection (RTTI = Run-Time Type Information, typové informace za běhu), ale tak, aby to zůstalo srozumitelné, dobře laditelné a aby to nebylo skrytě založeno na konvencích nebo hrách s názvy.
Podstata: „magie“ obvykle nevzniká samotným RTTI, ale implicitními pravidly. Pokud jsou mapovací pravidla explicitně v atributech, konverze jsou centralizované a chyby jasně pojmenovávají příčinu, stává se RTTI nástrojem místo překvapení.
Proč RTTI-mapping v Delphi často selhává
RTTI-založené mapování v reálných systémech málokdy ztroskotá na myšlence, častěji na okrajových podmínkách:
- Legacy-Datenformen: Null/Empty/0 nejsou od sebe čistě odděleny, typy polí se mění, řetězce obsahují „N/A“.
- Schleichende Konventionen: „Pole se jmenuje jako Property“ funguje do prvního aliasu, joinu nebo refaktorizovaného názvu property.
- Schwer zu debuggen: Když mapper „prostě nic nenastaví“, později chybí příčina. V provozu je to jedovaté.
- Performance-Mythen: RTTI je paušálně označováno za „pomalé“, i když je problém většinou v chybějícím cachování.
Udržitelný přístup by proto měl (1) mít explicitní mapovací metadata, (2) jasně řešit konverzi a null-semantiku, (3) dodávat chyby a ladicí výstupy a (4) cacheovat RTTI-informace.
Delphi RTTI für Mapping ohne Magie: Designprinzipien
Následující vzor je záměrně „nudný“ v tom nejlepším slova smyslu: pravidla jsou viditelná, vedlejší efekty omezené a lze jej postupně zavádět do stávajících modulů.
- Attribute statt Namenskonvention: Property dostane atribut, který pojmenuje zdrojový sloupec.
- Opt-in: Nastavují se pouze označené Properties. Žádná překvapení způsobená „všemi publikovanými properties“.
- Konvertierung an einer Stelle: Variant/String/Integer/Boolean/Enum/Nullable jsou mapovány centrálně.
- Debug-Mode: Volitelně se protokoluje, která pole byla nastavena/přeskočena – s uvedením důvodu.
- RTTI-Caching: Nejdražší části (seznam properties, vyhodnocení atributů) se připraví pro každý typ.
Source-Schnipsel: Attribut-Mapping mit RTTI, Caching und Debug
Snippet namapuje jeden řádek (např. z BDE-Ablosung mit nativer Anbindung via TDataSet) na objekt. Místo pevného vázání mapperu na TField používáme malé reader‑rozhraní. V praxi je to cenné, protože později můžete stejnou logiku použít i pro JSON, INI, CSV nebo API‑responses.
unit RttiMapping;
interface
uses
System.SysUtils, System.Rtti, System.TypInfo, System.Generics.Collections,
System.Variants;
type
// Explicitní mapování: Property <- jméno zdroje
MapFromAttribute = class(TCustomAttribute)
private
FName: string;
public
constructor Create(const AName: string);
property Name: string read FName;
end;
// Malá abstrakce: poskytne hodnotu a rozliší existenci/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: pouze Properties s atributem
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(‚Konverze na boolean selhala: „%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 rozsah: %d‘, [Ord]);
Exit(Ord);
end;
Name := VarToStr(V);
Ord := GetEnumValue(AEnumType.Handle, Name);
if Ord < 0 then
raise ERttiMappingError.CreateFmt(‚Neznámý název výčtu: „%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;
// Konverze záměrně selektivní: raději jasně selhat než tiše „nějak“.
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(‚Mapování Set není implementováno pro %s‘, [AProp.Name]);
tkClass:
raise ERttiMappingError.CreateFmt(‚Mapování Class-Property není implementováno pro %s‘, [AProp.Name]);
else
raise ERttiMappingError.CreateFmt(‚TypeKind není podporován (%s) pro %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 nebo 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 chybí: „%s“ pro Property %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 nelze NULL smysluplně přiřadit.
Continue;
end;
V := AReader.GetValue(M.SourceName);
try
SetPropertyValue(ATarget, M.Prop, V);
if moDebug in AOptions then
begin
Msg := Format(‚Namapováno %s <- %s (%s)‘, [M.Prop.Name, M.SourceName, VarTypeAsText(VarType(V))]);
OutputDebugString(PChar(Msg));
end;
except
on E: Exception do
raise ERttiMappingError.CreateFmt(‚Chyba při mapování %s <- %s: %s‘,
[M.Prop.Name, M.SourceName, E.Message]);
end;
end;
end;
end.
K čemu to slouží
Získáte mapování, které lze v revizích kódu čistě posoudit:
- Každá namapovaná vlastnost (Property) je vizuálně označena (Attribut).
- Konverze je centralizovaná, díky tomu konzistentní a testovatelná.
- Chybová hlášení říkají, která Property a který zdroj jsou dotčeny.
- Debugovací režim vám v případě potřeby poskytne sled důkazů, aniž byste museli používat Breakpoints v produkčním procesu.
Okrajové podmínky a typické nástrahy
- NULL-Semantik: Bez vlastního Nullable-konceptu (např.
Nullable<T>nebo Option-Types) není „NULL setzen“ jednoznačné. V ukázce je NULL standardně přeskočeno. To je konzervativní a zabraňuje tichému přepisování. - TRttiContext-Lebensdauer: Vytváříme cache jednou na typ a Context poté zahodíme. To je obvyklé. Důležité je: nevytvářet pro každé přiřazení pole nový RTTI-Context.
- Threading: Cache je chráněna pomocí Monitoru. U vysoce paralelních mapování (např. REST-Server) byste navíc měli zvážit, zda cache už při startu „ohřát“ (Preload), abyste snížili soutěž o zámky (lock contention).
- PropertyType Kind:
tkClassatkSetjsou záměrně neimplementovány. Pro vnořené objekty byste buď měli mapovat rekurzivně (s jasnou Policy), nebo je vědomě přiřadit ručně. - Locale-Fallen:
varDoublepřesVarAsTypeje relativně robustní, ale řetězce jako „1,23“ vs. „1.23“ jsou přesto problém. Pokud vaše zdroje dodávají řetězce, je často lepší vlastní parser (s definovanou Culture).
Varianta pro FireDAC a TDataSet: Reader-Adapter místo provázání s Mapperem
V BDE-Ablosung mit nativer Anbindung- nebo v klasických VCL/Win32 aplikacích je zdrojem často TDataSet. Místo aby byl Mapper vázán na TField, napište adaptér, který implementuje rozhraní IValueReader. Výhoda: Mapper zůstává nezávislý na přístupu k datům (důležité, pokud chcete přístup k datům později přesunout do služeb nebo 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;
Konkrétní mapování pak vypadá takto:
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 se tento přístup vyplatí — a kde ne
Tento vzor se typicky vyplatí v těchto třech situacích:
- Postupná modernizace: Chcete zavést doménové objekty, aniž byste okamžitě kompletně předělali přístup k datům (typické při Delphi modernizace ve stávajících aplikacích).
- Rozhraní na krajích: CSV-/Excel-importy, REST-payloady nebo „smíšené“ zdroje dat vyžadují robustní konverzi a srozumitelné chybové hlášení.
- Udržovatelnost v týmu: Atributy zpřehledňují mapovací pravidla a činí je přezkoumatelnými, což ve větších codebasech má velkou hodnotu.
Existují také jasné limity použití:
- Komplexní grafy objektů (Child-Collections, cyklické reference) byste neměli „automagicky“ mapovat. Zde je obvykle stabilnější explicitní kód nebo samostatný Assembler/Factory-vzor.
- High-Throughput-Hotpaths (např. ETL hromadných dat) mají spíše prospěch z codegenerovaných mapperů nebo ručně optimalizovaného mapování, i když je RTTI cachováno.
- Nullable/Optional je samostatné téma. Pokud musíte skutečně rozlišit mezi „neexistuje“, „NULL“ a „výchozí hodnotou“, měli byste to vyjádřit v doménovém modelu, ne skrývat v mapperu.
Zařazení v architektuře a provozu
Z architektonického hlediska je tento mapper infrastrukturní komponentou na rozhraní mezi reprezentací dat a doménou. Nezastupuje čisté vrstvení, může jej však umožnit: přístup k datům (FireDAC, SQL, Views) může zůstat pragmatický, zatímco doména zůstane konzistentní. V vícevrstvých systémech (často označovaných jako Layer-3 architektura: UI, doména/služby, infrastruktura) patří mapper do infrastruktury a používají ho služby, ne UI formuláře.
Pro provoz důležité: Neaktivujte trvale moDebug v produkčních službách, ale cíleně. Pro obtížně reprodukovatelné problémy s daty je užitečné mít přepínatelnou diagnostickou cestu (konfigurace, feature-flag). Jinak hrozí objem logů a nežádoucí vedlejší efekty.
Závěr: RTTI ano, ale pouze s jasnými pravidly
Delphi RTTI pro mapování bez magie funguje dobře, když používáte RTTI jako nástroj pro deklarativní metadata – nikoli jako pozvánku ke skrytým heuristikám. Atributy jako opt-in, centralizovaná konverze, cache na typ a srozumitelné chybové texty přemění téma z „neprůhledné“ na „provozuschopné“. Přístup není záměrně univerzální: pro vnořené grafy, striktní null-semantiku nebo maximální výkon potřebujete další stavební kameny. Jako robustní most mezi strukturami datasetů/legacy a modernějšími doménovými objekty je to v mnoha Delphi-kódbázích právě ten pragmatický krok, který modernizaci vůbec umožňuje.
Pokud v zavedené Delphi-aplikaci právě narážíte na hranice mapování, kvalitu dat nebo postupnou modernizaci, můžeme to společně čistě nastavit a přizpůsobit vaší architektuře: Kontaktujte nás.
V odborném kontextu mají také Delphi Rtti Mapping a Attribute Mapping Delphi důležitou roli, pokud musí integrace, datové toky a další vývoj hladce spolupracovat.