No último artigo, deixei um pequeno desafio. Não recebi retorno… mas sei que algumas pessoas ficaram curiosas a respeito do questionamento que fiz. Vamos relembrar:

O código do método Excluir (abaixo), deixou a impressão de que algo poderia ser melhorado quando projetamos a sua base para os métodos Inserir e Salvar?

function TDaoUib.Excluir(ATabela: TTabela): Integer;
var
  NomeTab: string;
  CamposPk: TResultArray;
  Sep: string; //separador
  Campo: string;
 
  Contexto  : TRttiContext;
  TipoRtti : TRttiType;
  PropRtti : TRttiProperty;
begin
  NomeTab  := PegaNomeTab(ATabela);
 
  CamposPk := PegaPks(ATabela);
 
  Contexto  := TRttiContext.Create;
  try
    TipoRtti := Contexto.GetType( ATabela.ClassType );
 
    with Qry do
    begin
      close;
      SQL.Clear;
      sql.Add('Delete from ' + NomeTab);
      sql.Add('Where');
      //percorrer todos os campos da chave primária
      Sep := '';
      for Campo in CamposPk do
      begin
        sql.Add(Sep+ Campo + '= :' + Campo);
        Sep := ' and ';
        // setando os parâmetros
        for PropRtti in TipoRtti.GetProperties do
          if CompareText(PropRtti.Name, Campo) = 0 then
            begin
              ConfigParametro(Qry, PropRtti, Campo, ATabela); // <-- recortamos o case e no seu lugar inserimos uma procedure
            end;
      end;
      //executa delete
      Prepare();
      ExecSQL;
      Result := RowsAffected;
    end;
  finally
    Contexto.free;
  end;
end;

Eu já havia criado o método ConfigParametro objetivando evitar a repetição de código:

procedure TDaoUib.ConfigParametro(AQuery: TUIBQuery; AProp: TRttiProperty;
  ACampo: string; ATabela: TTabela);
begin
  with AQuery do
  begin
    case AProp.PropertyType.TypeKind of
      tkInt64,
      tkInteger:
      begin
        Params.ByNameAsInteger[ACampo] :=
           AProp.GetValue(ATabela).AsInteger;
      end;
      tkChar,
      tkString,
      tkUString:
      begin
        Params.ByNameAsString[ACampo] := 
           AProp.GetValue(ATabela).AsString;
      end;
      tkFloat:
      begin
         Params.ByNameAsCurrency[ACampo] := 
           AProp.GetValue(ATabela).AsCurrency;
      end;
      tkVariant:
      begin
        Params.ByNameAsVariant[ACampo] := 
          AProp.GetValue(ATabela).AsVariant;
      end;
    else
      raise Exception.Create('Tipo de campo não conhecido: ' + 
         AProp.PropertyType.ToString);
    end;
  end;

Este método trata da configuração adequada de cada parâmetro da query. Isto seria necessário também nos métodos Inserir e Salvar (que ainda serão implementados). Desta forma, alcançamos nosso objetivo, porém em parte…

Olhe atentamente para o método Excluir. Veja que, por exemplo, quando estivermos implementando a função Inserir precisaremos praticamente das mesmas variáveis: Contexto, TipoRtti, PropRtti, NomeTab (para pegar o nome da tabela) e CamposPk (chave primária).

Se copiarmos todas estas variáveis para os demais métodos, estaremos replicando código na classe TDaoUib. E isso não se dará somente na classe em questão mas também nas outras classes DAO que venhamos a implementar em nosso projeto. Estaremos criando uma bola de neve que acabará enterrando nossa busca por:

  • Reusabilidade de código – conseguida na maioria dos casos via herança;
  • Escalabilidade – pode ser vista como a capacidade de uma aplicação crescer facilmente sem aumentar demasiadamente a sua complexidade ou comprometer o seu desempenho;
  • Mantenabilidade – ser de fácil manutenção

E agora, como evitar tal replicação?

