Construo de Aplicaes de Acesso a Banco de Dados Parte I (Cadastro Simples)
O objetivo deste material demonstrar possveis cdigos (enxutos e seguros) para realizar operaes de acesso/manuteno em uma tabela com uma ou mais relaes do tipo 1xn (um para muitos) utilizando componentes DataWare (componentes que representam automaticamente informaes contidas em uma origem de dados. O modelo de dados utilizado neste exemplo utiliza duas tabelas relacionadas da seguinte forma:
Para fazer a demonstrao ser utilizada uma janela de manuteno semelhante figura abaixo com o devido tratamento dos eventos que possam ocorrer objetivando um cdigo simples que possa funcionar para qualquer tela de cadastro com caractersticas semelhantes. De forma a organizar o cdigo um componente ActionList ser utilizado com a criao e implementao de 7 aes (actNovo, actLocalizar, actSalvar, actCancelar, actExcluir, actSair, actLocalizarPorDescricao).
Durante o desenvolvimento dos cdigos algumas variveis so utilizadas e precisaro ter seu contedo substitudo de acordo com a tabela na qual a manuteno ser feita. A seguir o detalhamento do significado de tais variveis:
Tabela: Componente do tipo SQLQuery (guia SQLdb) que contm em sua propriedade Fields, todos os campos fsicos da tabela que tero manuteno; CampoChave: Nome do campo chave da tabela no pode ser duplicado e que identifica um registro unicamente (o tipo de tal campo deve obrigatoriamente ser numrico inteiro). CampoDescricao: Campo descritivo, que serve para buscas parciais (geralmente um nome ou uma descrio do contedo do registro). CREATE TABLE UNIDADES ( SIGLA CHAR(3) NOT NULL, NOME_UNIDADE VARCHAR(60) NOT NULL );
CREATE TABLE INGREDIENTES ( ID_INGREDIENTE INTEGER NOT NULL, DESCRICAO VARCHAR(100) NOT NULL, UNIDADE CHAR(3) NOT NULL ); Edit DBEdit (ParentColor = true, ReadOnly = true)
A manuteno de uma ou mais tabelas envolve a ativao de uma consulta (inicializao de uma transao e busca dos dados) e o seu encerramento (liberao dos recursos e encerramento da transao). Um cdigo para fazer tais procedimentos muito comum em uma aplicao de manuteno de dados. Neste sentido, a seguir so apresentadas duas sub-rotinas que sero futuramente referenciadas em diversas outras partes do cdigo. O primeiro procedimento AbrirConsultas() responde pela ativao da transao (se a mesma no estiver ativa) e pela abertura das consultas, iniciando pelas tabelas relacionadas e por ltimo a tabela principal. O segundo procedimento FecharConsultas inicialmente encerra a tabela principal e as tabelas relacionadas e por fim encerra a transao. Uma prxima chamada ao procedimento de AbrirConsultas() far a inicializao de uma nova transao, podendo dessa forma visualizar dados inseridos por outros usurios.
procedure AbrirConsultas(); begin //Se a transao no est ativa ento inicia ela if not SQLTransaction.Active then SQLTransaction.StartTransaction;
if not TabelaRelacionada1.Active then TabelaRelacionada1.Open; //Se as tabelas if not TabelaRelacionada2.Active then TabelaRelacionada2.Open; //relacionadas no esto if not TabelaRelacionadaN.Active then TabelaRelacionadaN.Open; //ativas ento ative-as //se a tabela no est ativa ento a ative if not Tabela.Active then Tabela.Open; end;
procedure FecharConsultas(); begin //se as tabelas relacionadas esto ativas ento as mesmas devem ser fechadas if TabelaRelacionada1.Active then TabelaRelacionada1.Close; if TabelaRelacionada2.Active then TabelaRelacionada2.Close; if TabelaRelacionadaN.Active then TabelaRelacionadaN.Close; //se a tabela est ativa ento ela deve ser encerrada if Tabela.Active then Tabela.Close; //se a transao estiver ativa a mesma deve ser encerrada if SQLTransaction.Active then SQLTransaction.EndTransaction; end;
Abertura e Encerramento da J anela de Cadastro
Os eventos de abertura (OnShow) e fechamento (OnClose e OnCloseQuery) de um formulrio tambm merecem ateno. No primeiro caso o estado das tabelas envolvidas na aplicao precisa ser ajustado (as tabelas devem ser iniciadas fechadas, aguardando uma ao do usurio). No caso do fechamento devemos verificar se existem alteraes pendentes que no foram salvas assim como encerrar os DataSets utilizados.
Evento OnShow
FecharConsultas(); HabilitarDesabilitarControles;
Evento OnCloseQuery
if TestaSeDesejaSalvar() <> mrCancel then CanClose:=true else CanClose:=false;
Evento OnClose
FecharConsultas(); Fechamento da Aplicao (ao actSair)
A ao de sada deve simplesmente fechar o formulrio (mtodo close). As demais verificaes ficam a cargo dos eventos descritos anteriormente: OnClose (disparado antes do fechamento) e OnCloseQuery (disparado antes do fechamento para confirmar se a aplicao ser ou no fechada).
Close;
Caderno Didtico Lazarus IDE Pgina 72
72 Universidade Federal de Santa Maria Concorrncia e Auto-incremento de um campo chave numrico
Em geral, um campo chave inteiro no deve estar disponvel nem para o usurio informar, muito menos alterar. Neste sentido, faz-se necessrio uma rotina que produza uma numerao sequencial. Os SGBDs modernos oferecem mecanismos de gerao automtica destes valores (campos conhecidos como Autoincremento ou Autonumerao), mas o uso destes recursos em algumas situaes impede que o programador tenha o controle sobre a numerao gerada. A proposta do cdigo que ser sugerido de produzir a partir dos prprios dados, a cada nova incluso, um valor para o campo chave. Um detalhe importante que precisa ser observado antes da construo deste cdigo: a possibilidade do banco de dados e/ou a tabela em questo estar sendo compartilhada e mais de um usurio realizar acesso simultneo aos dados. Neste caso faz-se necessrio tomar o cuidado de nunca gerar dois cdigos iguais para registros diferentes, situao essa que produziria uma duplicao de chave primria ocasionando um erro e impedindo o salvamento do registro. A seguir sero analisados dois cdigos, o primeiro para tabelas locais, onde no existe acesso simultneo, ou seja, somente um usurio/aplicao realiza o acesso de cada vez. Em ambos os caso ser utilizada uma funo para produzir e retornar o ltimo valor disponvel para ser utilizado em um novo registro.
function BuscaUltimoCodigo(): integer; var retorno : integer; //varivel auxiliar para guardar o cdigo novo index_old : string; //varivel auxiliar para guardar a indexao atual begin //guarda o campo que indexa a tabela index_old:=Tabela.IndexFieldNames;
//troca a indexao da tabela para o campo chave Tabela.IndexFieldNames:=CampoChave;
AbrirConsultas(); //Inicia transao e abre as consultas
//posiciona o cursor no ltimo registro Tabela.Last;
//guarda na varivel retorno o valor do campo chave + 1 retorno := Tabela.FieldByName(CampoChave).asInteger + 1;
//volta a indexao da tabela como a mesma estava originalmente Tabela.IndexFieldNames:=index_old;
//se o retorno for 0 (zero - tabela vazia) ento o valor a ser retornado 1 (um) if retorno = 0 then retorno := 1;
//A funo devolve para o cdigo que a chamou o valor de retorno BuscaUltimoCodigo := retorno; end;
Havendo a necessidade de realizar acessos simultneos em uma tabela, como por exemplo, em um supermercado, onde diversos computadores esto lendo e gravando alteraes nas tabelas simultaneamente, uma alternativa interessante para o controle da gerao de valores para o campo chave a criao de uma tabela auxiliar que contm um campo para identificar o nome da tabela e outro para armazenar o prximo cdigo a ser utilizado. Algo semelhante ao demonstrado na figura e no script abaixo:
CREATE TABLE IDS_DISPONIVEIS ( NOME_TABELA VARCHAR(30) NOT NULL PRIMARY KEY, ULT_ID INTEGER NOT NULL );
Caderno Didtico Lazarus IDE Pgina 73
73 Universidade Federal de Santa Maria A seguir uma alternativa funo BuscaUltimoCodigo dessa vez considerando acessos simultneos. Neste cdigo faz-se a utilizao de um componente SQLQuery e tambm do componente SQLTransaction (recomenda-se a utilizao de um componente SQLTransaction adicional, e no aquele que eventualmente j existe na aplicao). Diferentemente da funo BuscaUltimoCodigo demonstrada anteriormente o cdigo a seguir independente de tabela, ou seja, ele ir receber como parmetro o nome da tabela do qual se deseja produzir o prximo identificador. Este cdigo pode ficar disponvel em uma unit de acesso geral (uma sugesto seria utilizar a unit do DataModule onde esto configuradas as conexes ao banco de dados).
function BuscaUltimoCodigo(nome_tabela: string): integer; const MAX_TENTATIVAS = 5; //Numero de tentativas que sero feitas TEMPO_ESPERA = 100; //Tempo de espera em milissegundos aps cada tentativa var ult_id : integer; //varivel para guardar e retornar o ltimo valor tentativas : integer; //varivel para controlar o nmero de tentativas executadas begin tentativas:=0; ult_id:=0;
//Enquanto o nmero de tentativas no alcanar o limite while tentativas <= MAX_TENTATIVAS do begin try try //Se a transao auxiliar estiver ativa ento ela deve ser encerrada if SQLTransactionAux.Active then SQLTransactionAux.EndTransaction; SQLTransactionAux.StartTransaction; //Inicia uma nova transao //Se a query auxiliar estiver ativa ento ela deve ser fechada if SQLQueryAux.Active then SQLQueryAux.Close;
//Limpa e adiciona ao texto da query o comando update SQLQueryAux.SQL.Clear; SQLQueryAux.SQL.Add('update ids_disponiveis set ult_id = ult_id + 1 ' + 'where nome_tabela = ' + QuotedStr(nome_tabela)); SQLQueryAux.ExecSQL; //executa o comando update
//Limpa e adiciona ao texto da query o comando select SQLQueryAux.SQL.Clear; SQLQueryAux.SQL.Add('select ult_id from ids_disponiveis ' + 'where nome_tabela = ' + QuotedStr(nome_tabela)); SQLQueryAux.Open; //executa o comando select
//Atribui a varivel ult_id o valor do campo retornado pelo select ult_id:=SQLQueryAux.FieldByName('ULT_ID').asInteger;
SQLTransactionAux.Commit; //Encerra a transao gravando os dados break; //Encerra o lao de repetio
except //Caso algum erro ocorra SQLTransactionAux.Rollback; //Cancela as alteraes feitas no banco de dados
tentativas:=tentativas+1; //Incrementa o nmero de tentativas sleep(TEMPO_ESPERA); //Aguarda o tempo configurado para tentar novamente end; finally //Aps concluir o cdigo (se existirem ou no erros de execuo) //Testa se a transao no esta ativa e volta a ativa-la if not SQLTransactionAux.Active then SQLTransactionAux.StartTransaction; end; //Se o n de tentativas alcanou o mximo ento gera um erro para bloquear a aplicao if tentativas = MAX_TENTATIVAS then raise Exception.Create('No foi possvel obter um identificador nico'); end;
//A funo devolve para o cdigo que a chamou o valor de retorno BuscaUltimoCodigo := ult_id; end;
Caderno Didtico Lazarus IDE Pgina 74
74 Universidade Federal de Santa Maria
Verificando Alteraes em um Registro
Um DataSet (conjunto de dados) troca de estado em diversos momentos (hora est sendo consultada, hora est recebendo um registro, hora est editando um registro, etc.) e durante as mudanas de estado necessrio saber o que o usurio quer fazer com as informaes que at ento no foram salvas. Suponha, por exemplo, uma situao onde o usurio clica no boto que faz a incluso e antes de salvar ele clica novamente sobre o boto da incluso, o que aconteceria com os dados digitados que no foram salvos? Considerando situaes como essas, faz-se necessrio questionar o usurio sobre o que o mesmo deseja fazer com o registro atual: salv-lo, cancelar as alteraes feitas ou cancelar a ao que ele eventualmente tenha feito (no caso do exemplo anterior, o clique dado no boto novo). A seguir uma funo que dependendo da situao do DataSet questiona o usurio acerca do que fazer diante de uma situao de perda de dados no salvos. A funo retorna trs possveis valores: mrYes (os dados devem ser salvos), mrNo (os dados no devem ser salvos) e mrCancel (a ao disparada no deve ser executada).
function TestaSeDesejaSalvar(): integer; var retorno : integer; //Varivel auxiliar para armazenar a resposta begin retorno := mrNo; //Por padro o retorno no salvar
//Se dados estiverem sendo includos ou editados if Tabela.State in [dsInsert, dsEdit] then begin //Alimenta a varivel com a resposta do usurio retorno := MessageDlg('Confirmao', 'Deseja salvar as alteraes?', mtInformation, [mbYes, mbNo, mbCancel], 0);
if retorno = mrYes then //Se a resposta for sim actSalvar.Execute() //executa a ao de salvar else if retorno = mrNo then //Se a resposta for no begin Tabela.Cancel; //Cancela as alteraes FecharConsultas(); //Fecha os DataSets e encerra a transao end; end; //A funo devolve para o cdigo que a chamou o valor de retorno TestaSeDesejaSalvar := retorno; end;
Inicializao, Validao dos Dados e Autorizao de Edio dos Controles Visuais
Comumente algumas informaes precisam ser inicializadas no ato da incluso ou ento validadas antes de um salvamento. A seguir so propostos trs subrotinas com propsitos distintos, porm teis nas demais aes que sero descritas posteriormente. Seu cdigo vai depender de cada situao e neste material apenas so disponibilizados exemplos.
Procedimento para Inicializar os Campos de um Formulrio
procedure InicializarCampos; begin //Cdigo opcional para fazer algum tipo de inicializao. //Exemplo ... deixar um LookupComboBox sem nenhum item selecionado: DBLkpCmbBxUnidade.ItemIndex := -1;
//Tornar padro um determinado item de um DBRadioGroup DBRdGrpTipo.ItemIndex := 1; end;
Caderno Didtico Lazarus IDE Pgina 75
75 Universidade Federal de Santa Maria
Funo para Validar os Campos de um Formulrio
function ValidarCampos(): boolean; var retorno : boolean; //varivel auxiliar para guardar o retorno da funo begin retorno := true; //por padro no h erros
//Se o tamanho do campo sem os espaos em branco for 0 if Length(Trim(DBEdtDescricao.Text)) = 0 then begin DBEdtDescricao.SetFocus; //Coloca o foco no componente com problema retorno := false; //Atribui false para a varivel de retorno //Mostra uma mensagem de erro MessageDlg('A descrio obrigatria e no foi informada.', mtError, [mbOK], 0); Abort; //Aborta a execuo do programa end;
//Se nenhum item do ComboBox foi selecionado if DBLkpCmbBxUnidade.ItemIndex = -1 then begin DBLkpCmbBxUnidade.SetFocus; //Coloca o foco no componente com problema retorno := false; //Atribui false para a varivel de retorno //Mostra uma mensagem de erro MessageDlg('A unidade obrigatria e no foi informada', mtError, [mbOK], 0); Abort; //Aborta a execuo do programa end;
//A funo devolve para o cdigo que a chamou o valor de retorno ValidarCampos := retorno; end;
Procedimento para Habilitar ou Desabilitar o Acesso os Campos de um Formulrio
A propriedade Enabled dos componentes DataWare atribuda em funo do estado do DataSet ao qual esto conectados:
procedure HabilitarDesabilitarControles; begin DBEdtCodigo.Enabled:=Tabela.Active; DBEdtDescricao.Enabled:=Tabela.Active; DBLkpCmbBxUnidade.Enabled:=Tabela.Active; DBEdtUnidade.Enabled:=Tabela.Active; end;
Incluso de um novo registro (ao actNovo)
Para realizar a incluso de um novo registro a nica exigncia a gerao de um valor para o campo chave primria. No cdigo abaixo este valor armazenado em uma varivel e logo em seguida a tabela colocada em modo de insero.
var codigo : integer; //varivel para guardar o valor da chave primria begin if TestaSeDesejaSalvar() <> mrCancel then //Caso a ao no tenha sido cancelada begin AbrirConsultas();//Abre as consultas e inicia a transao
codigo := BuscaUltimoCodigo(); //Varivel cdigo alimentada com um novo valor // ou codigo := BuscaUltimoCodigo('NOME_DA_TABELA');
Tabela.Insert; //Coloca o DataSet em modo de insero
Caderno Didtico Lazarus IDE Pgina 76
76 Universidade Federal de Santa Maria //Atribui ao campo chave da tabela o cdigo gerado Tabela.FieldByName(CampoChave).asInteger := codigo;
Persistncia (gravao) do registro atual (ao actSalvar)
A efetivao dos dados na base de dados deve ser feita se o estado da tabela for compatvel e se no houver erros nos dados informados (tipos incompatveis, valores obrigatrios, etc.). O comando Post grava os dados no DataSet e o comando ApplyUpdates envia o DataSet para ser persistido na base de dados. Havendo sucesso nessas duas etapas os dados podem ser efetivados (commit) e em no havendo sucesso as alteraes devem ser canceladas (rollback).
var chave: integer; //varivel auxiliar para guardar o identificador do registro begin //Verifica se a o DataSet est em Insero ou Edio de dados if Tabela.State in [dsInsert, dsEdit] then begin if ValidarCampos() then //Se os campos foram corretamente validados begin try //Guarda o identificador do registro chave := Tabela.FieldByName(CampoChave).Value;
Tabela.Post; //Grava os dados no DataSet Tabela.ApplyUpdates; //Envia os dados para o Servidor de BD SQLTransaction.Commit; //Realiza um Commit e encerra a transao ativa
AbrirConsultas(); //Inicia uma nova transao e abre as consultas
//Posiciona o DataSet no registro em que se encontrava antes do salvamento Tabela.Locate(CampoChave, chave, []); except //Ocorrendo um erro do tipo Exception on E : Exception do begin //Exibe a mensagem do erro ShowMessage('No foi possvel salvar as informaes. ' + #13 + 'Ocorreram os seguintes erros ' + #13 + E.Message);
//Desfaz as alteraes feitas na base de dados SQLTransactionPrincipal.Rollback; end; end; end;
HabilitarDesabilitarControles; end; end;
Cancelamento das alteraes do registro atual (ao actCancelar)
O cancelamento das alteraes em um DataSet distinto durante uma Insero (onde os dados no existiam) e em uma Edio (onde existiam valores que foram alterados). No caso do cancelamento de uma edio os dados anteriores devem retornar. No caso do cancelamento de uma insero, o
Caderno Didtico Lazarus IDE Pgina 77
77 Universidade Federal de Santa Maria DataSet deve ser fechado para que a ao de incluso seja novamente acionada. Em ambos os casos o cancelamento feito nos dados contidos no DataSet sem envolver a base de dados diretamente.
//Cdigo da ao actCancelar
var sit : TDataSetState; //Varivel auxiliar para guardar o estado do DataSet begin
sit := Tabela.State; //Captura o estado do DataSet antes do cancelamento
//Se o DataSet est em Insero ou Edio if sit in [dsInsert, dsEdit] then begin //Confirma se os dados sero de fato cancelados if MessageDlg('Confirmao', 'Tem certeza que deseja cancelar as alteraes ?', mtInformation, [mbYes, mbNo], 0) = mrYes then begin //Se sim, cancela a edio no DataSet Tabela.Cancel;
//Se o estado inicial era insero ento os DataSets e a transao sero fechados if sit = dsInsert then FecharConsultas(); end;
HabilitarDesabilitarControles; end; end;
Excluso do registro atual (ao actExcluir)
A excluso de um registro pressupe que o usurio esteja posicionado sobre o registro ao qual deseja excluir. aconselhvel solicitar uma confirmao antes da excluso.
if Tabela.Active then //Testa se a tabela est ativa begin //Confirma se a excluo deve ser feita if MessageDlg('Confirmao','Tem certeza que deseja excluir o registro ' + Tabela.FieldByName(CampoDescricao).asString, mtConfirmation, [mbYes, mbNo], 0) = mrYes then begin try Tabela.Delete; //Deleta o registro no DataSet Tabela.ApplyUpdates; //Envia as alteraes para o BD //Efetiva as alteraes no BD SQLTransaction.Commit; except on E : Exception do begin SQLTransaction.Rollback; //Cancela as alteraes no BD //Exibe uma mensagem indicando que no foi possvel fazer a excluso MessageDlg('No foi possvel excluir o registro ' + #13 + 'Possivelmente existem dados em outras tabelas ' + 'que esto relacionados a este registro.', mtError, [mbOk], 0); end; end;
FecharConsultas(); //Encerra a transao e fecha os DataSets
end;
HabilitarDesabilitarControles; end;
Caderno Didtico Lazarus IDE Pgina 78
78 Universidade Federal de Santa Maria Localizao de Registros
A localizao de um registro normalmente feita de duas maneiras. Na primeira, o usurio, conhecendo o valor do campo chave da tabela (um cdigo, por exemplo), o informa e o programa tenta fazer uma busca exata. Na segunda maneira, o usurio informa parte de uma informao descritiva acerca da tabela e a aplicao tenta fazer uma busca aproximada com os dados informados. Neste ltimo caso, o cdigo utilizado normalmente no evento OnChange (ao mudar o contedo) de uma Edit normal.
Busca por chave primria (actLocalizar)
var codigo : string; //Varivel auxiliar para guardar o cdigo informado begin //Se a ao no for cancelada if TestaSeDesejaSalvar() <> mrCancel then begin try FecharConsultas(); //Encerra a transao e os DataSets //Solicita ao usurio que informe um cdigo if InputQuery('Cdigo', 'Informe o cdigo que voc deseja localizar', codigo) then begin StrToInt(codigo); //Tenta fazer uma converso para evitar erros
//Inicia a transao e abre os DataSets AbrirConsultas();
//Se uma busca exata no encontrar nenhum registro if not Tabela.Locate(CampoChave, Codigo, []) then begin FecharConsultas(); //Mostra mensagem que o registro no foi encontrado MessageDlg('Cdigo no encontrado!', mtWarning, [mbOk], 0); end;
//Caso registro tenha sido encontrado o DataSet o ter posicionado end; except FecharConsultas(); //Se houver algum erro, fecha o DataSet end;
HabilitarDesabilitarControles; end; end;
Busca por campo descritivo (actLocalizarPorDescricao)
//Se h algo informado na edit (o tamanho sem os espaos em branco maior que zero) if Length(Trim(EdtDescricao.Text)) > 0 then begin if TestaSeDesejaSalvar() <> mrCancel then //Se a ao no for cancelada begin //Encera e na sequncia inicia a transao e os datasets FecharConsultas(); AbrirConsultas();
//Realiza uma busca parcial e sem distino de maisculas/minsculas Tabela.Locate(CampoDescricao,EdtDescricao.Text,[loCaseInsensitive, loPartialKey]); end;