Net-Base Žurnāls

08.05.2026

Delphi RTTI kartēšanai bez maģijas: atribūtu bāzēts, atkļūdojams un saderīgs ar mantoto kodu

Pragmatisks kartēšanas paraugs ar Delphi RTTI: atribūti, nevis konvencijas; kontrolētas konvertēšanas; skaidri kļūdu ziņojumi; atkļūdošanas režīms, kas darbībā tiešām palīdz. Ar koda fragmentiem Dataset vai Record uz objekta kartēšanai bez slēptas maģijas.

08.05.2026

Ikvienam, kas uztur esošu biznesa programmatūru saistībā ar Delphi, ir pazīstama šī spriedze: no vienas puses vēlamies strukturētus domēnu objektus un skaidras slāņu robežas, no otras puses pastāv Datasets, Variants, CSV-importi, saskarnes payloadi vai REST-API, kas „kaut kā“ jāizmēro uz objektiem. Tieši šeit ātri nonāk pie Delphi RTTI für Mapping ohne Magie: t.i., mapešana, izmantojot Reflection (RTTI = Run-Time Type Information, tipu informācija izpildes laikā), taču tā, lai tā būtu pārskatāma, labi debugojama un nebalstītos slepenās konvencēs vai nosaukumu trikos.

Galvenais: «maģija» parasti nerodas pati no RTTI, bet gan no implicitām noteikumu kopām. Ja mapešanas noteikumi ir eksplītēti atribūtos, konvertācijas centralizētas un kļūdas norāda skaidru cēloni, RTTI kļūst par instrumentu, nevis pārsteigumu.

Kāpēc RTTI-Mapping in Delphi bieži neizdodas

RTTI bāzēta mapešana reālās sistēmās reti vien iziet greizi idejas dēļ; parasti to ierobežo malas nosacījumi:

  • Legacy-Datenformen: Null/Empty/0 nav skaidri atdalīti, lauka tipi mainās, virknes satur “N/A”.
  • Schleichende Konventionen: “Lauks saucas kā Property” strādā līdz pirmajam aliasam, join vai refaktorētam īpašuma nosaukumam.
  • Schwer zu debuggen: Ja maperis „vienkārši neko nestāda“, vēlāk trūkst cēloņa noteikšanas. Ražošanā tas ir bīstami.
  • Performance-Mythen: RTTI vispārināti apzīmē kā “lēnu”, lai gan bieži vien problēma ir neadekvāts kešošanas trūkums.

Tāpēc ilgtspējīgam risinājumam jāparedz (1) eksplītēti mapping-metadati, (2) centralizēta konvertācija un null-semantikas apstrāde, (3) kļūdu un debug izvades, un (4) RTTI informācijas kešošana.

Delphi RTTI für Mapping ohne Magie: Designprinzipien

Zemāk redzamais paraugs ir apzināti «niecīgs» labākajā nozīmē: noteikumi ir redzami, blakusefekti ierobežoti, un to var pakāpeniski ieviest esošajos moduļos.

  • Attribute statt Namenskonvention: Īpašumam piešķirts atribūts, kas nosauc avota kolonnu.
  • Opt-in: Tiek iestatītas tikai atzīmētās Properties. Nav pārsteigumu no “visām publicētajām Properties”.
  • Konvertierung an einer Stelle: Variant/String/Integer/Boolean/Enum/Nullable tiek kartēti centrālajā vietā.
  • Debug-Mode: Pēc izvēles tiek reģistrēts, kuri lauki tika iestatīti/izlaisti — ar iemeslu.
  • RTTI-Caching: Dārgākās daļas (property saraksts, atribūtu izvērtēšana) tiek sagatavotas katram tipam.

Source-Schnipsel: Attribut-Mapping mit RTTI, Caching und Debug

Šis fragments ataino vienu rindu (piem., no BDE-Ablosung mit nativer Anbindung caur TDataSet) uz objektu. Tā vietā, lai maperi stingri sasaistītu ar TField, mēs izmantojam nelielu Reader interfeisu. Praktiskā ziņā tas ir vērtīgi, jo vēlāk to pašu loģiku var izmantot arī JSON, INI, CSV vai API atbildēm.

unit RttiMapping;

interface

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

type
// Eksplizīta kartēšana: īpašība <- avota nosaukums
MapFromAttribute = class(TCustomAttribute)
private
FName: string;
public
constructor Create(const AName: string);
property Name: string read FName;
end;