Anonimous Method

Para resolver a questão acima, eu pensei em algumas alternativas como, por exemplo, herança e poliformismo. Mas, e por que não usar um novo recurso do Delphi e que é ainda pouco explorado?

Estou falando de Anonimous Method (métodos anônimos).

Abra a unidade Atributos.pas. E abaixo de TResultArray, vamos criar um record com as variáveis que iremos passar no parâmetro do método anônimo:

unit Atributos;

interface

uses
  Base, Rtti;

type
  TResultArray = array of string;

  TCamposAnoni = record
    NomeTabela: string;
    Sep: string; // <-- utilizada para a separação dos campos no SQL
    PKs: TResultArray; // chave primária 
    TipoRtti: TRttiType; 
  end;
...

Agora, abaixo deste record, vamos declarar nosso método anônimo:

...
  TCamposAnoni = record
    NomeTabela: string;
    Sep: string; // <-- utilizada para a separação dos campos no SQL
    PKs: TResultArray; // chave primária 
    TipoRtti: TRttiType; 
  end;

 TFuncReflexao = reference to function(ACampos: TCamposAnoni): Integer;
...

O método anônimo TFuncReflexao tem um parâmetro do tipo TCamposAnoni. Em vez deste record, eu poderia ter criado parâmetros representando cada objeto necessário, como por exemplo:

 TFuncReflexao = reference to function(ANomeTabela, ASep: string; 
     Pks: TResultArray; TipoRtti: TRttiType): Integer;

Porém, da forma que está, simplifica bastante a utilização do método.

Agora, criaremos um método que servirá de base para o reflection da classe TTabela, o ReflexaoSQL. Coloque acima da função PegaNomeTab:

...
 TCampoPk = class(TCampos)
  public
    function IsPk: Boolean; override;
  end;

  //Reflection para os comandos Sql
  function ReflexaoSQL(ATabela: TTabela; AnoniComando: TFuncReflexao): Integer;

  function PegaNomeTab(ATabela : TTabela): string;
  function PegaPks(ATabela : TTabela): TResultArray;
...

Implementamos esta função da seguinte forma:

...
implementation

function ReflexaoSQL(ATabela: TTabela; AnoniComando: TFuncReflexao): Integer;
var
  ACampos: TCamposAnoni;
  Contexto  : TRttiContext;
begin
  ACampos.NomeTabela := PegaNomeTab(ATabela);

  ACampos.PKs   := PegaPks(ATabela);

  Contexto  := TRttiContext.Create;
  try
    ACampos.TipoRtti := Contexto.GetType( ATabela.ClassType );

    //executamos os comandos Sql através do método anônimo
    ACampos.Sep := '';
    Result := AnoniComando(ACampos);

  finally
    Contexto.free;
  end;
end;
...

ReflexaoSql recebe um objeto do tipo TTabela e o método anônimo. Observe que esta function foi baseada no método Excluir. Desta forma, os códigos que iriam ser replicados na classe TDaoUib foram centralizados num único ponto, evitando a repetição do código.

Segue código completo de Atributos.pas:

unit Atributos;

interface

uses
  Base, Rtti;

type
  TResultArray = array of string;

  TCamposAnoni = record
    NomeTabela: string;
    Sep: string;
    PKs: TResultArray;
    TipoRtti: TRttiType;
  end;

  TFuncReflexao = reference to function(ACampos: TCamposAnoni): Integer;

  TNomeTabela = class(TCustomAttribute)
  private
    FNomeTabela: string;
  public
    constructor Create(ANomeTabela: string);
    property NomeTabela: string read FNomeTabela write FNomeTabela;
  end;

  TCampos = class(TCustomAttribute)
  public
    function IsPk: Boolean; virtual;
  end;

  TCampoPk = class(TCampos)
  public
    function IsPk: Boolean; override;
  end;

  //Reflection para os comandos Sql
  function ReflexaoSQL(ATabela: TTabela; AnoniComando: TFuncReflexao): Integer;

  function PegaNomeTab(ATabela : TTabela): string;
  function PegaPks(ATabela : TTabela): TResultArray;


