Net-Base Žurnalas

08.05.2026

Delphi RTTI žemėlapiavimui be magijos: atributais pagrįstas, lengvai derinamas ir suderinamas su paveldėtosiomis sistemomis

Pragmatiškas mapavimo modelis su Delphi RTTI: atributai vietoje konvencijų, kontroliuojamos konvertacijos, aiškūs klaidų pranešimai ir derinimo režimas, kuris eksploatacijoje iš tiesų padeda. Su kodo ištraukomis Dataset arba Record į objektą mapavimui be paslėptos magijos.

08.05.2026

Kas valdo augusią verslo programinę įrangą Delphi, pažįsta šį įtempimą: vienoje pusėje siekiama struktūruotų domeno objektų ir aiškių sluoksnių, kitoje — yra Datasets, Variants, CSV importai, sąsajų payload’ai arba REST-API, kurie „kažkaip“ turi būti priskirti objektams. Būtent čia dažnai kertamasi su Delphi RTTI žemėlapavimu be magijos: t. y. žemėlapavimu per Reflection (RTTI = Run-Time Type Information, tipo informacija vykdymo metu), bet taip, kad jis būtų suprantamas, gerai derinamas ir neslėptųsi už konvencijų ar vardų triukų.

Pagrindinė mintis: „magija“ dažniausiai nekyla dėl pačio RTTI, o dėl numanomų taisyklių. Jei žemėlapavimo taisyklės aiškiai nurodytos atributuose, konvertavimai centralizuoti ir klaidos nurodo aiškią priežastį, RTTI tampa įrankiu, o ne netikėtumu.

Kodėl RTTI žemėlapavimas sistemoje Delphi dažnai suklumpa

RTTI pagrįstas žemėlapavimas realiose sistemose retai žlunga dėl idėjos — dažniau dėl ribojančių sąlygų:

  • Paveldėtos duomenų formos: Null/Empty/0 nėra aiškiai atskiriami, laukų tipai kinta, stringai gali turėti „N/A“.
  • Palaipsniui plintančios konvencijos: „laukas vadinamas kaip savybė“ veikia tol, kol neatsiranda pirmasis alias, join arba perrefaktorizuotas savybės pavadinimas.
  • Sunku derinti: jei mapper tiesiog „nieko nenustato“, vėliau trūksta priežasties. Eksploatacijoje tai gali būti pražūtinga.
  • Produktyvumo mitai: RTTI dažnai pabraukiamas kaip „lėtas“, nors dažniausiai problema yra trūkstamas kešavimas.

Tvari strategija turėtų todėl (1) turėti aiškius žemėlapavimo metaduomenis, (2) aiškiai tvarkyti konvertavimą ir nulės semantiką, (3) pateikti klaidų ir derinimo žurnalus ir (4) kešuoti RTTI informaciją.

Delphi RTTI žemėlapavimas be magijos: dizaino principai

Žemiau pateiktas modelis sąmoningai „nuobodus“ gerąja prasme: taisyklės yra matomos, šalutinis poveikis ribotas, ir jį galima žingsnis po žingsnio integruoti į esamus modulius.

  • Atributai vietoj vardų konvencijos: savybė gauna atributą, kuris nurodo šaltinio stulpelį.
  • Opt-in: nustatomos tik pažymėtos savybės. Jokios staigmenos dėl „visų publikuotų savybių“.
  • Konvertavimas vienoje vietoje: Variant/String/Integer/Boolean/Enum/Nullable yra centralizuotai žemėlapinami.
  • Debug-Mode: neprivalomai registruojama, kurie laukai buvo nustatyti arba praleisti — kartu su priežastimi.
  • RTTI-Caching: brangiausios dalys (savybių sąrašas, atributų įvertinimas) paruošiamos kiekvienam tipui.

Šaltinio kodo fragmentas: atributų žemėlapis su RTTI, kešavimu ir derinimu

Šis pavyzdys atvaizduoja vieną eilutę (pvz. iš BDE-Ablosung su gimtąja prijungtimi per TDataSet) į objektą. Užuot tvirtai susieję mapperį su TField, naudojame mažą Reader sąsają. Tai praktiškai vertinga, nes vėliau tą pačią logiką galėsite naudoti ir JSON, INI, CSV arba API-Responses.

unit RttiMapping;

interface

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

