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. :lol:

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:

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.

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:

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.

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:

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 ; ^

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:

type
  TOperacao = (tpInsert, tpUpdate, tpNone);

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:

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.

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.

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.

14 thoughts on “Delphi: Com ou Sem DAO? Eis a questão!”

  1. 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…

    1. 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.

  2. 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 ?

    1. 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.

  3. 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

    1. 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:

      • – Poderia fazer como no artigo e passar um dataset no parâmetro da função Read, caso seja necessário retornar mais de um registro; ou
      • – Passar um objeto da classe TCEP no parâmetro da função Read, caso cada Cep retorne apenas um resultado (um endereço); ou
      • – Passar o Cep (string) no parâmetro e retornar um objeto da classe TCEP.

      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:

        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;
      

      Gere os métodos (Ctrl+Shift+C). Seguindo o que foi dito no artigo, chegamos ao seguinte resultado:

      { 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;
      

      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:

      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;
      

      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.

  4. 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

  5. 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.

    1. 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.

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.