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?
[sourcecode language=”delphi”]
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;
[/sourcecode]
Eu já havia criado o método ConfigParametro objetivando evitar a repetição de código:
[sourcecode language=”delphi”]
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;
[/sourcecode]
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:
[sourcecode language=”delphi”]
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;
…
[/sourcecode]
Agora, abaixo deste record, vamos declarar nosso método anônimo:
[sourcecode language=”delphi”]
…
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;
…
[/sourcecode]
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:
[sourcecode language=”delphi”]
TFuncReflexao = reference to function(ANomeTabela, ASep: string;
Pks: TResultArray; TipoRtti: TRttiType): Integer;
[/sourcecode]
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:
[sourcecode language=”delphi”]
…
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;
…
[/sourcecode]
Implementamos esta função da seguinte forma:
[sourcecode language=”delphi”]
…
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;
…
[/sourcecode]
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:
[sourcecode language=”delphi”]
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.
[/sourcecode]
Continuando, abra a unidade DaoUib.pas e altere o método Excluir:
[sourcecode language=”delphi”]
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;
[/sourcecode]
Estamos quase lá, mas ainda tem a abertura e fechamento da query. Crie acima do método Excluir mais 2 métodos:
[sourcecode language=”delphi”]
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;
[/sourcecode]
Mais uma vez, altere Excluir:
[sourcecode language=”delphi”]
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;
[/sourcecode]
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:
[sourcecode language=”delphi”]
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.
[/sourcecode]
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.