Qui gestiona software empresarial consolidat a Delphi coneix el camp de tensió: d’una banda es volen objectes de domini estructurats i capes clares, i de l’altra hi ha Datasets, Variants, imports CSV, payloads d’interfícies o una REST-API que d’alguna manera cal mapar a objectes. Precisament aquí s’acaba arribant ràpidament a Delphi RTTI per a mapping sense màgia: és a dir, mapping per Reflection (RTTI = Run-Time Type Information, informació de tipus en temps d’execució), però de manera que sigui rastrejable, fàcil de depurar i que no depengui en secret de convencions o jocs de noms.
El punt clau: la «màgia» generalment no sorgeix de l’RTTI en si, sinó de regles implícites. Si, en canvi, les regles de mapping estan explícitament en atributs, les conversions estan centralitzades i els errors indiquen una causa clara, l’RTTI es converteix en una eina en lloc d’una sorpresa.
Per què l’RTTI-mapping a Delphi sovint falla
El mapping basat en RTTI rarament fracassa per la idea en sistemes reals, sinó per condicions límit:
- Formes de dades heredades: Null/Empty/0 no estan clarament separats, els tipus de camps canvien, les cadenes contenen «N/A».
- Convencions que s’infiltren: «el camp té el mateix nom que la Property» funciona fins al primer àlies, join o nom de Property refactoritzat.
- Difícil de depurar: quan un mapper «simplement no assigna res», més endavant falta la causa. En producció això és verí.
- Mites de rendiment: l’RTTI es titlla en bloc de «lent», encara que sovint el problema sigui la manca de caching.
Un enfocament viable hauria per tant d'(1) tenir metadades de mapping explícites, (2) tractar clarament la conversió i la semàntica de null, (3) generar errors i sortides de debug, i (4) cachejar la informació RTTI.
Delphi RTTI per a mapping sense màgia: principis de disseny
El patró següent és deliberadament «avorrit» en el millor sentit: les regles són visibles, els efectes secundaris es limiten, i es pot aplicar pas a pas als mòduls existents.
- Atributs en comptes de convencions de nom: la Property rep un atribut que especifica la columna d’origen.
- Opt-in: només s’assignen les Properties marcades. Cap sorpresa amb «totes les Properties publicades».
- Conversió en un sol lloc: Variant/String/Integer/Boolean/Enum/Nullable es mapegen de manera centralitzada.
- Mode de depuració: opcionalment es registra quins camps s’han assignat/omitit — amb la causa.
- RTTI-Caching: les parts més costoses (llista de Properties, avaluació d’atributs) es preparen per tipus.
Fragment de codi: Attribut-Mapping amb RTTI, Caching i Debug
El snippet mapeja una fila (p. ex. d’una BDE-substitució amb integració nativa via TDataSet) a un objecte. En comptes d’encadenar el mapper fixament a TField, fem servir una petita interfície reader. Això és valuós en la pràctica, perquè més endavant podreu reutilitzar la mateixa lògica també per a JSON, INI, CSV o respostes d’API.
unit RttiMapping;
interface
uses
System.SysUtils, System.Rtti, System.TypInfo, System.Generics.Collections,
System.Variants;
type
// Mapeig explícit: Propietat <- nom d’origen
MapFromAttribute = class(TCustomAttribute)
private
FName: string;
public
constructor Create(const AName: string);
property Name: string read FName;
end;
// Petita abstracció: proporcionar valor + distingir existència/NULL
IValueReader = interface
[‚{7D1E5864-7D3A-4D30-BD1C-0A94F7E6C0EF}‘]
function HasValue(const AName: string): Boolean;
function IsNull(const AName: string): Boolean;
function GetValue(const AName: string): Variant;
end;
TRttiMapOptions = set of (moDebug, moIgnoreMissing, moIgnoreNull);
ERttiMappingError = class(Exception);
TRttiMapper = class
private
type
TPropMap = record
Prop: TRttiProperty;
SourceName: string;
end;
TTypeCache = class
Props: TArray<TPropMap>;
end;
private
class var FCache: TObjectDictionary<PTypeInfo, TTypeCache>;
class var FCacheLock: TObject;
class function GetOrBuildCache(ATypeInfo: PTypeInfo): TTypeCache; static;
class function FindMapFromAttr(const AProp: TRttiProperty): string; static;
class procedure SetPropertyValue(const AInstance: TObject; const AProp: TRttiProperty;
const AValue: Variant); static;
class function VariantToBoolean(const V: Variant): Boolean; static;
class function VariantToEnumOrdinal(AEnumType: TRttiType; const V: Variant): Integer; static;
public
class constructor Create;
class destructor Destroy;
class procedure MapToObject(const AReader: IValueReader; const ATarget: TObject;
const AOptions: TRttiMapOptions = [moIgnoreMissing]); static;
end;
implementation
{ MapFromAttribute }
constructor MapFromAttribute.Create(const AName: string);
begin
inherited Create;
FName := AName;
end;
{ TRttiMapper }
class constructor TRttiMapper.Create;
begin
FCache := TObjectDictionary<PTypeInfo, TTypeCache>.Create([doOwnsValues]);
FCacheLock := TObject.Create;
end;
class destructor TRttiMapper.Destroy;
begin
FCache.Free;
FCacheLock.Free;
end;
class function TRttiMapper.FindMapFromAttr(const AProp: TRttiProperty): string;
var
Attr: TCustomAttribute;
begin
Result := “;
for Attr in AProp.GetAttributes do
if Attr is MapFromAttribute then
Exit(MapFromAttribute(Attr).Name);
end;
class function TRttiMapper.GetOrBuildCache(ATypeInfo: PTypeInfo): TTypeCache;
var
Ctx: TRttiContext;
RType: TRttiType;
P: TRttiProperty;
L: TList<TPropMap>;
Src: string;
M: TPropMap;
begin
TMonitor.Enter(FCacheLock);
try
if FCache.TryGetValue(ATypeInfo, Result) then
Exit;
Result := TTypeCache.Create;
Ctx := TRttiContext.Create;
RType := Ctx.GetType(ATypeInfo);
L := TList<TPropMap>.Create;
try
for P in RType.GetProperties do
begin
if not P.IsWritable then
Continue;
// Opt-in: només propietats amb atribut
Src := FindMapFromAttr(P);
if Src = “ then
Continue;
M.Prop := P;
M.SourceName := Src;
L.Add(M);
end;
Result.Props := L.ToArray;
finally
L.Free;
end;
FCache.Add(ATypeInfo, Result);
finally
TMonitor.Exit(FCacheLock);
end;
end;
class function TRttiMapper.VariantToBoolean(const V: Variant): Boolean;
var
S: string;
begin
if VarIsBool(V) then
Exit(V);
if VarIsNumeric(V) then
Exit(V <> 0);
S := Trim(VarToStr(V)).ToLower;
if (S = ‚1‘) or (S = ‚true‘) or (S = ‚t‘) or (S = ‚y‘) or (S = ‚yes‘) then
Exit(True);
if (S = ‚0‘) or (S = ‚false‘) or (S = ‚f‘) or (S = ’n‘) or (S = ’no‘) then
Exit(False);
raise ERttiMappingError.CreateFmt(‚Conversió booleana fallida: „%s“‚, [VarToStr(V)]);
end;
class function TRttiMapper.VariantToEnumOrdinal(AEnumType: TRttiType; const V: Variant): Integer;
var
Ord: Integer;
Name: string;
begin
if VarIsNumeric(V) then
begin
Ord := Integer(V);
if (Ord < GetTypeData(AEnumType.Handle)^.MinValue) or
(Ord > GetTypeData(AEnumType.Handle)^.MaxValue) then
raise ERttiMappingError.CreateFmt(‚Ordinal d“enum fora de l“interval: %d‘, [Ord]);
Exit(Ord);
end;
Name := VarToStr(V);
Ord := GetEnumValue(AEnumType.Handle, Name);
if Ord < 0 then
raise ERttiMappingError.CreateFmt(‚Nom d“enum desconegut: „%s“‚, [Name]);
Result := Ord;
end;
class procedure TRttiMapper.SetPropertyValue(const AInstance: TObject;
const AProp: TRttiProperty; const AValue: Variant);
var
V: TValue;
T: TRttiType;
Ord: Integer;
begin
T := AProp.PropertyType;
// Conversió deliberadament selectiva: millor fallar de manera clara que „d’alguna manera“ silenciosament.
case T.TypeKind of
tkUString, tkString, tkLString, tkWString:
V := TValue.From<string>(VarToStr(AValue));
tkInteger, tkInt64:
V := TValue.From<Int64>(VarAsType(AValue, varInt64));
tkFloat:
V := TValue.From<Double>(VarAsType(AValue, varDouble));
tkEnumeration:
begin
if T.Handle = TypeInfo(Boolean) then
V := TValue.From<Boolean>(VariantToBoolean(AValue))
else
begin
Ord := VariantToEnumOrdinal(T, AValue);
V := TValue.FromOrdinal(T.Handle, Ord);
end;
end;
tkSet:
raise ERttiMappingError.CreateFmt(‚Mapping de Set no implementat per a %s‘, [AProp.Name]);
tkClass:
raise ERttiMappingError.CreateFmt(‚Mapping de propietat de classe no implementat per a %s‘, [AProp.Name]);
else
raise ERttiMappingError.CreateFmt(‚TypeKind no suportat (%s) per a %s‘,
[GetEnumName(TypeInfo(TTypeKind), Ord(T.TypeKind)), AProp.Name]);
end;
AProp.SetValue(AInstance, V);
end;
class procedure TRttiMapper.MapToObject(const AReader: IValueReader;
const ATarget: TObject; const AOptions: TRttiMapOptions);
var
Cache: TTypeCache;
M: TPropMap;
V: Variant;
Msg: string;
begin
if (ATarget = nil) or (AReader = nil) then
raise ERttiMappingError.Create(‚MapToObject: Reader o Target és nil‘);
Cache := GetOrBuildCache(ATarget.ClassInfo);
for M in Cache.Props do
begin
if not AReader.HasValue(M.SourceName) then
begin
if not (moIgnoreMissing in AOptions) then
raise ERttiMappingError.CreateFmt(‚Falta la font: „%s“ per a la propietat %s‘,
[M.SourceName, M.Prop.Name]);
Continue;
end;
if AReader.IsNull(M.SourceName) then
begin
if moIgnoreNull in AOptions then
Continue;
// Sense una mecànica Nullable/Optional no es pot assignar NULL de manera significativa.
Continue;
end;
V := AReader.GetValue(M.SourceName);
try
SetPropertyValue(ATarget, M.Prop, V);
if moDebug in AOptions then
begin
Msg := Format(‚Mapejat %s <- %s (%s)‘, [M.Prop.Name, M.SourceName, VarTypeAsText(VarType(V))]);
OutputDebugString(PChar(Msg));
end;
except
on E: Exception do
raise ERttiMappingError.CreateFmt(‚Error de mapeig a %s <- %s: %s‘,
[M.Prop.Name, M.SourceName, E.Message]);
end;
end;
end;
end.
Per a què serveix
Rebreu un mapping que es pot avaluar netament en code-reviews:
- Cada propietat mapejada està marcada visualment (atribut).
- La conversió és centralitzada, per tant consistent i verificable mitjançant proves.
- Els missatges d’error indiquen quina propietat i quina font estan afectades.
- Un mode de depuració li proporciona, en cas de dubte, la cadena d’evidències, sense que necessiti breakpoints en el procés productiu.
Condicions límit i esculls típics
- NULL-Semantik: Sense un concepte Nullable propi (p. ex.
Nullable<T>o Option-Types) establir a «NULL» no és inequívoc. En el fragment, NULL s’omet per defecte. Això és conservador i evita sobreescriptures silencioses. - TRttiContext-Lebensdauer: Construïm el cache una vegada per tipus i descartem el Context després. Això és habitual. Important: No crear un nou RTTI-Context per cada assignació de camp.
- Threading: El cache està protegit via Monitor. En mapatges d’alta concurrència (p. ex. REST-Server) hauríeu de comprovar a més si preconstruir el cache a l’inici («warm build», Preload) per reduir la contenció de bloquejos.
- PropertyType Kind:
tkClassitkSetno estan implementats intencionadament. Per a objectes empetitits hauria d’aplicar-se mapatge recursiu (amb una política clara) o assignacions manuals conscients. - Locale-Fallen:
varDoubleviaVarAsTypeés relativament robust, però cadenes com «1,23» vs. «1.23» segueixen sent un problema. Si les vostres fonts retornen strings, sovint és millor un parser propi (amb una Culture definida).
Variante per a FireDAC i TDataSet: Reader-Adapter en lloc d’acoblament del Mapper
En aplicacions BDE-Ablosung mit nativer Anbindung o en aplicacions clàssiques VCL/Win32, la font sovint és un TDataSet. En lloc d’enllaçar el Mapper a TField, escriviu un adaptador que implementi la interfície IValueReader. L’avantatge: el Mapper roman independent de l’accés a dades (important si més endavant externalitzeu l’accés a dades a serveis o a un REST-Server).
uses Data.DB, System.Variants, RttiMapping;
type
TDataSetValueReader = class(TInterfacedObject, IValueReader)
private
FDS: TDataSet;
public
constructor Create(ADS: TDataSet);
function HasValue(const AName: string): Boolean;
function IsNull(const AName: string): Boolean;
function GetValue(const AName: string): Variant;
end;
constructor TDataSetValueReader.Create(ADS: TDataSet);
begin
inherited Create;
FDS := ADS;
end;
function TDataSetValueReader.HasValue(const AName: string): Boolean;
begin
Result := (FDS <> nil) and (FDS.FindField(AName) <> nil);
end;
function TDataSetValueReader.IsNull(const AName: string): Boolean;
var
F: TField;
begin
F := FDS.FindField(AName);
Result := (F = nil) or F.IsNull;
end;
function TDataSetValueReader.GetValue(const AName: string): Variant;
begin
Result := FDS.FieldByName(AName).Value;
end;
Així es presenta un mapatge concret:
type
TOrderRow = class
private
FId: Int64;
FCustomerNo: string;
FIsClosed: Boolean;
public
[MapFrom('order_id')]
property Id: Int64 read FId write FId;
[MapFrom('customer_no')]
property CustomerNo: string read FCustomerNo write FCustomerNo;
[MapFrom('is_closed')]
property IsClosed: Boolean read FIsClosed write FIsClosed;
end;
// ...
var
Row: TOrderRow;
Reader: IValueReader;
begin
Row := TOrderRow.Create;
try
Reader := TDataSetValueReader.Create(MyQuery);
TRttiMapper.MapToObject(Reader, Row, [moIgnoreMissing, moDebug, moIgnoreNull]);
finally
Row.Free;
end;
end;
On té sentit l’enfocament — i on no
Aquest patró sol tenir sentit típicament en tres situacions:
- Modernització progressiva: Voleu introduir objectes de domini sense referim completament l’accés a dades de seguida (típic en la Delphi Modernisierung en aplicacions existents).
- Fronteres d’interfície: imports CSV/Excel, càrregues REST o fonts de dades mixtes requereixen una conversió robusta i missatges d’error clars.
- Mantenibilitat en l’equip: Els atributs fan visibles i revisables les regles de mapeig, cosa que en bases de codi grans és especialment valuós.
També hi ha límits d’aplicació clars:
- Grafos d’objectes complexos (col·leccions filles, referències cíclicques) no haurien de mapar-se „automagicament“. Aquí un codi explícit o un patró assembler/factory separat sol ser més estable.
- Rutes d’alt rendiment (p. ex. ETL de dades massives) es beneficien més de mapeigs generats per codi o d’un mapeig optimitzat a mà, fins i tot si RTTI està emmagatzemat a la memòria cau.
- Nullable/Optional és un tema propi. Si cal distingir realment entre „absent“, „NULL“ i „per defecte“, això cal expressar-ho en el model de domini, no amagar-ho al mapper.
Enquadrament en l’arquitectura i l’operació
Des d’una perspectiva arquitectònica, aquest mapper és una component d’infraestructura en el límit entre la representació de dades i la lògica de domini. No substitueix una separació neta en capes, però la pot permetre: l’accés a dades (FireDAC, SQL, vistes) pot continuar sent pragmàtic, mentre el domini es manté consistent. En sistemes multicapa (sovint anomenada Layer-3 Architektur: UI, Domain/Services, Infrastruktur) el mapper pertany a la infraestructura i és utilitzat per serveis, no per formularis d’interfície d’usuari.
Operativament important: no activeu moDebug de manera permanent en serveis productius, sinó de forma dirigida. Per a problemes de dades difícils de reproduir, convé disposar d’un camí de diagnosi commutable (configuració, Feature-Flag). Altrament, això pot incrementar el volum de logs i provocar efectes secundaris.
Conclusió: RTTI sí, però només amb directrius clares
Delphi RTTI per a mapeig sense màgia funciona bé quan utilitzeu RTTI com una eina per a metadades declaratives — no com una invitació a heurístiques implícites. Atributs com a opt-in, conversió centralitzada, memòria cau per tipus i missatges d’error comprensibles porten la qüestió d’«opac» a «operatiu». L’enfocament no és universal per disseny: per a grafs anidats, semàntica estricta de nul·litats o rendiment màxim calen components addicionals. Com a pont robust entre estructures Dataset/Legacy i objectes de domini més moderns, però, en moltes bases de codi Delphi és precisament el pas pragmàtic que fa possible la modernització.
Si en una aplicació consolidada Delphi està encallat en punts de mapeig, qualitat de dades o en una modernització pas a pas, podem posar-ho en funcionament conjuntament de manera neta i adaptar-ho a la seva arquitectura: contacti’ns.
En l’àmbit tècnic també tenen un paper important Delphi Rtti Mapping i Attribute Mapping Delphi quan cal que integracions, fluxos de dades i evolució funcionin conjuntament de manera neta.
Parlar d’un projecte o d’una iniciativa de modernització amb Net-Base.