Net-Base Tímarit

10.05.2026

Kortlagning gagnasetta í hluti fyrir óvenjulegar eldri gagnauppbyggingar: stöðug, auðvelt að rekja villur, án ORM-galdra

Þegar eldri gagnasöfn hafa þróast sögulega detta staðlaðar kortlagningarlausnir oft út vegna alias-dálka, blöndunar gagnagerða og breytilegra JOIN-strúktúra. Þetta kóðabrot sýnir áreiðanlega, auðvelda í bilanaleit kortlagningu gagnasafna í hluti í Delphi með kortlagningarplani, umbreytum og NULL-semantík.

10.05.2026

Í eldri Delphi-kerfum er Dataset-til-objekta kortlagning sjaldnast hreinn „einn reitur = einn eiginleiki“-kassi. Í sérsniðnum fyrirtækjakerfum rekst þú í staðinn á alias-dálka úr views, join-niðurstöður með tvöföldum reit nöfnum, „tóm“ gildi sem 0 eða ' ', týpufesta reiti sem í dag skila VARCHAR en á morgun INTEGER, og dálka sem einfaldlega vantar eftir tegund leitarviðmóts. Þarna dettur margur mapper-inn: annað hvort verður hann of „dularfullur“ (og því erfiðari í villuleit), eða hann er svo strangur að eitt valkvætt reit stöðvar rekstur.

Þessi kóðasneið sýnir pragmatískan mapper fyrir Delphi, sem með vilja er ekki ORM, en sem tekur næmlega á helstu arfleifðar- jaðartilvikunum: ótvíræð reitaupplausn, stýrt umbreytingarferli, null-semantík, valkvæðir reitir og skýr villuskilaboð. Hann hentar fyrir Data-Access-Layer (DAL, þ.e. lag sem innlykur gagnaaðgang) eða Repository-mynstur – og er auðvelt að samhæfa við BDE-aflausn með innbyggðri tengingu (Delphis gagnaaðgangsbókasafn fyrir mörg gagnagrunnakerfi).

Af hverju staðlað kortlagning bregst hjá gömlum uppbyggingum

Nokkrar algengar orsakir úr rekstri sem sjaldan koma fram í „hreinu“ nýhönnuðu kerfi:

  • Óljós reit nöfn: Join skilar ID úr mörgum töflum; í dataset-inu birtist það sem ID, ID_1 eða hefur verið endurnefnt með SQL-alíasi.
  • Semantískir nullar: 0 merkir „óþekkt“, '1899-12-30' merkir „engin dagsetning“, ' ' merkir „ekki skráð“.
  • Breytingar í týpu: View kastar ekki; drifari skilar ftWideString í stað ftInteger. Variant-umbreyting verður uppspretta villa.
  • Valkvæðir dálkar: Leitarviðmót notar mismunandi SELECT-lista eftir filterum. Kóðinn gerir ráð fyrir að reitir séu „alltaf“ til staðar.
  • Auðveld villuleit: Þegar mapping hverfur inn í RTTI er erfitt að greina villur hjá viðskiptavinum (hvers reitur, hvaða gildi, hvaða týpa?).

Aðferð: Kortlagningarplan frekar en reglur, með stýrðum umbreytingum

Kjarninn er kortlagningarplan: listi reglna „eiginleiki X kemur úr reit A eða B, er valkvætt/skyldu, notar ummyndara Y“. Með þessu er kortlagningin yfirlýst en ekki „ósýnileg“ eins og í mörgum ORM-mekanískum. Að auki getur mapper-inn kastað greinargóðri undantekningu fyrir hvern reit, þar með nafn reits, gagnaform og hrá gildi.

Mikilvægt: Við kortleggjum með vilja frá TDataSet, ekki frá tiltekinni BDE-Ablosung mit nativer Anbindung-klassa. Þannig helst samhæfni við TFDQuery, TClientDataSet eða aðrar ytri íhlutir.

Kóðasneið: Villuleitanleg Dataset-til-objekta kortlagning fyrir arfleifðar dálka