implementation

function ReflexaoSQL(ATabela: TTabela; AnoniComando: TFuncReflexao): Integer;
var
  ACampos: TCamposAnoni;
  Contexto  : TRttiContext;
begin
  ACampos.NomeTabela := PegaNomeTab(ATabela);

  ACampos.PKs   := PegaPks(ATabela);

  Contexto  := TRttiContext.Create;
  try
    ACampos.TipoRtti := Contexto.GetType( ATabela.ClassType );

    //executamos os comandos Sql através deste método anônimo
    ACampos.Sep := '';
    Result := AnoniComando(ACampos);

  finally
    Contexto.free;
  end;
end;

function PegaNomeTab(ATabela : TTabela): string;
var
  Contexto  : TRttiContext;
  TipoRtti : TRttiType;
  AtribRtti : TCustomAttribute;
begin
  Contexto := TRttiContext.Create;
  TipoRtti := Contexto.GetType(ATabela.ClassType);
  try
    for AtribRtti in TipoRtti.GetAttributes do
      if AtribRtti Is TNomeTabela then
        begin
          Result := (AtribRtti as TNomeTabela).NomeTabela;
          Break;
        end;
  finally
    Contexto.Free;
  end;
end;

function PegaPks(ATabela : TTabela): TResultArray;
var
  Contexto  : TRttiContext;
  TipoRtti  : TRttiType;
  PropRtti  : TRttiProperty;
  AtribRtti : TCustomAttribute;
  i: Integer;
begin
  Contexto := TRttiContext.Create;
  try
    TipoRtti := Contexto.GetType(ATabela.ClassType);
    i:=0;
    for PropRtti in TipoRtti.GetProperties do
      for AtribRtti in PropRtti.GetAttributes do
        if AtribRtti Is TCampos then
          if (AtribRtti as TCampos).isPk then
          begin
            SetLength(Result, i+1);
            Result[i] := PropRtti.Name;
            inc(i);
          end;
  finally
    Contexto.Free;
  end;
end;

{ TCampos }

function TCampos.IsPk: Boolean;
begin
  Result := False;
end;

{ TCampoPk }

function TCampoPk.IsPk: Boolean;
begin
  Result := True;
end;

{ TNomeTabela }

constructor TNomeTabela.Create(ANomeTabela: string);
begin
  FNomeTabela := ANomeTabela;
end;

end.

Continuando, abra a unidade DaoUib.pas e altere o método Excluir:

function TDaoUib.Excluir(ATabela: TTabela): Integer;
var
  Comando: TFuncReflexao;
begin
  //crio uma variável do tipo TFuncReflexao - um método anônimo
  Comando := function(ACampos: TCamposAnoni): Integer
  var
    Campo: string;
    PropRtti: TRttiProperty;
  begin
    with Qry do
    begin
      close;
      SQL.Clear;
      sql.Add('Delete from ' + ACampos.NomeTabela);
      sql.Add('Where');

      //percorrer todos os campos da chave primária
      ACampos.Sep := '';
      for Campo in ACampos.PKs do
      begin
        sql.Add(ACampos.Sep+ Campo + '= :' + Campo);
        ACampos.Sep := ' and ';
        // setando os parâmetros
        for PropRtti in ACampos.TipoRtti.GetProperties do
          if CompareText(PropRtti.Name, Campo) = 0 then
            begin
              ConfigParametro(Qry, PropRtti, Campo, ATabela);
            end;
      end;

      //executa delete
      Prepare();
      ExecSQL;
      Result := RowsAffected;
    end;
  end;

  //reflection da tabela e execução da query preparada acima.
  Result := ReflexaoSQL(ATabela, Comando);
end;

Estamos quase lá, mas ainda tem a abertura e fechamento da query. Crie acima do método Excluir mais 2 métodos:

