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.