Net-Base Revista

08.05.2026

Delphi RTTI para mapeamento sem mágica: baseado em atributos, depurável e compatível com sistemas legados

Um padrão de mapeamento pragmático com Delphi RTTI: atributos em vez de convenções, conversões controladas, mensagens de erro claras e um modo de depuração que realmente ajuda em produção. Com trechos de código-fonte para mapeamento de Dataset ou Record para objeto sem mágica oculta.

08.05.2026

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: tkClass e tkSet não estão implementados intencionalmente. Para objetos aninhados você deve mapear recursivamente (com política clara) ou atribuir manualmente de forma deliberada.
  • Locale-Fallen: varDouble via VarAsType é 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).

Delphi
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:

Delphi
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:

  1. 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).
  2. Fronteiras de interface: importações CSV/Excel, payloads REST ou fontes de dados „mistas“ requerem conversão robusta e mensagens de erro claras.
  3. 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.

Partilhar publicação

Compartilhar esta publicação diretamente

LinkedIn, X, XING, Facebook, WhatsApp e e‑mail estão imediatamente disponíveis. Para o Instagram, preparamos o link e um texto curto de imediato.

E-mail

O Instagram abre numa nova aba. O link e o texto curto são copiados previamente para a área de transferência.