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 nó ' ', 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 é ansinID,ID_1nó tá sé athainmnithe trí alias SQL. - Nullaí seimanta:
0ciallaí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í
ftWideStringin ionadftInteger. É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
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.
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.
FieldByNameuair 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.