Nenhum comentário Download


Delphi – Parte VI

 

Vimos no artigo anterior, a alteração do nosso projeto onde parametrizamos os cadastros e utilizamos uma maneira mais prática para trabalhar com dbExpress. Criamos um cadastro onde precisamos pesquisar qual registro queremos alterar, sem a necessidade de colocar registros desnecessários em memória no ClientDataSet.

 

Neste artigo veremos como utilizar o ClientDataSet para usar dados em memória e usar transação para cadastros master/detail. Teremos um cadastro de vendas com a venda em si e seus itens (produtos). As mesmas possuem tabelas especificas, mas tem um relacionamento, assim, precisamos criar a venda e nos itens, temos que vincular o mesmo com a venda.

 

Nesse caso, não podemos ter problemas de erros ao acrescentar nada (vendas ou itens), pois não podemos ter itens sem venda e nem venda, sem itens. Por isso, vamos usar transação, que nos da a certeza que todos os dados serão inseridos.

O que é transação

O contexto de transação é um dos mais importantes na tecnologia cliente/servidor. Ela garante que os dados serão inseridos corretamente no banco através da aplicação, ou seja, possui consistência. Uma transação por si só, é qualquer operação de escrita em uma tabela do banco.

 

Essa operação pode ser inserção, exclusão ou atualização. Comandos que alteram a estrutura dos objetos, não tem transação vinculada. Vamos a um exemplo simples para entendermos. Saque em um caixa eletrônico. Imagine que você esta realizando o saque e após digitar a senha esta esperando o dinheiro. Nesse momento, acontece um blecaute e a energia do caixa é cortada.

 

Você ainda não pegou o dinheiro, mas ele já foi debitado de sua conta? Uma transação em um ambiente de aplicação é chamada de “tudo ou nada”, ou seja, tudo deve ser executado (verificação de saldo, baixa do valor sacado, liberação do dinheiro) ou nada deve ser feito (principalmente o lançamento de débito na conta).

 

Toda transação deve ter um inicio e um final. O final vai depender do que ocorrer no decorrer da mesma. Ela pode finalizar com sucesso, onde o comando Commit será executado no banco, ou ela será cancelada, onde o Roolback irá desfazer tudo que foi realizado até o inicio da transação.

 

Abaixo, temos a Listagem 1 e 2, onde temos dois exemplos de utilização de transação com dbExpress. A Listagem 1 utiliza um exemplo até a versão 2010 do dbExpress. A Listagem 2 temos a utilização de transações no Delphi XE2.

 

Listagem 1. Executando transações com dbExpress

var 

  trans: TTransactionDesc;

begin

  try

    t.IsolationLevel := xilREADCOMMITTED;

    SQLConnection.StartTransaction(trans);

    

                           //comandos que deseja executar no banco

 

    SQLConnection.Commit(trans);

  except

    //caso ocorra algum erro, o comando Rollback cancela tudo

                           SQLConnection.Rollback(trans);

  end;

end;

 

Listagem 2. Executando transações com dbExpress no Delphi XE2

var

  trans: TDBXTransaction;

begin

  try

    trans := SQLConnection.BeginTransaction(TDBXIsolations.ReadCommitted);

                          

    //comandos que deseja executar no banco

 

    SQLConnection.CommitFreeAndNil(trans);

  except

    //caso ocorra algum erro, o comando Rollback cancela tudo

    SQLConnection.RollbackFreeAndNil(trans);

  end;

end;

 

Veja que as duas são bastante parecidas. Na Listagem 1 criamos uma variável do tipo TTransactionDesc. Após configuramos o nível de isolamento da transação, utilizando a opção read comitted, iniciamos a transação usando BeginTransaction do componente SQLConnection.

 

Após executar os comandos no banco, finalizamos a transação com o Commit. Caso ocorra, algum erro, nosso código esta dentro de um bloco try...except, assim podemos usar o Rollback para cancelar todos os comandos.

 

Temos que ter cuidado na configuração do nível de isolamento. Read commited indica que vamos ler (se for o caso), apenas os dados gravados, ou seja, os dados reais do banco. Se usarmos a opção dirty read (xilDIRTYREAD) vamos poder ler os dados pendentes, ou seja, dados que ainda não foram persistidos no banco.

 

Esses dados podem estar pendentes de uma transação e caso aconteça algo, podem ser descartados, assim, podemos também visualizar dados que não foram inseridos no banco. Analise qual a melhor forma que você precisa usar em sua aplicação. Lembrando também que o nível de isolamento esta relacionado ao banco de dados, que pode variar de acordo com o mesmo.

 

