Eis que estou de volta! E agora vai ser bem complicado arrumar uma desculpa pela demora… portanto, me perdoem. É o que me resta a fazer.

No recesso do Carnaval, após um tempo sem olhar o blog, aproveitei para dar uma atualizada em seus plugins. Ao acessar a área de administração, me deparei com vários pedidos relacionados à nossa série de ORM. Mais especificamente sobre a questão: Relacionamentos.

Até hoje eu particularmente não senti falta deste recurso, visto que a princípio meu maior desejo era acabar com o uso – interminável! – dos métodos e parâmetros das querys (ParamByName, FieldByName, AsInteger, AsString, AsDateTime…). Além disso, queria abstrair o máximo possível o processo de CRUD no meu sistema, evitando assim ficar dependente de componentes de conexão como o IBX, UIB, FireDac, etc.

Creio que para os objetivos citados, o nosso projeto já cumpre muito bem o seu papel, de forma simples e leve. Mas como vocês têm me pedido, vou iniciar o trato do relacionamento.

Será interessante acessar os fontes do projeto para ir acompanhando, visto que apenas colocarei trechos dos códigos e não a unit completa. Além disso, neste post me aterei, no que se refere a suíte de componentes, a unit que trata do FireDac, ou seja, Lca.Orm.FireDac. Cabendo a você repassar para a sua suíte o que for demonstrado aqui.

Fontes atualizados estão em: https://github.com/luizsistemas/ORM-Basico-Delphi

Relacionamento 1:N e Tabelas Exemplo

Não é o foco abordar neste post a normalização de tabelas e cardinalidade. O objetivo será única e exclusivamente definir que tipo de relacionamento iremos trabalhar.

Afim de dar um insight, um direcionamento a quem pretende aventurar-se por estas águas, eu vou mostrar uma alternativa de como implementar o processamento do relacionamento cuja a cardinalidade seja 1:N.

Para os nossos testes, utilizaremos o exemplo de um funcionário, que se encontra em uma cidade e está lotado em um departamento. O funcionário está no lado N (vários), visto que UM departamento pode ter 0, 1 ou VÁRIOS funcionários (vá entender o porquê de um departamento não ter ninguém, mas…). Da mesma forma, UMA cidade por ter 0, 1 ou VÁRIOS funcionários.

No Github do nosso projeto, na pasta teste/bd, contém o script de criação destas tabelas, ou seja, tabela FUNCIONARIO e DEPTO (a tabela Cidade já havia sido criada no início da série) e seus relacionamentos.

Refatorar é preciso

Teremos de fazer alguns ajustes para, aí sim, partir para a nova implementação.

As razões para isso se deve ao fato que antes, somente um comando era executado por vez por requisição. Por isso temos um objeto chamado FQuery em TDaoFireDac, que é global da classe, para execução de comandos SQL (inserts, updates e deletes).

Porém, este cenário deverá ser alterado, visto que poderão ocorrer várias chamadas a este objeto numa mesma requisição. Por exemplo, ao alterar funcionário, poderemos também inserir ou alterar um departamento. O mesmo vale para cidade. Sendo o FQuery global, inevitavelmente haverão conflitos, visto que um mesmo objeto estará recebendo várias instruções SQL concomitantemente numa mesma requisição.

Portanto, devemos mudar a forma inicial pensada para o FQuery. Este não mais poderá ser uma variável global. E fazendo uma análise bem rápida, percebemos também que nós temos métodos que constroem querys, ou seja, os métodos GetID, GetMax e GetRecordCount, cujos objetos deveriam ser destruídos dentro do escopo do método, e não é isso que acontece atualmente. Eles continuam em memória até a finalização da aplicação por conta do parâmetro, “Application”, passado na criação de cada objeto query.

Ajustes finos

Devemos primeiro, renomear a interface IDaoBase para IDaoBaseComandos, em Lca.Orm.Base. E a classe TQueryFireDac, para TParamsFireDac.

Além disso, dando uma passada pelas units que compõem o projeto, iremos retirar comentários desnecessários do código e corrigir o nome de algumas variáveis e métodos (CamelCase e padronização). Ajustes finos que melhoram o entendimento e leitura do código. Acompanhe pelo fontes para visualizar todas as pequenas e importantes alterações efetuadas.

Nova Interface e nova Classe: IQuery e TQueryFD

Vamos criar em Lca.Orm.Base a interface IQuery:

  IQuery = interface
    ['{52E7E2A0-C3E7-41FC-86B1-50A50220C474}']
    function Sql: TStrings;
    function Dataset: TDataset;
    function RowsAffected: Integer;
    function RecordCount: Integer;
    procedure Executar;
    procedure Abrir;
  end;

