Quem opera software de negócios crescido em Delphi conhece o dilema: por um lado deseja-se objetos de domínio estruturados e camadas claras, por outro existem Datasets, Variants, importações CSV, payloads de interface ou uma REST-API que precisam ser “de alguma forma” mapeados para objetos. É aí que se chega rapidamente ao Delphi RTTI para mapeamento sem mágica: ou seja, mapeamento por reflection (RTTI = Run-Time Type Information, informações de tipo em tempo de execução), mas de forma que seja rastreável, bem depurável e que não dependa sorrateiramente de convenções ou joguinhos de nomes.
O ponto central: “mágica” normalmente não surge pela RTTI em si, mas por regras implícitas. Se as regras de mapeamento estiverem explícitas em atributos, as conversões centralizadas e os erros indicarem uma causa clara, a RTTI vira uma ferramenta em vez de uma surpresa.
Por que o mapeamento RTTI em Delphi frequentemente falha
O mapeamento baseado em RTTI raramente fracassa pela ideia em sistemas reais; falha por condições de contorno:
- Formatos de dados legados: Null/Empty/0 não estão claramente separados, tipos de campo mudam, strings contêm “N/A”.
- Convenções que se instalam: “o campo tem o mesmo nome da propriedade” funciona até o primeiro alias, join ou nome de propriedade refatorado.
- Difícil de depurar: quando um mapper “simplesmente não define nada”, a causa some depois. Em operação isso é altamente problemático.
- Mitos de desempenho: RTTI é rotulada de forma generalizada como “lenta”, embora na maior parte das vezes a falta de cache seja o problema.
Uma abordagem viável deve, portanto, (1) ter metadados de mapeamento explícitos, (2) tratar conversão e semântica de nulo de forma clara, (3) fornecer mensagens de erro e saídas de debug e (4) armazenar em cache as informações RTTI.
Delphi RTTI para mapeamento sem mágica: princípios de design
O padrão a seguir é propositadamente “sem surpresas” no melhor dos sentidos: regras são visíveis, efeitos colaterais limitados, e pode ser aplicado gradualmente em módulos existentes.
- Atributos em vez de convenção de nomes: a propriedade recebe um atributo que nomeia a coluna de origem.
- Opt-in: apenas propriedades marcadas são definidas. Sem surpresas por “todas as propriedades publicadas”.
- Conversão em um único ponto: Variant/String/Integer/Boolean/Enum/Nullable são mapeados centralmente.
- Modo de depuração: opcionalmente registra-se quais campos foram definidos/ignorados — com motivo.
- Cache de RTTI: as partes mais custosas (lista de propriedades, avaliação de atributos) são preparadas por tipo.
Trecho de código: mapeamento por atributo com RTTI, cache e debug
O snippet mapeia uma linha (p. ex. de BDE-Ablosung mit nativer Anbindung via TDataSet) para um objeto. Em vez de vincular o mapper diretamente a TField, usamos uma pequena interface Reader. Na prática isso é valioso, pois posteriormente você pode reutilizar a mesma lógica para JSON, INI, CSV ou respostas de API.
unit RttiMapping;
interface
uses
System.SysUtils, System.Rtti, System.TypInfo, System.Generics.Collections,
System.Variants;
type
// Mapeamento explícito: Propriedade <- nome de origem
MapFromAttribute = class(TCustomAttribute)
private
FName: string;
public
constructor Create(const AName: string);
property Name: string read FName;
end;
// Pequena abstração: fornecer valor + distinguir 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: apenas propriedades com atributo
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(‚Falha na conversão para Booleano: „%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 do enum fora do intervalo: %d‘, [Ord]);
Exit(Ord);
end;
Name := VarToStr(V);
Ord := GetEnumValue(AEnumType.Handle, Name);
if Ord < 0 then
raise ERttiMappingError.CreateFmt(‚Nome do enum desconhecido: „%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;
// Conversão intencionalmente seletiva: preferível falhar claramente do que silenciosamente „de algum modo“.
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(‚Mapeamento de Set não implementado para %s‘, [AProp.Name]);
tkClass:
raise ERttiMappingError.CreateFmt(‚Mapeamento de propriedade de classe não implementado para %s‘, [AProp.Name]);
else
raise ERttiMappingError.CreateFmt(‚TypeKind não suportado (%s) para %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 ou Target é 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(‚Fonte ausente: „%s“ para a propriedade %s‘,
[M.SourceName, M.Prop.Name]);
Continue;
end;
if AReader.IsNull(M.SourceName) then
begin
if moIgnoreNull in AOptions then
Continue;
// Sem mecanismo Nullable/Optional, não é possível atribuir NULL de forma sensata.
Continue;
end;
V := AReader.GetValue(M.SourceName);
try
SetPropertyValue(ATarget, M.Prop, V);
if moDebug in AOptions then
begin
Msg := Format(‚Mapped %s <- %s (%s)‘, [M.Prop.Name, M.SourceName, VarTypeAsText(VarType(V))]);
OutputDebugString(PChar(Msg));
end;
except
on E: Exception do
raise ERttiMappingError.CreateFmt(‚Erro de mapeamento em %s <- %s: %s‘,
[M.Prop.Name, M.SourceName, E.Message]);
end;
end;
end;
end.
Para que serve
Você obtém um mapeamento que pode ser avaliado de forma clara em revisões de código:
- Cada propriedade mapeada é destacada visualmente (atributo).
- A conversão é centralizada, tornando-a consistente e testável.
- Mensagens de erro indicam qual propriedade e qual fonte estão afetadas.
- Um modo de depuração fornece, em caso de dúvida, a cadeia de evidências, sem que você precise usar breakpoints no ambiente de produção.
Condições de contorno e armadilhas típicas
- NULL-Semantik: Sem um conceito próprio de nulabilidade (por exemplo
Nullable<T>ou Option-Types) atribuir „NULL“ não é inequívoco. No snippet, NULL é ignorado por padrão. Isso é conservador e evita sobrescritas silenciosas. - Duração do TRttiContext: Construímos o cache uma vez por tipo e descartamos o Context em seguida. Isso é comum. Importante: não criar um novo RTTI-Context por atribuição de campo.
- Threading: O cache é protegido via Monitor. Em mapeamentos altamente paralelos (por exemplo REST-Server) você deve também verificar se constrói o cache já na inicialização (Preload), para reduzir contenção de bloqueios.
- PropertyType Kind:
tkClassetkSetnão estão implementados intencionalmente. Para objetos aninhados você deve mapear recursivamente (com política clara) ou atribuir manualmente de forma deliberada. - Locale-Fallen:
varDoubleviaVarAsTypeé relativamente robusto, mas strings como „1,23“ vs. „1.23“ ainda são um problema. Se suas fontes entregam strings, um parser próprio (com Culture definida) costuma ser melhor.
Variante para FireDAC e TDataSet: Reader-Adapter em vez de acoplamento do Mapper
Em aplicações BDE-Ablosung mit nativer Anbindung ou nas clássicas VCL/Win32 a fonte é frequentemente um TDataSet. Em vez de ligar o Mapper a TField, implemente um adaptador que satisfaça a interface IValueReader. Vantagem: o Mapper permanece independente do acesso a dados (importante se você deslocar o acesso a dados posteriormente para services ou para um 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;
Com isso, um mapeamento concreto fica assim:
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;
Onde a abordagem vale a pena – e onde não
Esse padrão compensa tipicamente em três situações:
- Modernização por etapas: pretende introduzir objetos de domínio sem refazer imediatamente todo o acesso a dados (clássico em Delphi Modernização em aplicações legadas).
- Fronteiras de interface: importações CSV/Excel, payloads REST ou fontes de dados „mistas“ requerem conversão robusta e mensagens de erro claras.
- Manutenibilidade em equipe: atributos tornam as regras de mapeamento visíveis e auditáveis, o que vale ouro em bases de código maiores.
Existem também limites claros para o uso:
- Grafos de objeto complexos (coleções filhas, referências cíclicas) não devem ser mapeados de forma „automágica“. Aqui, código explícito ou um padrão separado de assembler/factory costuma ser mais estável.
- Rotas críticas de alto throughput (p.ex. ETL de grande volume) beneficiam-se mais de mapeadores gerados por código ou de mapeamento otimizado manualmente, mesmo com RTTI em cache.
- Nullable/Optional é um tema próprio. Se for necessário distinguir entre „não existente“, „NULL“ e „Default“, isso deve ser expresso no modelo de domínio, não escondido no mapper.
Enquadramento na arquitetura e operação
Do ponto de vista arquitetural, este mapeador é um componente de infraestrutura na fronteira entre representação de dados e domínio. Ele não substitui uma separação em camadas limpa, mas pode torná‑la possível: o acesso a dados (FireDAC, SQL, Views) pode permanecer pragmático, enquanto o domínio se mantém consistente. Em sistemas em camadas (frequentemente referidos como Layer-3 arquitetura: UI, Domain/Services, infraestrutura) o mapeador pertence à infraestrutura e é utilizado por serviços, não por formulários de UI.
Do ponto de vista operacional: não ative moDebug permanentemente em serviços de produção; use-o de forma seletiva. Para problemas de dados de difícil reprodução, é útil dispor de um caminho de diagnóstico comutável (configuração, feature flag). Caso contrário, corre‑se o risco de volume excessivo de logs e efeitos colaterais.
Conclusão: RTTI sim, mas apenas com diretrizes claras
Delphi RTTI para mapeamento sem mágica funciona bem quando você utiliza RTTI como ferramenta para metadados declarativos — não como um convite a heurísticas ocultas. Atributos como opt-in, conversão centralizada, cache por tipo e mensagens de erro compreensíveis fazem a transição de “opaco” para “operacional”. A abordagem não é propositalmente universal: para grafos aninhados, semântica de nulos estrita ou desempenho máximo, serão necessários componentes adicionais. Como ponte robusta entre estruturas Dataset/Legacy e objetos de domínio mais modernos, porém, em muitas bases de código Delphi é exatamente o passo pragmático que torna a modernização possível.
Se, numa aplicação Delphi legada, você está preso em arestas de mapeamento, qualidade de dados ou modernização por etapas, podemos configurar isso de forma ordenada e integrá-lo à sua arquitetura: entre em contato.
No âmbito funcional, Delphi Rtti Mapping e Attribute Mapping Delphi também desempenham um papel importante quando integrações, fluxos de dados e evolução precisam operar de forma coordenada.
Discutir projeto ou iniciativa de modernização com Net-Base.