Na Listagem 2, a configuração do nível de isolamento esta no BeginTransaction e para a confirmação e cancelamento do código, temos respectivamente CommitFreeAndNil e RollbackFreeAndNil.

Criando o formulário de cadasto

Nesse exemplo, não iremos usar a tela de cadastro semelhante aos cadastros anteriores. Vamos criar uma tela do zero, pois as funcionalidades são diferentes das telas de cadastro. A idéia aqui como já comentado, é ter uma tela onde possamos indicar informações da venda (código do cliente, código do empregado, data de venda e valor) e os itens dessa venda (produto).

 

Vamos deixar o usuário escolher em uma pesquisa, o cliente e empregado e digitar a data da venda. A questão do valor da venda, esta ligada a soma do valor dos produtos multiplicados pela quantidade.

 

Esse cálculo e a atribuição ao campo, faremos via código. Veja na Figura 1 como ficara a tela de cadastro.

 

 

Figura 1. Tela de cadastro de vendas

 

Não precisamos criar os componentes de pesquisa para cliente e empregado. Os respectivos ClientDataSets estão no Data Module de pesquisa. Precisamos apenas criar o de produto, caso você não tenha feito, como sugestão no último artigo.

Veja no código a seguir, o comando SQL para a pesquisa de produto:

 

select nCdProduto, sNmProduto

from PRODUTO

where UPPER(sNmProduto) like :sNmProduto

 

Configure o parâmetro semelhante ao que fizemos no artigo anterior. Agora, configure os DataSources do cliente, empregado e produto para os respectivos ClientDataSets do Data Module de pesquisa. Faça o mesmo para os DBTexts dos campos.

Criando uma tela de pesquisa genérica

Como podemos ver na Figura 1, a tela de vendas terá uma pesquisa para cliente, empregado e produto. Não vamos criar três telas de pesquisa, vamos criar apenas uma, para ser usada nos dois casos (genérica). Diferente do que fizemos com a tela de pesquisa de setor.

 

Nota: Você pode adaptar a tela de pesquisa de setor para ficar genérica, como mostraremos agora.

 

A tela de pesquisa em si, será simples. Basicamente, basta um grid, um edit e um botão de confirmação. A boa notícia, que podemos utilizar a técnica de apenas mudar o DataSet esperado pelo Grid.

 

Nota: poderíamos criar um componente especifico para esse tipo de funcionalidade. Veremos isso no decorrer do nosso curso.

 

Veja na Figura 2 a tela de pesquisa.

 

 

Figura 2. Tela de pesquisa genérica do sistema

 

Ao olhar o código, você irá notar que temos configuração para o nome das colunas do Grid, além de mostrar a quantidade de registros encontrados. No Edit da tela de pesquisa, vamos codificar seu evento OnKeyPress com o código da Listagem 3.

 

Listagem 3. Codificando o KeyPress do Edit

if Key = #13 then

begin

  dsPesquisa.DataSet.Close;

  (dsPesquisa.DataSet as TClientDataSet).Params[0].AsString :=

     UpperCase('%'+ edtPesquisa.Text + '%');

  dsPesquisa.DataSet.Open;

 

 StatusBar1.SimpleText := Format('%d registro(s) encontrado(s)',

   [dsPesquisa.DataSet.RecordCount]);

  btnOk.Enabled := dsPesquisa.DataSet.RecordCount > 0;

  ...

end;

 

Veja que para chamar os métodos Close e Open precisamos apenas usar a propriedade DataSet do DataSource. Como a propriedade Params esta implementada em ClientDataSet, precisamos fazer um cast para podermos configurar a respectiva propriedade.

 

Por fim, apenas informamos na barra de status a quantidade de registros retornados pela pesquisa e se forem retornados registros o OK ficara habilitado. O botão OK, apenas fecha e muda a propriedade ModalResult  do formulário.

 

Precisamos disso, para que o formulário chamador da tela saiba qual o retorno esperado. Usamos o seguinte código:

 

Close;

ModalResult := mrOk;

 

Agora, na tela de cadastro de venda, precisamos fazer a chamada em cada botão para o formulário de pesquisa genérico. Na Listagem 4 temos como será a chamada dos botões.

 

Listagem 4. Chamada para a tela genérica de pesquisa

private

   nCdCliente: integer;

   nCdEmpregado: integer;

   nCdProduto: integer;

...

Cliente