Essa interface terá a responsabilidade de resolver o problema de múltiplas requisições SQL gerarem conflitos, além de evitar que os objetos criados nos métodos que constroem querys (GetId, GetMax, GetRecordCount, ConsultaSql, ConsultaAll, etc.) ficassem em memória até o fechamento da aplicação.

Partiremos para a implementação da classe concreta TQueryFD, Lca.Orm.Comp.FireDac:

...  
TQueryFD = class(TInterfacedObject, IQuery)
  private
    FQuery: TFDQuery;
    FSql: TStrings;
    function RecordCount: Integer;
    procedure Abrir;
  public
    constructor Create(Conexao: TFDConnection; Transacao: TFDTransaction);
    destructor Destroy; override;
    function Sql: TStrings;
    function Dataset: TDataset;
    function RowsAffected: Integer;
    procedure Executar;
  end;
...
implementation

...
{ TQueryFireDac }

constructor TQueryFD.Create(Conexao: TFDConnection; Transacao: TFDTransaction);
begin
  FQuery := TFDQuery.Create(nil);
  FQuery.Connection := Conexao;
  FQuery.Transaction := Transacao;
  FSql := FQuery.SQL;
end;

destructor TQueryFD.Destroy;
begin
  FQuery.Free;
  inherited;
end;

procedure TQueryFD.Executar;
begin
  FQuery.Prepare;
  FQuery.ExecSQL;
end;

procedure TQueryFD.Abrir;
begin
  FQuery.Open;
end;

function TQueryFD.Dataset: TDataset;
begin
  Result := FQuery;
end;

function TQueryFD.RowsAffected: Integer;
begin
  Result := FQuery.RowsAffected;
end;

function TQueryFD.RecordCount: Integer;
begin
  Result := FQuery.RecordCount;
end;

function TQueryFD.Sql: TStrings;
begin
  Result := FSql;
end;

Template Method

Devemos reorganizar TDaoFireDac. A ideia aqui é criar uma classe base chamada TDaoBase. Esta nova classe conterá tudo o que não é específico de uma suíte de componente de acesso a dados. Ou seja, será uma classe abstrata contendo alguns métodos implementados. Um design pattern que se encaixa nesta definição é o Template Method. TDaoBase será o nosso template, e as classes filhas ( TDaoFireDac e TDaoIbx, etc. ) passarão herdar dela.

Abra Lca.Orm.Base, e declare essa nova classe:

