Olá
Depois de alguns dias off, finalmente encontrei um tempinho para mais um artigo: DAO (Data Access Object).
O Delphi nos oferece muitas facilidades. Por exemplo, é possivel criar uma tela de cadastro sem utilizar uma linha de código sequer! Basta adicionar um DBGrid e um DBNavigator ligados a um dataset, e pronto! Isto pode ser muito bom para iniciantes e pequenos programas. Porém, para sistemas maiores, que exigem alto desempenho, preparados para as mudanças de requisitos e com um alto reuso de código, este tipo de facilidade pode tornar-se uma armadilha.
Desde o início, sempre procurei ao máximo não depender de componentes datawares, ou seja, DBEdits, DBComboEdits, DBNvigators, etc… Sempre tive a preopucação com a quantidade de recursos que são alocados em meus sistemas. Para tentar mimizar a carga, em grande parte, utilizo componentes desconectados (Edits, JvCalcEdit [jvc]…). É claro, não dá para fugir de tudo, como por exemplo, do ótimo DBGrid! É só não exagerar.
Outra vantagem de se trabalhar com os Edits e afins está relacionado aos comandos DML (insert, update, select, delete) dos Bancos de Dados Sql. Quando você mesmo se encarrega de criar as instruções Sql, tem a possibilidade de um controle maior sobre estes comandos, evitando gerar mais tráfego do que o necessário na rede, aumentando o desempenho como um todo.
CRUD – Create Read Update Delete
Uma das tarefas mais repetitivas que todo programador executa em sistemas que utilizam alguma base de dados é sem dúvida nenhuma as atividades relacionadas ao CRUD, em outras palavras, comandos para inserção, leitura(consulta), atualização e exlusão dos dados (persistência na base de dados).
Antigamente, eu criava um form com os botões responsáveis pelo CRUD. Ótimo, dava pro gasto! Porém, reuso de código que é bom, nada!
Eu sentia que o processo entre uma tela e outra era muito parecido, às vezes continham até o mesmo Sql. Quando um novo atributo surgia em determinada tabela, eu tinha que sair buscando no sistema os SQL’s desta tabela para inserir o novo campo. Isso é um trabalho desgastante e chato. E aí, volta e meia, lá vinha o cliente reclamando de erro em uma tela que antes funcionara perfeitamente, porém após uma última atualização passou a apresentar erro. Geralmente relacionado com a divergência entre o sistema e o banco de dados, ou seja, lembrei de atualizar uma sql e esqueci de outra. 😆
Definindo o padrão
Eu não estava satisfeito com a forma como vinha trabalhando minhas telas de cadastro e isso me motivou a buscar novas técnicas.
Quando iniciei os estudos dos Padrões de Projeto e acabei me tornando um viciado em refatorar código (este artigo mesmo já foi várias vezes refatorado :)), conheci o DAO (na época pouca coisa relacionada ao Delphi, tendo os exemplos geralmente em Java) e senti que era um padrão interessante a ser utilizado em meus sistemas. No Wiki, possui a seguinte definição:
DAO (acrônimo de Data Access Object), é um padrão para persistência de dados que permite separar regras de negócio das regras de acesso a banco de dados. Numa aplicação que utilize a arquitetura MVC, todas as funcionalidades de bancos de dados, tais como obter as conexões, mapear objetos Java para tipos de dados SQL ou executar comandos SQL, devem ser feitas por classes de DAO.
Neste artigo não abordarei os temas MVC (Model View Controller), MVP (Model View Presenter) e nem MVVM (Model View ViewModel). Apenas DAO simplesmente.
Projeto sem o DAO
Para mostrar na prática, como de costume, irei pegar um projeto (simples) que possui um form e um datamodule que não utiliza o conceito e então refatorar o código, transformando-o em um projeto com DAO. No fim do artigo, irei disponibilizar os fontes. Vamos lá!
O banco de dados a ser utilizado segue o seguinte esquema (neste caso, Firebird 2.1):
Como pode ser visto, nosso Bd possui apenas duas tabelas, uma view, duas procedures e um generator. A tabela irá junto com os fontes.
Para o exemplo, utilizei a biblioteca IBX que acompanha o Delphi. Veja o projeto em execução:
Duplo clique sobre o registro no Dbgrid e vamos para a aba de lançamento:
Vemos o formulário principal, contendo um PageControl com duas abas (Lista contas a receber e Lançamento) e botões para a incluir, pesquisar, salvar, cancelar (a edição) e excluir.
O ponto principal deste projeto são os códigos dos botões. Porém, para melhor entendimento, segue código completo do formulário:
[sourcecode language=”delphi”]
type
TOperacao = (tpNone, tpIncluir, tpSalvar);
TfrmPrincipal = class(TForm)
pcPrincipal: TPageControl;
tabLista: TTabSheet;
tabLancto: TTabSheet;
dsRec: TDataSource;
dbgLista: TDBGrid;
edID: TEdit;
edDocumento: TEdit;
edCliente: TEdit;
edNomeCliente: TEdit;
edEmissao: TEdit;
edVencimento: TEdit;
edValor: TEdit;
Label1: TLabel;
Label2: TLabel;
Label3: TLabel;
Label4: TLabel;
Label5: TLabel;
Label6: TLabel;
toolBarraPrincipal: TToolBar;
btnIncluir: TToolButton;
btnPesquisar: TToolButton;
btnSalvar: TToolButton;
ImageList1: TImageList;
btnExcluir: TToolButton;
btnCancelar: TToolButton;
procedure FormCreate(Sender: TObject);
procedure btnIncluirClick(Sender: TObject);
procedure dbgListaDblClick(Sender: TObject);
procedure btnSalvarClick(Sender: TObject);
procedure btnExcluirClick(Sender: TObject);
procedure btnCancelarClick(Sender: TObject);
procedure btnPesquisarClick(Sender: TObject);
procedure edDocumentoChange(Sender: TObject);
procedure pcPrincipalChange(Sender: TObject);
private
FOperacao: TOperacao;
{ Private declarations }
procedure ClearFields;
procedure SetOperacao(const Value: TOperacao);
protected
public
{ Public declarations }
property Operacao : TOperacao read FOperacao write SetOperacao;
end;
var
frmPrincipal: TfrmPrincipal;
implementation
uses
uDmPrinc;
{$R *.dfm}
procedure TfrmPrincipal.FormCreate(Sender: TObject);
begin
dm.sqlReceber.Open;
pcPrincipal.ActivePageIndex := 0;
Operacao := tpNone;
ClearFields;
end;
procedure TfrmPrincipal.btnIncluirClick(Sender: TObject);
begin
pcPrincipal.ActivePageIndex := 1;
ClearFields;
Operacao := tpIncluir;
end;
procedure TfrmPrincipal.ClearFields;
var
i: integer;
begin
for i := 0 to ComponentCount – 1 do
begin
if Components[i] is TEdit then
TEdit(Components[i]).Clear
end;
end;
procedure TfrmPrincipal.dbgListaDblClick(Sender: TObject);
begin
with dm.sqlReceber do
begin
edID.text := fieldbyname(‘id’).AsString;
edDocumento.Text := fieldbyname(‘documento’).AsString;
edCliente.Text := fieldbyname(‘clienteid’).AsString;
edNomeCliente.Text := fieldbyname(‘nomecliente’).AsString;
edEmissao.Text := fieldbyname(‘emissao’).AsString;
edVencimento.Text := fieldbyname(‘vencimento’).AsString;
edValor.Text := formatfloat(‘,0.00’, fieldbyname(‘valor’).ascurrency);
end;
pcPrincipal.ActivePageIndex := 1;
end;
procedure TfrmPrincipal.SetOperacao(const Value: TOperacao);
begin
FOperacao := Value;
btnIncluir.Enabled := FOperacao = tpNone;
btnSalvar.Enabled := (FOperacao in [tpIncluir, tpSalvar]);
btnExcluir.Enabled := FOperacao = tpNone;
btnPesquisar.Enabled := FOperacao = tpNone;
btnCancelar.Enabled := not (FOperacao = tpNone);
end;
procedure TfrmPrincipal.btnSalvarClick(Sender: TObject);
var
_Reg: integer;
begin
if not dm.ibTrans.InTransaction then
dm.ibTrans.StartTransaction;
try
with dm.exec do
begin
if Operacao = tpIncluir then
begin
_Reg := dm.gerarID(‘gen_receberid’);
close;
SQL.Clear;
sql.Add(‘insert into receber values (‘);
sql.Add(‘:id, :documento, :clienteid, :emissao, :vencimento, :valor)’);
ParamByName(‘id’).AsInteger := _Reg;
ParamByName(‘documento’).asstring := edDocumento.Text;
ParamByName(‘clienteid’).asstring := edCliente.Text;
ParamByName(‘emissao’).asstring := edEmissao.Text;
ParamByName(‘vencimento’).asstring := edVencimento.Text;
ParamByName(‘valor’).AsCurrency := StrToFloat(edValor.Text);
ExecQuery;
edID.Text := IntToStr(_Reg);
end
else
begin
close;
SQL.Clear;
sql.Add(‘update receber set documento=:documento, clienteid=:clienteid,’);
SQL.Add(‘emissao=:emissao, vencimento=:vencimento, valor=:valor’);
sql.Add(‘where id=:id’);
ParamByName(‘documento’).asstring := edDocumento.Text;
ParamByName(‘clienteid’).asstring := edCliente.Text;
ParamByName(‘emissao’).asstring := edEmissao.Text;
ParamByName(‘vencimento’).asstring := edVencimento.Text;
ParamByName(‘valor’).AsCurrency := StrToFloat(edValor.Text);
ParamByName(‘id’).AsString := edID.Text;
ExecQuery;
end;
end;
dm.ibTrans.Commit;
Operacao := tpNone;
dm.sqlReceber.Close;
dm.sqlReceber.Open;
ShowMessage(‘Registro salvo!’);
except
on e: Exception do
begin
dm.ibTrans.Rollback;
btnCancelarClick(nil);
Application.ShowException(e);
end;
end;
end;
procedure TfrmPrincipal.btnExcluirClick(Sender: TObject);
begin
if pcPrincipal.ActivePageIndex=0 then
begin
dbgListaDblClick(nil);
end;
if MessageDlg(‘Tem certeza que deseja continuar?’,mtConfirmation,[mbYes,mbNo],0)=mrno then exit;
try
if not dm.ibTrans.InTransaction then
dm.ibTrans.StartTransaction;
try
with dm.exec do
begin
close;
SQL.Clear;
sql.Add(‘delete from receber where id=:id’);
ParamByName(‘id’).AsString := edID.text;
ExecQuery;
end;
dm.ibTrans.Commit;
dm.sqlReceber.Close;
dm.sqlReceber.Open;
ShowMessage(‘Registro exlcuído!’);
except
on e: Exception do
begin
dm.ibTrans.Rollback;
Application.ShowException(e);
end;
end;
finally
btnCancelarClick(nil);
end;
end;
procedure TfrmPrincipal.btnCancelarClick(Sender: TObject);
begin
ClearFields;
pcPrincipal.ActivePageIndex := 0;
Operacao := tpNone;
end;
procedure TfrmPrincipal.btnPesquisarClick(Sender: TObject);
var
_Documento: string;
begin
_Documento := InputBox(‘Informe o número do documento’,’Documento’,”);
try
try
with dm.sqlReceber do
begin
close;
SQL.Clear;
if Trim(_documento)=” then
sql.Add(‘select * from vw_receber’)
else
begin
sql.Add(‘select * from vw_receber where documento = :documento’);
ParamByName(‘documento’).AsString := UpperCase(_Documento);
end;
Open;
if recordcount = 0 then
begin
ShowMessage(‘Registro não encontrado!’);
end;
end;
except
on e: Exception do
begin
Application.ShowException(e);
end;
end;
finally
btnCancelarClick(nil);
end;
end;
procedure TfrmPrincipal.edDocumentoChange(Sender: TObject);
begin
if Trim(edID.Text) <> ” then
begin
Operacao := tpSalvar;
end;
end;
procedure TfrmPrincipal.pcPrincipalChange(Sender: TObject);
begin
if pcPrincipal.ActivePageIndex = 0 then
if Operacao <> tpNone then
btnCancelarClick(nil);
end;
end.
[/sourcecode]
Observe que o nosso formulário além de mostrar os componentes visuais na tela, tem a responsabilidade de manipular os dados diretamente em nosso banco de dados, ou seja, é muita responsabilidade atribuída ao form.
Você pode estar pensando: puxa, mas está funcionando que é uma beleza!
Sim, num primeiro momento funciona muito bem. Porém, os requisitos mudam! Disto não temos como fugir. Então imagine que, em dado momento, surge a necessidade de ter uma tela, além dessa já criada, para gerar vários contas a receber num mesmo processo, como por exemplo, no parcelamento de uma venda.
Nesta nova tela, você iria necessitar basicamente dos mesmos códigos CRUD. Estes códigos estariam num loop, mas a ideia seria a mesma.
Desta forma, já haveria duplicação de código em seu sistema. O que se configura como um sistema com baixa reusabilidade de código. Digamos que surja um novo requisito, ou melhor, um novo atributo na tabela Receber. Olha o problema! A manutenção começa a complicar.
Este é um exemplo bem simples, mas num sistema complexo o custo de manutenção seria elevado! Além disso, o programa não estará preparado para crescer e o seu tempo de vida será minimizado.
Refatorando o Sistema
Agora que definimos o problema, como resolver?
POO (Programação Orientado a Objetos) e Padrões de Projeto, com certeza!
Como eu disse, iremos nos concentrar no DAO, mas saiba que existem vários caminhos, vários padrões de projeto, vários meios de adotar um mesmo padrão! A forma que adotei para o DAO é a minha sugestão pessoal.
O primeiro passo, será criar a nossa classe TReceber. Para isto, vamos criar uma nova unit chamada uReceber.pas:
[sourcecode language=”delphi”]
unit uReceber;
interface
uses uBase, uDmPrinc, db, IBQuery, Classes, Forms;
type
TReceber = class
private
FValor: Currency;
FClienteid: integer;
FId: Integer;
FDocumento: string;
FVencimento: TDateTime;
FEmissao: TDateTime;
procedure SetClienteid(const Value: integer);
procedure SetDocumento(const Value: string);
procedure SetEmissao(const Value: TDateTime);
procedure SetId(const Value: Integer);
procedure SetValor(const Value: Currency);
procedure SetVencimento(const Value: TDateTime);
public
property Id: Integer read FId write SetId;
property Documento: string read FDocumento write SetDocumento;
property Clienteid: integer read FClienteid write SetClienteid;
property Emissao: TDateTime read FEmissao write SetEmissao;
property Vencimento: TDateTime read FVencimento write SetVencimento;
property Valor: Currency read FValor write SetValor;
end;
TRecDao = class
private
class function ComandoSql(AReceber: TReceber): Boolean;
public
{métodos CRUD (Create, Read, Update e Delete)
para manipulação dos dados}
class function Insert(AReceber: TReceber): Boolean; //create
class function Read(AQuery: TIBQuery; ADocumento: string): integer;
class function Update(AReceber: TReceber): Boolean;
class function Delete(AID: Integer): Boolean;
end;
implementation
uses IBSQL, SysUtils;
{ TReceber }
procedure TReceber.SetClienteid(const Value: integer);
begin
FClienteid := Value;
end;
procedure TReceber.SetDocumento(const Value: string);
begin
FDocumento := Value;
end;
procedure TReceber.SetEmissao(const Value: TDateTime);
begin
FEmissao := Value;
end;
procedure TReceber.SetId(const Value: Integer);
begin
FId := Value;
end;
procedure TReceber.SetValor(const Value: Currency);
begin
FValor := Value;
end;
procedure TReceber.SetVencimento(const Value: TDateTime);
begin
FVencimento := Value;
end;
{ TRecDao }
class function TRecDao.ComandoSql(AReceber: TReceber): Boolean;
begin
Result := false;
if not dm.ibTrans.InTransaction then
dm.ibTrans.StartTransaction;
try
with dm.exec do
begin
Close;
sql.clear;
SQL.Add(‘execute procedure receber_iu(‘);
sql.Add(‘:id, :documento, :clienteid, :emissao, :vencimento, :valor)’);
ParambyName(‘id’).AsInteger := AReceber.Id;
ParambyName(‘documento’).asstring := AReceber.Documento;
ParambyName(‘clienteid’).AsInteger := AReceber.Clienteid;
ParambyName(‘emissao’).AsDateTime := AReceber.Emissao;
ParambyName(‘vencimento’).AsDateTime := AReceber.Vencimento;
ParambyName(‘valor’).AsCurrency := AReceber.Valor;
ExecQuery;
end;
result := true;
except
on e: Exception do
begin
dm.ibTrans.Rollback;
Application.ShowException(e);
end;
end;
end;
class function TRecDao.Delete(AID: Integer): Boolean;
begin
Result := False;
if not dm.ibTrans.InTransaction then
dm.ibTrans.StartTransaction;
try
with dm.exec do
begin
close;
SQL.Clear;
sql.Add(‘delete from receber where id=:id’);
ParamByName(‘id’).AsInteger := AId;
ExecQuery;
end;
dm.ibTrans.Commit;
dm.sqlReceber.Close;
dm.sqlReceber.Open;
Result := True;
except
on e: Exception do
begin
dm.ibTrans.Rollback;
Application.ShowException(e);
end;
end;
end;
class function TRecDao.Insert(AReceber: TReceber): Boolean;
begin
AReceber.Id := dm.gerarID(‘gen_receberid’);
result := ComandoSql(AReceber);
end;
class function TRecDao.Update(AReceber: TReceber): Boolean;
begin
result := ComandoSql(AReceber);
end;
class function TRecDao.Read(AQuery: TIBQuery; ADocumento: string): integer;
begin
with AQuery do
begin
close;
sql.clear;
sql.Add(‘Select * from vw_receber’);
if Trim(ADocumento)<>” then
begin
sql.Add(‘where documento=:documento’);
Params[0].AsString := UpperCase(ADocumento);
end;
Open;
result := recordcount;
end;
end;
end.
[/sourcecode]
Note que além da classe TReceber, temos uma classe com métodos estáticos, que é a nossa classe TRecDao. Desta forma, não precisaremos instanciar um novo objeto toda vez que necessitarmos dos métodos CRUD.
Observe que centralizei os comando sql de insert e update num mesmo método (ComandoSql), que faz uso de uma StoreProcedure. Será ela quem irá definir se é uma inclusão ou atualização. Veja a DDL desta procedure:
[sourcecode language=”sql”]
SET TERM ^ ;
CREATE OR ALTER PROCEDURE RECEBER_IU (
id integer,
documento varchar(20),
clienteid integer,
emissao timestamp,
vencimento timestamp,
valor decimal(15,2))
as
begin
if (exists(select id from receber where (id = :id))) then
update receber
set documento = :documento,
clienteid = :clienteid,
emissao = :emissao,
vencimento = :vencimento,
valor = :valor
where (id = :id);
else
insert into receber (
id,
documento,
clienteid,
emissao,
vencimento,
valor)
values (
:id,
:documento,
:clienteid,
:emissao,
:vencimento,
:valor);
end^
SET TERM ; ^
[/sourcecode]
Ah! Além da unit uReceber.pas, criei uma outra chamada uBase.pas, para guardar as classes base, variáveis e constantes globais, caso seja necessário. A princípio, teremos apenas um tipo definido:
[sourcecode language=”delphi”]
type
TOperacao = (tpInsert, tpUpdate, tpNone);
[/sourcecode]
Servirá para controlar os estados do nosso formulário. Não coloquei diretamente na unit uReceber, visto que num sistema real não teríamos apenas a classe TReceber, muito pelo contrário, teríamos várias outras classes que necessitariam ter acesso à unit uBase.
Agora vamos alterar o nosso formulário principal:
[sourcecode language=”delphi”]
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, Grids, DBGrids, DB, ComCtrls, ImgList, ToolWin, StdCtrls, uBase,
uReceber;
type
TfrmPrincipal = class(TForm)
pcPrincipal: TPageControl;
tabLista: TTabSheet;
tabLancto: TTabSheet;
dsRec: TDataSource;
dbgLista: TDBGrid;
edID: TEdit;
edDocumento: TEdit;
edCliente: TEdit;
edNomeCliente: TEdit;
edEmissao: TEdit;
edVencimento: TEdit;
edValor: TEdit;
Label1: TLabel;
Label2: TLabel;
Label3: TLabel;
Label4: TLabel;
Label5: TLabel;
Label6: TLabel;
toolBarraPrincipal: TToolBar;
btnIncluir: TToolButton;
btnPesquisar: TToolButton;
btnSalvar: TToolButton;
ImageList1: TImageList;
btnExcluir: TToolButton;
btnCancelar: TToolButton;
procedure FormCreate(Sender: TObject);
procedure btnIncluirClick(Sender: TObject);
procedure dbgListaDblClick(Sender: TObject);
procedure btnSalvarClick(Sender: TObject);
procedure btnExcluirClick(Sender: TObject);
procedure btnCancelarClick(Sender: TObject);
procedure btnPesquisarClick(Sender: TObject);
procedure edDocumentoChange(Sender: TObject);
procedure pcPrincipalChange(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
FOperacao: TOperacao;
FReceber: TReceber;
{ Private declarations }
procedure ClearFields;
procedure SetOperacao(const Value: TOperacao);
procedure SetReceber;
protected
public
{ Public declarations }
property Operacao : TOperacao read FOperacao write SetOperacao;
end;
var
frmPrincipal: TfrmPrincipal;
implementation
uses
uDmPrinc;
{$R *.dfm}
procedure TfrmPrincipal.FormCreate(Sender: TObject);
begin
dm.sqlReceber.Open;
FReceber := TReceber.Create;
pcPrincipal.ActivePageIndex := 0;
Operacao := tpNone;
ClearFields;
end;
procedure TfrmPrincipal.SetReceber;
begin
with FReceber do
begin
Id := StrToInt(edID.Text);
Documento := edDocumento.Text;
Clienteid := StrToInt(edCliente.text);
Emissao := StrToDateTime(edEmissao.Text);
Vencimento := StrToDateTime(edVencimento.Text);
Valor := StrToFloat(edValor.Text);
end;
end;
procedure TfrmPrincipal.btnIncluirClick(Sender: TObject);
begin
pcPrincipal.ActivePageIndex := 1;
ClearFields;
Operacao := tpInsert;
end;
procedure TfrmPrincipal.ClearFields;
var
i: integer;
begin
for i := 0 to ComponentCount – 1 do
begin
if Components[i] is TEdit then
TEdit(Components[i]).Clear
end;
end;
procedure TfrmPrincipal.dbgListaDblClick(Sender: TObject);
begin
with dm.sqlReceber do
begin
edID.text := fieldbyname(‘id’).AsString;
edDocumento.Text := fieldbyname(‘documento’).AsString;
edCliente.Text := fieldbyname(‘clienteid’).AsString;
edNomeCliente.Text := fieldbyname(‘nomecliente’).AsString;
edEmissao.Text := fieldbyname(‘emissao’).AsString;
edVencimento.Text := fieldbyname(‘vencimento’).AsString;
edValor.Text := formatfloat(‘,0.00’, fieldbyname(‘valor’).ascurrency);
end;
pcPrincipal.ActivePageIndex := 1;
end;
procedure TfrmPrincipal.SetOperacao(const Value: TOperacao);
begin
FOperacao := Value;
btnIncluir.Enabled := FOperacao = tpNone;
btnSalvar.Enabled := (FOperacao in [tpInsert, tpUpdate]);
btnExcluir.Enabled := FOperacao = tpNone;
btnPesquisar.Enabled := FOperacao = tpNone;
btnCancelar.Enabled := not (FOperacao = tpNone);
end;
procedure TfrmPrincipal.btnSalvarClick(Sender: TObject);
var
_Result: Boolean;
begin
// grava dados dos edits no objeto FReceber;
SetReceber;
if Operacao = tpInsert then
_Result := TRecDao.Insert(FReceber)
else
_Result := TRecDao.Update(FReceber);
if _Result then
begin
dm.sqlReceber.Close;
dm.sqlReceber.Open;
ShowMessage(‘Registro salvo!’);
end;
Operacao := tpNone;
end;
procedure TfrmPrincipal.btnExcluirClick(Sender: TObject);
begin
if pcPrincipal.ActivePageIndex=0 then
begin
dbgListaDblClick(nil);
end;
if MessageDlg(‘Tem certeza que deseja continuar?’,mtConfirmation,[mbYes,mbNo],0)=mrno then exit;
try
TRecDao.Delete(StrToInt(edID.Text));
finally
btnCancelarClick(nil);
end;
end;
procedure TfrmPrincipal.btnCancelarClick(Sender: TObject);
begin
ClearFields;
pcPrincipal.ActivePageIndex := 0;
Operacao := tpNone;
end;
procedure TfrmPrincipal.btnPesquisarClick(Sender: TObject);
begin
TRecDao.Read(dm.sqlReceber, InputBox(‘Informe o número do documento’,’Documento’,”));
end;
procedure TfrmPrincipal.edDocumentoChange(Sender: TObject);
begin
if Trim(edID.Text) <> ” then
begin
Operacao := tpUpdate;
end;
end;
procedure TfrmPrincipal.pcPrincipalChange(Sender: TObject);
begin
if pcPrincipal.ActivePageIndex = 0 then
if Operacao <> tpNone then
btnCancelarClick(nil);
end;
procedure TfrmPrincipal.FormDestroy(Sender: TObject);
begin
FReceber.Free;
end;
end.
[/sourcecode]
Basicamente, adicionei ao uses as units uReceber e uBase, criei um objeto FReceber e uma procedure SetReceber que irá setar as propriedades do objeto com os valores dos Edits, e por fim, tirei os comandos Sql do form. Menos uma responsabilidade com que ele terá que lhe dar, visto que deixamos a classe TRecDao encarregado do CRUD.
Se precisarmos manipular os dados da tabela Receber em outra parte do nosso projeto, bastará instanciar a classe TReceber, assim como foi feito acima, setar suas propriedades e fazer a persistência utilizando o TRecDao. Note que eu não precisei instanciar esta última, visto que seus métodos são estáticos.
Poderemos utilizar tanta vezes quanto for necessário este processo, ou seja, reuso de código total!
Poderíamos tratar de forma diferente as transactions (prevendo múltiplos updates) e tirar a dependência do datamodule em nossa classe TReceber, melhorando ainda mais a abstração, criando e retornando os datasets diretamente na classe. Porém, como eu disse, esta é uma sugestão de uso do DAO. A partir daqui, você poderá explorar novos meios, novas técnicas.
Código Fonte: http://dl.dropbox.com/u/478707/BlogDoLuiz-Dao.zip
Fico por aqui. Críticas e sugestões, comentem abaixo. Se gostou deste post, clique no botão Curtir abaixo.
Abraços.
Luiz, o artigo está muito bom, mas ficaria melhor e mais interessante se implementasse algo que fizesse uso das novidades inseridas no Delphi nas ultimas versões como generics, nova rtti, atributos…
Olá Daniel, obrigado pela participação.
Bom, realmente seria muito interessante abordar os temas que você citou. Infelizmente o período de testes do meu trial XE2 expirou e eu não tenho como fazer artigos nesta IDE. Pelo menos não até conseguir recursos para adquirir o XE2. Estou na batalha.
Eu já até abordei um pouco sobre Generics, e no comentário do post Clonando um Objeto, coloquei um link para um artigo muito bom sobre RTTI.
Abraços.
Muito bom o artigo, Luiz !
O Delphi não é a minha ferramenta de trabalho, mas sempre gostei dela e estou procurando aprender fazendo alguns projetos pequenos para alguns amigos, e não dá pra fugir da O.O.P.
Deixa eu fazer uma pergunta dá pra fazer isso utilizando apenas dbexpress, nada de DAO ou os dois se complementariam ?
Olá Sérgio
O importante aqui é entender o papel de cada um.
O Dbexpress é um conjunto de componentes que permite, de forma leve e rápida, acesso a diversos bancos de dados. Poderíamos dizer que ele tem basicamente a mesma função do IBX, porém, não é específico como o IBX o é (Interbase/Firebird). Assim como o IBX, você pode desenvolver aplicações com o Dbexpress sem utilizar um padrão de projeto. Contudo, note que no artigo eu tentei mostrar o problema de se ignorar as boas práticas de programação.
Já o DAO é um padrão de projeto, um Design Pattern. Os padrões de projetos permitem reaproveitar soluções previamente testadas e aprovadas. O DAO nos ajuda a seguir um padrão para as operações CRUD que desejamos efetuar em nossa base de dados.
Eu utilizei o IBX, mas poderia ter utilizado o Dbexpress, sem problema nenhum.
Abraços.
Gostaria de dar parabéns pelo artigo, é difícil encontrar um exemplo completo como o seu. Obrigado pela contribuição!
Fico feliz que tenha gostado, Luciano.
Valeu irmao, vai pra lista de favoritos aqui para me ajudar com ideias em novos projetos. Obrigado pela demonstração.
Primeiramente, gostaria de agradecer por um material tão minucioso na exemplificação, difícil achar material abordando o assunto como foi abordado aqui. Luiz, como faria para listar, por exemplo, o relacionamento das tabelas? Por exemplo, tenho uma o cadastro de Clientes e dentro desse cadastro, preciso informar o CEP, que é buscado em uma outra tabela para completar informações como endereço, bairro, cidade e uf. Gostaria de exibir essas informações(endereço, bairro, cidade, uf), que são de uma tabela “CEP” por exemplo, dentro da tela de clientes
Olá Thiago,
Creio que você já tenha uma classe TCliente com uma propriedade CEP. Você precisa então, criar uma classe chamada TCEP (poderia ser também TLogradouro, mas vamos seguir o padrão e nomear com o nome da tabela que você informou acima).
Você poderia utilizar algumas formas para retornar o resultado da pesquisa:
Você pode implementar todas estas formas, bastando dar overload na chamada do Read. Porém, para este exemplo, irei demostrar a segunda forma.
Vamos criar a nossa classe TCEP:
[sourcecode language=”delphi”]
TCEP = class
private
FUF: string;
FCidade: string;
FEndereco: string;
FBairro: string;
FCodCep: string;
procedure SetBairro(const Value: string);
procedure SetCodCep(const Value: string);
procedure SetCidade(const Value: string);
procedure SetEndereco(const Value: string);
procedure SetUF(const Value: string);
public
property CodCep: string read FCodCep write SetCodCep;
property Endereco: string read FEndereco write SetEndereco;
property Bairro: string read FBairro write SetBairro;
property Cidade: string read FCidade write SetCidade;
property UF: string read FUF write SetUF;
end;
TCEPDao = class
private
class function ComandoSql(ACEP: TCEP): Boolean;
public
class function Insert(ACEP: TCEP): Boolean; //create
class function Read(ACEP: TCEP): Boolean;
class function Update(ACEP: TCEP): Boolean;
class function Delete(ACodCep: string): Boolean;
end;
[/sourcecode]
Gere os métodos (Ctrl+Shift+C). Seguindo o que foi dito no artigo, chegamos ao seguinte resultado:
[sourcecode language=”delphi”]
{ TCEP }
procedure TCEP.SetBairro(const Value: string);
begin
FBairro := Value;
end;
procedure TCEP.SetCodCep(const Value: string);
begin
FCodCep := Value;
end;
procedure TCEP.SetCidade(const Value: string);
begin
FCidade := Value;
end;
procedure TCEP.SetEndereco(const Value: string);
begin
FEndereco := Value;
end;
procedure TCEP.SetUF(const Value: string);
begin
FUF := Value;
end;
{ TCEPDao }
class function TCEPDao.ComandoSql(ACEP: TCEP): Boolean;
begin
Result := false;
if not dm.ibTrans.InTransaction then
dm.ibTrans.StartTransaction;
try
with dm.exec do
begin
Close;
sql.clear;
SQL.Add(‘execute procedure cep_iu(‘);
sql.Add(‘:codcep, :endereco, :cidade, :bairro, :uf)’);
ParambyName(‘codcep’).AsString := ACEP.CodCep;
ParambyName(‘endereco’).AsString := ACEP.Endereco;
ParambyName(‘cidade’).AsString := ACEP.Cidade;
ParambyName(‘bairro’).AsString := ACEP.Bairro;
ParambyName(‘uf’).AsString := ACEP.UF;
ExecQuery;
end;
result := true;
except
on e: Exception do
begin
dm.ibTrans.Rollback;
Application.ShowException(e);
end;
end;
end;
class function TCEPDao.Delete(ACodCep: string): Boolean;
begin
Result := False;
if not dm.ibTrans.InTransaction then
dm.ibTrans.StartTransaction;
try
with dm.exec do
begin
close;
SQL.Clear;
sql.Add(‘delete from Cep where CodCep=:CodCep’);
ParamByName(‘CodCep’).AsString := ACodCep;
ExecQuery;
end;
dm.ibTrans.Commit;
dm.sqlReceber.Close;
dm.sqlReceber.Open;
Result := True;
except
on e: Exception do
begin
dm.ibTrans.Rollback;
Application.ShowException(e);
end;
end;
end;
class function TCEPDao.Insert(ACEP: TCEP): Boolean;
begin
result := ComandoSql(ACEP);
end;
class function TCEPDao.Read(ACEP: TCEP): Boolean;
var
AQuery: TIBQuery;
begin
AQuery := TIBQuery.Create(nil);
try
with AQuery do
begin
Database := dm.Db;
close;
sql.clear;
sql.Add(‘Select * from cep’);
sql.Add(‘where CodCep=:CodCep’);
Params[0].AsString := ACEp.CodCep;
Open;
// pega resultado
if recordcount > 0 then
begin
ACEP.Endereco := fieldbyname(‘Endereco’).AsString;
ACEP.Bairro := fieldbyname(‘Bairro’).AsString;
ACEP.Cidade := fieldbyname(‘Cidade’).AsString;
ACEP.UF := fieldbyname(‘UF’).AsString;
Result := True;
end;
end;
finally
FreeAndNil(AQuery);
end;
end;
class function TCEPDao.Update(ACEP: TCEP): Boolean;
begin
result := ComandoSql(ACEP);
end;
[/sourcecode]
A principal diferença entre o que foi visto no artigo e o que está acima é que no read passamos como parâmetro a classe TCep em vez de um dataset.
Agora no seu formulário de cadastro de clientes, você pode instanciar um objeto que será responsável por mostrar os dados do endereço. Por exemplo, digamos que ao sair do campo CEP, você queira mostrar o resultado em labels. Você pode fazer da seguinte forma:
[sourcecode language=”delphi”]
procedure TForm1.edCepExit(Sender: TObject);
var
Cep: TCEP;
begin
Cep := TCEP.Create;
try
//passamos o conteúdo do edit para o campo codcep da classe tcep.
Cep.CodCep := edCep.Text;
//chamamos o método responsável por pesquisar o cep
if TCEPDao.Read(Cep) then
begin
with Cep do
begin
//preenchemos os labels
lbEndereco.Caption := Endereco;
lbBairro.Caption := Bairro;
lbCidade.Caption := Cidade;
lbUF.Caption := UF;
end;
end
else
ShowMessage(‘Cep não encontrado!’);
finally
FreeAndNil(Cep);
end;
end;
[/sourcecode]
Basicamente é isso! Lembrando que este é um exemplo simples do assunto. Para um projeto real, devemos observar alguns fatores importantes, como por exemplo:
– O banco de dados pode vir a ser trocado por outro no futuro (firebird, MS Sql, MySql, …)?
– As definições das tabelas sofrem muitas alterações (novos campos, alteração de tamanho , etc.)?
– O software a ser desenvolvido é pequeno ou trata-se de um projeto de grande porte?
Isso e muitas outras coisas devem ser levadas em consideração na hora de construir suas classes. Para grandes projetos, deve desde o início procurar deixar o sistema preparado/aberto para as mudanças. Neste exemplo, o projeto é específico para o Firebird, visto que usamos o InterbaseExpress(IBX). O ideal é que utilizemos um conjunto de componentes que possibilite, de forma simples e tranquila, a mudança para um outro banco de dados caso seja necessário no futuro. Neste caso, o recomendado seria a utilização do DbExpress.
Abraços.
Obrigado pela resposta tão esclarecedora Luiz. Hoje estou trabalhando com banco de dados Mysql e componente Zeoslib para conexão com o banco, e o projeto não é tão grande, é para controle de pedidos da empresa onde trabalho. Grande abraço
Poder colocar anexo novamente, este abaixo esta com link quebrado.
Código Fonte: http://dl.dropbox.com/u/478707/BlogDoLuiz-Dao.zip
Infelizmente, esse não tenho mais. Tive algum problema com meu dropbox. Estou aos poucos migrando os arquivos que ainda encontrar.
Opinião.
Não gosto muito da ideia de colocar vários type de classes em uma única Unit.
No caso, do seu exemplo: “TReceber” e “TRecDao” da Unit “uReceber”.
Ou seja, prefiro deixar em arquivo separados.
Já vi units enormes por causa desta flexibilidade.
Gostaria da opinião de vocês.
Eis ai um assunto que pode levar a uma longa discussão.
Contrariamente ao que fiz no post, também tento deixar a unit o mais clean possível, porém se analisarmos a nossa própria linguagem, o Delphi, esta possui units com centenas de classes num único arquivo (veja a Winapi.Windows, por exemplo). Funcionando como Namespaces, agrupam assuntos relacionados num mesmo arquivo.
Além disso, não podemos confundir colocar várias classes num único arquivo com classes com múltiplas responsabilidades. A primeira creio ser mais uma questão de decisão de cada desenvolvedor ou empresa desenvolvedora. A segunda, aí sim, é uma quebra do princípio Single-responsibility (princípio de responsabilidade única) do SOLID.
Abraços.