procedure TDaoUib.FechaQuery;
begin
  Qry.Close;
  Qry.SQL.Clear;
end;

function TDaoUib.ExecutaQuery: Integer;
begin
  with Qry do
  begin
    Prepare();
    ExecSQL;
    Result := RowsAffected;
  end;
end;

Mais uma vez, altere Excluir:

function TDaoUib.Excluir(ATabela: TTabela): Integer;
var
  Comando: TFuncReflexao;
begin
  //crio uma variável do tipo TFuncReflexao - um método anônimo
  Comando := function(ACampos: TCamposAnoni): Integer
  var
    Campo: string;
    PropRtti: TRttiProperty;
  begin
    FechaQuery;
    with Qry do
    begin
      sql.Add('Delete from ' + ACampos.NomeTabela);
      sql.Add('Where');

      //percorrer todos os campos da chave primária
      ACampos.Sep := '';
      for Campo in ACampos.PKs do
      begin
        sql.Add(ACampos.Sep+ Campo + '= :' + Campo);
        ACampos.Sep := ' and ';
        // setando os parâmetros
        for PropRtti in ACampos.TipoRtti.GetProperties do
          if CompareText(PropRtti.Name, Campo) = 0 then
            begin
              ConfigParametro(Qry, PropRtti, Campo, ATabela);
            end;
      end;
    end;
    Result := ExecutaQuery;
  end;

  //reflection da tabela e execução da query preparada acima.
  Result := ReflexaoSQL(ATabela, Comando);
end;

Analisando o código:

  • Da linha 6 a 32 definimos o que o nosso método anônimo irá fazer, ou seja, irá montar o comando Sql e setar os parâmetros da query.
  • Linha 11, fecho a query e limpo a propriedade SQL.
  • Note que da linha 7 a 9, tivemos que declarar algumas variáveis locais (local do método anônimo), visto ser necessário no loop. Se declarar estas variáveis fora do método, irá gerar erro.
  • Linha 31, com o comando SQL montado, chamo a procedure ExecutaQuery. Mas atenção: até aqui somente definimos o que o método anônimo irá fazer. Não significa que já estamos atualizando o nosso banco de dados.
  • Na linha 35, finalmente a query será executada através do método ReflexaoSql, e o registro será deletado do banco de dados.

Isso no começo pode dar um nó em nosso cérebro – primeiro você define o que o método irá fazer e só depois é que realmente o processo é executado. Até por ser um recurso novo da Ide, talvez lhe pareça um tanto confuso agora, mas quando assimilar o conceito, você verá grande utilidade nos métodos anônimos e a sua implementação se tornará mais natural.

Eu poderia ter utilizado uma vez mais este recurso no código acima, na abertura e fechamento da query para ser preciso. Porém, para manter simples e não complicar a criação e destruição dos objetos (por questão de escopo das variáveis), os dois métodos criados (FechaQuery e ExecutaQuery) já resolvem o problema.

Código completo de DaoUib.pas:

unit DaoUib;

interface

uses Base, Rtti, Atributos, uib, system.SysUtils;

type
  TDaoUib = class(TInterfacedObject, IDaoBase)
  private
    FDatabase: TUIBDataBase;
    FTransacao: TUIBTransaction;
    // Este método configura os parâmetros da AQuery.
    procedure ConfigParametro(AQuery: TuibQuery; AProp: TRttiProperty; ACampo: string;  ATabela: TTabela);
    procedure FechaQuery;

    function ExecutaQuery: Integer;
  public
    Qry: TUIBQuery;

    constructor Create(ADatabase: TUIBDataBase; ATransacao: TUIBTransaction);

    function Inserir(ATabela: TTabela): Integer;
    function Salvar(ATabela: TTabela): Integer;
    function Excluir(ATabela: TTabela): Integer;

    function InTransaction: Boolean;
    procedure StartTransaction;
    procedure Commit;
    procedure RollBack;

  end;