...
IDaoBaseComandos = interface  //antigo IDaoBase
    ['{6E2AFB66-465B-4924-9221-88E283E81EA7}']
    function GerarClasse(ATabela, ANomeUnit: string; ANomeClasse: string = ''): string;
    function GetID(ATabela:TTabela; ACampo: string): Integer; overload;
    function GetID(Generator: string): Integer; overload;
    function Inserir(ATabela: TTabela): Integer; overload;
    function Inserir(ATabela: TTabela; ACampos: array of string;
... 
 TDaoBase = class(TInterfacedObject)
  private
  protected
    FSql: IBaseSql;
    FDataSet: TDataSet;
    FParams: IQueryParams;
    procedure SetDataSet(const Value: TDataSet);
    procedure BuscarRelacionamento(ATabela: TTabela; APropRtti: TRttiProperty;
      Relacionamento: TCustomAttribute; AQuery: TDataSet); virtual; abstract;
    procedure AtualizarRelacionamento(ATabela: TTabela; APropRtti: TRttiProperty;
      Relacionamento: TCustomAttribute; AQuery: TDataSet); virtual; abstract;
    procedure SetarDadosFromDataSet(ADataset: TDataset; PropRtti: TRttiProperty; Objeto: TValue; Campo: string);
  public
    constructor Create;
    property DataSet: TDataSet read FDataSet write SetDataSet;
  end;

Note que TDaoBase descende de TInterfacedObject. O motivo você entenderá quando da declaração da classe filha TDaoFiredac. Essa classe possui dois métodos abstratos, AtualizarRelacionamento e BuscarRelacionamento. Sendo abstrados, não são implementados diretamente nela, mas sim nas classes filhas. Segue implementação dos demais métodos de TDaoBase:

...

{ TDaoBase }

constructor TDaoBase.Create;
begin
  FSql := TPadraoSql.Create;
end;

procedure TDaoBase.SetarDadosFromDataSet(ADataset: TDataset; PropRtti: TRttiProperty; Objeto: TValue; Campo: string);
var
  DataType: TFieldType;
begin
  DataType := ADataSet.FieldByName(Campo).DataType;
  case DataType of
    ftInteger:
      begin
        PropRtti.SetValue(Objeto.AsObject,
          TValue.FromVariant(ADataSet.FieldByName(Campo).AsInteger));
      end;
    ftString, ftWideString, ftWideMemo:
      begin
        PropRtti.SetValue(Objeto.AsObject,
          TValue.FromVariant(ADataSet.FieldByName(Campo).AsString));
      end;
    ftBCD, ftFMTBcd, ftFloat:
      begin
        PropRtti.SetValue(Objeto.AsObject,
          TValue.FromVariant(ADataSet.FieldByName(Campo).AsFloat));
      end;
    ftCurrency:
      begin
        PropRtti.SetValue(Objeto.AsObject,
          TValue.FromVariant(ADataSet.FieldByName(Campo).AsCurrency));
      end;
    ftDate, ftDateTime:
      begin
        PropRtti.SetValue(Objeto.AsObject,
          TValue.FromVariant(ADataSet.FieldByName(Campo).AsDateTime));
      end;
  else
    raise Exception.Create('Tipo de campo não conhecido: ' + PropRtti.PropertyType.ToString);
  end;
end;

procedure TDaoBase.SetDataSet(const Value: TDataSet);
begin
  FDataSet := Value;
end;

Em resumo movemos os objetos FSql e FDataSet da classe filha para a nova classe. Temos o método chamado SetarDadosFromDataSet, e como o próprio nome já diz, grava os dados vindos de um dataset e não de properties. Perceba que neste método os Datatypes são diferentes, pois vêm do TDataSet. Eis o motivo de precisarmos deste método.

Se observar atentamente, o mesmo poderia ter sido feito com a IQuery e TQueryFD. Porém, no atual estágio, nenhum dado poderia ser movido para a classe template. Então, no momento, não vejo vantagem de tal abordagem. Quem sabe futuramente.

Na declaração de TDaoFireDac, devemos fazer uma alteração, pois esta passará a implementar tanto TDaoBase quando IDaoBaseComandos. Agora você já sabe o motivo de TDaoBase herdar de TInterfacedObject. Segue código:

unit Lca.Orm.Comp.FireDac;

interface
...

 TDaoFireDac = class(TDaoBase, IDaoBaseComandos)
  private
    FConexao: TFDConnection;
    FTransacao: TFDTransaction;
  protected

...

Finalmente, implementando o relacionamento

Ajustes feitos, vamos ao que realmente é objeto de interesse deste post.

O primeiro passo será criar um atributo que guarde os dados do relacionamento: chave estrangeira (FK), tabela relacionada, chave primária (Pk) da tabela relacionada e o tipo de dado.

Vá em Lca.Orm.Atributos, e insira o atributo chamado AttFk:

unit Lca.Orm.Atributos;

interface

uses
  Lca.Orm.Base, Rtti, System.Classes, Data.DB;
...
AttFk = class(TCustomAttribute)
  private
    FCampoFk,
    FTabela,
    FPk: string;
    FTipo: TTypeKind;
  public
    constructor Create(CampoFk, Tabela, Pk: string; Tipo: TTypeKind = tkInteger);
    property CampoFk: string read FCampoFk;
    property Tabela: string read FTabela;
    property Pk: string read FPk;
  end;
...

Na implementação do constructor, apenas setamos os parâmetros recebidos nos fields correspondentes:

...
constructor AttFk.Create(CampoFk, Tabela, Pk: string; Tipo: TTypeKind);
begin
  FCampoFk := CampoFk;
  FTabela := Tabela;
  FPk := Pk;
  FTipo := Tipo;
end;
...

Sem segredo até aqui. Apesar de simples, isso nos dará tudo o que é preciso para atingirmos nosso objetivo.

O próximo passo será ir nos métodos Inserir, Salvar, Buscar e ConsultaGen, e inserir o código responsável por processar o relacionamento. Iniciemos pelo método Inserir:

function TDaoFireDac.Inserir(ATabela: TTabela; ACampos: array of string; AFlag: TFlagCampos): Integer;
var
  PropRtti: TRttiProperty;
  RttiType: TRttiType;
  AtribFk: AttFk;
  NomeTabela: string;
  Comando: IQuery;
begin
  try
    TAtributos.Get.ValidaTabela(ATabela, ACampos, AFlag);
    RttiType := TRttiContext.Create.GetType(ATabela.ClassType);
    NomeTabela := TAtributos.Get.PegaNomeTab(ATabela);
    Comando := TQueryFD.Create(FConexao, FTransacao);
    Comando.Sql.Text := FSql.GerarSqlInsert(NomeTabela, RttiType, ACampos, AFlag);
    for PropRtti in RttiType.GetProperties do
    begin
      if (Length(ACampos) > 0) then
      begin
        if not (TAtributos.Get.LocalizaCampo(PropRtti.Name, TAtributos.Get.PegaPks(ATabela))) then
        begin
          if ((AFlag = fcIgnore) and (TAtributos.Get.LocalizaCampo(PropRtti.Name, ACampos))) or
             ((AFlag = fcAdd) and (not TAtributos.Get.LocalizaCampo(PropRtti.Name, ACampos))) then
            Continue;
        end;
      end;
      AtribFk := TAtributos.Get.GetAtribFk(PropRtti);
      if Assigned(AtribFk) then
        AtualizarRelacionamento(ATabela, PropRtti, AtribFk, Comando.Dataset)
      else
        TAtributos.Get.ConfiguraParametro(PropRtti, PropRtti.Name, ATabela, Comando.Dataset, FParams);
    end;
    Comando.Executar;
    Result := Comando.RowsAffected;
  except
    raise;
  end;
end;

No código:

  • Criamos um objeto do tipo do atributo AttFk se no loop atual existe um atributo do tipo de relacionamento (26);
  • Se sim, executamos o método AtualizarRelacionamento (28). Este método foi definido na classe base como abstrato e ainda não foi implementado em TDaoFireDac;
  • Se não, processamos normalmente a property, como era feito antes (30);

Como mencionado acima, o método AtualizarRelacionamento ainda não consta em TDaoFireDac. Portanto, vamos resolver esta pendência. Vá na seção protected da classe e insira:

...
TDaoFireDac = class(TDaoBase, IDaoBaseComandos)
  private
    FConexao: TFDConnection;
    FTransacao: TFDTransaction;
  protected
    procedure AtualizarRelacionamento(ATabela: TTabela; APropRtti: TRttiProperty;
      Relacionamento: TCustomAttribute; AQuery: TDataSet); override;
...

Na implementação de AtualizarRelacionamento, temos:

...
procedure TDaoFireDac.AtualizarRelacionamento(ATabela: TTabela; APropRtti: TRttiProperty;
  Relacionamento: TCustomAttribute; AQuery: TDataSet);
var
  Objeto: TTabela;
  RttiType: TRttiType;
  Prop: TRttiProperty;
begin
  if (APropRtti.GetValue(ATabela).AsObject is TTabela) then
  begin
    Objeto := (APropRtti.GetValue(ATabela).AsObject as TTabela);
    RttiType := TRttiContext.Create.GetType(Objeto.ClassType);
    for Prop in RttiType.GetProperties do
    begin
      if CompareText(Prop.Name, AttFK(Relacionamento).Pk) = 0 then
      begin
        TAtributos.Get.ConfiguraParametro(Prop, AttFK(Relacionamento).CampoFk, Objeto, AQuery, FParams);
        Break;
      end;
    end;
    if GetRecordCount(Objeto, [AttFk(Relacionamento).Pk]) = 0 then
      Inserir(Objeto)
    else
      Salvar(Objeto);
  end;
end;

Segue explicação:

  • Verificamos se a propriedade é do tipo TTabela (9). Essa propriedade contém os dados da tabela relacionada;
  • Se sim, setamos o conteúdo da propriedade na variável Objeto (11);
  • Fazemos então um loop em suas propriedades em busca de sua chave primária (15);
  • Se é chave primária, seta o valor da propriedade no Objeto (17);
  • Finalizado o loop, de posse do valor (ou valores) da chave primária, definimos se insere ou se salva os dados na tabela relacionada (21).

Para o método Salvar, segue conforme foi feito no Inserir:

function TDaoFireDac.Salvar(ATabela: TTabela; ACampos: array of string; AFlag: TFlagCampos): Integer;
var
  PropRtti: TRttiProperty;
  RttiType: TRttiType;
  Comando: IQuery;
  AtribFk: AttFk;
begin
  try
    TAtributos.Get.ValidaTabela(ATabela, ACampos, AFlag);
    RttiType := TRttiContext.Create.GetType(ATabela.ClassType);
    Comando := TQueryFD.Create(FConexao, FTransacao);
    Comando.Sql.Text := FSql.GerarSqlUpdate(ATabela, RttiType, ACampos, AFlag);
    for PropRtti in RttiType.GetProperties do
    begin
      if (Length(ACampos) > 0) and not (TAtributos.Get.LocalizaCampo(PropRtti.Name, TAtributos.Get.PegaPks(ATabela))) then
      begin
        if ((AFlag = fcAdd) and (not TAtributos.Get.LocalizaCampo(PropRtti.Name, ACampos))) or ((AFlag = fcIgnore) and (TAtributos.Get.LocalizaCampo(PropRtti.Name, ACampos))) then
          Continue;
      end;
      AtribFk := TAtributos.Get.GetAtribFk(PropRtti);
      if Assigned(AtribFk) then
        AtualizarRelacionamento(ATabela, PropRtti, AtribFk, Comando.Dataset)
      else
        TAtributos.Get.ConfiguraParametro(PropRtti, PropRtti.Name, ATabela, Comando.Dataset, FParams);
    end;
    Comando.Executar;
    Result := Comando.RowsAffected;
  except
    raise;
  end;
end;

Nada de novo. Vamos para o método Buscar:

function TDaoFireDac.Buscar(ATabela: TTabela): Integer;
var
  Campo: string;
  PropRtti: TRttiProperty;
  RttiType: TRttiType;
  AtribFk: AttFk;
  Query: IQuery;
begin
  RttiType := TRttiContext.Create.GetType(ATabela.ClassType);
  Query := TQueryFD.Create(FConexao, nil);
  Query.Sql.Text := FSql.GerarSqlSelect(ATabela);
  for Campo in TAtributos.Get.PegaPks(ATabela) do
  begin
    for PropRtti in RttiType.GetProperties do
    begin
      if CompareText(PropRtti.Name, Campo) = 0 then
      begin
        TAtributos.Get.ConfiguraParametro(PropRtti, Campo, ATabela, Query.Dataset, FParams);
        Break;
      end;
    end;
  end;
  Query.Abrir;
  Result := Query.RecordCount;
  ATabela.Limpar;
  if Result > 0 then
  begin
    for PropRtti in RttiType.GetProperties do
    begin
      AtribFk := TAtributos.Get.GetAtribFk(PropRtti);
      if Assigned(AtribFk) then
        BuscarRelacionamento(ATabela, PropRtti, AtribFk, TFDQuery(Query.Dataset))
      else
        TAtributos.Get.SetarDadosTabela(PropRtti, PropRtti.Name, ATabela, Query.Dataset, FParams);
    end;
  end;
end;

A diferença é que neste método chamamos BuscarRelacionamento e não AtualizarRelacionamento. É preciso implementar este novo método em TDaoFireDac:

...
TDaoFireDac = class(TDaoBase, IDaoBaseComandos)
  private
    FConexao: TFDConnection;
    FTransacao: TFDTransaction;
  protected
    procedure AtualizarRelacionamento(ATabela: TTabela; APropRtti: TRttiProperty;
      Relacionamento: TCustomAttribute; AQuery: TDataSet); override;

    procedure BuscarRelacionamento(ATabela: TTabela; APropRtti: TRttiProperty;
      Relacionamento: TCustomAttribute; AQuery: TDataSet); override;
...

Implementação do método BuscarRelacionamento:

procedure TDaoFireDac.BuscarRelacionamento(ATabela: TTabela; APropRtti: TRttiProperty;
  Relacionamento: TCustomAttribute; AQuery: TDataSet);
var
  Contexto: TRttiContext;
  Objeto: TTabela;
  RttiType: TRttiType;
  Prop: TRttiProperty;
  KeyLocalized: Boolean;
begin
  KeyLocalized := False;
  if (APropRtti.GetValue(ATabela).AsObject is TTabela) then
  begin
    Contexto := TRttiContext.Create;
    try
      Objeto := (APropRtti.GetValue(ATabela).AsObject as TTabela);
      RttiType := Contexto.GetType(Objeto.ClassType);
      for Prop in RttiType.GetProperties do
      begin
        if CompareText(Prop.Name, AttFk(Relacionamento).Pk) = 0 then
        begin
          TAtributos.Get.SetarDadosTabela(Prop, AttFk(Relacionamento).CampoFk, Objeto, AQuery, FParams);
          KeyLocalized := True;
          Break;
        end;
      end;
      if KeyLocalized then
        Buscar(Objeto);
    finally
      Contexto.Free;
    end;
  end;
end;

Bem simples, não é mesmo? Assim como no AtualizarRelacionamento, setamos a chave primário no Objeto (21). Se foi localizado a chave primária, buscamos o Objeto (27).

Por fim, o método ConsultaGen. E este eu lhes digo que tem uma pegadinha. Vamos a ele:

function TDaoFireDac.ConsultaGen<T>(ATabela: TTabela; ACamposWhere: array of string): TObjectList<T>;
var
  Contexto: TRttiContext;
  Campo: string;
  TipoRtti: TRttiType;
  PropRtti: TRttiProperty;
  Query: IQuery;
  Objeto: TValue;
  AtribFk: AttFk;
begin
  Contexto := TRttiContext.Create;
  try
    Result := TObjectList<T>.Create;
    TipoRtti := Contexto.GetType(ATabela.ClassType);
    Query := TQueryFD.Create(FConexao, nil);
    Query.SQL.Text := FSql.GerarSqlSelect(ATabela, ACamposWhere);
    for Campo in ACamposWhere do
    begin
      if not TAtributos.Get.PropExiste(Campo, PropRtti, TipoRtti) then
        raise Exception.Create('Campo ' + Campo + ' não existe no objeto!');
      for PropRtti in TipoRtti.GetProperties do
      begin
        if CompareText(PropRtti.Name, Campo) = 0 then
        begin
          TAtributos.Get.ConfiguraParametro(PropRtti, Campo, ATabela, Query.Dataset, FParams);
          Break;
        end;
      end;
    end;
    Query.Abrir;
    while not Query.Dataset.Eof do
    begin
      Objeto := TObjectFactory<T>.Get.CriarInstancia;
      TipoRtti := Contexto.GetType(ATabela.ClassType);
      for PropRtti in TipoRtti.GetProperties do
      begin
        AtribFk := TAtributos.Get.GetAtribFk(PropRtti);
        if Assigned(AtribFk) then
          BuscarRelacionamento(Objeto.AsType<T>, PropRtti, AtribFk, Query.Dataset)
        else
          SetarDadosFromDataset(Query.Dataset, PropRtti, Objeto.AsType<T>, PropRtti.Name);
      end;
      Result.Add(Objeto.AsType<T>);
      Query.Dataset.Next;
    end;
  finally
    Contexto.Free;
  end;
end;

Note que em vez de:

Objeto := (APropRtti.GetValue(ATabela).AsObject as TTabela);

Temos:

Objeto := TObjectFactory<T>.Get.CriarInstancia;

Ou seja, tivemos de utilizar uma fábrica de objeto (que ainda será criada, diga-se de passagem) em detrimento da primeira opção. O motivo é simples, pois veja que no primeiro, instanciamos o objeto como sendo do tipo TTabela. Tudo certo se não fosse por um pequeno detalhe: o Objeto terá todo o comportamento previsto em uma classe TTabela, inclusive seu construtor. E é aí que inviabiliza esta forma de utilização. Pois, vejamos a entidade Funcionario, não mencionada até o momento, mas em tempo o faço abaixo:

unit Funcionario;

interface

uses
  Lca.Orm.Atributos, Lca.Orm.Base, Cidade, Depto;

type
  [AttTabela('FUNCIONARIO')]
  TFuncionario = class(TTabela)
  private
    FBairro: string;
    FSalario: Currency;
    FId: Integer;
    FDepto: TDepto;
    FNome: string;
    FCidade: TCidade;
    FEndereco: string;
    FCpf: string;
  public
    constructor Create;
    destructor Destroy; override;
    [AttPK]
    property Id: Integer read FId write FId;
    property Nome: string read FNome write FNome;
    property Cpf: string read FCpf write FCpf;
    property Endereco: string read FEndereco write FEndereco;
    property Bairro: string read FBairro write FBairro;
    [AttNotNull('Cidade')]
    [AttFk('IDCIDADE', 'CIDADE', 'ID')]
    property Cidade: TCidade read FCidade write FCidade;
    [AttNotNull('Departamento')]
    [AttFk('IDDEPTO', 'DEPTO', 'ID')]
    property Depto: TDepto read FDepto write FDepto;
    property Salario: Currency read FSalario write FSalario;
  end;

implementation

{ TFuncionario }

constructor TFuncionario.Create;
begin
  FCidade := TCidade.Create;
  FDepto := TDepto.Create;
end;

destructor TFuncionario.Destroy;
begin
  FCidade.Free;
  FDepto.Free;
  inherited;
end;

Para esta classe não utilizamos o construtor padrão de um TObject, e sim um modificado para a instanciação dos objetos das tabelas relacionadas.

Entende como isso muda tudo? Se utilizarmos o construtor de TTabela, não estaremos instanciando os objetos relacionados a TFuncionario, e ao tentar acessar os atributos Cidade e Departamento desta classe obteremos um Access Violation na fuça!

Portanto, é necessário criar a fábrica de objetos. Para isso, abra Lca.Orm.Base e declare a interface e classe:

...
  IObjectFactory<T:TTabela> = interface
    ['{50ACF26D-52D9-490A-B22D-F672B344AB94}']
    function CriarInstancia: T;
  end;

  TObjectFactory<T:TTabela> = class (TInterfacedObject, IObjectFactory<T>)
  public
    class function Get: IObjectFactory<T>;
    function CriarInstancia: T;
  end;
...

Segue implementação dos métodos:

...
{ TObjectFactory<T> }

function TObjectFactory<T>.CriarInstancia: T;
var
  AValue: TValue;
  Contexto: TRttiContext;
  TipoRtti: TRttiType;
  MetodoCriar: TRttiMethod;
  TipoInstancia: TRttiInstanceType;
begin
  Contexto := TRttiContext.Create;
  try
    TipoRtti := Contexto.GetType(TypeInfo(T));
    MetodoCriar := TipoRtti.GetMethod('Create');
    if Assigned(MetodoCriar) and TipoRtti.IsInstance then
    begin
      TipoInstancia := TipoRtti.AsInstance;
      AValue := MetodoCriar.Invoke(TipoInstancia.MetaclassType, []);
      Result := AValue.AsType<T>;
    end;
  finally
    Contexto.Free;
  end;
end;

class function TObjectFactory<T>.Get: IObjectFactory<T>;
begin
  Result := TObjectFactory<T>.Create;
end;
...

O método CriarInstancia basicamente localiza o método Create do tipo T (genérico) e o invoca (executa). O resultado é retornado na Linha 20. Assim, garantimos que o constructor chamado será o do tipo passado (T) e não o constructor padrão de TTabela. Como, ao consultar, estaremos fazendo algo do tipo:

Lista := Dao.ConsultaGen<TFuncionario>(Funcionario, ...

Ou seja, passamos o tipo TFuncionario, o construtor chamado será de TFuncionario e não de TTabela. Espero que eu tenha sido claro. 😒

De código para o relacionamento, pelo menos para início dos testes, é isso.

Testes

Junto com os fontes, vai um novo form (ufrmTesteRelacionamento) para os testes:

Com relação ao código do botão de busca genérica, cabe um pequeno adendo:

var
  Lista: TObjectList<TFuncionario>;
  I: Integer;
begin
  Memo.Lines.Clear;
  Memo.Lines.Add('Teste do método ConsultaGen, obtendo como retorno objeto(s) do tipo especificado.');
  Memo.Lines.Add('');
  Funcionario.Limpar;
  Funcionario.Id := StrToIntDef(edCodFunc.Text, 0);
  Lista := Dao.ConsultaGen<TFuncionario>(Funcionario, ['Id']);
  try
    for I := 0 to Lista.Count - 1 do
    begin
      Funcionario.CopyProps(Lista.Items[I]);
      Memo.Lines.Add('Registro no DataSet: ' + IntToStr(Funcionario.Id));
      Memo.Lines.Add('');
      AtualizaForm;
    end;
  finally
    Lista.Free;
  end;
end;

Note que na linha 14, foi adicionado um novo método para agilizar a cópia dos dados de um objeto para outro. O método CopyProps varre a propriedade de objeto passado no parâmetro e repassa os valores para objeto que chamou o método. Segue o código (unit Lca.Orm.Base, classe TTabela):

...
  TTabela = class
  public
    procedure Limpar;
    procedure CopyProps(From: TTabela);
  end;
...

implementation

...
procedure TTabela.CopyProps(From: TTabela);
var
  Contexto: TRttiContext;
  TipoRtti, TipoFrom: TRttiType;
  PropRtti, PropFrom: TRttiProperty;
begin
  Contexto := TRttiContext.Create;
  try
    TipoRtti := Contexto.GetType(Self.ClassType);
    TipoFrom := Contexto.GetType(From.ClassType);
    for PropRtti in TipoRtti.GetProperties do
    begin
      for PropFrom in TipoFrom.GetProperties do
        if SameText(PropFrom.Name, PropRtti.Name) then
        begin
          if PropRtti.PropertyType.TypeKind = tkClass then
          begin
            (PropRtti.GetValue(Self).AsObject as TTabela).CopyProps((PropFrom.GetValue(From).AsObject as TTabela));
          end
          else
            PropRtti.SetValue(Self, PropFrom.GetValue(From));
          Break;
        end;
    end;
  finally
    Contexto.Free;
  end;
end;

Nada de novo, temos apenas um loop que varre as propriedades e repassa para o objeto que chamou o método. Porém, na linha 27, verificamos se o tipo de dado é tkClass, ou seja, verificamos se é uma classe relacionada a uma tabela, se sim, recursivamente, chamamos o método CopyProps, e assim atualizamos a tabela relacionada também.

No código do botão de inserir:

var
  Registros: Integer;
begin
  Memo.Clear;
  Memo.Lines.Add('Teste do método Inserir.');
  Memo.Lines.Add('');
  FormToObjetos;
  Dao.StartTransaction;
  try
    Funcionario.Id := Dao.GetID(Funcionario, 'Id');
    Registros := Dao.Inserir(Funcionario);
    Dao.Commit;
    AtualizaForm;
    Memo.Lines.Add(Format('Registro inserido: %d', [Registros]));
  except
    on E: Exception do
    begin
      Dao.RollBack;
      ShowMessage('Ocorreu um problema ao executar operação: ' + e.Message);
    end;
  end;
end;

Nele, chamamos o método FormToObjetos, que simplesmente irá pegar os valores inseridos nos edits do form e repassar para objeto Funcionario. O grande destaque é que, na linha 11, apenas chamando o método inserir para um objeto, que no caso é o Funcionario, já irá atualizar também as tabelas Depto e Cidade. Segue código de FormToObjetos:

procedure TfrmTesteRelacionamento.FormToObjetos;
var
  Fmt: TFormatSettings;
begin
  Funcionario.Id := StrToIntDef(edCodFunc.Text, 0);
  Funcionario.Nome := edNomeFunc.Text;
  Funcionario.Cpf := edCpf.Text;
  Funcionario.Endereco := edEndereco.Text;
  Funcionario.Bairro := edBairro.Text;
  Funcionario.Cidade.Id := StrToIntDef(edCodCidade.Text, 0);
  Funcionario.Cidade.Nome := edNomeCidade.Text;
  Funcionario.Cidade.Uf := edUF.Text;
  Funcionario.Cidade.Ibge := StrToIntDef(edIbge.Text, 0);
  Funcionario.Depto.Id := StrToIntDef(edCodDep.Text, 0);
  Funcionario.Depto.Nome := edNomeDep.Text;
  Fmt := TFormatSettings.Create;
  Fmt.DecimalSeparator := ',';
  Funcionario.Salario := StrToFloat(StringReplace(edSalario.Text, '.', '', [rfReplaceAll]), Fmt);
end;

Estes são os destaques que eu achei importante pontuar na hora dos testes.

Considerações finais

Este post já está bem extenso, então irei parar por aqui. No Github ( https://github.com/luizsistemas/ORM-Basico-Delphi ), você encontra os fontes atualizados.

Com relação ao relacionamento, já conseguimos consultar um objeto e este já vem com os objetos relacionados a ele. Da mesma forma, ao inserir e salvar. Para melhor desempenho, dica de uma possível alteração, seria a de implementar o design pattern Lazy Loading, e somente carregar os dados das tabelas relacionadas quando este for acessado. Ou então, na alteração de dados, ter uma flag para informar se deseja ou não atualizar determinada tabela. São, digamos, ajustes bem vindos. Mas por ora, fico por aqui.

Abraços.

Desenvolvedor de software desde 1995. Em 1998, abriu sua própria empresa, a Lukas Sistemas, desde então passou a atender diversas empresas, principalmente autopeças. Apaixonado por Delphi, porém não o impede de flertar com outras linguagens sempre que possível. Mora na cidade de Balsas/MA com sua esposa e dois filhos.

2 thoughts on “Relacionamentos – Que tal um ORM Básico? Parte 18”

  1. Olá Luiz, Gostaria de ver com você um tema que pouco amplo na minha opinião sobre o design patterns MVP (Model-View-Presenter) para sistemas em pascal Delphi.

    Gosto muito do seu projeto de ORM para facilitar o uso do MCV mas gostaria de ir mais além com a utilização do MVP sendo aplicada na prática.

    Quando puder comentar e talvez criar uma postagem sobre o assunto fica a minha sugestão!

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Esse site utiliza o Akismet para reduzir spam. Aprenda como seus dados de comentários são processados.