Kóðinn útfærir:

  • Reitaupplausn yfir forgangslista (alias/fallbacks)
  • Meðhöndlun skyldra/valfrjálsra reita
  • Null-semantík í gegnum ummyndara (t.d. 0 => Null)
  • Áreiðanleg villuskilaboð með samhengi
  • Debug-hook til að rekja kortlagningarvandamál í prófun eða stuðningsmálum

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);

// Umbreytir tekur Variant og skilar Variant (t.d. Null, Integer, String, TDateTime sem 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: kallar á setter fyrir hverja Spec. Ekki RTTI: skýr úthlutun er auðveldari í villuleit.
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 í stað FieldByName: valfrjálst, án undantekninga
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 er ekki virkt.‘);

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(‚Kortunarvilla: nauðsynlegur reitur fyrir %s fannst ekki. Frambjóðendur: [%s]‘,
[Spec.TargetMember, CandidatesJoined]));
end
else
Continue; // valfrjálst: einfaldlega sleppa
end;

Raw := F.Value; // Variant; tekur tillit til 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 eftir umbreytingu er villa (algengara en maður heldur)
if (Spec.Required = mrRequired) and VarIsNull(Val) then
begin
FT := FieldTypeToString(F.DataType);
raise EDataMappingError.Create(
Spec.TargetMember,
F.FieldName,
FT,
VariantToDiag(Raw),
Format(‚Kortunarvilla: %s er nauðsynlegt, en gildi er NULL eftir umbreytingu. Reitur %s (%s), hrágildi=%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(‚Kortunarvilla við %s úr reit %s (%s), hrágildi=%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);
// samþykkir einnig ‚0‘ sem streng
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);

// Viljandi strangt: engin „Try“ til að fela gæðavandamál gagna.
// Snið getur verið mismunandi eftir legacy; ef þarf má parametrera hér með TFormatSettings.
D := ISO8601ToDate(S, False);
Result := D;
end;
end;

end.

Hvernig á að nota mapperinn í framkvæmd (án RTTI en samt snyrtilega)

Mapperinn kallar á Assign(TargetMember, Value)-callback-fall. Þetta gerir úthlutunina skýra (og þar með auðveldari að greina villur) og forðast RTTI-aðgerðir í hot-path. Í framkvæmd býrðu fyrir hvert hlut/DTO (Data Transfer Object, þ.e. gagnaflutningshlutur) til lítinn „úthluta“.

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;

Tilgangur: Kortlagningin er lýst á einum stað (Specs), en úthlutunin er skýr. Í Legacy-aðstæðum er þetta oft betri ákvörðun um málamiðlun en fullsjálfvirkt RTTI-kortlagning, því þið sjáið strax hvaða eiginleiki er háður hvaða reitanafni.

Skilyrði: Aðferðin gerir ráð fyrir virku Dataseti og núverandi skráarstöðu. Fyrir bunka-innflutninga keyrir þið utanum með while not DS.Eof do og kallar MapCustomer fyrir hverja röð.

Gildrur: Athugið VarToStr hjá BLOB- eða memo-reitum; þar ættu eigin breytarar að vera notaðir. Og: „Required“ gildir hér eftir breytarann. Ef C_TrimToNull setur required-reit í null er það meðvitað – gagnagæði þurfa þá að skýrast hjá uppruna eða í ferlinu.

Breytingar: Í stað strengja sem target getað þið notað enum til að útiloka innsláttarvillur. Sem kostur er einnig að vista Assign-fallið fyrir hverja Spec sem TProc<Variant>, þá hverfur target-strengurinn alveg (örlítið meiri boilerplate, en færri villumöguleikar).

Staðsetning í arkitektúr: DAL/Repository, skráning og rekstur

Í lagskiptum arkitektúr (vanalega: UI – Business – gagnaaðgangur) á þessi kortlagning að tilheyra gagnaaðgangslaginu eða repository. Mikilvægt er að datasetið sé ekki „sleppt áfram“: hlutir/DTOs eru stöðugri viðmót, sérstaklega þegar þið síðar bætir við REST-APIs eða úthýsið hlutum í C# þjónustur.

Fyrir rekstur og stuðning er Debug-Hook OnDebug verðmætur. Með honum er hægt í prófunum eða við endurtekna stuðningsmála að skrá hvaða reitir voru raunverulega kortlagðir. Í framleiðslukerfum ætti þessi skráning að vera stýranleg og hægt að slökkva á henni, annars verður logging of dýrt eða of gagnamikið.

Rétt notkun Debug-Hook

  • Unit-prófanir: Athugaðu hvort tiltekin SQL-skipun skilar í raun öllum skyldureitunum.
  • Greining: Við viðskiptavinasvandamál sérðu strax „Reitur fannst ekki“ vs. „Gildi var ekki hægt að umbreyta“.
  • Flutningsfasar: Þegar vistað er yfir á ný nöfn fyrir views/dálka getur þú haldið lista yfir mögulega reiti samhliða þar til allt er flutt.

Hvenær þessi nálgun bilar (og hvað er þá betra)

Sýnd dataset-til-objekta kortlagning er öflug þegar gagnaveitan er óstöðug og þú þarft samt determinískt hegðun. Hún bilar almennt í tveimur tilfellum:

  • Mjög stór gagnamagn (t.d. massaflutning): Variant-breyting og leit eftir reitanafni getur orðið tímakostnaðarsöm. Þá borgar sig fyrirreiknað reitindex-caching fyrir hvert SQL (t.d. FieldByName einu sinni per Dataset, ekki per röð).
  • Margar DTO-gerðir: Ef þið skrifið hundruð mappera þá vex boilerplate-kóðinn. Þá getur RTTI-byggð nálgun með attribútum verið skynsamleg – en aðeins ef þið stjórnið Debug-útgáfum og konverterum af fullri festu.

Góður millileið er: reitaupplausn og umbreyting eins og hér (skýr, villuþolin þar sem þarf), en með mynduðum kóða (t.d. yfir innri sniðmát) í stað „handskrifaðs“ kóða.

Niðurstaða: Stöðugleiki með skýrum reglum – með skýrum notkunarmörkum

Hjá legacy-datasetum með aliasum, valfrjálsum dálkum og sögulegri null-semantík reynist dataset-til-objekta kortlagning einkum vel ef hún er skýrt uppsett og greiningarhæf. Mapping-plan byggður á kandídatlistum, skyldu/valfrjáls og konverterum skilar einmitt þessu: þú getur stöðugt lagað arfleiðargögnin í áföngum án þess að innleiða ORM strax eða normalísera gagnagrunninn „alltaf í einu“.

Takmarkanirnar eru við öfgakenna frammistöðu og mjög margar týpur – þá þarftu caching eða sjálfvirka kóðagerð. Fyrir dæmigerðan viðskiptahugbúnað með vöxnum ferlum er þessi nálgun hins vegar áreiðanlegt tæki til að losa um tengsl milli gagnaaðgangs og léns-/dómainlíkana og gera þau viðhaldsvænni.

Ef þú þarft aðra skoðun eða trausta markarkitektúr í tilviki konkret legacy-mapping (FireDAC, Views, Join-Wildwuchs, Null-Semantik) er næsta skref yfirleitt stutt greining með endurteknum dæmum. Kontakt:

Í faglegu samhengi gegna einnig Delphi Dataset Mapping og Legacy Delphi mikilvægu hlutverki þegar samþættingar, gagnastreymi og áframhaldandi þróun þurfa að spila vel saman.

Ræddu verkefni eða nútímavæðingarverkefni með Net-Base.

Deila færslu

Deila þessari færslu beint

LinkedIn, X, XING, Facebook, WhatsApp og tölvupóstur eru strax tiltækir. Fyrir Instagram undirbúum við hlekk og stuttan texta beint.

Tölvupóstur

Instagram opnast í nýjum flipa. Tengill og stuttur texti eru afritaðir í klippiborðið á undan.