Den som driv vaksen forretningsprogramvare i Delphi kjenner spennet: På den eine sida ønskjer ein strukturerte domeneobjekt og klare lag, på den andre sida finst det Datasets, Variants, CSV-impotar, grensesnittpayloads eller ein REST-API som «på ein eller annan måte» må mappast til objekt. Det er her ein raskt endar opp med Delphi RTTI für Mapping ohne Magie: altså mapping via Reflection (RTTI = Run-Time Type Information, typeinformasjon ved køyring), men slik at det er etterprøveleg, lett å feilsøkje og ikkje skjult bero på konvensjonar eller namnspel.
Kjernen: «Magien» oppstår som regel ikkje av RTTI i seg sjølv, men av implisitte reglar. Når mapping-reglar derimot står eksplisitt i attributt, konverteringar er sentraliserte og feil gir ein klar årsak, blir RTTI eit verktøy i staden for ei overrasking.
Kvifor RTTI-mapping i Delphi ofte sviktar
RTTI-basert mapping feilar i reale system sjeldan på ideen, men på randvilkår:
- Legacy-Datenformen: Null/Empty/0 er ikkje klart skilte, felttype endrar seg, strengar inneheld „N/A“.
- Smygande konvensjonar: „Feld heißt wie Property“ fungerer til det første aliaset, joinen eller det refaktorerte Property-namnet.
- Vanskeleg å feilsøkje: Når ein mapper «enkelt ikkje set noko», manglar årsaka seinare. I drift er det skadleg.
- Myter om ytelse: RTTI blir generelt stempla som „treigt“, sjølv om det oftast er manglande caching som er problemet.
Ein berekraftig tilnærming bør difor ha (1) eksplisitt mapping-metadata, (2) klar handtering av konvertering og null-semantikk, (3) feilmeldingar og debug-utdata, og (4) caching av RTTI-opplysningar.
Delphi RTTI for Mapping utan magi: Designprinsipp
Følgjande mønster er medvite «kjedelig» i beste meining: reglar er synlege, sideverknader avgrensa, og ein kan innføre det trinnvis i eksisterande modul.
- Attribut i staden for namnekonvensjon: Property får eit attribut som namngjev kjeldekolonnen.
- Opt-in: Berre markerte Properties blir sett. Ingen overraskingar frå „alle publizierten Properties“.
- Konvertering på ein stad: Variant/String/Integer/Boolean/Enum/Nullable blir sentralt mappa.
- Debug-modus: Valfritt blir det logga kva felt som blei sette/hoppa over – med grunn.
- RTTI-Caching: Dei dyreste delane (Propertyliste, evaluering av attributt) blir førebudd per type.
Kodeutdrag: attributt-mapping med RTTI, caching og debug
Utdraget mappar ei rad (t.d. frå BDE-Ablosung mit nativer Anbindung via TDataSet) til eit objekt. I staden for å kople mapperen tett til TField, nyttar vi eit lite reader-grensesnitt. Dette er i praksis verdifullt, fordi du seinare kan bruke same logikk for JSON, INI, CSV eller API-responsar.
unit RttiMapping;
interface
uses
System.SysUtils, System.Rtti, System.TypInfo, System.Generics.Collections,
System.Variants;
type
// Eksplisitt mapping: Property <- kjeldenamn
MapFromAttribute = class(TCustomAttribute)
private
FName: string;
public
constructor Create(const AName: string);
property Name: string read FName;
end;
// Liten abstraksjon: levere verdi og skille mellom eksistens og 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: berre Properties med attributt
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('Feil ved konvertering til Boolean: "%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 utanfor gyldig område: %d', [Ord]);
Exit(Ord);
end;
Name := VarToStr(V);
Ord := GetEnumValue(AEnumType.Handle, Name);
if Ord < 0 then
raise ERttiMappingError.CreateFmt('Enum-namn ukjend: "%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;
// Konvertering bevisst selektiv: heller feile tydeleg enn å feile stille "på ein eller annan måte".
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 ikkje implementert for %s', [AProp.Name]);
tkClass:
raise ERttiMappingError.CreateFmt('Class-Property-mapping ikkje implementert for %s', [AProp.Name]);
else
raise ERttiMappingError.CreateFmt('TypeKind ikkje støtta (%s) for %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 eller Target er 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('Kjelde manglar: "%s" for Property %s',
[M.SourceName, M.Prop.Name]);
Continue;
end;
if AReader.IsNull(M.SourceName) then
begin
if moIgnoreNull in AOptions then
Continue;
// Utan Nullable/Optional-mekanikk kan ein ikkje setje NULL på ein meiningsfull måte.
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('Mapping-feil ved %s <- %s: %s',
[M.Prop.Name, M.SourceName, E.Message]);
end;
end;
end;
end.
Kvifor dette er nyttig
De får eit mapping som let seg vurdere ryddig i Code-Reviews:
- Kvar mappa Property er visuelt merka (Attribut).
- Konverteringa er sentralisert, og dermed konsistent og testbar.
- Feilmeldingane seier, kva for Property og kva for kjelde som er ramma.
- Ein Debug-Modus gir ved behov bevisrekka, utan at ein treng Breakpoints i produksjonsprosessen.
Rammevilkår og typiske fallgruver
- NULL-semantikk: Utan eit eige Nullable-Konzept (t.d.
Nullable<T>eller Option-Types) er «å setje NULL» ikkje entydig. I snippetet blir NULL som standard oversett. Dette er konservativt og hindrar stille overskrivingar. - TRttiContext-levetid: Vi byggjer cachen ein gong per type og kastar Contexten etterpå. Dette er vanleg. Viktig: Ikkje bygg ein ny RTTI-Context per felttilordning.
- Threading: Cachen er via Monitor beskytta. I høgparallelle Mappings (t.d. REST-Server) bør ein i tillegg vurdere om ein byggjer cachen «varm» ved oppstart (Preload) for å redusere lock-contention.
- PropertyType Kind:
tkClassogtkSeter med vilje ikkje implementert. For innlaga objekt bør ein anten mappe rekursivt (med klar policy) eller medvite tilordne manuelt. - Locale-fallar:
varDoubleviaVarAsTypeer relativt robust, men strenger som «1,23» vs. «1.23» er framleis eit tema. Dersom kjeldene dine leverer strenger, er ein eigen parser (med definert Culture) ofte betre.
Variant for FireDAC og TDataSet: Reader-Adapter i staden for Mapper-Kopplung
I BDE-Ablosung mit nativer Anbindung- eller klassiske VCL/Win32-applikasjonar er kjelda ofte eit TDataSet. I staden for å binde Mapperen til TField, skriv du ein adapter som implementerer interfacet IValueReader. Fordelen: Mapperen held seg uavhengig av datatilgangen (viktig om du seinare flyttar datatilgang til tenester eller ein 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;
Slik ser eit konkret mapping ut:
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;
Kor denne tilnærminga løner seg – og kvar ikkje
Dette mønsteret løner seg vanlegvis i tre situasjonar:
- Gradvis modernisering: Du vil introdusere domenobjekt utan å byggje om dataåtkomsten fullstendig med ein gong (klassisk ved Delphi modernisering i eksisterande applikasjonar).
- Grenseflater: CSV-/Excel-importar, REST-Payloads eller „blanda“ datakjelder treng robust konvertering og gode feilmeldingar.
- Vedlikehald i teamet: Attributt gjer mapping-reglar synlege og gjennomgåelege, noko som er særs verdifullt i større kodebasar.
Det finst også klare grenser for bruken:
- Komplekse objektgrafar (barn-kolleksjonar, sykliske referansar) bør du ikkje ‚automagisk‘ mappe. Her er eksplisitt kode eller eit eige Assembler/Factory-mønster vanlegvis meir stabilt.
- High-Throughput-Hotpaths (t.d. massedata-ETL) har større fordel av kodegenererte mapperar eller handoptimalisert mapping, sjølv om RTTI er cacha.
- Nullable/Optional er eit eige tema. Dersom du verkeleg må skilje mellom „ikkje til stades“, „NULL“ og „Default“, bør du uttrykkje det i domenemodellen, ikkje skjule det i mapperen.
Innordning i arkitektur og drift
Frå arkitektursynspunkt er denne mapperen ein infrastrukturkomponent på grensa mellom datarepresentasjon og domene. Han erstattar ikkje ei klar lagdeling, men kan gjere den mogleg: Dataåtkomsten (FireDAC, SQL, Views) kan framleis vere pragmatisk, medan domenet held seg konsistent. I fleirlagssystem (oft kalla Layer-3 arkitektur: UI, Domain/Services, Infrastruktur) høyrer mapperen til i infrastrukturen og blir brukt av tenester, ikkje av UI-skjema.
Driftsmessig viktig: Aktiver moDebug ikkje permanent i produktive tenester, men målretta. For vanskeleg å reprodusere dataproblem er det fornuftig å ha ein brytar for diagnostikk (konfigurasjon, feature-flag). Elles risikerer du stort loggvolum og biverknader.
Konklusjon: RTTI ja, men berre med klare retningslinjer
Delphi RTTI for Mapping utan magi fungerer godt når du nyttar RTTI som eit verkemiddel for deklarative metadata – ikkje som ei invitasjon til stille heuristikkar. Attribut som opt-in, sentralisert konvertering, cache per type og tydelege feilmeldingar tek temaet frå «uoversiktleg» til «driftsklart». Denne tilnærminga er medvite ikkje universell: For innfløkte grafar, streng null-semantikk eller maksimal ytelse treng du fleire komponentar. Som ei robust bru mellom dataset-/legacy-strukturar og meir moderne domenobjekt er ho i mange Delphi-kodebasar nett det pragmatiske steget som gjer modernisering i det heile teke mogleg.
Hvis du i ei eksisterande Delphi-applikasjon sit fast i mapping-grenser, datakvalitet eller trinnvis modernisering, kan vi setje dette opp ryddig saman og tilpasse det til arkitekturen din: ta kontakt.
I fagleg samanheng spelar også Delphi Rtti Mapping og Attribute Mapping Delphi ei viktig rolle når integrasjonar, dataflyt og vidareutvikling må fungere godt saman.