Bei gewachsenen Delphi-Systemen ist Dataset’ten Nesneye Eşleme nadiren temiz „bir alan = bir property“ durumu. Bireysel kurumsal yazılımlarda bunun yerine View’lerden gelen alias sütunlar, aynı alan adlarına sahip join sonuçları, 0 veya ' ' olarak gelen „boş“ değerler, bugün VARCHAR yarın INTEGER döndürebilen tiplenmiş alanlar ve arama diyaloğuna bağlı olarak hiç yer almayan sütunlarla karşılaşırsınız. Tam da burada birçok mapper hata verir: Ya çok „magik“leşirler (ve bu nedenle debuglamak zorlaşır), ya da öylesine katı olurlar ki tek bir isteğe bağlı alan bile işletmeyi durdurur.
Dieser Source-Schnipsel zeigt einen pragmatischen Mapper für Delphi, der bewusst kein ORM ist, aber die wichtigsten Legacy-Randfälle sauber adressiert: eindeutige Feldauflösung, kontrollierte Konvertierung, Null-Semantik, optionale Felder und nachvollziehbare Fehlermeldungen. Er eignet sich für Data-Access-Layer (DAL, also eine Schicht, die Datenzugriff kapselt) oder Repository-Patterns – und lässt sich gut mit BDE-Ablosung mit nativer Anbindung (Delphis Datenzugriffsbibliothek für viele DBs) kombinieren.
Neden standart eşleme eski yapılarda başarısız olur
„Temiz“ bir yeniden tasarımda nadiren görülen, işletmede tipik birkaç sebep:
- Çok anlamlı alan adları: Join birden fazla tablodan
IDgetirir; Dataset’te bu genellikleID,ID_1olur veya SQL takma adıyla yeniden adlandırılır. - Semantiksel Null’lar:
0„bilinmiyor“ demektir,'1899-12-30'„tarih değil“,' '„girilmemiş“ anlamına gelir. - Değişken tipler: Bir View cast yapmaz; sürücü
ftWideStringyerineftIntegerdöndürüyor olabilir. Variant dönüşümü hata kaynağı haline gelir. - Opsiyonel sütunlar: Bir arama diyaloğu filtreye bağlı olarak farklı SELECT listeleri kullanır. Kod ise alanları „her zaman“ bekler.
- Hata ayıklanabilirlik: Eşleme RTTI içinde kaybolduğunda, müşteri verilerinde hata bulmak zorlaşır (hangi alan, hangi değer, hangi tip?).
Yaklaşım: Eşleme Planı yerine Konvansiyon değil, kontrollü dönüştürme ile
Özünde bir Eşleme Planı var: „Property X alan A veya B’den gelir, opsiyonel/gerekli, dönüştürücü Y kullanır“ şeklinde bir kural listesi. Böylece eşleme deklaratif kalır ama birçok ORM mekanizmasında olduğu gibi „görünmez“ olmaz. Ayrıca mapper her alan için alan adı, veri tipi ve ham değer dahil açıklayıcı bir istisna atabilir.
Önemli: Biz kasıtlı olarak TDataSet üzerinden eşleme yapıyoruz, belirli bir BDE-Ablosung mit nativer Anbindung sınıfından değil. Bu sayede TFDQuery, TClientDataSet veya üçüncü taraf bileşenlerle uyumluluk korunur.
Kaynak-Schnipsel: Legacy sütunlar için hata ayıklanabilir Dataset’ten Nesneye Eşleme
Kod şu özellikleri uygular:
- Alan çözümü için bir öncelik listesi (Alias’ler / Fallback’ler)
- Gerekli / Opsiyonel işleme
- Konvertör üzerinden null semantiği (ör.
0 => Null) - Kontekst içeren istikrarlı hata mesajları
- Test veya destek durumunda eşleme problemlerini izleyebilmek için bir debug-hook
unit Legacy.DatasetMapper;
interface
uses
System.SysUtils, System.Variants, System.Generics.Collections, Data.DB;
type
EDataMappingError = class(Exception)
private
FFieldNames: string;
FTarget: string;
FDataType: string;
FRawValue: string;
public
constructor Create(const ATarget, AFieldNames, ADataType, ARawValue, AMsg: string);
property Target: string read FTarget;
property FieldNames: string read FFieldNames;
property DataType: string read FDataType;
property RawValue: string read FRawValue;
end;
TMapRequired = (mrOptional, mrRequired);
TMapDebugEvent = reference to procedure(
const TargetMember: string;
const SourceField: string;
const SourceType: TFieldType;
const SourceValue: Variant);
// Konvertör Variant alır ve Variant döndürür (örn. Null, Integer, String, TDateTime double olarak)
TFieldConverter = reference to function(const V: Variant): Variant;
TFieldSpec = record
TargetMember: string;
SourceCandidates: TArray<string>;
Required: TMapRequired;
Converter: TFieldConverter;
class function Create(const ATarget: string; const ACandidates: array of string;
ARequired: TMapRequired; const AConverter: TFieldConverter): TFieldSpec; static;
end;
TLegacyDatasetMapper = class
private
FOnDebug: TMapDebugEvent;
function FindFieldByCandidates(DS: TDataSet; const Candidates: TArray<string>): TField;
function FieldTypeToString(FT: TFieldType): string;
function VariantToDiag(const V: Variant): string;
public
property OnDebug: TMapDebugEvent read FOnDebug write FOnDebug;
// MapOne: her Spec için setter'ı çağırır. RTTI yok: açık atama daha iyi hata ayıklanır.
procedure MapOne(DS: TDataSet; const Specs: TArray<TFieldSpec>;
const Assign: TProc<string, Variant>);
end;
// Yardımcı konvertörler
function C_TrimToNull: TFieldConverter;
function C_ZeroToNull: TFieldConverter;
function C_StrictInt: TFieldConverter;
function C_DateFromStringOrNull: TFieldConverter;
implementation
{ EDataMappingError }
constructor EDataMappingError.Create(const ATarget, AFieldNames, ADataType, ARawValue, AMsg: string);
begin
inherited Create(AMsg);
FTarget := ATarget;
FFieldNames := AFieldNames;
FDataType := ADataType;
FRawValue := ARawValue;
end;
{ TFieldSpec }
class function TFieldSpec.Create(const ATarget: string; const ACandidates: array of string;
ARequired: TMapRequired; const AConverter: TFieldConverter): TFieldSpec;
var
I: Integer;
begin
Result.TargetMember := ATarget;
SetLength(Result.SourceCandidates, Length(ACandidates));
for I := 0 to High(ACandidates) do
Result.SourceCandidates[I] := ACandidates[I];
Result.Required := ARequired;
Result.Converter := AConverter;
end;
{ TLegacyDatasetMapper }
function TLegacyDatasetMapper.FieldTypeToString(FT: TFieldType): string;
begin
Result := GetEnumName(TypeInfo(TFieldType), Ord(FT));
end;
function TLegacyDatasetMapper.VariantToDiag(const V: Variant): string;
begin
if VarIsNull(V) then Exit('NULL');
if VarIsEmpty(V) then Exit('EMPTY');
try
Result := VarToStr(V);
except
Result := '<yazdırılamayan variant>';
end;
end;
function TLegacyDatasetMapper.FindFieldByCandidates(DS: TDataSet; const Candidates: TArray<string>): TField;
var
Name: string;
begin
Result := nil;
for Name in Candidates do
begin
// FieldByName yerine FindField: isteğe bağlı olarak kullanılabilir, istisna atmaz
Result := DS.FindField(Name);
if Result <> nil then
Exit;
end;
end;
procedure TLegacyDatasetMapper.MapOne(DS: TDataSet; const Specs: TArray<TFieldSpec>;
const Assign: TProc<string, Variant>);
var
Spec: TFieldSpec;
F: TField;
Raw, Val: Variant;
CandidatesJoined: string;
I: Integer;
FT: string;
begin
if (DS = nil) then
raise EArgumentNilException.Create('DS');
if not DS.Active then
raise EInvalidOperation.Create('Dataset etkin değil.');
for Spec in Specs do
begin
F := FindFieldByCandidates(DS, Spec.SourceCandidates);
if (F = nil) then
begin
if Spec.Required = mrRequired then
begin
CandidatesJoined := '';
for I := 0 to High(Spec.SourceCandidates) do
begin
if I > 0 then CandidatesJoined := CandidatesJoined + ', ';
CandidatesJoined := CandidatesJoined + Spec.SourceCandidates[I];
end;
raise EDataMappingError.Create(
Spec.TargetMember,
CandidatesJoined,
'n/a',
'n/a',
Format('Mapping hatası: %s için Required alan bulunamadı. Adaylar: [%s]',
[Spec.TargetMember, CandidatesJoined]));
end
else
Continue; // isteğe bağlı: doğrudan atla
end;
Raw := F.Value; // Variant; Null'ı dikkate alır
if Assigned(FOnDebug) then
FOnDebug(Spec.TargetMember, F.FieldName, F.DataType, Raw);
try
if Assigned(Spec.Converter) then
Val := Spec.Converter(Raw)
else
Val := Raw;
// Required: Konvertörden sonra Null olmak bir hatadır (düşündüğünüzden daha sık rastlanır)
if (Spec.Required = mrRequired) and VarIsNull(Val) then
begin
FT := FieldTypeToString(F.DataType);
raise EDataMappingError.Create(
Spec.TargetMember,
F.FieldName,
FT,
VariantToDiag(Raw),
Format('Mapping hatası: %s Required, fakat değer dönüşüm sonrası NULL. Alan %s (%s), ham değer=%s',
[Spec.TargetMember, F.FieldName, FT, VariantToDiag(Raw)]));
end;
Assign(Spec.TargetMember, Val);
except
on E: EDataMappingError do
raise;
on E: Exception do
begin
FT := FieldTypeToString(F.DataType);
raise EDataMappingError.Create(
Spec.TargetMember,
F.FieldName,
FT,
VariantToDiag(Raw),
Format('Mapping hatası: %s için %s alanından (%s) değer eşlenirken, ham değer=%s: %s',
[Spec.TargetMember, F.FieldName, FT, VariantToDiag(Raw), E.Message]));
end;
end;
end;
end;
{ Konverter }
function C_TrimToNull: TFieldConverter;
begin
Result := function(const V: Variant): Variant
var
S: string;
begin
if VarIsNull(V) or VarIsEmpty(V) then Exit(Null);
S := Trim(VarToStr(V));
if S = '' then
Result := Null
else
Result := S;
end;
end;
function C_ZeroToNull: TFieldConverter;
begin
Result := function(const V: Variant): Variant
var
N: Int64;
begin
if VarIsNull(V) or VarIsEmpty(V) then Exit(Null);
// Ayrıca '0' string'ini de tolere eder
N := StrToInt64(Trim(VarToStr(V)));
if N = 0 then
Result := Null
else
Result := N;
end;
end;
function C_StrictInt: TFieldConverter;
begin
Result := function(const V: Variant): Variant
begin
if VarIsNull(V) or VarIsEmpty(V) then Exit(Null);
Result := StrToInt(Trim(VarToStr(V)));
end;
end;
function C_DateFromStringOrNull: TFieldConverter;
begin
Result := function(const V: Variant): Variant
var
S: string;
D: TDateTime;
begin
if VarIsNull(V) or VarIsEmpty(V) then Exit(Null);
S := Trim(VarToStr(V));
if (S = '') or (S = '1899-12-30') then Exit(Null);
// Kasıtlı olarak katı: 'Try' veri kalitesini örtmesin.
// Format legacy'e göre değişebilir; gerekirse burada TFormatSettings ile parametreleyin.
D := ISO8601ToDate(S, False);
Result := D;
end;
end;
end.
Mapper’ı pratikte kullanma (RTTI olmadan, fakat yine de zarif)
Mapper bir Assign(TargetMember, Value)-callback fonksiyonunu çağırır. Bu, atamayı açık tutar (dolayısıyla iyi debuglanabilir) ve hot-path’te RTTI erişimlerini önler. Pratikte her nesne/DTO (Data Transfer Object, yani veri taşıma nesnesi) için küçük bir „atayıcı“ oluşturursunuz.
type
TCustomer = class
public
Id: Integer;
ExternalNo: string;
DisplayName: string;
BirthDate: TDateTime; // optional in Legacy
end;
function MapCustomer(DS: TDataSet; Mapper: TLegacyDatasetMapper): TCustomer;
var
C: TCustomer;
Specs: TArray<TFieldSpec>;
begin
C := TCustomer.Create;
try
Specs := [
TFieldSpec.Create('Id', ['CUSTOMER_ID', 'ID', 'C_ID'], mrRequired, C_StrictInt),
TFieldSpec.Create('ExternalNo', ['EXT_NO', 'CUSTOMERNO'], mrOptional, C_TrimToNull),
TFieldSpec.Create('DisplayName', ['NAME', 'DISPLAYNAME', 'C_NAME'], mrRequired, C_TrimToNull),
TFieldSpec.Create('BirthDate', ['BIRTHDATE', 'DOB'], mrOptional, C_DateFromStringOrNull)
];
Mapper.MapOne(DS, Specs,
procedure(const Target: string; const V: Variant)
begin
if Target = 'Id' then
C.Id := V
else if Target = 'ExternalNo' then
C.ExternalNo := VarToStrDef(V, '')
else if Target = 'DisplayName' then
C.DisplayName := VarToStr(V)
else if Target = 'BirthDate' then
begin
if VarIsNull(V) then
C.BirthDate := 0
else
C.BirthDate := V;
end
else
raise EInvalidOperation.Create('Unbekanntes TargetMember: ' + Target);
end);
Result := C;
except
C.Free;
raise;
end;
end;Amaç: Mapping tek bir yerde merkezi olarak tanımlanır (Specs), ancak atama açık kalır. Legacy durumlarda bu genellikle tam otomatik RTTI-mapping’e göre daha iyi bir trade-off kararıdır; çünkü hangi özelliğin hangi alan adlarına bağlı olduğunu hemen görürsünüz.
Önkoşullar: Yaklaşım aktif bir Dataset ve güncel bir record pozisyonu bekler. Toplu içe aktarımlar için dışta while not DS.Eof do ile iterasyon yapın ve her satır için MapCustomer çağırın.
Dikkat Edilmesi Gerekenler: BLOB veya memo alanlarda VarToStr‚e dikkat edin; bu durumlarda kendi dönüştürücülerinizi kullanmalısınız. Ve: burada „Required“, dönüştürücü sonrası anlamına gelir. Eğer C_TrimToNull bir Required alanı null yapıyorsa, bu kasten böyledir — veri kalitesi kaynağında veya süreçte ele alınmalıdır.
Varyantlar: String-target’lar yerine yazım hatalarını engellemek için bir Enum kullanabilirsiniz. Alternatif olarak Assign fonksiyonunu her Spec için bir TProc<Variant> olarak saklayabilirsiniz; böylece Target-string tamamen ortadan kalkar (biraz daha fazla boilerplate, ancak hata olasılığı daha da düşer).
Mimariye dahil etme: DAL/Repository, Logging ve İşletme
Katmanlı bir mimaride (tipik: UI – Business – Veri erişimi) bu mapping veri erişim katmanına veya bir repository’ye ait olmalıdır. Önemli olan dataset’in „iletim“ yapılmamasıdır: Nesneler/DTO’lar daha stabil bir arayüzdür, özellikle ileride REST-API’leri eklemek veya parçaları C# Services olarak dışa taşımak istediğinizde.
İşletme ve destek için Debug-Hook OnDebug faydalıdır. Testlerde veya yeniden üretilebilir destek vakalarında hangi alanların gerçekten eşlendiğini günlüğe kaydedebilirsiniz. Üretim sistemlerinde bunun hedefli ve kapatılabilir olması gerekir; aksi halde günlükleme maliyetli ya da veri açısından ağır hale gelir.
Debug-Hook’u verimli kullanma
- Unit-Tests: Belirli bir SQL ifadesinin gerçekten tüm Required alanları sağlayıp sağlamadığını kontrol edin.
- Diagnose: Müşteri sorunlarında hemen “Alan yoktu” ile “Değer dönüştürülemedi” durumlarını ayırt edebilirsiniz.
- Migrationsphasen: View/ sütun adlarını değiştirirken her şey taşınana kadar aday listelerini paralel olarak sürdürebilirsiniz.
Bu yaklaşım ne zaman yetersiz kalır (ve o durumda ne daha uygundur)
Gösterilen Dataset‑to‑Object eşlemesi, veri kaynağı değişken olduğunda ve yine de deterministik davranış gerektiğinde güçlüdür. Tipik olarak iki durumda yetersiz kalır:
- Çok büyük hacimler (örn. toplu dışa aktarma): Variant dönüştürme ve alan adına göre arama hissedilir maliyet getirebilir. Bu durumda her SQL için önceden hesaplanmış alan indeks önbelleklemesi (ör.
FieldByNamedataset başına bir kez, satır başına değil) faydalıdır. - Çok sayıda DTO tipi: Yüzlerce mapper yazmanız durumunda boilerplate artar. O zaman RTTI tabanlı bir yaklaşım özniteliklerle mantıklı olabilir — fakat yalnızca debug çıktıları ve dönüştürücüleri sıkı şekilde kontrol ediyorsanız.
İyi bir ara yol şudur: Alan çözümü ve dönüştürme burada olduğu gibi (açık, gerektiğinde hata toleranslı) yapılır, ancak “elle yazılmış” yerine üretilmiş kod (ör. dahili şablonlar aracılığıyla) kullanılır.
Sonuç: Açık kurallarla kararlılık – belirgin kullanım sınırlarıyla
Alias’lı, isteğe bağlı sütunlara ve tarihsel null‑semantiğe sahip legacy datasetlerde Dataset‑to‑Object eşleme özellikle açık ve tanılama yapılabilir kaldığında başarılı olur. Aday listeleri, Required/Optional ve dönüştürücülerden oluşan eşleme planı tam olarak bunu sağlar: Eski yükleri kademeli olarak stabil hale getirebilirsiniz, ORM’yi hemen devreye almak ya da veritabanını “bir kerede” normalleştirmek zorunda kalmazsınız.
Sınırlar ekstrem performans gereksinimlerinde ve çok sayıda tip olduğunda ortaya çıkar — o zaman önbellekleme veya otomatik kod üretimine ihtiyaç duyarsınız. Ancak olgun süreçlere sahip tipik iş yazılımları için bu yaklaşım, veri erişimi ile alan modellerini yeniden ayrık ve sürdürülebilir hale getirmek için güvenilir bir araçtır.
Eğer belirli bir legacy‑eşlemede (FireDAC, Views, Join‑karmaşası, Null‑Semantik) ikinci bir görüşe veya sağlam bir hedef mimariye ihtiyacınız varsa, bir sonraki adım genellikle tekrarlanabilir örneklerle kısa bir analizdir. İletişim:
Uzmanlık alanında Delphi Dataset Mapping ve Legacy Delphi da entegrasyonlar, veri akışları ve devam eden geliştirme temiz bir şekilde birlikte çalışması gerektiğinde önemli bir rol oynar.