Í 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ð semID,ID_1eða hefur verið endurnefnt með SQL-alíasi. - Semantískir nullar:
0merkir „óþ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“.
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.
FieldByNameeinu 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.