Net-Base Iris

10.05.2026

Mapaíocht tacar sonraí go réad do struchtúir seanré neamhghnácha: cobhsaí, éasca le dífhabhtú, gan draíocht ORM

Nuair a bhíonn sraitheanna sonraí seanaimseartha forbartha go stairiúil, briseann mapálaithe caighdeánacha go minic de bharr colún aliás, meascán cineálacha agus struchtúir join atá ag athrú. Léiríonn an sliocht cód seo mapáil chobhsaí, atá éasca le dífhabhtú, ó shraith sonraí go réad i Delphi: le plean mapála, comhshóiteoirí agus seamántacht nialach...

10.05.2026

I gcás córais atá fásaithe Delphi is annamh a bhíonn an mapáil ó Dataset go Réad an chás glan „réimse amháin = propairtí amháin“. I mbogearraí ghnólachtaí shaincheaptha teachtann tú ina ionad sin ar cholúin alias ó viewanna, torthaí Join le hainmneacha réimse dúbailte, luachanna „folamh“ mar 0' ', réimsí cláraithe a sheachadtar mar VARCHAR inniu agus INTEGER amárach, agus colúin nach mbíonn ann de réir dialóige cuardaigh. Tá sé dóthain áit ina dtéann go leor mappálaithe ar fud na mbealaí: bíonn siad ró-„dhraíochtúil“ (agus mar sin deacair le dífhabhtú), nó tá siad chomh docht gur stopfaidh réimse roghnach amháin an tseirbhís.

Tá an sliogán cód seo ina mappálaí praiticiúil do Delphi, ar intinn dó nach bhfuil sé ina ORM, ach a dhíríonn go soiléir ar na príomhchásanna imeall Legacy: réiteach réimsí gan chonspóid, tiontú rialaithe, seimantacht Null, réimsí roghnacha agus teachtaireachtaí earráide intuigthe. Oireann sé do Data-Access-Layer (DAL, sraith a chuimsíonn rochtain sonraí) nó patrúin Repository – agus is féidir é a chomhcheangal go maith le athsholáthar BDE le nasc dúchais (leabharlann rochtana sonraí Delphi do iliomad DBanna).

Cén fáth a theipeann ar mhappáil chaighdeánach i struchtúir sheana

Cúiseanna tipiciúla ó oibriú a fhaightear i gcórais sheana a bhíonn ann nach bhfeictear go minic i ndearadh nua, “glan”:

  • Ainmneacha réimse amhrasach: Soláthraíonn JOIN an ID ó iliáin táblaí éagsúla; sa Dataset glaotar é ansin ID, ID_1 nó tá sé athainmnithe trí alias SQL.
  • Nullaí seimanta: 0 ciallaíonn “anaithnid”, '1899-12-30' is “gan dáta”, ' ' is “gan líonadh”.
  • Cineálacha atá ag athrú: Ní dhéanann view castáil; soláthraíonn an tiománaí ftWideString in ionad ftInteger. Éiríonn tiontú Variant ina foinse earráide.
  • Colúin roghnacha: Úsáideann dialóg cuardaigh liostaí SELECT éagsúla de réir scagaire. Tá an cód ag súil leis na réimsí “i gcónaí”.
  • Inrochtana don dhífhabhtú: Má éalaíonn an mappáil isteach i RTTI, bíonn sé deacair earráide a aimsiú i sonraí custaiméara (cén réimse, cén luach, cén cineál?).

Cur chuige: Plean mappála in ionad coinbhinsiún, le tiontú rialaithe

Is é an croíphointe ná Plean Mappála: liosta rialacha “Tagann Property X ó réimse A nó B, tá sé roghnach/riachtanach, úsáideann sé tiontaire Y”. Coinníonn sé an mappáil dearbhaitheach ach ní “dothuigthe” mar a bhíonn i go leor meicníochtaí ORM. Ina theannta sin, is féidir leis an mappálaí eisceacht léiritheach a ardú in aghaidh gach réimse, lena n-áirítear ainm réimse, cineál sonraí agus an luach amh.

Tábhachtach: déanaimid mapáil go sainráite ó TDataSet, ní ó rang sonrach BDE-Ablosung mit nativer Anbindung. Coinníonn sé sin comhoiriúnacht le TFDQuery, TClientDataSet nó comhpháirteanna tríú páirtí.

Sliogán cód: Mappáil Dataset-go-Réad inléite le haghaidh colún Legacy