try

   frmPesquisa := TfrmPesquisa.Create(self);

   DMPesquisa.cdsPesquisaCliente.Open;

   frmPesquisa.dsPesquisa.DataSet := DMPesquisa.cdsPesquisaCliente;

   frmPesquisa.Descricao := 'Nome do cliente';

   if frmPesquisa.ShowModal <> mrOk then

      DMPesquisa.cdsPesquisaCliente.Close

   else

      cdsCliente := DMPesquisa.cdsPesquisaCliente.FieldByName(

        'nCdCliente').AsInteger;

finally

   frmPesquisa.Free;

end;

 

Empregado

try

   frmPesquisa := TfrmPesquisa.Create(self);

   DMPesquisa.cdsPesquisaEmpregado.Open;

   frmPesquisa.dsPesquisa.DataSet := DMPesquisa.cdsPesquisaEmpregado;

   frmPesquisa.Descricao := 'Nome do empregado';

   if frmPesquisa.ShowModal <> mrOk then

     DMPesquisa.cdsPesquisaEmpregado.Close

   else

      cdsEmpregado := DMPesquisa.cdsPesquisaEmpregado.FieldByName(

        'nCdEmpregado').AsInteger;

finally

   frmPesquisa.Free;

end;

 

Produto

try

   frmPesquisa := TfrmPesquisa.Create(self);

   DMPesquisa.cdsPesquisaProduto.Open;

   frmPesquisa.dsPesquisa.DataSet := DMPesquisa.cdsPesquisaProduto;

   frmPesquisa.Descricao := 'Nome do produto';

   if frmPesquisa.ShowModal <> mrOk then

     DMPesquisa.cdsPesquisaProduto.Close

   else

     cdsProduto := DMPesquisa.cdsPesquisaProduto.FieldByName(

       'nCdProduto').AsInteger;

finally

   frmPesquisa.Free;

end;

 

Primeiro, criamos variáveis auxiliares que vão armazenar o código dos respectivos cadastros, que serão usados no armazenamento dos itens. Veja que a diferença do código que chama a tela de pesquisa, fica por conta da configuração do ClientDataSet de pesquisa que vamos utilizar. Na ordem, criamos o formulário, abrimos o componente de pesquisa e configuremos o DataSource da tela.

 

Após, indicamos o nome da coluna do Grid e por fim, verificamos o retorno da tela modal. Devemos fazer isso, por que o DBText com o nome do cliente, empregado ou produto, esta vinculado ao componente de pesquisa. Se o usuário não escolher nada e fechar a tela, continuaríamos com a vinculação do nome, o que não é correto.

 

Por isso, fechamos o componente de pesquisa, se o usuário não escolher nenhum item na tela de pesquisa. Veja na Figura 3 a tela em execução.

 

 

Figura 3. Tela de pesquisa genérica

 

Dica: Você pode disponibilizar para o usuário, digitar o código ou o nome ao invés do botão de pesquisa. O sistema faz a busca e caso não retorne nenhum registro, você apresenta a tela de pesquisa. Fica como dica.

 

Colocando os dados em memória

Agora, precisamos configurar o ClientDataSet da tela para receber os dados do produto e armazenar os mesmos em memória. Clique com o botão direito no cdsItens e escolha Fields Editor. No editor, basta usar a combinação de teclas Crtl + N para abrir a tela para criar o campo confirme vemos a Figura 4.

 

 

Figura 4. Criando o campo no cdsItens

 

Vamos criar o primeiro campo, que será o que vai armazenar o código do produto. Em Name digite “nCdProduto” e em Type escolha Integer. Como escolhemos Integer não precisamos configurar o Size. Na Tabela 1 temos os outros campos que devem compor o cdsItens.

 

Name

Type

Size

Field type

sNmProduto

String

50

Data

nVlProduto

Currency

--

Data

nQtProduto

Float

--

Data

SubTotal

Currency

--

InternalCalc

Total

Currency

--

Aggregate

Tabela 1. Campos do ClientDataSet

 

Para o campo Total, precisamos configurar a sua propriedade Expression para “SUM(SubTotal)”. Para o campo SubTotal precisamos acessar o evento OnCalcFields do cdsItens e adicionar o seguinte código:

 

cdsItensSubTotal.AsCurrency :=

   (cdsItensnVlProduto.AsCurrency * cdsItensnQtProduto.AsFloat);

 

No código, apenas realizamos o cálculo do subtotal, que é a multiplicação do valor pela quantidade do produto. Por fim, altere para True a propriedade AggregatesActive do cdsItens. Agora, precisamos codificar o botão que vai armazenar cada produto escolhido pelo usuário. Veja na Listagem 5 o código do botão.

 

Listagem 5. Botão de adicionar o item

if (nCdProduto = 0) then

   ShowMessage('É necessário escolher um produto')

else if (edtValor.Text = '') then

   ShowMessage('Campo Valor: preenchimento obrigatório.')

