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.
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:
tkClassvetkSetkası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üzerindenVarAsTypenispeten 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).
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:
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:
- Aşamalı Modernizasyon: Veri erişimini hemen tamamen yeniden düzenlemeden domain nesneleri getirmek istiyorsunuz (mevcut uygulamalarda klasik olarak Delphi Modernizasyon sırasında).
- Arayüz kenarları: CSV-/Excel aktarımları, REST-payload’ları veya “karma” veri kaynakları sağlam dönüşüm ve iyi hata mesajları gerektirir.
- 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.