Cuireann an cód i bhfeidhm:

  • Réiteach réimsí trí liosta tosaíochta (aliasí/fallbackanna)
  • Láimhseáil Riachtanach/Roghnach
  • Seimantacht Null trí thiontaire (m.sh. 0 => Null)
  • Teachtaireachtaí earráide seasta le comhthéacs
  • Hook dífhabhtaithe chun fadhbanna mappála a leanúint i dtástáil nó i gcás tacaíochta
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);

  // Konverter erhält Variant und liefert Variant (z. B. Null, Integer, String, TDateTime als 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: ruft Setter für jede Spec auf. Kein RTTI: explizite Zuweisung ist besser debugbar.
    procedure MapOne(DS: TDataSet; const Specs: TArray<TFieldSpec>;
      const Assign: TProc<string, Variant>);
  end;

// Hilfs-Konverter
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
    // FindField statt FieldByName: optional möglich, ohne 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 ist nicht 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('Mapping-Fehler: Required-Feld für %s nicht gefunden. Kandidaten: [%s]',
            [Spec.TargetMember, CandidatesJoined]));
      end
      else
        Continue; // optional: schlicht überspringen
    end;

    Raw := F.Value; // Variant; berücksichtigt 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 nach Konverter ist ein Fehler (häufiger als man denkt)
      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-Fehler: %s ist Required, aber Wert ist NULL nach Konvertierung. Feld %s (%s), Rohwert=%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-Fehler bei %s aus Feld %s (%s), Rohwert=%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);
    // toleriert auch '0' als 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);

    // Absichtlich strikt: kein "Try" verschluckt Datenqualität.
    // Format kann je nach Legacy variieren; ggf. hier über TFormatSettings parametrieren.
    D := ISO8601ToDate(S, False);
    Result := D;
  end;
end;

end.

Conas an Mapper a úsáid go praiticiúil (gan RTTI, ach fós go galánta)

Glaonn an Mapper ar fheidhm chúlghlao Assign(TargetMember, Value). Coinníonn sé sin an t-aistriú soiléir (agus dá bharr sin inmharthana le haghaidh dífhabhtaithe) agus seachnaíonn sé rochtain RTTI sa hot-path. Sa chleachtas, tógann tú do “Zuweiser” beag don réad/DTO (Data Transfer Object, is é sin réad iompar sonraí) in aghaidh gach réada.

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;

Cuspóir: Tá an mapeáil curtha síos go lárnach ag an áit amháin (Specs), ach fanann na haistrithe soiléir. I gcásanna Legacy is gnách gurb é seo an rogha malairte níos fearr ná mapeáil RTTI uathoibríoch iomlán, toisc go bhfeiceann tú láithreach cén property a bhaineann le cén ainm réimse.

Coinníollacha: Tá an cur chuige ag súil le Dataset gníomhach agus le seasamh taifead reatha. Do iompórtálacha beartán, déan itheachán seachtrach le while not DS.Eof do agus glaoigh MapCustomer do gach sraith.

Rudaí le tabhairt faoi deara: Tabhair aird ar VarToStr le BLOBanna nó réimsí Memo; ba chóir duit do thiontaire féin a úsáid ansin. Agus: ciallaíonn “Required” anseo tar éis an tiontóra. Má chuireann C_TrimToNull réimse Required go Null, tá sé sin ar intinn — caithfidh cáilíocht sonraí a bheith réitithe ag an fhoinse nó sa phróiseas.

Leaganacha: In áit Targetanna sreang is féidir leat Enum a úsáid chun botúin scríbhneoireachta a eisiamh. Mar mhalairt, is féidir an fheidhm Assign a shábháil in aghaidh gach Spec mar TProc<Variant>, agus ansin ní bheidh gá leis an Target-String ar chor ar bith (beagán níos mó boilerplate, ach níos lú cineál botúin).

Chuir i gcomhthéacs na ailtireachta: DAL/Repository, Logging agus Oibríocht

I ailtireacht shraithe (gnáth: UI – Business – rochtain sonraí) ba chóir an mapeáil seo a bheith sa chiseal rochtana sonraí nó i repository. Tá sé tábhachtach nach ndéantar an Dataset a “thrasghlasáil”: is comhéadan níos seasmhaí iad réada/DTOs, go háirithe má chuireann tú APIs REST leis níos déanaí nó má aschuir tú codanna mar C# Services.

