Kushdo që drejton softuer biznesi të zhvilluar me kalimin e kohës në Delphi e njeh këtë tension: nga njëra anë dëshiron objekte domene të strukturuara dhe shtresa të qarta, nga ana tjetër ekzistojnë Datasets, Variantë, importime CSV, payload-e të ndërfaqeve ose një REST-API që në një mënyrë apo tjetër duhet të mapohen mbi objekte. Pikërisht këtu përfundojmë shpejt tek Delphi RTTI për Mapping ohne Magie: domethënë mapping përmes Reflection (RTTI = Run-Time Type Information, informacion tipi gjatë kohës së ekzekutimit), por në një mënyrë të kuptueshme, lehtësisht e debugueshme dhe që nuk varej fshehurazi nga konventa ose lojëra emrash.
Pika kyçe: „magjia“ zakonisht nuk vjen nga RTTI-në në vetvete, por nga rregulla implikite. Kur rregullat e mapping-ut vendosen shprehimisht në atribute, konvertimet janë të centralizuara dhe gabimet emërtojnë qartë shkakun, RTTI bëhet një mjet dhe jo një surprizë.
Pse RTTI-Mapping në Delphi shpesh dështon
Mapping-u bazuar në RTTI rrallë dështojnë për shkak të idesë, por për shkak të kushteve anësore:
- Forma të dhënash legacy: Null/Empty/0 nuk janë ndarë qartë, llojet e fushave ndryshojnë, string-et përmbajnë „N/A“.
- Konventa që përhapen ngadalë: „Fusha ka të njëjtin emër si Property“ funksionon deri te alias-i i parë, Join ose emri i Property i refaktorizuar.
- E vështirë për t’u debug-uar: Kur një mapper „thjesht nuk vendos asgjë“, mungon më vonë shkaku. Në prodhim kjo është helmuese.
- Mythe për performancën: RTTI etiketohen përgjithësisht si „i ngadaltë“, megjithëse shpesh problemi është mungesa e caching-ut.
Prandaj një qasje e qëndrueshme duhet të (1) ketë metadata të qarta për mapping, (2) trajtojë qartë konvertimin dhe semantikën e null-it, (3) prodhojë gabime dhe dalje debug dhe (4) cache-ojë informacionet RTTI.
Delphi RTTI për Mapping ohne Magie: Parimet e dizajnit
Modeli i mëposhtëm është qëllimisht „i mërzitshëm“ në kuptimin më të mirë: rregullat janë të dukshme, efektet anësore janë të kufizuara dhe mund të futet hap pas hapi në modulet ekzistuese.
- Atribute në vend të konventave të emrit: Property merr një atribut që emërton kolonën burimore.
- Opt-in: Vendosen vetëm Properties e shënuara. Nuk ka surpriza nga „të gjitha Properties e publikuara“.
- Konvertimi në një vend: Variant/String/Integer/Boolean/Enum/Nullable mapohen në mënyrë të centralizuar.
- Debug-Mode: Opsional protokollohet se cilat fusha u vendosën/iu anashkaluan – me arsyen përkatëse.
- RTTI-Caching: Pjesët më të shtrenjta (lista e Properties, vlerësimi i atributeve) përgatiten për çdo tip.
Source-Schnipsel: Attribut-Mapping mit RTTI, Caching und Debug
Snippet-i mapon një rresht (p.sh. nga BDE-zëvendësim me lidhje native via TDataSet) në një objekt. Në vend që të lidhim mapper-in ngushtësisht me TField, përdorim një ndërfaqe të vogël Reader. Kjo është e vlefshme në praktikë sepse më vonë mund të përdorni të njëjtën logjikë edhe për JSON, INI, CSV ose përgjigjet e API-së.
unit RttiMapping;
interface
uses
System.SysUtils, System.Rtti, System.TypInfo, System.Generics.Collections,
System.Variants;
type
// Mapim i eksplicitë: Property <- emri i burimit
MapFromAttribute = class(TCustomAttribute)
private
FName: string;
public
constructor Create(const AName: string);
property Name: string read FName;
end;
// Abstraksion i vogël: jep vlerë dhe ndan ekzistencën/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: vetëm Properties me 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(‚Konvertimi në Boolean dështoi: „%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(‚Ordinali i enum jashtë diapazonit: %d‘, [Ord]);
Exit(Ord);
end;
Name := VarToStr(V);
Ord := GetEnumValue(AEnumType.Handle, Name);
if Ord < 0 then
raise ERttiMappingError.CreateFmt(‚Emri i Enum i panjohur: „%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;
// Konvertimi qëllimisht selektiv: më mirë të dështojë qartë sesa të dështojë „në mënyrë të paqartë“.
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(‚Mapimi i Set-eve nuk është implementuar për %s‘, [AProp.Name]);
tkClass:
raise ERttiMappingError.CreateFmt(‚Mapimi i pronës së klasës nuk është implementuar për %s‘, [AProp.Name]);
else
raise ERttiMappingError.CreateFmt(‚TypeKind nuk mbështetet (%s) pë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 ose Target është 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(‚Burimi mungon: „%s“ për Property %s‘,
[M.SourceName, M.Prop.Name]);
Continue;
end;
if AReader.IsNull(M.SourceName) then
begin
if moIgnoreNull in AOptions then
Continue;
// Pa mekanikën Nullable/Optional, NULL nuk mund të vendoset në mënyrë të kuptueshme.
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(‚Gabim i mapimit te %s <- %s: %s‘,
[M.Prop.Name, M.SourceName, E.Message]);
end;
end;
end;
end.
Për çfarë shërben kjo
Merrni një mapping që mund të vlerësohet qartë në rishikimet e kodit:
- Çdo Property e mapuar është e shënuar vizual (atribut).
- Konvertimi është qendror, duke qenë kështu konsistent dhe i testueshëm.
- Mesazhet e gabimit tregojnë, cila Property dhe cili burim janë të prekur.
- Një modalitet debug ju jep, në rast dyshimi, zinxhirin e provave, pa pasur nevojë për Breakpoints në procesin produktiv.
Kushtet kornizë dhe kurthet tipike
- NULL-Semantik: Pa një koncept të veçantë për Nullable (p.sh.
Nullable<T>ose Option-Types) vendosja e NULL nuk është e qartë. Në snippet NULL anashkalohet si parazgjedhje. Kjo është konservative dhe parandalon mbishkrime të heshtura. - TRttiContext-Lebensdauer: Ndërtojmë cache njëherë për çdo tip dhe hedhim Context-in më pas. Kjo është e zakonshme. E rëndësishme: mos ndërtoni një RTTI-Context të ri për çdo caktim fushe.
- Threading: Cache është i mbrojtur përmes Monitor. Në mappings me paralelizëm të lartë (p.sh. REST-Server) duhet gjithashtu të kontrolloni nëse duhet të ndërtoni cache-in „ngrohtë“ gjatë nisjes (Preload), për të reduktuar konkurencën për bllokim.
- PropertyType Kind:
tkClassdhetkSetjanë qëllimisht jo të implementuara. Për objektet e ngulitura duhet të maponi rekursiv (me politikë të qartë) ose të caktoni me dorë dhe me vetëdije. - Kurthet e lokalizimit:
varDoublepërmesVarAsTypeështë relativisht robust, por stringje si „1,23“ vs. „1.23“ mbeten problem. Nëse burimet tuaja kthejnë stringje, shpesh është më mirë një parser i vetin (me Culture të përcaktuar).
Varianti për FireDAC dhe TDataSet: Reader-Adapter statt Mapper-Kopplung
Në aplikacione BDE-Ablosung mit nativer Anbindung ose klasike VCL/Win32 burimi shpesh është një TDataSet. Në vend që t’i lidhni Mapper-in me TField, shkruani një adapter që implementon ndërfaqen IValueReader. Përparësia: Mapper-i mbetet i pavarur nga qasja në të dhëna (e rëndësishme kur dëshironi të nxjerrni qasjen në të dhëna më vonë në services ose në një 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;
Kështu duket një mapping konkret:
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;
Kur ky qasja ia vlen – dhe ku jo
Ky model zakonisht është i dobishëm në tre situata:
- Modernizim në hapa: Dëshironi të futni objekte domeni pa rindërtuar menjëherë qasjen e të dhënave (klasikisht te Delphi Modernizim të aplikacioneve ekzistuese).
- Në pika të ndërfaqeve: Importet CSV/Excel, REST-Payloads ose burime të dhënash „të përziera“ kërkojnë një konvertim të besueshëm dhe raportim të qartë të gabimeve.
- Mirëmbajtje në ekip: Atributet bëjnë të dukshme dhe të kontrollueshme rregullat e mappimit, që në baza kodi më të mëdha është shumë e vlefshme.
Ekzistojnë edhe kufizime të qarta të përdorimit:
- Grafet komplekse të objekteve (child-collections, referenca ciklike) nuk duhet t’i mapponi „automagjikisht“. Këtu kodi eksplicit ose një model i ndarë Assembler/Factory është zakonisht më i qëndrueshëm.
- Rrugë kritike me përpunim të lartë (p.sh. ETL për të dhëna masive) përfitojnë më shumë nga mapper të gjeneruar me kod ose mapping i optimizuar me dorë, edhe nëse RTTI është i cache-uar.
- Nullable/Optional është një temë e veçantë. Nëse duhet vërtet të dalloni midis „i munguar“, „NULL“ dhe „Default“, duhet ta shprehni këtë në modelin e domenit, jo ta fshehni në mapper.
Vendosja në arkitekturë dhe operim
Nga perspektiva e arkitekturës, ky mapper është një komponent infrastrukture në kufirin midis reprezentimit të të dhënave dhe domenit. Ai nuk zëvendëson një ndarje të qartë të shtresave, por mund ta mundësojë atë: Qasja në të dhëna (FireDAC, SQL, Views) mund të mbetet pragmatike, ndërsa domeni mbetet i qëndrueshëm. Në sistemet me shumë shtresa (shpesh quajtur Layer-3 Arkitekturë: UI, Domena/Shërbime, Infrastrukturë) mapper-i i përket infrastrukturës dhe përdoret nga shërbimet, jo nga formularët e UI-së.
Nga pikëpamja operacionale e rëndësishme: Mos aktivizoni moDebug në mënyrë të përhershme në shërbimet prodhuese, por vetëm në mënyrë të synuar. Për probleme me të dhëna që janë të vështirë për t’u riprodhuar, është e arsyeshme të keni një rrugë diagnostike të aktivizueshme (konfigurim, feature-flag). Përndryshe rrezikoni volumin e lartë të log-eve dhe efekte anësore.
Përfundim: RTTI po, por vetëm me udhëzime të qarta
Delphi RTTI për mapimin pa magji funksionon mirë kur përdorni RTTI si vegël për metadata deklarative – jo si ftesë për heuristika të padukshme. Atributet si opt-in, konvertimi i centralizuar, cache për tip dhe tekste gabimi të kuptueshme e çojnë temën nga „i paqartë“ në „i operueshëm“. Qasja me qëllim nuk është universale: për grafë të ndërlikuara, semantikë strikte për vlerën null ose performancë maksimale ju duhen komponentë shtesë. Si urë e qëndrueshme midis strukturave Dataset/Legacy dhe objekteve më moderne të domenit, ai në shumë baza kodi të Delphi është hapi pragmatik që e bën modernizimin të mundur.
Nëse në një aplikacion të zhvilluar Delphi po ngecni te kufijtë e mapimit, cilësia e të dhënave ose modernizimi në hapa, ne mund ta ngrisim këtë së bashku në mënyrë të pastër dhe ta përshtatim në arkitekturën tuaj: Na kontaktoni.
Në kontekstin teknik, Delphi Rtti Mapping dhe Attribute Mapping Delphi luajnë gjithashtu një rol të rëndësishëm kur integrimet, rrjedhat e të dhënave dhe zhvillimi i mëtejshëm duhet të funksionojnë së bashku në mënyrë të pastër.
Diskutoni një projekt ose iniciativë modernizimi me Net-Base.