Net-Base Dergi

08.05.2026

Delphi RTTI ile sihir gerektirmeyen eşleme: öznitelik tabanlı, hata ayıklanabilir ve legacy-uyumlu

Delphi RTTI ile pragmatik bir eşleme deseni: konvansiyonlar yerine öznitelikler, kontrollü dönüşümler, açık hata mesajları ve üretim ortamında gerçekten yardımcı olan bir hata ayıklama modu. Gizli sihir içermeyen Dataset- veya Record'tan nesneye eşleme için kaynak kod parçacıklarıyla.

08.05.2026

Yaşanmış iş yazılımlarını Delphi üzerinde çalıştıranlar bu gerilimi bilir: bir yanda yapılandırılmış alan (domain) nesneleri ve net katmanlar istenir, diğer yanda Datasets, Variants, CSV içe aktarımları, arayüz payload’ları veya bir REST-API vardır ki bunların „bir şekilde“ nesnelere eşlenmesi gerekir. Tam burada hızlıca Delphi RTTI ile sihirsiz eşleme noktasına gelirsiniz: yani Reflection ile eşleme (RTTI = Çalışma Zamanı Tür Bilgisi, Run-Time Type Information), fakat izlenebilir, iyi debug yapılabilir ve örtük konvansiyonlara veya isim oyunlarına dayanmayan şekilde.

Önemli nokta: „sihir“ genelde RTTI’nin kendisinden değil, örtük kurallardan doğar. Eşleme kuralları açıkça özniteliklerde yer alırsa, dönüşümler merkezi olarak yönetilirse ve hatalar net bir neden belirtirse, RTTI sürpriz yerine bir araç haline gelir.

Neden Delphi’de RTTI tabanlı eşleme sık sık başarısız olur

RTTI tabanlı eşleme gerçek sistemlerde nadiren fikirde başarısız olur; sorun çoğunlukla yan koşullardır:

  • Legacy veri biçimleri: Null/Empty/0 düzgün ayrılmamış, alan tipleri değişiyor, stringler „N/A“ içeriyor.
  • Sinsi yayılan konvansiyonlar: „Alan, Property ile aynı isimde“ kuralı ilk alias, join veya refaktörize edilmiş Property adıyla bozulur.
  • Debuglanması zor: Eğer bir eşleyici „basitçe hiçbir şey atamazsa“, sonrasında nedenini bulmak zor olur. İşletmede bu zehirdir.
  • Performans efsaneleri: RTTI genelde topluca „yavaş“ olarak damgalanır; oysa çoğunlukla sorun eksik önbelleklemedir.

Bu nedenle sürdürülebilir bir yaklaşım (1) açık eşleme meta verilerine sahip olmalı, (2) dönüşüm ve null semantiğini net ele almalı, (3) hata ve debug çıktıları sağlamalı ve (4) RTTI bilgilerini önbelleğe almalıdır.

Delphi RTTI ile sihirsiz eşleme: Tasarım ilkeleri

Aşağıdaki desen kasıtlı olarak en iyi anlamıyla „sıkıcı“: kurallar görünürdür, yan etkiler sınırlıdır ve mevcut modüllere adım adım entegre edilebilir.

  • İsim konvansiyonu yerine öznitelikler: Property, kaynak sütunu belirten bir özniteliğe sahip olur.
  • Opt-in: Sadece işaretlenmiş Property’ler atanır. „Tüm published Property’ler“ yüzünden sürpriz olmaz.
  • Dönüştürme tek bir yerde: Variant/String/Integer/Boolean/Enum/Nullable merkezi olarak eşlenir.
  • Debug modu: İsteğe bağlı olarak hangi alanların atandığı/atlandığı sebepleriyle birlikte kaydedilir.
  • RTTI önbellekleme: En maliyetli parçalar (Propertyliste, Attributeauswertung) her tip için önceden hazırlanır.

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

Bu kod parçası bir satırı (ör. BDE-yerel bağlamayla değişim üzerinden TDataSet) bir nesneye eşler. Mapper’ı TField’a sıkı sıkıya bağlamak yerine küçük bir Reader arayüzü kullanıyoruz. Bu pratikte değerlidir, çünkü aynı mantığı daha sonra JSON, INI, CSV veya API-Responses için de kullanabilirsiniz.

Delphi
unit RttiMapping;

interface

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

