Þeir sem reka þróuð fyrirtækjaforrit í Delphi þekkja þessa spennu: Annars vegar ósk um skipulagða lénahluti (domain objects) og skýra lagskiptingu, hins vegar gagnasett, Variant‑gildi, CSV‑innflutninga, viðmótspayloads eða REST‑API sem þarf „einhvern veginn“ að kortleggja á hluti. Þarna lendir maður fljótt á Delphi RTTI fyrir kortlagningu án töfra: þ.e. kortlagning með reflection (RTTI = Run‑Time Type Information, gerðaupplýsingar á keyrslutíma), en þannig að hún verði eftirsjáanleg, vel hægt að villaeftirlit og ekki falin bakvið duldar samkomulagsreglur eða nafna‑leikrit.
Kjarni málsins: „töfrar“ skapast yfirleitt ekki vegna RTTI sjálfs heldur vegna óskráðra regluverka. Ef kortlagningarreglur eru hins vegar skýrt tilgreindar með eigindamerkingum (Attribute), umbreytingar miðstýrðar og villur skila skýrri orsök, þá verður RTTI að verkfæri fremur en óvæntingu.
Af hverju RTTI‑kortlagning í Delphi mistekst oft
RTTI‑grunduð kortlagning bregst í raunverulegum kerfum sjaldan vegna hugmyndarinnar sjálfrar, heldur vegna jaðarskilyrða:
- Legacy‑gagnamynstur: Null/Empty/0 eru ekki skýrlega aðskilin, reitsgerðir breytast, strengir innihalda „N/A“.
- Ósjáinlegar samkomulagsreglur: „Reitur heitir eins og eiginleiki“ virkar þangað til fyrsta alias, join eða endurnefning eiginleikanafnsins kemur.
- Vandamál við villaeftirlit: Ef mapperinn „setur einfaldlega ekkert“, vantar síðar orsök. Í rekstri er það alvarlegt.
- Frammistöðumytur: RTTI er gjarnan klippt sem „hægt“, þó sé yfirleitt skortur á skyndiminni (caching) sem veldur vandanum.
Traustur nálgun ætti því að (1) hafa skýr kortlagningar‑metagögn, (2) meðhöndla umbreytingu og null‑semantík skýrt, (3) skila villum og villaúttaki og (4) skyndiminna RTTI‑upplýsingar.
Delphi RTTI fyrir kortlagningu án töfra: Hönnunarreglur
Eftirfarandi mynstur er með viljandi „leiðinlegt“ í besta skilningi: reglur sjást, aukaverkanir eru takmarkaðar og hægt er að færa það skref fyrir skref inn í núverandi einingar.
- Attribute í stað nafnavenja: Eiginleiki fær attribut sem tilgreinir uppsprettudálkinn.
- Opt‑in: Aðeins merktir eiginleikar eru settir. Engar óvæntar breytingar vegna „alls birts eiginleika“.
- Umbreyting á einum stað: Variant/String/Integer/Boolean/Enum/Nullable eru kortlögð miðstætt.
- Debug‑Mode: Valfrjálst er skráð hvaða reitir voru settir eða hoppuð yfir – með skýrum rökum.
- RTTI‑Caching: Dýrustu hlutar ferlisins (listi yfir eiginleika, úrvinnsla attributa) eru undirbúnir fyrir hvern týpu.
Source‑Schnipsel: Attribut‑Mapping mit RTTI, Caching und Debug
Þessi kóðabútur kortleggur eina línu (t.d. úr BDE‑Ablosung mit nativer Anbindung via TDataSet) á hlut. Í stað þess að tengja mapperinn fast við TField notum við lítinn Reader‑viðmót. Í rekstri er þetta verðmætt, því sömu rökreglu má síðar nota fyrir JSON, INI, CSV eða API‑svar.unit RttiMapping;
interface
uses
System.SysUtils, System.Rtti, System.TypInfo, System.Generics.Collections,
System.Variants;
type
// Skýr kortlagning: Property <- Quellname
MapFromAttribute = class(TCustomAttribute)
private
FName: string;
public
constructor Create(const AName: string);
property Name: string read FName;
end;
// Lítil aðgreining: skila gildi og greina tilvist/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: aðeins Properties með Attribut
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(‚Breyting í boolean mistókst: „%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 utan gildissviðs: %d‘, [Ord]);
Exit(Ord);
end;
Name := VarToStr(V);
Ord := GetEnumValue(AEnumType.Handle, Name);
if Ord < 0 then
raise ERttiMappingError.CreateFmt(‚Enum-nafn óþekkt: „%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;
// Umbreyting er meðvitað valin: betra að mistakast skýrt en að gera eitthvað óljóst.
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 ekki útfært fyrir %s‘, [AProp.Name]);
tkClass:
raise ERttiMappingError.CreateFmt(‚Class-Property mapping ekki útfært fyrir %s‘, [AProp.Name]);
else
raise ERttiMappingError.CreateFmt(‚TypeKind ekki studdur (%s) fyrir %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 eða 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(‚Uppspretta vantar: „%s“ fyrir Property %s‘,
[M.SourceName, M.Prop.Name]);
Continue;
end;
if AReader.IsNull(M.SourceName) then
begin
if moIgnoreNull in AOptions then
Continue;
// Án Nullable/Optional-mechanismu er ekki hægt að setja NULL á merkingarbæran hátt.
Continue;
end;
V := AReader.GetValue(M.SourceName);
try
SetPropertyValue(ATarget, M.Prop, V);
if moDebug in AOptions then
begin
Msg := Format(‚Kortlagt %s <- %s (%s)‘, [M.Prop.Name, M.SourceName, VarTypeAsText(VarType(V))]);
OutputDebugString(PChar(Msg));
end;
except
on E: Exception do
raise ERttiMappingError.CreateFmt(‚Kortlagningarvilla við %s <- %s: %s‘,
[M.Prop.Name, M.SourceName, E.Message]);
end;
end;
end;
end.
Hvers vegna þetta er gagnlegt
Þið fáið kortlagningu sem hægt er að meta skýrt við kóðaskoðun:
- Hver kortlögður eiginleiki er sjónrænt merktur (atribút).
- Umbreytingin er miðlæg, þannig samkvæm og prófanleg.
- Villuskilaboðin tilgreina nákvæmlega hvern eiginleika og hverja uppsprettu sem tengist.
- Villugreiningarhamur gefur ykkur, ef þörf krefur, sönnunarkeðjuna án þess að þurfa stöðvunarpunkt (breakpoints) í framleiðsluferlinum.
Skilyrði og algengar gildrur
- NULL-Semantik: Án eigin Nullable-hugtaks (t.d.
Nullable<T>eða Option-Types) er „NULL setja“ ekki ótvírætt. Í sýnishorninu er NULL sjálfgefið sleppt. Þetta er varfærni og kemur í veg fyrir þögla yfirsögn. - TRttiContext-Lebensdauer: Við byggjum cache-inn einu sinni fyrir hvern týpu og förum svo með Context-inn burt. Þetta er algengt. Mikilvægt: Ekki byggja nýjan RTTI-Context fyrir hverja reitúthlutun.
- Threading: Cache-inn er varið með Monitor. Í mjög samhliða kortlögunum (t.d. REST-Server) ættuð þið, sem viðbót, að athuga hvort byggja eigi cache-inn „heitan“ við upphaf (Preload) til að draga úr læsaárekstrum (lock contention).
- PropertyType Kind:
tkClassogtkSeteru meðvitað ekki útfærð. Fyrir innfelldar hluti ættuð þið annaðhvort að kortleggja endurkvæmt (með skýra stefnu) eða úthluta handvirkt. - Locale-Fallen:
varDoublemeðVarAsTypeer tiltölulega stöðug, en strengir eins og „1,23“ vs. „1.23“ eru samt mál. Ef uppsprettur ykkar skila strengjum er oft betra að nota sérsniðinn parser með skilgreindri menningarstillingu.
Variante für FireDAC und TDataSet: Reader-Adapter statt Mapper-Kopplung
Í BDE-Ablosung mit nativer Anbindung- eða klassískum VCL/Win32-forritum er uppspretta oft TDataSet. Í stað þess að binda Mapperinn við TField skrifið þið adapter sem uppfyllir viðmótið IValueReader. Kosturinn: Mapperinn helst óháður gagnaaðgangi (mikilvægt ef þið flytjið gagnaaðgang síðar í þjónustur eða á 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;
Þá lítur tiltekin kortlagning svona út:
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;
Hvar þessi nálgun borgar sig – og hvar ekki
Þetta mynstur borgar sig yfirleitt í þremur aðstæðum:
- Skrefvís uppfærsla: Þið viljið innleiða lénsobjekt án þess að endurskipuleggja gagnaaðganginn strax í heild (klassískt við Delphi Nútímavæðing í eldri forritum).
- Samskeyti aðmörk: CSV-/Excel-innflutningar, REST-payloads eða „blandaðar“ gagnagjafar þurfa trausta umbreytingu og skýrar villuboð.
- Viðhald innan teymis: Atribútar gera reglur mappunar sýnilegar og endurskoðanlegar, sem er gífurlega mikilvægt í stærri kóða-bösum.
Nýtingartakmarkanir eru einnig skýrar:
- Flóknir hlutagrafar (Child-Collections, zyklische Referenzen) ætti ekki að mappa „sjálfvirkt“. Hér er skýrari kóði eða sérhæft Assembler/Factory-mynstur yfirleitt stöðugra.
- High-Throughput-Hotpaths (t.d. fjöldagagna-ETL) hafa meira gagn af kóðagjörnunum mapperum eða handfínstilltu mappingi, jafnvel þótt RTTI sé í skyndiminni.
- Nullable/Optional er sérstakt mál. Ef þið þurfið raunverulega aðgreiningu á milli „ekki til staðar“, „NULL“ og „Default“, ber að láta það koma fram í lénsmodelinu, ekki fela það í mappernum.
Staðsetning innan arkitektúrs og reksturs
Úr arkitektúr-sjónarhorni er þessi Mapper innviðahluti á mörkum gagnasýningar og léns. Hann kemur ekki í staðinn fyrir skýra lagskiptingu, en getur gert hana mögulega: Gagnaaðgangur (FireDAC, SQL, Views) má áfram vera pragmatískur, á meðan lénið helst samræmt. Í flerlaga kerfum (oft nefnt sem Layer-3 arkitektúr: UI, Domain/Services, innviðir) tilheyrir mapperinn innviðum og er notaður af þjónustum, ekki af UI-eyðublöðum.
Rekstrarlega mikilvægt: Kveikið ekki á moDebug varanlega í framleiðsluþjónustum, heldur markvisst. Fyrir gagnavandamál sem erfitt er að endurframkalla er skynsamlegt að hafa skiptanlegan greiningarleið (stillingar, feature-flag). Annars stefnir í mikið logmagn og aukaverkanir.
Niðurstaða: RTTI já, en aðeins með skýrum rammaviðmiðum
Delphi RTTI fyrir kortlagningu án töfra virkar vel þegar þú notar RTTI sem verkfæri fyrir yfirlýsandi metagögn – ekki sem boð um dulta heurístík. Atribut sem opt‑in, miðstýrð umbreyting, skyndiminni fyrir hverja týpu og skiljanleg villuboð færa málið frá „ógegnsætt“ yfir í „rekstrarfært“. Aðferðin er meðvitað ekki alhliða: Fyrir innfellda grafa, stranga núll‑semantík eða hámarks frammistöðu þarftu frekari íhluti. Sem traust brú milli gagnasetta/eldri kerfistrúktúra og nútímalegra viðfangseininga er hann í mörgum Delphi-kóðagrunnum einmitt hagnýta skrefið sem gerir nútímavæðingu yfirhöfuð mögulega.
Ef þú ert í vaxinni Delphi-umsókn fastur vegna kortlagningarbrúnna, gagnagæða eða stigvísrar nútímavæðingar, getum við sett þetta upp saman á hreinan hátt og lagað inn í arkitektúrinn þinn: Hafðu samband.
Í faglegu samhengi gegna einnig Delphi RTTI‑kortlagning og atribút‑kortlagning Delphi mikilvægu hlutverki þegar samþættingar, gagnastraumar og áframhaldandi þróun þurfa að spila hreint saman.