type
// Eksplizitinis atvaizdavimas: savybė <- šaltinio pavadinimas
MapFromAttribute = class(TCustomAttribute)
private
FName: string;
public
constructor Create(const AName: string);
property Name: string read FName;
end;

// Maža abstrakcija: reikšmės tiekimas + egzistavimo/NULL atskyrimas
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: tik savybės su atributu
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 konvertavimas nepavyko: „%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 ordinalas už leistinų ribų: %d‘, [Ord]);
Exit(Ord);
end;

Name := VarToStr(V);
Ord := GetEnumValue(AEnumType.Handle, Name);
if Ord < 0 then
raise ERttiMappingError.CreateFmt(‚Enum pavadinimas nežinomas: „%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;

// Konvertavimas sąmoningai selektyvus: geriau aiškiai nepavykti nei tyliai „kažkaip“.
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-mappingas neįgyvendintas: %s‘, [AProp.Name]);

tkClass:
raise ERttiMappingError.CreateFmt(‚Klasės savybės atvaizdavimas neįgyvendintas: %s‘, [AProp.Name]);
else
raise ERttiMappingError.CreateFmt(‚TypeKind nepalaikomas (%s) savybei %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 arba Target yra 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(‚Šaltinis nerastas: „%s“ savybei %s‘,
[M.SourceName, M.Prop.Name]);
Continue;
end;

if AReader.IsNull(M.SourceName) then
begin
if moIgnoreNull in AOptions then
Continue;
// Be Nullable/Optional mechanikos NULL negalima prasmingai nustatyti.
Continue;
end;

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

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

end.

Kam tai reikalinga

Gaunate mappingą, kurį galima aiškiai įvertinti kodo peržiūrose:

  • Kiekviena susieta savybė yra vizualiai pažymėta (atributas).
  • Konvertavimas vykdomas centralizuotai, todėl yra nuoseklus ir testuojamas.
  • Klaidos pranešimai nurodo, kurią savybę ir kurį šaltinį tai liečia.
  • Derinimo režimas, esant abejonėms, pateikia įrodymų grandinę, todėl jums nereikia įterpti breakpoint’ų gamybiniame procese.

Apribojimai ir tipinės kliūtys

  • NULL-Semantik: Be nuosavo Nullable-koncepto (pvz. Nullable<T> arba Option-Types) „NULL nustatymas“ nėra vienareikšmis. Snipete NULL pagal nutylėjimą praleidžiamas. Tai konservatyvu ir apsaugo nuo tyliai vykstančių perrašymų.
  • TRttiContext-Lebensdauer: Talpyklą kuriame vieną kartą kiekvienam tipui ir vėliau išmetame Context. Tai įprasta. Svarbu: nekurti naujo RTTI-Context kiekvienai lauko priskirčiai.
  • Threading: Talpykla apsaugota per Monitor. Labai paraleliuose mapping’uose (pvz. REST-Server) verta papildomai apsvarstyti talpyklos „užkūrimą“ paleidimo metu (Preload), kad sumažintumėte lock-contention.
  • PropertyType Kind: tkClass ir tkSet specialiai neįgyvendinti. Dėl įdėtinių objektų turėtumėte arba mappinti rekursyviai (su aiškia politika), arba sąmoningai priskirti rankiniu būdu.
  • Locale-Fallen: varDouble per VarAsType yra santykinai robustus, bet string’ai kaip „1,23“ vs. „1.23“ vis tiek kelia problemų. Jei jūsų šaltiniai grąžina string’us, dažnai geriau turėti atskirą parser’į (su apibrėžta Culture).

Variantas für FireDAC und TDataSet: Reader-Adapteris statt Mapper-Kopplung

In BDE-Ablosung mit nativer Anbindung- arba klasikiniuose VCL/Win32 sprendimuose šaltinis dažnai yra TDataSet. Vietoj to, kad susietumėte Mapperį su TField, parašykite adapterį, kuris įgyvendina sąsają IValueReader. Privalumas: Mapperis lieka nepriklausomas nuo duomenų prieigos (svarbu, jei duomenų prieigą vėliau perkelsite į servisus arba į 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;

Tokiu atveju konkretus mappingas atrodytų taip:

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;

Kur verta taikyti požiūrį – ir kur ne

Šis modelis paprastai yra naudingas trimis atvejais:

  1. Palaipsnė modernizacija: Norite įdiegti domeno objektus, neperstatydami iš karto viso duomenų prieigos sluoksnio (klasikinis atvejis Delphi modernizacijoje esamose programose).
  2. Sąsajų ribos: CSV-/Excel importai, REST-Payloads arba „mišrūs“ duomenų šaltiniai reikalauja patikimos konversijos ir aiškių klaidų pranešimų.
  3. Palaikymas komandoje: Atributai daro susiejimo taisykles matomas ir peržiūrimas, kas didelėse kodo bazėse yra itin vertinga.

Taip pat yra aiškios taikymo ribos:

  • Kompleksiniai objektų grafai (Child-Collections, ciklinės nuorodos) nereikėtų „automatizuotai“ mapinti. Čia aiškus rankinis kodas arba atskiras Assembler/Factory modelis dažniausiai yra stabilesnis.
  • Didelio pralaidumo kritiniai keliai (pvz. Massendaten-ETL) labiau pasiteisina naudojant kodo generuotus mapperius arba rankiniu būdu optimizuotą mapingą, net jei RTTI yra kešuotas.
  • Nullable/Optional yra atskira tema. Jei jums iš tiesų reikia atskirti „nėra“, „NULL“ ir „numatytoji“ reikšmes, tai turėtumėte išreikšti domeno modelyje, o ne slėpti mappere.

Vieta architektūroje ir eksploatavime

Iš architektūrinės perspektyvos šis mapperis yra infrastruktūros komponentas, esantis ant ribos tarp duomenų reprezentacijos ir domeno. Jis nepakeičia aiškios sluoksniuotės, tačiau gali ją leisti: duomenų prieiga (FireDAC, SQL, Views) gali išlikti pragmatiška, tuo tarpu domenas lieka nuoseklus. Daugiasluoksnėse sistemose (dažnai vadinamoje Layer-3 architektūra: UI, Domain/Services, infrastruktūra) mapperis priklauso infrastruktūrai ir naudojamas servisų, o ne UI formų.

Eksploataciškai svarbu: nepalikite moDebug visada įjungto produkciniuose servisuose — naudokite jį tik taikliai. Sunkiai atkuriamoms duomenų problemoms pravartu turėti perjungiamą diagnostikos kelią (konfigūracija, Feature-Flag). Priešingu atveju gresia didžiulis logų kiekis ir šalutiniai poveikiai.

Išvada: RTTI taip, bet tik su aiškiomis gairėmis

Delphi RTTI žemėlapiavimui be magijos gerai veikia, kai RTTI naudojate kaip priemonę deklaratyviai metainformacijai — ne kaip kvietimą paslėptomis heuristikoms. Atributai kaip pasirinkimas (opt-in), centralizuotas konvertavimas, talpykla kiekvienam tipui ir aiškūs klaidų pranešimai perkelia temą nuo „neaišku“ iki „tinkama eksploatacijai“. Požiūris sąmoningai nėra universalus: sudėtingiems grafams, griežtai nulio semantikai ar maksimaliam našumui reikės papildomų komponentų. Tačiau kaip tvirta jungtis tarp Dataset/Legacy-struktūrų ir modernesnių domeno objektų jis daugelyje Delphi kodo bazių yra būtent tas pragmatiškas žingsnis, kuris iš tiesų leidžia pradėti modernizaciją.

Jei savo užaugusioje Delphi-programoje šiuo metu strigote prie žemėlapiavimo ribų, duomenų kokybės arba palaipsninės modernizacijos, galime tai kartu tvarkingai įdiegti ir pritaikyti jūsų architektūrai: Susisiekite.

Profesiniame kontekste taip pat svarbų vaidmenį atlieka Delphi Rtti žemėlapiavimas ir atributų žemėlapiavimas Delphi, kai integracijos, duomenų srautai ir tolimesnė plėtra turi sklandžiai veikti kartu.

Aptarti projektą arba modernizacijos iniciatyvą su Net-Base.

Pasidalinti įrašu

Tiesiogiai pasidalinti šiuo įrašu

LinkedIn, X, XING, Facebook, WhatsApp ir el. paštas yra iš karto prieinami. Instagramui paruošiame nuorodą ir trumpą tekstą iš karto.

El. paštas

Instagram atidaromas naujame skirtuke. Nuoroda ir trumpas tekstas iš anksto nukopijuojami į iškarpinę.