type
  // Açık eşleme: Property <- Kaynak adı
  MapFromAttribute = class(TCustomAttribute)
  private
    FName: string;
  public
    constructor Create(const AName: string);
    property Name: string read FName;
  end;

  // Küçük soyutlama: Değer sağlama ve varlık/NULL ayrımı
  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: Yalnızca attribute sahibi Property'ler
        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 dönüştürmesi başarısız oldu: "%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 ordinali aralık dışında: %d', [Ord]);
    Exit(Ord);
  end;

  Name := VarToStr(V);
  Ord := GetEnumValue(AEnumType.Handle, Name);
  if Ord < 0 then
    raise ERttiMappingError.CreateFmt('Enum adı bilinmiyor: "%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;

  // Dönüştürme kasıtlı olarak seçici: sessizce "bir şekilde" başarısız olmaktansa açıkça hata vermek tercih edilir.
  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 eşlemesi uygulanmadı: %s için', [AProp.Name]);

    tkClass:
      raise ERttiMappingError.CreateFmt('Sınıf-Property eşlemesi uygulanmadı: %s için', [AProp.Name]);
  else
    raise ERttiMappingError.CreateFmt('TypeKind desteklenmiyor (%s) için %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 veya Target 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('Kaynak eksik: "%s" Property %s için',
          [M.SourceName, M.Prop.Name]);
      Continue;
    end;

    if AReader.IsNull(M.SourceName) then
    begin
      if moIgnoreNull in AOptions then
        Continue;
      // Nullable/Optional mekanizması olmadan NULL anlamlı şekilde atanamaz.
      Continue;
    end;

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

    try
      SetPropertyValue(ATarget, M.Prop, V);
      if moDebug in AOptions then
      begin
        Msg := Format('Eşlendi %s <- %s (%s)', [M.Prop.Name, M.SourceName, VarTypeAsText(VarType(V))]);
        OutputDebugString(PChar(Msg));
      end;
    except
      on E: Exception do
        raise ERttiMappingError.CreateFmt('Eşleme hatası %s <- %s: %s',
          [M.Prop.Name, M.SourceName, E.Message]);
    end;
  end;
end;

end.

Bunun amacı

Kod incelemelerinde net biçimde değerlendirilebilecek bir eşleme elde edersiniz:

  • Her eşlenen özellik görsel olarak işaretlenir (öznitelik).
  • Dönüştürme merkezi bir yerde yapılır; bu sayede tutarlı ve test edilebilir olur.
  • Hata iletileri, hangi özellik ve hangi kaynağın etkilendiğini belirtir.
  • Bir debug modu gerektiğinde kanıt zincirini sağlar; üretim sürecinde breakpoint’lere ihtiyaç duymazsınız.

Sınır koşulları ve tipik tuzaklar

  • NULL semantiği: Kendi Nullable kavramınız (ör. Nullable<T> veya Option-Types) yoksa „NULL atama“ belirsizdir. Snippet’te NULL varsayılan olarak atlanır. Bu temkinli bir yaklaşımdır ve sessiz üstüne yazmaları önler.
  • TRttiContext yaşam süresi: Cache’i her tip için bir kez oluşturuyoruz ve ardından Context’i atıyoruz. Bu yaygındır. Önemli olan: Her alan ataması için yeni bir RTTI-Context oluşturmayın.
  • Threading: Cache Monitor aracılığıyla korunur. Yüksek paralellikteki eşlemelerde (ör. REST-Server) kilit rekabetini azaltmak için Cache’i başlatırken „warm“ oluşturup oluşturmadığınızı (Preload) ayrıca kontrol etmelisiniz.
  • PropertyType Kind: tkClass ve tkSet kasıtlı olarak uygulanmamıştır. İç içe nesneler için ya özyinelemeli olarak eşleyin (belirgin bir politika ile) ya da bilinçli olarak elle atayın.
  • Yerel ayar tuzakları: varDouble üzerinden VarAsType nispeten sağlamdır, ancak dizeler (örn. „1,23“ vs. „1.23“) yine de sorun oluşturur. Kaynaklarınız string döndürüyorsa, tanımlı bir Culture ile kendi ayrıştırıcınızı kullanmak genellikle daha iyidir.

FireDAC ve TDataSet için varyant: Mapper bağlaması yerine Reader-Adaptörü

BDE-Ablosung mit nativer Anbindung veya klasik VCL/Win32 uygulamalarında kaynak genellikle bir TDataSet olur. Mapper’ı TField‚e bağlamak yerine, IValueReader arayüzünü karşılayan bir Adapter yazın. Avantajı: Mapper veri erişiminden bağımsız kalır (veri erişimini daha sonra servisler veya bir REST-Server üzerine taşıyorsanız bu önemlidir).

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;

Böylece somut bir eşleme şöyle görünür:

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;

Hangi durumlarda bu yaklaşım işe yarar – hangi durumlarda yaramaz

Bu desen tipik olarak üç durumda uygundur:

  1. Aşamalı Modernizasyon: Veri erişimini hemen tamamen yeniden düzenlemeden domain nesneleri getirmek istiyorsunuz (mevcut uygulamalarda klasik olarak Delphi Modernizasyon sırasında).
  2. Arayüz kenarları: CSV-/Excel aktarımları, REST-payload’ları veya “karma” veri kaynakları sağlam dönüşüm ve iyi hata mesajları gerektirir.
  3. Takım içinde sürdürülebilirlik: Attribute’ler eşleme kurallarını görünür ve gözden geçirilebilir kılar; bu, büyük kod tabanlarında çok değerlidir.

Kullanım sınırları da nettir:

  • Karmaşık nesne grafikleri (Child-Collections, döngüsel referanslar) “otomagisch” şekilde eşlenmemelidir. Bu durumda açık kod veya ayrı bir Assembler/Factory deseni genellikle daha kararlıdır.
  • High-Throughput-Hotpaths (ör. Massendaten-ETL) kod-üretimli mapper’lerden veya elle optimize edilmiş eşlemeden daha çok fayda sağlar, RTTI önbelleğe alınmış olsa bile.
  • Nullable/Optional ayrı bir konudur. Gerçekten “mevcut değil”, “NULL” ve “Varsayılan” arasında ayrım yapmanız gerekiyorsa, bunu mapper içinde gizlemek yerine domain modelinde ifade etmelisiniz.

Mimari ve işletme açısından konumlandırma

Mimari açıdan bu mapper, veri temsili ile domain arasındaki sınırda yer alan bir altyapı bileşenidir. O temiz bir katmanı ortadan kaldırmaz, ama onu mümkün kılabilir: Veri erişimi (FireDAC, SQL, Views) pragmatik olmaya devam edebilir, domain ise tutarlı kalır. Çok katmanlı sistemlerde (genellikle Layer-3 Mimari olarak adlandırılır: UI, Domain/Services, Infrastruktur) mapper altyapıda yer alır ve UI formlarından ziyade servisler tarafından kullanılır.

İşletme açısından önemli: moDebug’ı üretim servislerinde kalıcı olarak etkinleştirmeyin; yalnızca hedefli olarak açın. Zor yeniden üretilebilen veri problemleri için yapılandırılabilir bir tanı yolu (konfigürasyon, özellik bayrağı) bulundurmak mantıklıdır. Aksi halde günlük hacminde patlama ve yan etkiler riski vardır.

Sonuç: RTTI evet, ama yalnızca belirgin çerçevelerle

Delphi RTTI ile sihirsiz Mapping iyi çalışır, eğer RTTI’yi deklaratif meta veriler için bir araç olarak kullanırsanız — örtük heuristiklere davet olarak değil. Attribute’lerin opt-in olması, merkezi dönüştürme, tip başına önbellek ve anlaşılır hata mesajları konuyu ‚anlaşılmaz’dan ‚işletilebilir’e taşır. Bu yaklaşım kasıtlı olarak evrensel değildir: iç içe geçmiş grafikler, katı null semantiği veya maksimum performans için ek yapı taşlarına ihtiyaç duyarsınız. Ancak Dataset/Legacy-Strukturen ile daha modern alan nesneleri arasında sağlam bir köprü olarak, birçok Delphi-kod tabanında modernizasyonu mümkün kılan tam da o pragmatik adımdır.

Eğer gelişmiş bir Delphi-uygulamasında şu anda eşleme sınırları, veri kalitesi veya kademeli modernizasyon nedeniyle takılı kalmışsanız, bunu birlikte temizce kurup mimarinize entegre edebiliriz: İletişime geçin.

Uzmanlık alanında, entegrasyonlar, veri akışları ve ileriye dönük geliştirme düzgün bir şekilde birlikte çalışması gerektiğinde Delphi RTTI Mapping ve Attribute Mapping Delphi de önemli bir rol oynar.

Projeyi veya modernizasyon girişimini Net-Base ile görüşün.

Gönderiyi paylaş

Bu gönderiyi doğrudan paylaş

LinkedIn, X, XING, Facebook, WhatsApp ve e-posta hemen kullanılabilir. Instagram için bağlantı ve kısa metni doğrudan hazırlıyoruz.

E-posta

Instagram yeni bir sekmede açılır. Bağlantı ve kısa metin önceden panoya kopyalanır.