Azok, akik évek alatt kinőtt üzleti szoftvert üzemeltetnek Delphi-ben, ismerik a feszültséget: egyik oldalról strukturált doménobjektumokat és tisztán elkülönülő rétegeket akarnak, másrészt vannak Datasets, Variants, CSV-importok, Schnittstellenpayloadok vagy egy REST-API, amelyeket „valahogy” objektumokra kell térképezni. Pont itt jut el az ember gyorsan a Delphi RTTI a térképezéshez varázslat nélkül-hoz: azaz térképezés Reflection segítségével (RTTI = Run-Time Type Information, futásidejű típusinformáció), de úgy, hogy nyomon követhető legyen, jól debugolható és ne támaszkodjon titokban konvenciókra vagy névjátékokra.
A lényeg: a „mágia” általában nem magából az RTTI-ből származik, hanem az implicit szabályokból. Ha a térképezési szabályok ezzel szemben explicit attribútumokban szerepelnek, a konverziók centralizáltak és a hibák egyértelmű okot adnak meg, az RTTI eszközzé válik a meglepetés helyett.
Miért bukik gyakran meg az RTTI-alapú térképezés Delphi-ben
Az RTTI-alapú térképezés a valós rendszerekben ritkán az ötlet miatt bukik meg, sokkal inkább a keretfeltételek miatt:
- Legacy-adatformák: Null/Empty/0 nincs tisztán elkülönítve, a mezőtípusok változnak, a stringek „N/A”-t tartalmaznak.
- Fokozatosan kialakuló konvenciók: „A mező ugyanazt a nevet viseli, mint a Property” működik az első alias, join vagy refaktorált Property-névig.
- Nehéz hibakeresés: Ha egy mapper „egyszerűen semmit sem állít be”, később hiányzik az ok. Üzemeltetésben ez végzetes lehet.
- Teljesítmény-mítoszok: Az RTTI-t általánosan „lassúnak” bélyegzik, holott többnyire a hiányzó cache a probléma.
Egy életképes megközelítésnek ezért (1) explicit mapping-metadatákat kell tartalmaznia, (2) világosan kell kezelnie a konverziót és a null-szemantikát, (3) hibákat és debug-kimeneteket kell szolgáltatnia, és (4) cache-elnie kell az RTTI-információkat.
Delphi RTTI a térképezéshez varázslat nélkül: Tervezési elvek
A következő minta szándékosan „unalmas” a legjobb értelemben: a szabályok láthatóak, a mellékhatások korlátozottak, és lépésenként be lehet húzni meglévő modulokba.
- Attribútumok a névkonvenciók helyett: A Property kap egy attribútumot, amely megnevezi a forrásoszlopot.
- Opt-in: Csak a megjelölt Property-k lesznek beállítva. Nincsenek meglepetések az „összes publikus Property” által.
- Konvertálás egy helyen: Variant/String/Integer/Boolean/Enum/Nullable típusokat központilag térképezünk.
- Debug-mód: Opcionálisan naplózza, mely mezők kerültek beállításra/kihagyásra – indokkal együtt.
- RTTI-caching: A legdrágább részek (Property-lista, attribútumkiértékelés) típusonként előkészítve.
Forrásrészlet: Attribútum-alapú térképezés RTTI-vel, cache-eléssel és debuggal
A kódrészlet egy sort képez le (például egy BDE-Ablosung natív csatlakozással a TDataSet segítségével) egy objektumra. Ahelyett, hogy a mappert mereven a TField-hez kötnénk, egy kis Reader-interfészt használunk. Ez a gyakorlatban értékes, mert később ugyanazt a logikát JSON-hez, INI-hez, CSV-hez vagy API-válaszokhoz is használhatja.
unit RttiMapping;
interface
uses
System.SysUtils, System.Rtti, System.TypInfo, System.Generics.Collections,
System.Variants;
type
// Explicit leképezés: Property <- forrásnév
MapFromAttribute = class(TCustomAttribute)
private
FName: string;
public
constructor Create(const AName: string);
property Name: string read FName;
end;
// Kis absztrakció: érték szolgáltatása és meglét/NULL megkülönböztetése
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: csak attribútummal rendelkező Property-k
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-konverzió sikertelen: „%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-ordinális a megengedett tartományon kívül: %d‘, [Ord]);
Exit(Ord);
end;
Name := VarToStr(V);
Ord := GetEnumValue(AEnumType.Handle, Name);
if Ord < 0 then
raise ERttiMappingError.CreateFmt(‚Enum-név ismeretlen: „%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;
// A konverzió tudatosan szelektív: jobb egyértelmű hibát kapni, mint hogy csendben „valahogy“ működjön.
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 nincs implementálva %s számára‘, [AProp.Name]);
tkClass:
raise ERttiMappingError.CreateFmt(‚Class-property mapping nincs implementálva %s számára‘, [AProp.Name]);
else
raise ERttiMappingError.CreateFmt(‚TypeKind nem támogatott (%s) a %s számára‘,
[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 vagy Target 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(‚Forrás hiányzik: „%s“ a(z) %s Property számára‘,
[M.SourceName, M.Prop.Name]);
Continue;
end;
if AReader.IsNull(M.SourceName) then
begin
if moIgnoreNull in AOptions then
Continue;
// Nullable/Optional mechanika nélkül NULL értéket nem lehet érdemben beállítani.
Continue;
end;
V := AReader.GetValue(M.SourceName);
try
SetPropertyValue(ATarget, M.Prop, V);
if moDebug in AOptions then
begin
Msg := Format(‚Térképezve %s <- %s (%s)‘, [M.Prop.Name, M.SourceName, VarTypeAsText(VarType(V))]);
OutputDebugString(PChar(Msg));
end;
except
on E: Exception do
raise ERttiMappingError.CreateFmt(‚Térképezési hiba %s <- %s: %s‘,
[M.Prop.Name, M.SourceName, E.Message]);
end;
end;
end;
end.
Miért hasznos
Olyan leképezést kap, amely a kódáttekintések során tisztán értékelhető:
- Minden leképezett Property vizuálisan meg van jelölve (attribútum).
- A konverzió központi, ezáltal konzisztens és tesztelhető.
- A hibaüzenetek megadják, melyik Property és melyik forrás érintett.
- Egy debug-mód szükség esetén megadja a bizonyítékláncot, anélkül, hogy breakpointokra lenne szükség az éles folyamatban.
Korlátozások és tipikus buktatók
- NULL-Semantik: Saját Nullable-koncepció nélkül (pl.
Nullable<T>vagy Option-Types) a „NULL beállítása” nem egyértelmű. A példában a NULL alapértelmezés szerint át van ugrva. Ez konzervatív megközelítés és megakadályozza a csendes felülírásokat. - TRttiContext-Lebensdauer: A cache-t típusonként egyszer építjük fel, majd eldobjuk a Contextet. Ez megszokott. Fontos: Ne hozzon létre új RTTI-Contextet mezőhozzárendelésenként.
- Threading: A cache Monitorral védett. Nagyon párhuzamos leképezések (pl. REST-Server) esetén érdemes megvizsgálni, hogy a cache-t már indításkor „melegítik” (Preload), így csökkentve a Lock-Contentiont.
- PropertyType Kind:
tkClasséstkSetszándékosan nincsenek implementálva. Beágyazott objektumokhoz vagy rekurzívan kell leképezni (egyértelmű policyval), vagy tudatosan kézzel hozzárendelni. - Locale-Fallen:
varDoubleaVarAsType-on keresztül relatíve robusztus, de a „1,23” vs. „1.23” típusú stringek továbbra is problémát jelentenek. Ha a források stringeket adnak, gyakran jobb egy saját parser (meghatározott Culture-ral).
Változat FireDAC és TDataSet esetén: Reader-adapter a Mapper-kapcsolás helyett
BDE-Ablosung mit nativer Anbindung- vagy klasszikus VCL/Win32-alkalmazásokban a forrás gyakran egy TDataSet. Ahelyett, hogy a Mapper-t TField-hez kötné, írjon egy adaptert, amely megvalósítja a IValueReader interfészt. Előny: a Mapper független marad az adateléréstől (fontos, ha később az adatelérést szolgáltatásokba vagy egy REST-Server-be kívánja kiszervezni).
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;
Így néz ki egy konkrét leképezés:
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;
Hol érdemes ez a megközelítés – és hol nem
Ez a minta tipikusan három helyzetben éri meg:
- Fokozatos modernizáció: Doménobjektumokat szeretne bevezetni anélkül, hogy az adat-hozzáférést azonnal teljesen át kellene alakítani (klasszikus eset a Delphi Modernisierung meglévő alkalmazásoknál).
- Interfészhatárok: CSV-/Excel-importok, REST-payloadok vagy „kevert“ adatforrások robusztus konverziót és érthető hibajelzéseket igényelnek.
- Karbantarthatóság a csapatban: Az attribútumok láthatóvá és review-zhatóvá teszik a mapping-szabályokat, ami nagyobb kódbázisokban jelentős értéket képvisel.
Alkalmazási korlátok is egyértelműek:
- Komplex objektumgráfok (gyermekkollekciók, ciklikus hivatkozások) esetén ne mapelje ezeket „automagikusan“. Ilyenkor az explicit kód vagy egy külön assembler/factory-minta általában stabilabb.
- Magas áteresztésű kritikus útvonalak (pl. tömeges adatok ETL) inkább kódgenerált mapper-eket vagy kézzel optimalizált mapplást igényelnek, még akkor is, ha az RTTI gyorsítótárazva van.
- Nullable/Optional külön téma. Ha tényleg el kell választania a „nem létező“, a „NULL“ és az „alapértelmezett“ állapotokat, azt a doménmodellben kell kifejezni, és nem a mapperben elrejteni.
Besorolás az architektúrában és az üzemeltetésben
Architektúrális nézőpontból ez a mapper egy infrastruktúra-komponens az adatreprezentáció és a domén határán. Nem helyettesíti a tiszta rétegzést, de segítheti annak megvalósítását: az adathozzáférés (FireDAC, SQL, Views) továbbra is pragmatikus lehet, miközben a domén konzisztens marad. Többrétegű rendszerekben (gyakran Layer-3 architektúra néven: UI, domén/szolgáltatások, infrastruktúra) a mapper az infrastruktúrába tartozik és a szolgáltatások használják, nem a UI-űrlapok.
Üzemeltetési szempontból fontos: ne hagyja állandóan aktívként a moDebug-ot a termelési szolgáltatásokban, hanem célzottan használja. Nehezen reprodukálható adathibák esetén érdemes kapcsolható diagnosztikai útvonalat biztosítani (konfiguráció, feature-flag). Ellenkező esetben túl nagy log-volumen és nem kívánt mellékhatások jelentkezhetnek.
Következtetés: RTTI igen, de csak egyértelmű védőkorlátokkal
Delphi RTTI a varázslat nélküli Mappinghez jól működik, ha Ön az RTTI-t deklaratív metaadatok eszközeként használja – nem meghívás arra, hogy rejtett heuristikákat alkalmazzon. Az attribútumok opt‑inként, a központosított konverzió, a típusonkénti cache és az érthető hibaüzenetek az ügyet az „átláthatatlan”-ból az „üzemeltethető”-be helyezik. A megközelítés szándékosan nem univerzális: beágyazott gráfokhoz, szigorú nulla-szemantikához vagy maximális teljesítményhez további építőelemekre van szüksége. Mint robusztus híd a Dataset/Legacy-Strukturen és a modernebb doménobjektumok között, azonban sok Delphi-kódbázisban éppen ez a pragmatikus lépés az, amely egyáltalán lehetővé teszi a modernizálást.
Ha egy meglévő Delphi-alkalmazásban éppen a Mapping-kapcsolatoknál, az adatok minőségénél vagy a lépésenkénti modernizációnál akadozik a projekt, közösen tisztán felépítjük és az Ön architektúrájába illesztjük: Vegye fel velünk a kapcsolatot.
A szakmai környezetben a Delphi Rtti Mapping és az Attribute Mapping Delphi is fontos szerepet játszanak, amikor az integrációk, az adatfolyamok és a további fejlesztés tisztán kell, hogy együttműködjenek.
Projekt vagy modernizációs kezdeményezés megbeszélése Net-Base-vel.