Pri existujúcich Delphi-systémoch je Dataset-zu-Objekt Mapping zriedka ten čistý „jedno pole = jedna property“-prípad. V individuálnom podnikateľskom softvéri namiesto toho narazíte na aliasové stĺpce z viewov, výsledky JOIN‑ov s duplicitnými názvami polí, „prázdne“ hodnoty ako 0 alebo ' ', typované polia, ktoré dnes dodajú VARCHAR a zajtra INTEGER, a stĺpce, ktoré v závislosti od vyhľadávacieho dialógu jednoducho chýbajú. Práve tam mnohí mapperi zlyhávajú: buď sú príliš „magickí“ (a tým ťažko ladiateľní), alebo sú tak striktne navrhnutí, že už jedno voliteľné pole zastaví prevádzku.
Tento zdrojový útržok ukazuje pragmatický mapper pre Delphi, ktorý zámerne nie je ORM, ale čisto adresuje najdôležitejšie legacy okrajové prípady: jednoznačné rozlíšenie polí, kontrolovanú konverziu, nulovú sémantiku, voliteľné polia a zrozumiteľné chybové hlásenia. Hodí sa do Data-Access-Layer (DAL, teda vrstvy, ktorá zapuzdruje prístup k dátam) alebo Repository‑patternov – a dobre sa kombinuje s BDE-nahradením s natívnym pripojením (knižnica prístupu k dátam Delphi pre mnohé DB).
Prečo štandardné mapovanie pri starých štruktúrach zlyháva
Niekoľko typických príčin z prevádzky, ktoré pri „čistom“ novom návrhu málokedy vidieť:
- Dvojznačné názvy polí: JOIN vráti
IDz viacerých tabuliek; v datasete sa potom voláID,ID_1alebo je premenované SQL aliasom. - Sémantické nulové hodnoty:
0znamená „neznáme“,'1899-12-30'je „žiadny dátum“,' 'je „nevyplnené“. - Kolísavé typy: View nekastuje; ovládač dodá
ftWideStringmiestoftInteger. Konverzie variantov sa stávajú zdrojom chýb. - Voliteľné stĺpce: Vyhľadávací dialóg podľa filtra používa rôzne SELECT zoznamy. Kód však očakáva, že polia budú „vždy“ prítomné.
- Ladiateľnosť: Keď sa mapovanie stratí v RTTI, hľadanie chyby v dátach zákazníka je náročné (ktoré pole, aká hodnota, aký typ?).
Prístup: Mapping‑Plan namiesto konvencie, s kontrolovanou konverziou
Jadro je Mapping‑Plan: zoznam pravidiel „Property X pochádza z poľa A alebo B, je optional/required, používa konvertor Y“. Tým zostáva mapovanie deklaratívne, ale nie „neviditeľné“ ako u mnohých ORM mechanizmov. Mapper môže pre každé pole vyhodiť výstižnú výnimku vrátane názvu poľa, dátového typu a surovej hodnoty.
Dôležité: Mapujeme zámerne z TDataSet, nie z konkrétnej BDE-Ablosung mit nativer Anbindung‑triedy. Tým zostáva kompatibilné s TFDQuery, TClientDataSet alebo aj cudzími komponentmi.
Ukážka zdrojového kódu: Ladiateľné mapovanie datasetu na objekt pre legacy stĺpce
Kód implementuje:
- Riešenie polí cez zoznam priorít (aliasy / záložné možnosti)
- Spracovanie povinných/voľiteľných polí
- Nulovú sémantiku cez konvertory (napr.
0 => Null) - Stabilné chybové hlásenia s kontextom
- Debugovací hook, aby bolo možné problémy s mapovaním v teste alebo v podpore reprodukovať
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);
// Konvertor prijíma Variant a vracia Variant (napr. Null, Integer, String, TDateTime ako Double)
TFieldConverter = reference to function(const V: Variant): Variant;
TFieldSpec = record
TargetMember: string;
SourceCandidates: TArray
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
function FieldTypeToString(FT: TFieldType): string;
function VariantToDiag(const V: Variant): string;
public
property OnDebug: TMapDebugEvent read FOnDebug write FOnDebug;
// MapOne: volá setter pre každú špecifikáciu. Nie RTTI: explicitné priradenie je ľahšie debugovateľné.
procedure MapOne(DS: TDataSet; const Specs: TArray
const Assign: TProc
end;
// Pomocné konvertory
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 := ‚<nevytlačiteľný variant>‘;
end;
end;
function TLegacyDatasetMapper.FindFieldByCandidates(DS: TDataSet; const Candidates: TArray
var
Name: string;
begin
Result := nil;
for Name in Candidates do
begin
// FindField namiesto FieldByName: voliteľné, bez výnimky
Result := DS.FindField(Name);
if Result <> nil then
Exit;
end;
end;
procedure TLegacyDatasetMapper.MapOne(DS: TDataSet; const Specs: TArray
const Assign: TProc
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 nie je aktívny.‘);
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(‚Chyba mapovania: požadované pole pre %s nebolo nájdené. Kandidáti: [%s]‘,
[Spec.TargetMember, CandidatesJoined]));
end
else
Continue; // voliteľné: jednoducho preskočiť
end;
Raw := F.Value; // Variant; zohľadňuje 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 po konverzii je chyba (častejšia, než sa zdá)
if (Spec.Required = mrRequired) and VarIsNull(Val) then
begin
FT := FieldTypeToString(F.DataType);
raise EDataMappingError.Create(
Spec.TargetMember,
F.FieldName,
FT,
VariantToDiag(Raw),
Format(‚Chyba mapovania: %s je požadované, ale hodnota je NULL po konverzii. Pole %s (%s), surová hodnota=%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(‚Chyba mapovania pri %s z poľa %s (%s), surová hodnota=%s: %s‘,
[Spec.TargetMember, F.FieldName, FT, VariantToDiag(Raw), E.Message]));
end;
end;
end;
end;
{ Konvertory }
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);
// toleruje aj ‚0‘ ako reťazec
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);
// Úmyselne prísne: žiadne „Try“, ktoré by zakrylo kvalitu dát.
// Formát sa môže líšiť podľa legacy; prípadne tu parametrizovať cez TFormatSettings.
D := ISO8601ToDate(S, False);
Result := D;
end;
end;
end.
Ako prakticky používať Mapper (bez RTTI, no stále elegantne)
Mapper volá Assign(TargetMember, Value) callback funkciu. To udržuje priradenie explicitné (a tým dobre laditeľné) a vyhýba sa RTTI-prístupom v Hot-Path. V praxi si pre každý objekt/DTO (Data Transfer Object, teda transportný objekt pre údaje) postavíte malý „priraďovač“.
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;Účel: Mapovanie je centralizovane popísané na jednom mieste (Specs), ale priradenie zostáva explicitné. V legacy situáciách je to zvyčajne lepšie kompromisné rozhodnutie než plne automatické RTTI-mapping, pretože okamžite vidíte, ktorá property závisí od ktorého názvu poľa.
Okolnosti: Prístup predpokladá aktívny Dataset a aktuálnu pozíciu záznamu. Pre dávkové importy iterujete zvonka cez while not DS.Eof do a voláte MapCustomer pre každý záznam.
Úskalia: Dávajte pozor na VarToStr pri BLOBoch alebo memo poliach; tam by ste mali použiť vlastné konvertory. A: „Required“ znamená tu po konvertore. Ak C_TrimToNull nastaví required pole na Null, je to zámer – kvalita dát sa musí riešiť pri zdroji alebo v procese.
Varianty: Namiesto stringových targetov môžete použiť aj enum, aby ste vylúčili preklepy. Alternatívne sa dá Assign-funkcia ukladať pre každý Spec ako TProc<Variant>, čím úplne odpadne Target-string (trochu viac opakujúceho sa kódu, ale menej tried chýb).
Zaradenie do architektúry: DAL/Repository, Logging a prevádzka
Vo vrstvenej architektúre (typicky: UI – Business – prístup k dátam) patrí toto mapovanie do vrstvy prístupu k dátam alebo do Repository. Dôležité je, aby sa Dataset „nepreposielal“ ďalej: objekty/DTOs sú stabilnejšie rozhranie, najmä ak neskôr doplníte REST-APIs alebo presuniete časti do C# Services.
Pre prevádzku a podporu sa osvedčí Debug-Hook OnDebug. Môžete s ním v testoch alebo pri reprodukovateľných prípadoch podpory protokolovať, ktoré polia boli skutočne namapované. V produkčných systémoch by to malo byť cielené a vypínateľné, inak sa logovanie stane príliš nákladným alebo príliš objemným z hľadiska dát.
Rozumné využitie Debug-Hooku
- Unit-Testy: Overiť, či daný SQL dotaz skutočne dodáva všetky Required polia.
- Diagnostika: Pri problémoch u zákazníka okamžite rozlíšite „Feld war nicht da“ vs. „Wert konnte nicht konvertiert werden“.
- Migračné fázy: Pri prechode na iné Views/mená stĺpcov môžete paralelne udržiavať zoznamy kandidátov, až kým nie je všetko presunuté.
Kedy tento prístup zlyháva (a čo je potom lepšie)
Predvádzané mapovanie dataset → objekt je silné, ak je zdroj dát nestabilný a potrebujete napriek tomu deterministické správanie. Typicky zlyhá v dvoch situáciách:
- Veľké objemy (napr. hromadný export): Variant-konverzia a vyhľadávanie podľa mena poľa môže byť citeľné. Vtedy sa oplatí predpočítané cachovanie indexu polí pre každý SQL (napr.
FieldByNameraz na Dataset, nie na riadok). - Veľa DTO-typov: Ak píšete stovky mapperov, stane sa z toho boilerplate. Potom môže mať zmysel RTTI-založený prístup s atribútmi – ale len ak prísne kontrolujete debug výstupy a konvertory.
Dobrý kompromis je: rozlíšenie polí a konverzia tak, ako tu (explicitne, tolerantne tam, kde treba), ale s generovaným kódom (napr. cez interné šablóny) namiesto „ručne písaného“.
Záver: Stabilita vďaka explicitným pravidlám – s jasnými hranicami použitia
Pri legacy datasetoch s aliasmi, voliteľnými stĺpcami a historickou nulovou sémantikou je mapovanie dataset → objekt úspešné najmä vtedy, keď zostane explicitné a diagnostikovateľné. Mapovací plán z kandidátnych zoznamov, Required/Optional a konvertorov presne toto umožňuje: môžete postupne stabilizovať dedičné zvyšky bez toho, aby ste hneď zavádzali ORM alebo databázu „naraz“ normalizovali.
Hranice sú pri extrémnom výkone a pri veľmi veľkom počte typov – vtedy potrebujete caching alebo automatizovanú generáciu kódu. Pre typickú Business-Software s vyvinutými procesmi je tento prístup však spoľahlivým nástrojom na opätovné oddelenie a udržiavateľnosť prístupu k dátam a doménových modelov.
Ak pri konkrétnom legacy-mappingu (FireDAC, Views, neprehľadný nárast JOIN-ov, Null-Semantik) potrebujete druhý názor alebo spoľahlivú cieľovú architektúru, ďalším krokom je zvyčajne krátka analýza s reprodukovateľnými príkladmi. Kontakt:
V odbornom kontexte zohrávajú tiež Delphi Dataset Mapping a Legacy Delphi dôležitú rolu, keď musia integrácie, dátové toky a ďalší rozvoj čisto spolu fungovať.
Projekt oder Modernisierungsvorhaben mit Net-Base besprechen.