else if (edtQuantidade.Text = '') then

   ShowMessage('Campo Quantidade: preenchimento obrigatório.')

else

begin

   if not cdsItens.Active then

      cdsItens.CreateDataSet;

 

   cdsItens.Insert;

   cdsItensnCdProduto.AsInteger := nCdProduto;

   cdsItenssNmProduto.AsString :=

      DMPesquisa.cdsPesquisaProduto.FieldByName('sNmProduto').AsString;

   cdsItensnVlProduto.AsCurrency := StrToCurr(edtValor.Text);

   cdsItensnQtProduto.AsFloat := StrToFloat(edtQuantidade.Text);

   cdsItens.Post;

 

   edtValor.Text := '';

   edtQuantidade.Text := '';

   DMPesquisa.cdsPesquisaProduto.Close;

   nCdProduto = 0;

end;

 

O código faz primeiramente algumas validações para saber se o usuário escolheu um produto e preencheu os campos Valor e Quantidade. Após, é verificado se o ClientDataSet não esta ativo, vamos chamar o método CreateDataSet que cria em memória o espaço necessário para adicionarmos os itens.

 

Adiante, simplesmente, passamos para os campos do cdsItens, os valores necessários e no final, limpamos controles de tela e componentes. Você já pode testar a inserção de itens no cadastro (Figura 5).

 

 

Figura 5. Inserindo itens na tela de vendas

 

Para mostrar o total geral, basta adicionar um DBText abaixo do Grid e vincular o mesmo ao campo Total do cdsItens. Para o botão de excluir (ao lado do Grid), basta chamar o Delete do cdsItens.

Criando as Stored Procedures

Na Listagem 5 temos as Stored Procedures que vamos utilizar na inserção da venda e itens.

 

Listagem 5. Stored Procedures para inserir venda e itens

CREATE PROCEDURE INSERT_VENDA

    @nCdCliente         int,

    @nCdEmpregado       int,

    @tDtVenda                  datetime,

    @nVlVenda                  decimal(9,2),

    @nCdVenda                  int output

 

AS

BEGIN

    INSERT INTO VENDA (nCdCliente, nCdEmpregado, tDtVenda, nVlVenda)

    VALUES (@nCdCliente, @nCdEmpregado, @tDtVenda, @nVlVenda)

 

    select @nCdVenda = @@IDENTITY from VENDA

END

GO

 

CREATE PROCEDURE INSERT_ITEM

    @nCdVenda           int,

    @nCdProduto  int,

    @nVlItem            decimal(9,2),

    @nQtItem            decimal(9,2)

AS

BEGIN

    INSERT INTO ITENS (nCdVenda, nCdProduto, nVlItem, nQtItem)

    VALUES (@nCdVenda, @nCdProduto, @nVlItem, @nQtItem)

 

    UPDATE PRODUTO

      SET nQtEstoque = nQtEstoque - @nQtItem

      WHERE nCdProduto = @nCdProduto

END

 

Note que as duas SP são bastante simples, apenas estamos usando o comando INSERT nas respectivas tabelas. Uma diferença fica na INSERT_VENDA, onde, após executar o comando INSERT, retornamos o código da venda (que é um identity) usando o atributo @@IDENTITY.

 

Esse retorno esta em um parâmetro de saída, ou seja, um parâmetro declarado no cabeçalho da Stored Procedure, mas que não preenchemos o mesmo, ele será preenchido no corpo da SP.

 

Assim, na aplicação, pegamos esse valor do código da venda para inserir na INSERT_ITEM, para fazermos o relacionamento das tabelas. Nessa procedure, ainda fizemos uma regra de negócio, onde ao incluir o item da venda, vamos diminuir a sua quantidade na tabela PRODUTO. Assim, concentramos na procedure a funcionalidade de baixa o estoque.

Usando componetes SQLStoredProc

Agora, vamos adicionar no Data Module, dois SQLStoredProc e vincular os mesmos com as Stored Procedures criadas anteriormente. Note que ao adicionar as SPs a propriedade Params é preenchida automaticamente (Figura 6).

 

 

Figura 6. Parâmetros da Stored Procedure no SQLStoredProc

 

Agora, vamos criar uma função responsável por repassar os dados dos itens e vendas para os respectivos componentes SQLStoredProc. Veja na Listagem 6 o código.

 

Listagem 6. Método para gravar a Venda e seus itens

function InserirVenda(nCdCliente, nCdEmpregado: integer;

  tDtVenda: TDateTime; cdsItens: TClientDataSet): boolean;

var

  nCdVenda: integer;

  i: integer;

  trans: TDBXTransaction;

