Net-Base Revistë

10.05.2026

Mapimi i dataset-it në objekt për struktura të vjetra jo‑standarde: i qëndrueshëm, i debugueshëm, pa magji të ORM

Kur setet e të dhënave të trashëguara janë formuar historikisht, mappuesit standardë shpesh dështojnë përballë kolonave me alias, përzierjeve të tipeve dhe strukturave të ndryshueshme të JOIN-eve. Ky fragment i kodit burimor tregon një mapim robust, të debugueshëm nga seti i të dhënave në objekt në Delphi: me plan mapimi, konvertues, semantikë për NULL...

10.05.2026

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 ID nga disa tabela; në Dataset shfaqet si ID, ID_1 ose është riemëruar me alias në SQL.
  • Null-e semantike: 0 do 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 ftWideString në 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
Delphi
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“.

Delphi
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. FieldByName një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.

Ndaje postimin

Shpërndaj këtë postim drejtpërdrejt

LinkedIn, X, XING, Facebook, WhatsApp dhe E‑Mail janë menjëherë të disponueshme. Për Instagram po përgatitim menjëherë lidhjen dhe tekstin e shkurtër.

Postë elektronike

Instagram hapet në një skedë të re. Linku dhe teksti i shkurtër kopjohen më parë në memorjen e kopjimit.