Në sistemet e zhvilluara Delphi mapimi nga Dataset në objekt rrallë është rasti i pastër „një fushë = një pronësi“. Në softuerin individual të ndërmarrjeve hasni në kolonat alias nga Views, rezultate Join me emra fushash të dyfishtë, vlera „të zbrazëta“ si 0 ose ' ', fusha të tipizuara që sot sjellin VARCHAR dhe nesër INTEGER, dhe kolona që, varësisht nga dialogu i kërkimit, thjesht nuk janë të pranishme. Pikërisht aty dështojnë shumë mapper-a: Ose bëhen «magjikë» (dhe kështu të vështirë për debug), ose janë aq striktë sa edhe një fushë opsionale ndalon punën e sistemit.
Kjo copë kodi tregon një mapper pragmatik për Delphi, i cili qëllimisht nuk është një ORM, por adreson qartë skenarët kryesorë të legacy-së: zgjidhje unike të fushave, konvertim të kontrolluar, semantikë NULL, fusha opsionale dhe mesazhe gabimi të gjurmueshme. Ai përshtatet për Data-Access-Layer (DAL, pra një shtresë që kapsulon aksesin në të dhëna) ose për repository-patterns – dhe mund të kombinohet mirë me BDE-Ablosung me lidhje native (Delphis Datenzugriffsbibliothek für viele DBs).
Pse dështojnë mapimet standard në strukturat e vjetra
Disa shkaqe tipike nga driftsimi, që rrallë shihen në një ridizajn „të pastër“:
- Emra fushash të dykuptimta: Join sjell
IDnga disa tabela; në Dataset shfaqet siID,ID_1ose është riemëruar me alias në SQL. - Null-e semantike:
0do të thotë „i panjohur“,'1899-12-30'është „jo-datë“,' 'do të thotë „s’është i mbajtur“. - Tipa të ndryshueshëm: Një View nuk bën cast; driver-i jep
ftWideStringnë vend tëftInteger. Konvertimi i Variant bëhet burim gabimesh. - Kolona opsionale: Një dialog kërkimi përdor lista SELECT të ndryshme varësisht nga filter-at. Kodi pritet që fushat të jenë „përherë“ të pranishme.
- Debuggability: Kur mapimi zhduket në RTTI, gjetja e gabimeve me të dhënat e klientit bëhet e vështirë (cila fushë, cila vlerë, cili tip?).
Qasja: Plan mapimi në vend të konventës, me konvertim të kontrolluar
Bërthama është një Mapping-Plan: një listë rregullash „Pronësia X vjen nga fusha A ose B, është opsionale/obligative, përdor Konverter Y“. Me këtë qendron mapimi deklarativ, por jo „i padukshëm“ si tek shumë mekanizma ORM. Për më tepër, mapper-i mund të hedhë për çdo fushë një përjashtim të qartë, përfshirë emrin e fushës, tipin e të dhënave dhe vlerën e papërpunuar.
E rëndësishme: Ne mapojmë qëllimisht nga TDataSet, jo nga një klasë konkrete BDE-Ablosung mit nativer Anbindung. Kështu mbetet i kompatibilizuar me TFDQuery, TClientDataSet ose edhe me komponentë të jashtëm.
Source-Schnipsel: Debugbares Dataset-zu-Objekt Mapping für Legacy-Spalten
Kodi implementon:
- Zgjidhjen e fushës përmes një liste prioritetesh (alias/rezervë)
- Trajtimin Required/Optional
- Semantikën NULL përmes Konverterëve (p.sh.
0 => Null) - Mesazhe gabimi të qëndrueshme me kontekst
- Një hook debug për të ndjekur problemet e mapimit në testim ose në rast support-i
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);
// Konvertuesi merr Variant dhe rikthen Variant (p.sh. Null, Integer, String, TDateTime si Double)
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: thërret setter për çdo Spec. Pa RTTI: caktimi eksplicit është më i mirë për debug.
procedure MapOne(DS: TDataSet; const Specs: TArray<TFieldSpec>;
const Assign: TProc<string, Variant>);
end;
// Konvertuesit
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 := '<unprintable 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
// Përdor FindField në vend të FieldByName: mundësisht opsionale, pa hedhur Exception
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 nuk është aktiv.');
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('Gabim mapimi: Fusha e kërkuar për %s nuk u gjet. Kandidatët: [%s]',
[Spec.TargetMember, CandidatesJoined]));
end
else
Continue; // opsional: thjesht kapërce
end;
Raw := F.Value; // Variant; merr parasysh NULL
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: NULL pas konvertimit është gabim (më shpesh se sa mendohet)
if (Spec.Required = mrRequired) and VarIsNull(Val) then
begin
FT := FieldTypeToString(F.DataType);
raise EDataMappingError.Create(
Spec.TargetMember,
F.FieldName,
FT,
VariantToDiag(Raw),
Format('Gabim mapimi: %s është i detyrueshëm, por vlera është NULL pas konvertimit. Fusha %s (%s), vlera e papërpunuar=%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('Gabim mapimi tek %s nga fusha %s (%s), vlera e papërpunuar=%s: %s',
[Spec.TargetMember, F.FieldName, FT, VariantToDiag(Raw), E.Message]));
end;
end;
end;
end;
{ Konvertuesit }
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);
// toleron edhe '0' si String
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);
// Qëllimisht strikt: asnjë 'Try' që të mos fshehë cilësinë e të dhënave.
// Formati mund të ndryshojë sipas sistemit legacy; nëse duhet parametrizoni këtu me TFormatSettings.
D := ISO8601ToDate(S, False);
Result := D;
end;
end;
end.
Si të përdorni Mapper-in në praktikë (pa RTTI, por megjithatë në mënyrë elegante)
Mapper thërret një funksion callback Assign(TargetMember, Value). Kjo e bën caktimin eksplisit (dhe rrjedhimisht të lehtë për debug) dhe shmang akseset RTTI në hot-path. Në praktikë ndërtoni për çdo objekt/DTO (Data Transfer Object, pra një objekt transporti për të dhëna) një të vogël „caktues“.
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;Qëllimi: Mapimi përshkruhet qendrorisht në një vend (Specs), por caktimi mbetet eksplizit. Në situata Legacy kjo zakonisht është vendimi më i mirë kompromis sesa një mapim plotësisht automatik RTTI, sepse shihni menjëherë se cila Property varet nga cilët emra fushe.
Kushtet paraprake: Qasja pret një DataSet aktiv dhe një pozicion rekordi aktual. Për importet batch iteroni në pjesën e jashtme me while not DS.Eof do dhe thërrisni MapCustomer për çdo rresht.
Rreziqet: Bëni kujdes me VarToStr për BLOB-et ose fushat Memo; aty duhet të përdorni konvertues të vet. Dhe: „Required“ këtu do të thotë pas konvertuesit. Nëse C_TrimToNull vë një fushë Required në Null, kjo është me qëllim – cilësia e të dhënave duhet të zgjidhet në burim ose në proces.
Varianta: Në vend të Target-eve si string mund të përdorni një Enum për të eliminuar gabimet në shtyp. Si alternativë funksioni Assign mund të ruhet për çdo Spec si TProc<Variant>, atëherë hiqet plotësisht Target-String (pak më shumë boilerplate, por më pak mundësi për gabime).
Renditja në arkitekturë: DAL/Repository, Logging und operimi
Në një arkitekturë me shtresa (tipike: UI – Business – Qasje në të dhëna) ky mapim i përket shtresës së qasjes së të dhënave ose një Repository. E rëndësishme është që DataSet-i të mos „përcillet“ përpara: Objektet/DTO-t janë ndërfaqja më e qëndrueshme, veçanërisht nëse më vonë shtoni API-të REST ose delegoni pjesë te C# shërbime.
Për operim dhe support ia vlen Debug-Hook OnDebug. Me të mund të regjistroni në teste ose në raste suporti të riprodhueshëm cilat fusha janë mapuar vërtetë. Në sistemet prodhuese kjo duhet të jetë e synuar dhe e fikshme, përndryshe regjistrimi mund të bëhet i shtrenjtë ose tepër i ngarkuar me të dhëna.
Përdorimi i Debug-Hook me mençuri
- Unit-Tests: Kontrolloni nëse një deklaratë SQL e caktuar me të vërtetë kthen të gjitha fushat e kërkuara.
- Diagnostikë: Në rastet e problemeve me klientin shihni menjëherë „Fusha nuk ishte aty“ vs. „Vlera nuk mund të konvertohej“.
- Fazat e migrimit: Kur ndryshoni Views/emra kolone, mund të mbani paralelisht lista kandidatësh derisa të përfundojë migrimi.
Kur dështon kjo qasje (dhe çfarë është më mirë atëherë)
Mapimi i treguar Dataset-te-Objekt është i fuqishëm kur burimi i të dhënave është i paqëndrueshëm dhe ju përsëri keni nevojë për sjellje deterministike. Ai dështon zakonisht në dy situata:
- Sasi shumë të mëdha (p.sh. eksport masiv): Konvertimi i Variant dhe kërkimi sipas emrit të fushës mund të bëhet i ndjeshëm. Atëherë ia vlen një caching i paraperllogaritur të indekseve të fushave për secilin SQL (p.sh.
FieldByNamenjëherë për Dataset, jo për rresht). - Shumë tipe DTO: Nëse shkruani qindra mapper, boilerplate bëhet problem. Atëherë një qasje e bazuar në RTTI me atribute mund të jetë e përshtatshme – por vetëm nëse kontrolloni në mënyrë rigoroze daljet e debug-ut dhe konvertuesit.
Një mesvlerë e mirë është: zgjidhja e fushave dhe konvertimi si këtu (eksplizit, tolerant ndaj gabimeve ku nevojitet), por me kod të gjeneruar (p.sh. përmes template-ve të brendshme) në vend të „të shkruarit me dorë“.
Përfundim: Stabilitet përmes rregullave eksplizite – me kufizime të qarta të përdorimit
Tek Legacy-Dataset-et me Aliases, kolona opsionale dhe semantikë historike të Null-it, mapimi Dataset-te-Objekt funksionon kryesisht kur mbetet eksplizit dhe i diagnostikueshëm. Plani i mapping-ut me lista kandidatësh, Required/Optional dhe konvertues krijon pikërisht këtë: mund të stabilizoni mbetjet historike hap pas hapi, pa futur menjëherë një ORM ose pa normalizuar bazën e të dhënave „njëherësh“.
Kufizimet shfaqen te performanca ekstreme dhe te numri shumë i madh i tipeve – atëherë ju duhet caching ose gjenerim i automatizuar i kodit. Për softuerin tipik të biznesit me procese të zhvilluara, kjo qasje megjithatë është një mjet i besueshëm për t’i bërë aksesin në të dhëna dhe modelet e domenit përsëri të ndara dhe të mirëmbajtshme.
Nëse te një mapping konkret Legacy (FireDAC, Views, proliferimi i JOIN-eve, Null-Semantik) ju duhet një opinion i dytë ose një arkitekturë target e besueshme, hapi i ardhshëm zakonisht është një analizë e shkurtër me shembuj të riprodhueshëm. Kontakt:
Në kontekstin profesional luajnë gjithashtu një rol të rëndësishëm Delphi Dataset Mapping dhe Legacy Delphi kur integrimet, rrjedhat e të dhënave dhe zhvillimi i mëtejshëm duhet të punojnë së bashku në mënyrë të pastër.
Diskutoni projektin ose përpjekjen e modernizimit me Net-Base.