begin

 

  try

    trans := DelphiTheClub.BeginTransaction(TDBXIsolations.ReadCommitted);

 

    //preenche a venda

    spVenda.Params[1].AsInteger := nCdCliente;

    spVenda.Params[2].AsInteger := nCdEmpregado;

    spVenda.Params[3].AsDateTime := tDtVenda;

    spVenda.Params[4].AsFloat :=

      StrToFloat(cdsItens.FieldByName('Total').AsString);

 

    //executa a SP

    spVenda.ExecProc;

 

    //pega o retorno

    nCdVenda := spVenda.Params[5].AsInteger;

 

    //adicione os itens

    for i := 0 to cdsItens.RecordCount -1 do

    begin

      spItens.Params[1].AsInteger := nCdVenda;

      spItens.Params[2].AsInteger :=

        cdsItens.FieldByName('nCdProduto').AsInteger;

      spItens.Params[3].AsCurrency :=

        cdsItens.FieldByName('nVlProduto').AsCurrency;

      spItens.Params[4].AsFloat :=

        cdsItens.FieldByName('nQtProduto').AsFloat;

 

      spItens.ExecProc;

 

      cdsItens.Next;

    end;

 

    DelphiTheClub.CommitFreeAndNil(trans);

 

    Result := true;

  except

    DelphiTheClub.RollbackFreeAndNil(trans);

    Result := false;

  end;

end;

 

Como podemos notar, esta usando transação. Primeiramente, preenchemos o componente da SP de venda e em uma variável adicionamos o retorno da SP, o código da venda.

 

Após, percorremos o ClientDataSet dos itens, preenchendo seus parâmetros e chamando o ExeProc. Caso aconteça algum problema, o bloco do except é executado e nada será executado no banco. A função retornará true se não tivermos problemas e false, se ocorrer algum erro.

No formulário da venda, basta chamar o método usando o código da Listagem 7.

 

Listagem 7. Validações para salvar a venda

if (nCdCliente = 0) then

  ShowMessage('É necessário escolher um cliente.')

else if (nCdEmpregado = 0) then

  ShowMessage('É necessário escolher um empregado.')

else if (not cdsItens.Active) or (cdsItens.RecordCount = 0) then

  ShowMessage('É necessário adicionar ao menos um item.')

else

begin

  if DM.InserirVenda(nCdCliente, nCdEmpregado, tDtVenda.DateTime,

    cdsItens) then

  begin

    MessageDlg('Venda inserida com sucesso.', mtInformation, [mbOK], 0);

 

    nCdCliente := 0;

    nCdEmpregado := 0;

    DMPesquisa.cdsPesquisaCliente.Close;

    DMPesquisa.cdsPesquisaEmpregado.Close;

    cdsItens.Close;

  end

  else

    ShowMessage('Ocorreu um erro.');

end;

 

No código, fizemos validações referentes aos campos obrigatórios da tela. Após, chamamos o InserirVenda do Data Module, verificando se o retorno será true para então, emitir uma mensagem e configurar as variáveis e componentes para iniciar uma nova venda.

 

Faça testes, inserindo vendas, itens etc. Confirme que os registros foram inseridos no banco. Simule um erro e confirme que nenhum registro será adicionado no banco, pois estamos usando transação.

SQL Server 2012

Quando iniciamos essa série, indicamos o uso do SQL Server 2008 Express. A Microsoft lançou a pouco tempo a versão 2012 do seu servidor de banco de dados. Rumores indicam que o XE3 terá suporte a essa nova versão do SQL Server.

Instalei o mesmo e começou a ocorrer o seguinte erro:

 

DBX Error:  Driver could not be properly initialized.  Client library may be missing, not installed properly, of the wrong version, or the driver may be missing from the system path.

 

Isso ocorre, por que o XE2 ainda não suporta o mesmo. Como eu posso usar o SQL Server 2012 com o XE2? Simples, instale o cliente do SQL Server 2008. Acesse o link de acordo seu SO:

 

32 bits: http://go.microsoft.com/fwlink/?LinkId=123717&clcid=0x416

64 bits: http://go.microsoft.com/fwlink/?LinkId=123718&clcid=0x416

Conclusões

Vimos nesse artigo, como utilizar transações com dbExpress. Nesses três artigos sobre Delphi e banco de dados, acredito que pudemos entender apenas uma parte das vastas possibilidades que encontramos no Delphi para trabalhar com banco de dados.

 

No próximo artigo vamos conhecer o poder da programação Orientada em Objetos, com herança visual, classes e muito mais.

Um grande abraço a todos e até a próxima!