// Neliela abstrakcija: piegādā vērtību un atšķir eksistenci/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: tikai īpašības ar atribūtu
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-konvertācija neizdevās: „%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 ārpus diapazona: %d‘, [Ord]);
Exit(Ord);
end;

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

// Konvertēšana apzināti selektīva: labāk skaidri neizdoties nekā klusējot „kaut kā“.
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 nav implementēts priekš %s‘, [AProp.Name]);

tkClass:
raise ERttiMappingError.CreateFmt(‚Klases īpašības kartēšana nav implementēta priekš %s‘, [AProp.Name]);
else
raise ERttiMappingError.CreateFmt(‚TypeKind netiek atbalstīts (%s) priekš %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 vai Target ir 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(‚Avots trūkst: „%s“ priekš īpašības %s‘,
[M.SourceName, M.Prop.Name]);
Continue;
end;

if AReader.IsNull(M.SourceName) then
begin
if moIgnoreNull in AOptions then
Continue;
// Bez Nullable/Optional mehānikas NULL nevar jēgpilni iestatīt.
Continue;
end;

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

try
SetPropertyValue(ATarget, M.Prop, V);
if moDebug in AOptions then
begin
Msg := Format(‚Kartēts %s <- %s (%s)‘, [M.Prop.Name, M.SourceName, VarTypeAsText(VarType(V))]);
OutputDebugString(PChar(Msg));
end;
except
on E: Exception do
raise ERttiMappingError.CreateFmt(‚Kartēšanas kļūda pie %s <- %s: %s‘,
[M.Prop.Name, M.SourceName, E.Message]);
end;
end;
end;

end.

Kāpēc tas ir lietderīgi

Jūs iegūstat mapējumu, ko koda pārskatēs var skaidri izvērtēt:

  • Katra mapētā īpašība ir vizuāli atzīmēta (atribūts).
  • Konvertēšana ir centralizēta, tādējādi konsekventa un testējama.
  • Kļūdu ziņojumi norāda, kura īpašība un kurš avots ir skarts.
  • Atkļūdošanas režīms sniedz, ja nepieciešams, pierādījumu ķēdi, bez nepieciešamības izmantot breakpoints produktīvajā procesā.

Robežnosacījumi un tipiskas paklupšanas vietas

  • NULL-Semantik: Bez atsevišķa Nullable-koncepta (piem., Nullable<T> vai Option-Types) NULL iestatīšana nav viennozīmīga. Šajā piemēra fragmentā NULL pēc noklusējuma tiek izlaists. Tas ir konservatīvi un novērš klusās pārrakstīšanas.
  • TRttiContext dzīves ilgums: Mēs būvējam kešu vienu reizi katram tipam un pēc tam izmetam Context. Tas ir ierasts. Svarīgi: neveidot jaunu RTTI-Context katrai lauka piešķiršanai.
  • Threading: Kešs ir aizsargāts ar Monitor. Ļoti paralēlos mapējumos (piem., REST-serveris) papildus pārbaudiet, vai kešs jau tiek uzbūvēts starta laikā „siltā“ režīmā (Preload), lai samazinātu Lock-Contention.
  • PropertyType tips: tkClass un tkSet ir apzināti neimplementēti. Iegultiem objektiem jāveic vai nu rekursīva mapēšana (ar skaidru politiku), vai apzināta manuāla piešķiršana.
  • Locale‑slazdi: varDouble caur VarAsType ir relatīvi robusts, tomēr virknes kā „1,23“ pret „1.23“ joprojām rada problēmas. Ja jūsu avoti nodrošina virknes, bieži labāks risinājums ir īpašs parsētājs (ar definētu Culture).

Varianta priekš FireDAC un TDataSet: Reader‑adapteris vietā Mapper‑saistīšanai

BDE-Ablosung mit nativer Anbindung vai klasiskās VCL/Win32 lietojumprogrammās avots bieži ir TDataSet. Tā vietā, lai sasaistītu Mapper ar TField, uzrakstiet adapteri, kas realizē interfeisu IValueReader. Priekšrocība: Mapper paliek neatkarīgs no datu piekļuves (svarīgi, ja vēlāk datu piekļuve tiek izvietota servisos vai uz REST-servera).

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;

Tādējādi konkrēts mapējums izskatās šādi:

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 šī pieeja atmaksājas – un kur ne

Šis paraugs parasti atmaksājas trīs situācijās:

  1. Pakāpeniska modernizācija: Jūs vēlaties ieviest domēna objektus, nepārbūvējot uzreiz datu piekļuvi pilnībā (klasisks gadījums pie Delphi modernizācijas esošajās lietojumprogrammās).
  2. Saskarnes robežas: CSV-/Excel-importi, REST-payloadi vai „jaukti“ datu avoti prasa robu konversiju un labas kļūdu ziņas.
  3. Uzturējamība komandā: Atribūti padara kartēšanas noteikumus redzamus un pārskatāmus, kas lielākās koda bāzēs ir īpaši vērtīgi.

Ir arī skaidras izmantošanas robežas:

  • Kompleksi objektu grafi (bērnu kolekcijas, cikliskas atsauces) nevajadzētu „automātiski“ kartēt. Šādos gadījumos parasti stabilāks risinājums ir eksplicīts kods vai atsevišķs assembler/factory modelis.
  • Augstas caurlaides kritiskās vietas (piem., masveida datu ETL) parasti gūst lielāku labumu no kodaģenerētiem mapperiem vai manuāli optimizētas kartēšanas, pat ja RTTI ir kešots.
  • Nullable/Optional ir atsevišķa tēma. Ja jums patiešām jāšķiro starp „neeksistē“, „NULL“ un „noklusējums“, to jāizpauž domēna modelī, nevis jāslēpj mapperī.

Iekļaušana arhitektūrā un ekspluatācijā

No arhitektūras viedokļa šis mapperis ir infrastruktūras komponents uz robežas starp datu reprezentāciju un domēnu. Tas neaizstāj skaidru slāņošanu, bet to var atbalstīt: datu piekļuve (FireDAC, SQL, Views) var palikt pragmatiskā, kamēr domēna modelis saglabā konsekvenci. Multislāņu sistēmās (bieži sauktas par Layer-3 arhitektūru: UI, Domain/Services, Infrastruktur) mapperis pieder infrastruktūrai un tiek izmantots no servisēm, nevis tieši no UI formām.

Darba kontekstā svarīgi: neiespējojiet moDebug pastāvīgi produkcijas servisā, bet mērķtiecīgi. Grūti reproducējamu datu problēmu gadījumā ir jēga nodrošināt pārslēdzamu diagnostikas ceļu (konfigurācija, feature-flag). Citādi draud žurnālu apjoma pieaugums un blakusparādības.

Secinājums: RTTI jā, bet tikai ar skaidrām vadlīnijām

Delphi RTTI kartēšanai bez maģijas darbojas labi, ja Jūs RTTI izmantojat kā rīku deklaratīviem metadatiem — nevis kā ielūgumu klusajām heuristikām. Atribūti kā opt-in, centralizēta konvertēšana, kešs katram tipam un saprotami kļūdu teksti paceļ tēmu no „necaurspīdīga” uz „darbotspējīgu”. Šis pieejas veids apzināti nav universāls: sarežģītākiem iekļautajiem grafiem, stingrai nulles semantikai vai maksimālai veiktspējai būs nepieciešami papildu bloki. Tomēr kā robusts tilts starp Dataset/Legacy-Strukturen un modernākiem domēnu objektiem tas daudzās Delphi koda bāzēs ir tieši tas pragmatiskais solis, kas padara modernizāciju iespējamu.

Ja Jūs strādājat ar izveidotu Delphi lietojumu un pašlaik iestrēdzat pie mapošanas robežām, datu kvalitātes vai pakāpeniskas modernizācijas, mēs to varam kopā rūpīgi uzstādīt un pielāgot Jūsu arhitektūrai: Sazināties.

Profesionālajā kontekstā arī Delphi RTTI mapēšana un atribūtu mapēšana Delphi spēlē svarīgu lomu, ja integrācijām, datu plūsmām un turpmākai attīstībai jādarbojas saskaņoti.

Apspriest projektu vai modernizācijas ieceri ar Net-Base.

Kopīgot ierakstu

Kopīgot šo ierakstu tieši

LinkedIn, X, XING, Facebook, WhatsApp un e-pasts ir uzreiz pieejami. Instagramam saiti un īsu tekstu sagatavosim nekavējoties.

E-pasts

Instagram atveras jaunā cilnē. Saite un īss teksts tiek iepriekš nokopēti starpliktuvē.