implementation

{ TDaoUib }

uses Vcl.forms, dialogs, System.TypInfo;

constructor TDaoUib.Create(ADatabase: TUIBDataBase; ATransacao: TUIBTransaction);
begin
  inherited Create;
  if not Assigned(ADatabase) then
    raise Exception.Create('UIBDatabase não informado!');

  if not Assigned(ATransacao) then
    raise Exception.Create('UIBTransaction não informado!');

  FDatabase := ADatabase;
  FTransacao := ATransacao;

  Qry := TUIBQuery.Create(Application);
  Qry.DataBase := FDatabase;
  Qry.Transaction := FTransacao;
end;

function TDaoUib.InTransaction: Boolean;
begin
  Result := FTransacao.InTransaction;
end;

procedure TDaoUib.StartTransaction;
begin
  FTransacao.StartTransaction;
end;

procedure TDaoUib.RollBack;
begin
  FTransacao.RollBack;
end;

procedure TDaoUib.Commit;
begin
  FTransacao.Commit;
end;

procedure TDaoUib.ConfigParametro(AQuery: TUIBQuery; AProp: TRttiProperty;
  ACampo: string; ATabela: TTabela);
begin
  with AQuery do
  begin
    case AProp.PropertyType.TypeKind of
      tkInt64,
      tkInteger:
      begin
        Params.ByNameAsInteger[ACampo] := AProp.GetValue(ATabela).AsInteger;
      end;
      tkChar,
      tkString,
      tkUString:
      begin
        Params.ByNameAsString[ACampo] := AProp.GetValue(ATabela).AsString;
      end;
      tkFloat:
      begin
         Params.ByNameAsCurrency[ACampo] := AProp.GetValue(ATabela).AsCurrency;
      end;
      tkVariant:
      begin
        Params.ByNameAsVariant[ACampo] := AProp.GetValue(ATabela).AsVariant;
      end;
    else
      raise Exception.Create('Tipo de campo não conhecido: ' + AProp.PropertyType.ToString);
    end;
  end;
end;

procedure TDaoUib.FechaQuery;
begin
  Qry.Close;
  Qry.SQL.Clear;
end;

function TDaoUib.ExecutaQuery: Integer;
begin
  with Qry do
  begin
    Prepare();
    ExecSQL;
    Result := RowsAffected;
  end;
end;

function TDaoUib.Excluir(ATabela: TTabela): Integer;
var
  Comando: TFuncReflexao;
begin
  //crio uma variável do tipo TFuncReflexao - um método anônimo
  Comando := function(ACampos: TCamposAnoni): Integer
  var
    Campo: string;
    PropRtti: TRttiProperty;
  begin
    FechaQuery;
    with Qry do
    begin
      sql.Add('Delete from ' + ACampos.NomeTabela);
      sql.Add('Where');

      //percorrer todos os campos da chave primária
      ACampos.Sep := '';
      for Campo in ACampos.PKs do
      begin
        sql.Add(ACampos.Sep+ Campo + '= :' + Campo);
        ACampos.Sep := ' and ';
        // setando os parâmetros
        for PropRtti in ACampos.TipoRtti.GetProperties do
          if CompareText(PropRtti.Name, Campo) = 0 then
            begin
              ConfigParametro(Qry, PropRtti, Campo, ATabela);
            end;
      end;
    end;
    Result := ExecutaQuery;
  end;

  //reflection da tabela e execução da query preparada acima.
  Result := ReflexaoSQL(ATabela, Comando);
end;

function TDaoUib.Inserir(ATabela: TTabela): Integer;
begin

end;

function TDaoUib.Salvar(ATabela: TTabela): Integer;
begin

end;

end.

Já estamos prontos para partir para o método Inserir e fazer testes de inserção e exclusão na base de dados. No próximo artigo, daremos continuidade ao nosso ORM básico. Fico por aqui, obrigado e até a próxima.

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.

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.