Maidir le hoibriú agus tacaíocht is fiú an Debug-Hook OnDebug. Is féidir leat leis sin i dtástálacha nó i gcásanna tacaíochta inbhéartaithe taifead a dhéanamh ar na réimsí a rinneadh a mapáil i ndáiríre. I gcórais táirgeachta ba chóir é a bheith sonrach agus inchealaithe, murach sin éireoidh logáil róchostasach nó ró-líonmhar ó thaobh sonraí de.

Úsáid chiallmhar an Debug-Hook

  • Unit-Tests: Seiceáil an dtugann ráiteas SQL áirithe i ndáiríre na réimsí riachtanacha go léir.
  • Diagnóis: I gcás fadhbanna cliant feicfidh tú láithreach an difríocht idir „ní raibh an réimse ann“ agus „ní raibh an luach in ann a bheith tiontaithe“.
  • Céimeanna imirce: Ag aistriú Views/ainmneacha colún is féidir leat liostaí iarrthóirí a chothabháil i gcomhthráth, go dtí go mbeidh gach rud aistrithe.

Cathain a théann an cur chuige seo in éag (agus céard atá níos fearr ansin)

Tá an léiriú Dataset-go-Objacht a thaispeántar láidir nuair atá an fhoinse sonraí míchobhsaí agus go dteastaíonn uait iompar determinist. Go ginearálta téann sé in éag i gcásanna den dá chineál seo:

  • Méideanna an-mhóra (m.sh. easpórtáil sluaite): Is féidir le tiontú Variant agus cuardach de réir ainm réimse a bheith intuartha. Sa chás sin bíonn sé fiúntach éindex réimse réamhghníomhaithe a chacheáil in aghaidh gach SQL (m.sh. FieldByName uair amháin in aghaidh an Dataset, ní in aghaidh na Row).
  • An-éagsúlacht cineálacha DTO: Má scríobhann tú céadta Mapper, éireoidh an iomarca boilerplate. Ansin d’fhéadfadh cur chuige bunaithe ar RTTI le airíonna a bheith oiriúnach — ach amháin má rialóidh tú aschuir Debug agus tiontairí go docht.

Réiteach idirmheánach maith ná: réiteach réimsí agus tiontú mar anseo (sonrach, foighneach ó thaobh earráidí de nuair is gá), ach le cód ginte (m.sh. trí theimpléid inmheánacha) seachas „scríofa de láimh“.

Conclúid: Cobhsaitheacht trí rialacha shonracha – le teorainneacha feidhme soiléire

Maidir le Legacy-Datasets le Aliases, colúin roghnacha agus seimiantacht náid stairiúil is rathúil an léiriú Dataset-go-Objacht go háirithe má fhanann sé sonrach agus in ann diagnóis a dhéanamh. Cruthaíonn an plean mapála bunaithe ar liostaí iarrthóirí, Required/Optional agus tiontairí díreach é sin: is féidir leat sean-ualach a chobhsú céim ar chéim, gan ORM a chur i bhfeidhm láithreach nó an bunachar sonraí a ghnáthú „ar fad“.

Tá teorainneacha ag baint le feidhmíocht eiseamplach agus le líon an-ard cineálacha — ansin beidh caching nó giniúint chód uathoibrithe de dhíth. Maidir le bogearraí gnó tipiciúla le próisis fhásaithe is cur chuige iontaofa é seo chun rochtain ar shonraí agus samhlacha réimse a scaradh arís agus a chothabháil.

Mura bhfuil tú cinnte faoi léiriú Legacy sonraíoch (FireDAC, Views, neamhrialtacht joinanna, Null-Semantik) agus más mian leat tuairim eile nó ailtireacht sprioc-inmharthana, is gnách go mbíonn an chéad chéim anailís ghairid le samplaí inbhéartaithe. Teagmháil:

Sa chomhthéacs ghairmiúil, tá ról tábhachtach ag Delphi Dataset Mapping agus Legacy Delphi nuair is gá go n-oibríonn comhtháthaithe, sreafaí sonraí agus forbairt le chéile go glan.

Pléigh tionscnamh nó tionscadal nuachóirithe le Net-Base.

Roinn an post

Roinn an t-alt seo go díreach

Tá LinkedIn, X, XING, Facebook, WhatsApp agus ríomhphost ar fáil láithreach. Do Instagram ullmhaímid nasc agus téacs gairid láithreach.

Ríomhphost

Osclaítear Instagram i gcluaisín nua. Cóipeáiltear an nasc agus an téacs gairid roimh ré isteach sa ghearrthaisce.