Net-Base Magasin

08.05.2026

Delphi RTTI for Mapping utan magi: attributtbasert, feilsøkbar og legacy-kompatibel

Eit pragmatisk mapping-mønster med Delphi RTTI: attributt i staden for konvensjonar, kontrollerte konverteringar, tydelege feilmeldingar og ein debug-modus som verkeleg hjelper i drift. Med kjeldekodestykkjer for dataset- eller record-til-objekt-mapping utan skjult magi.

08.05.2026

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.

Delphi
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: tkClass og tkSet er med vilje ikkje implementert. For innlaga objekt bør ein anten mappe rekursivt (med klar policy) eller medvite tilordne manuelt.
  • Locale-fallar: varDouble via VarAsType er 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).

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;

Slik ser eit konkret mapping ut:

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;

Kor denne tilnærminga løner seg – og kvar ikkje

Dette mønsteret løner seg vanlegvis i tre situasjonar:

  1. Gradvis modernisering: Du vil introdusere domenobjekt utan å byggje om dataåtkomsten fullstendig med ein gong (klassisk ved Delphi modernisering i eksisterande applikasjonar).
  2. Grenseflater: CSV-/Excel-importar, REST-Payloads eller „blanda“ datakjelder treng robust konvertering og gode feilmeldingar.
  3. 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.

Drøfte prosjekt eller moderniseringsprosjekt med Net-Base.

Del innlegg

Del dette innlegget direkte

LinkedIn, X, XING, Facebook, WhatsApp og e-post er tilgjengelege med ein gong. For Instagram klargjer vi lenke og kort tekst med det same.

E-post

Instagram opnar i ein ny fane. Lenkje og kort tekst blir kopiert til utklippstavla på førehand.