Apostila Pratica
Apostila Pratica
Apostila Pratica
Índice
Criando o Banco de Dados .................................................................................................................3
Criando a Aplicação ..........................................................................................................................12
Criando o DataModule Principal .......................................................................................................13
Criando o DataModule para o Cadastro de Pedidos ......................................................................18
Criando o Formulário de Cadastro de Pedidos ...............................................................................22
Parametrizando a Query ...................................................................................................................27
Joins ...................................................................................................................................................32
Joins - Buscando o Cliente ...............................................................................................................36
Ajustando UpdateMode do Provider - Performance na Atualização e Controle de Concorrência......43
Trabalhando com Mestre/Detalhe ....................................................................................................47
Mestre/Detalhe - Utilizando MasterSource/MasterFields (trafegando todos os registros detalhes) ...48
Mestre/Detalhe - Utilizando MasterSource/MasterFields (filtrando os registros detalhes) ..........54
Mestre/Detalhe - Utilizando NestedDataSet ....................................................................................56
Buscando Informações do Produto ..................................................................................................62
Trabalhando com Clones ..................................................................................................................64
Implementando mais o Projeto - Calculando Valor Total do Item..................................................67
Campos Aggregate ............................................................................................................................69
Campos InternalCalc .........................................................................................................................74
Generators – Trabalhando com Campos Auto-Incremento ...........................................................77
Criando o Cadastro de Tipos de Pedidos ........................................................................................85
Criando a Pesquisa de Pedidos .......................................................................................................91
Criando a Pesquisa de Clientes .....................................................................................................101
Ordenando Registros ......................................................................................................................107
Ordenando os Registros – Utilizando Índices Temporários .........................................................108
Ordenando os Registros – Utilizando Índices Persistentes .........................................................109
Filtrando Registros em Memória ....................................................................................................113
Filtrando os Registros em Memória – Utilizando a Propriedade Filter ........................................114
Filtrando os Registros em Memória – Utilizando Range ..............................................................115
Pesquisando os Registros em Memória ........................................................................................118
Trabalhando com Refresh ..............................................................................................................120
Trabalhando com Refresh – Utilizando o Método Refresh ..........................................................121
Trabalhando com Refresh – Utilizando o Método RefreshRecord ..............................................122
Desfazendo Alterações – CancelUpdates, UndoLastChange, RevertRecord e SavePoint ......124
Lendo e Gravando os Dados Localmente em Arquivos ...............................................................126
Trabalhando com Tabelas Somente em Memória ........................................................................129
Transações ......................................................................................................................................136
Transações – Utilizando SQLQuery ...............................................................................................137
Transações – Utilizando ClientDataSet .........................................................................................142
Eventos BeforeUpdateRecord e AfterUpdateRecord do Provider ...............................................146
Trabalhando com Múltiplas Tabelas – Definindo qual será Atualizada .......................................150
ReconcileError - Tratamento de Erros ...........................................................................................157
Monitorando mensagens do Banco de Dados ..............................................................................161
Distribuindo a aplicação ..................................................................................................................163
No caso do Firebird, temos apenas um arquivo, chamado de DataBase, onde conterá todas as
tabelas, índices, integridades, generators entre outros.
Existem diversas formas para criarmos o banco de dados, o Firebird disponibiliza um utilitário
chamado ISQL que é utilizado para criação e manipulação do banco de dados, porém não é
tão elegante, pois é executado no prompt do dos, portanto não temos um visual agradável.
Por este motivo utilizaremos uma ferramenta gráfica chamada IBExpert, que é um utilitário
para manipulação de banco de dados Interbase e Firebird. O IBExpert possui duas verões,
Personal (gratuita) e Full (paga), pode ser baixado diretamente do site: http://www.ibexpert.com
Criando o diretório
Antes de criarmos o banco de dados, criaremos a seguinte estrutura de diretório para seu
destino: C:\CursoClientDataSet\projeto\db
Criando o banco
Server:
Indica se o banco será criando localmente (Local) ou remotamente (Remote)
Server name:
Só estará disponível se optarmos por Remote na opção Server. Neste campo informamos o
Nome ou IP do Servidor.
Protocol:
Só estará disponível se optarmos por Remote na opção Server. Neste campo informamos o
tipo de protocolo de comunicação com o servidor, na maioria dos casos TCP/IP.
Database:
Caminho onde será criado o arquivo do banco de dados.
Username:
Nome do usuário do banco de dados. O usuário SYSDBA é o padrão do servidor.
Password:
Senha do usuário do banco de dados. A senha masterkey é a senha padrão do usuário
SYSDBA.
Page Size:
Normalmente deixamos 1024 que é o tamanho de página padrão. Mais detalhes a respeito
poderão ser encontrados na documentação do Interbase/Firebird.
Charset:
O mais recomendando é o WIN1252 para evitar problemas com acentos.
SQL Dialect:
O dialeto é útil para usarmos novos recursos que surgem no servidor de banco de dados que
não são compatíveis com as versões anteriores.
- Campos Data/Hora
- Campos numéricos
- Objetos delimitados por aspas duplas (‘’)
Server: Local
Database: C:\CursoClientDataSet\projeto\db\exemplo.fdb
Username: SYSDBA
Password: masterkey
Page Size: 1024
Charset: WIN1252
SQL Dialect: Dialect 3
Registrando o banco
Agora já temos o banco de dados criado. Nesta tela criaremos o velho e antigo “alias” que
estávamos acostumados a criar no SQLExplorer por exemplo, porém, este “alias” é utilizado
apenas no IBExpert, não estará disponível no Delphi.
Podemos perceber que temos muitos campos preenchidos, pois já informamos na tela anterior.
Server Version:
Aqui informamos qual banco/versão estaremos utilizando, pois o IBExpert permite-nos trabalhar
com diversas versões do Interbase e Firebird.
Database Alias:
Nome do alias para conexão que estamos criando.
Criando Conexão
Podemos perceber a conexão criada sendo exibida do lado esquerdo na lista de conexões,
como mostra a seguir:
Conexão criada
Criando as tabelas
Podemos criar as tabelas do banco de dados facilmente através de cliques, porém utilizaremos
scripts SQL para visualizarmos como manipulamos instruções SQL nesta ferramenta.
O Editor SQL do IBExpert pode ser acessado através da tecla de atalho F12 ou no menu
Tools->SQL Editor.
Editor SQL
Neste quadro é que devemos inserir nossas instruções SQL para manipulação do banco.
Devemos executar um COMMIT para que as alterações possam ser confirmadas no servidor
de banco de dados.
Para isso, podemos utilizar a tecla de atalho CTRL + ALT + C ou utilizar o botão Check:
Executando um Commit
Repita os mesmos passos com as instruções SQL a seguir para termos a estrutura do banco
pronta para utilizarmos em nosso projeto:
Criando a Aplicação
Antes de iniciarmos com a criação do projeto, criaremos o diretório onde o mesmo será
gravado:
C:\CursoClientDataSet\projeto\src
Ajustando o Delphi
Faremos um ajuste para que todo e qualquer Formulário/Data module criado não seja
definido como AutoCreate, pois sempre instanciaremos em tempo de execução, evitando
assim consumo de memória desnecessário e sobrecarga do sistema em sua inicialização.
Na tela que será exibida, clique na paleta Designer e desabilite a opção Auto create forms &
data modules.
Desabilitando a opção Auto create forms & data modules em Environment Options
Iniciando o projeto
Crie um novo projeto, ajuste o nome do formulário principal para frmPrincipal e salve-o como
ufrmPrincipal.pas. Em seguida salve o projeto com o nome CursoCdsProjeto.dpr.
Ao contrário do que muitos pensam, eles são super leves, não sobrecarregam a aplicação, pois
são derivados diretamente da classe TComponent.
Deixamos destacado que teremos um DataModule para cada módulo do sistema, não que isso
seja obrigatório, mas quanto mais pudermos separar os módulos, mais fácil ficarão as
manutenções, e assim também evitamos ter diversos componentes instanciados
desnecessariamente num único DataModule.
Adicionaremos nosso primeiro DataModule ao projeto, para isso, clique no menu File-
>New>Data Module.
Criando um DataModule
Este é o nosso DataModule principal, nele adicionaremos nosso componente de conexão que
será utilizado pelos outros componentes de acesso a dados do nosso sistema.
TSQLConnection (dbExpress)
Name: SqlConnPrincipal
LoginPrompt: False
Agora criaremos nossa conexão, para isso precisamos abrir o Editor de Conexões dando um
duplo clique no componente SQLConnection.
Editor de Conexões
Clicando no botão , será exibida uma janela para informarmos o Driver e o Nome da
conexão.
Apesar de estarmos utilizando o banco Firebird, nosso acesso será feito com o driver para
Interbase, pois já é nativo do Delphi. Podemos utilizá-lo com Firebird sem problemas. Existem
drivers dbExpress no mercado específicos para Firebird, no qual poderão ser encontrados nos
sites:
http://www.upscene.com
http://www.progdigy.com/UIB/
DataBase: C:\CursoClientDataSet\Projeto\db\exemplo.fdb
ServerCharSet: WIN1252
SQLDialect: 3
Vale lembrar que o suporte para IPX/SPX está descontinuado no Firebird 1.5.
Vamos testar se a conexão está funcionando, para isso basta clicar no botão
Agora criaremos um novo DataModule, ele será utilizado pelo formulário de Cadastro de
Pedidos e armazenará regras de negócios e componentes de acesso a dados relativos a este
módulo.
Siga o mesmo caminho utilizado na criação do primeiro DataModule, clique no menu File-
>New->Data Module.
...
implementation
uses
udmPrincipal;
...
TSQLQuery (dbExpress)
Name: qryPedido
SQLConnection: dmPrincipal.SqlConnPrincipal
SQL:
SELECT
*
FROM
PEDIDO
Devemos tomar cuidado com esta propriedade, pois ela é do tipo string, portanto podemos
estar digitando qualquer valor que o Delphi não irá fazer nenhum tipo de verificação em tempo
de projeto e se alterarmos o nome do componente dspPedido a propriedade não será
notificada, conseqüentemente não será ajustada automaticamente.
Com o SQL que utilizamos, todos os registros com todos os campos serão enviados ao
ClientDataSet, porém isto não é o ideal, mais adiante ajustaremos para trabalharmos com
parâmetros especificando também os campos, pois desta forma ganhamos performance e
reduzimos o tráfego na rede.
Seguindo o mesmo conceito, agora incluiremos mais um conjunto de componentes para tabela
de Tipo de Pedido que será utilizada como Lookup. Neste caso, ao invés de utilizarmos o
componente SQLQuery, utilizaremos o SQLTable, pois é uma tabela simples, não haverá
necessidade de definirmos o SQL, pois sempre retornaremos todos os registros.
TSQLTable (dbExpress)
Name: tblTipoPedido
SQLConnection: dmPrincipal.SqlConnPrincipal
TableName: TIPOPEDIDO
Podemos perceber que no componente SQLTable não informamos o SQL, definimos a tabela
na propriedade TableName, desta forma, quando forem requisitados os dados, internamente
será gerada uma query do tipo: SELECT * FROM TIPOPEDIDO.
Utilizaremos o componente SQLDataSet apenas para fins didáticos, mas não seria necessário
em nosso caso, vimos que o diferencial deste componente é que ele nos permite trabalharmos
de 3 formas: Querys, Tables e Stored Procedures.
TSQLDataSet (dbExpress)
Name: sdsCliente
SQLConnection: dmPrincipal.SqlConnPrincipal
CommandType: ctQuery
CommandText: SELECT CLI_CODIGO, CLI_NOME FROM CLIENTE
Para montarmos o SQL, podemos digitá-lo diretamente no quadro, clicar nos botões Add Table
To SQL e Add Field to SQL ou dar um duplo clique nas tabelas e colunas até formar o SQL
que desejamos.
...
implementation
uses
udmCadPedido;
...
Adicione 3 DataSources:
DataSource: dtsPedido
DataField: TP_CODIGO
ListSource: dtsTipoPedido
ListField: TP_DESCRICAO
KeyField: TP_CODIGO
DataSource: dtsPedido
DataField: CLI_CODIGO
ListSource: dtsCliente
ListField: CLI_NOME
KeyField: CLI_CODIGO
Codificando o formulário
Codificando os botões
Botão Incluir:
Botão Editar:
Botão Gravar:
Botão Cancelar:
Botão Excluir:
Entendendo os códigos
Os métodos que estamos executando são os mesmos que utilizamos nas TTable’s por
exemplo. A grande diferença está após a chamada dos métodos Post e Delete, pois chamamos
o método ApplyUpdates. Isto é necessário, pois ele é o método responsável em aplicar, gravar
fisicamente as mudanças feitas ao banco de dados.
O método ApplyUpdates é uma função que nos retorna o número de erros ocorrido na
atualização. Esta função exige um parâmetro, no qual indicamos o número de erros permitidos.
Este parâmetro poderá assumir os seguintes valores:
0: Indica que não permitimos nenhum tipo de erro, ou grava tudo com sucesso ou não grava
nada.
-1: Indica que queremos gravar tudo que for possível, o que der erro, será descartado, mas não
afetará aqueles que foram gravados com sucesso.
> 0: Qualquer número maior que zero estará indicando o número de erros permitidos
Implementamos desta forma, pois caso algum erro ocorra na atualização, precisamos limpar as
pendências, caso contrário, em uma próxima chamada ao método ApplyUpdates, o
ClientDataSet tentará gravar não somente as novas alterações, mas também as que ficaram
pendentes.
Dica: O uso dos métodos Post e Cancel são opcionais, os mesmos já são executados
automaticamente pelos métodos ApplyUpdates e CancelUpdates respectivamente.
Abra o frmPrincipal e insira um componente MainMenu ajustando-o para que fique com a
seguinte estrutura:
Ajustando Menus
...
implementation
uses
ufrmCadPedido;
...
No início configuramos o Delphi para que todos novos Formulários e DataModules não fossem
instanciados automaticamente, porém nosso DataModule principal é uma exceção, precisamos
instanciá-lo automaticamente e antes do formulário principal, já que poderíamos precisar utilizar
antes da instância deste, por exemplo, em uma tela de login.
Portanto, faremos este ajuste nas opções do Projeto. Acesse o menu Project->Options e
selecione a aba Forms.
Feito os ajustes, basta confirmar clicando no botão OK e a aplicação já está pronta para ser
testada.
Testando a aplicação
Execute a aplicação, inclua, edite e exclua Pedidos para verificar se os mesmos estão sendo
aplicados fisicamente. ao banco de dados.
Parametrizando a Query
Nossa query de Pedidos não possui uma cláusula WHERE que restrinja os registros, portanto
todos serão enviados ao ClientDataSet, gerando assim um tráfego na rede, diferente do que
ocorre no BDE quando trabalhamos com TQuery por exemplo, já que mesmo não colocando
um WHERE, os registros são trafegados conforme vão sendo requisitados.
Um exemplo prático
Se tivermos uma tabela com 500.000 registros e abrirmos com uma query do tipo SELECT *
FROM TABELA trabalhando com BDE/TQuery, certamente a abertura será instantânea, pois
neste momento os registros não foram trafegados pela rede, eles serão trafegados somente
quando requisitados através de códigos ou DataControls (dbgrids, dbnavigator, etc.). Já no
caso do ClientDataSet, pelo fato de trabalhar tudo em memória, demandará mais tempo, pois
ao abri-lo, ele requisitará os registros ao Provider e este requisitará a Query tudo que ela
retorna, enviando em seguida para o ClientDataSet, portanto todos registros serão trafegados
pela rede na abertura do ClientDataSet, gerando assim muito tráfego e lentidão.
Por este motivo, ao trabalharmos com este componente, devemos adotar alguns conceitos.
Novos conceitos
Um dos principais conceitos é trafegar apenas o necessário. Não mais abriremos um Grid com
todos os registros para o usuário navegar, teremos sempre uma tela de pesquisa
parametrizada para restringi-los.
Nesta tela de pesquisa até podemos permitir que o usuário edite o registro, porém pode ser
mais interessante termos uma tela somente para manutenção, na qual passamos como
parâmetro a chave do registro selecionado e esta tela faria um SELECT apenas neste registro.
Este modelo é interessante, pois existem casos onde temos tabelas com muitos campos,
inclusive campos blobs, registros detalhes, e isso poderia gerar um trafego na rede conforme o
número de registros retornados pela pesquisa fosse aumentando. Portanto, na tela de pesquisa
fazemos o SELECT apenas nos campos necessários para visualização, e na tela de edição
podemos fazer o SELECT em todos os campos, não haverá problemas, pois estaremos
trafegando apenas um registro.
Com base neste conceito, ajustaremos nossa Query para trafegar apenas um Pedido com base
na chave primária, na qual será passada como parâmetro.
Colocando em prática
SELECT
PED_NUMERO,
PED_DATA,
PED_VALOR,
TP_CODIGO,
CLI_CODIGO
FROM
PEDIDO
WHERE
PED_NUMERO = :PED_NUMERO
Editor de Parâmetros
Temos disponível o parâmetro PED_NUMERO. Selecione-o e ajuste as seguintes
propriedades:
DataType: ftInteger
ParamType: ptInput
Ao criar o form, alimentaremos o valor deste parâmetro com NULL, desta forma o
ClientDataSet será aberto e nenhum registro será trafegado, já que não existem Pedidos com
chave nula, assim ele estará vazio, pronto para receber um novo registro a ser cadastrado.
Entendendo o código
dmCadPedido.cdsPedido.FetchParams;
Esta linha pede para o Provider trazer ao ClientDataSet os parâmetros disponíveis no DataSet
ao qual ele (Provider) está ligado, ou seja, o parâmetro PED_NUMERO que criamos na Query
estará disponível agora na propriedade Params do ClientDataSet.
dmCadPedido.cdsPedido.Params.ParamByName('PED_NUMERO').Value := NULL;
Neste código apenas alimentamos o parâmetro PED_NUMERO com valor NULL, justamente
para termos o resultado que comentamos, de não trafegar nenhum registro pela rede, deixando
o ClientDataSet vazio para receber um novo cadastro.
Implementaremos a princípio uma busca simples, onde o usuário apenas informará o Número
do Pedido e alimentaremos o parâmetro com este valor para que assim possamos abrir apenas
o Pedido pesquisado.
Entendendo o código
Testando a aplicação
Joins
Implementaremos agora um recurso muito importante para reduzirmos tráfego na rede e
ganharmos performance.
Nos campos Cliente e Tipo de Pedido utilizamos Lookups para podermos visualizar os
registros e poder selecioná-los nos componente DBLookupComboBox.
No caso do Tipo de Pedido não haveria tanto problema, pois será uma tabela pequena, com
poucos registros, o maior problema é com tabelas grandes, por exemplo, a tabela de Clientes.
Neste caso, adotamos um outro método, o que chamamos de JOINS. Trazemos o Nome do
Cliente não através de um Lookup, mas sim na própria Query de Pedido, e para o usuário
selecionar um Cliente, devemos ter uma tela de Pesquisa de Cliente Parametrizada, reduzindo
assim o tráfego na rede, onde ele pesquisa e seleciona apenas o Cliente que deseja associar
ao Pedido.
Colocando em prática
A primeira coisa que precisamos fazer é ajustar o SQL do Pedido para que faça um JOIN com
a tabela de CLIENTE trazendo o campo NOME do respectivo Cliente pertencente ao Pedido.
Abra o DataModule dmCadPedido e ajuste o SQL do componente qryPedido para que fique
da seguinte forma:
SELECT
PED.PED_NUMERO,
PED.PED_DATA,
PED.PED_VALOR,
PED.CLI_CODIGO,
PED.TP_CODIGO,
CLI.CLI_NOME
FROM
PEDIDO PED
INNER JOIN CLIENTE CLI ON CLI.CLI_CODIGO = PED.CLI_CODIGO
WHERE
PED.PED_NUMERO = :PED_NUMERO
A junção é feita por um ou mais campos, os registros são unidos quando os campos tiverem o
mesmo valor, ou seja, para cada registro de Pedido, será localizado um Cliente cujo Código
(CLI.CLI_CODIGO) seja o mesmo associado ao Pedido (PED.CLI_CODIGO), encontrando,
teremos uma única linha contendo os dados do Pedido e do Cliente, assim podemos obter o
Nome do Cliente acessando o campo CLI.CLI_NOME.
O INNER JOIN exige que o registro da outra tabela obrigatoriamente seja encontrado, ou seja,
no nosso caso, o CLIENTE deve existir, se não, o registro do PEDIDO não será exibido. Nos
casos onde temos situações que o registro possa não existir, ou até mesmo quando o campo
pode ser NULO, devemos utilizar o LEFT JOIN ao invés de INNER JOIN, que possui a mesma
sintaxe, diferenciando apenas que ele não exige a existência do registro da tabela que estamos
unindo.
Caso o campo CLI_NOME não esteja disponível, basta abrir e fechar o ClientDataSet de
Pedidos através da propriedade Active para refletir o campo que adicionamos e assim
disponibilizá-lo para utilização.
Definimos a propriedade ReadOnly como True pois não poderíamos permitir o usuário alterar
o Nome do Cliente nesta tela.
Tela de Cadastro de Pedidos – Trocando DBLookupComboBox de Cliente pelo DBEdit e removendo o DataSource
- cdsCliente
- dspCliente
- sdsCliente
Testando a aplicação
Execute a aplicação, pesquise por um Pedido e ao abrí-lo, veremos que o Nome do Cliente
será exibido no DBEdit.
Precisamos agora implementar uma forma de poder informar o Cliente no Pedido, já que não
temos mais o Lookup.
Nesta primeira etapa teremos um campo onde o usuário informará o código do Cliente e depois
de digitado, buscamos o respectivo nome. Depois implementaremos uma tela de pesquisa de
Cliente parametrizada, onde o usuário poderá pesquisar pelo Cliente e depois de confirmado,
alimentamos o campo no Pedido com o respectivo Código do Cliente selecionado.
Colocando em prática
Implementaremos uma forma de buscar o Nome após o campo Código do Cliente ter sido
informado, portanto, a primeira coisa a ser feita é montar uma Query que busque o Nome do
Cliente através de um valor passado por parâmetro, no qual este valor será alimentado com o
Código do Cliente informado no cadastro do Pedido.
TSQLQuery
Name: qryInfoCliente
SQLConnection: dmPrincipal.SqlConnPrincipal
SQL:
SELECT
CLI_NOME
FROM
CLIENTE
WHERE
CLI_CODIGO = :CLI_CODIGO
Seguindo o que já vimos, ajuste o parâmetro CLI_CODIGO que definimos na query da seguinte
forma:
DataType: ftInteger
ParamType: ptInput
É muito comum fazer esses tipos de implementações no evento OnExit dos DBEdits. Não é um
erro, depende muito do caso, por se tratar de uma regra de negócio, o local mais adequado
seria no DataModule, no evento OnValidate do campo CLI_CODIGO.
Outro motivo muito importante também que nos leva a não codificar no evento OnExit é que, o
mesmo será disparado somente se um novo componente receber o foco, por exemplo, quando
saímos do campo. Se clicarmos no botão Gravar por exemplo, teremos o mesmo efeito, pois
estamos utilizando um TButton e esta classe recebe foco, porém se utilizássemos um
TSpeedButton, teríamos um problema, esta classe não recebe foco, portanto, o evento
OnExit não seria disparado neste caso.
Implementando o Evento
Entendendo o código
Este evento nos fornece o parâmetro Sender que é do tipo TField e representa o campo que
está sendo validado, no caso será o campo CLI_CODIGO. Utilizamos o objeto Sender para
acessarmos o valor do campo e assim poder buscar o nome do cliente com base neste valor.
Verificamos se o valor do campo não é nulo, não sendo, prosseguimos ajustando o valor do
parâmetro CLI_CODIGO na qryInfoCliente com o mesmo valor do campo CLI_CODIGO,
digitado no DBEdit. Em seguida abrimos nossa Query, verificamos se ela não está vazia, não
estando, pegamos o valor do campo CLI_NOME obtido na Query e atribuímos ao campo
CLI_NOME do ClientDataSet de Pedido (cdsPedido), assim teremos o nome do Cliente
alimentado, conseqüentemente estando disponível no DBEdit. Caso a Query esteja vazia,
geramos uma Exception para que o registro não seja gravado de forma alguma, barrando o
campo até que informe um código válido ou limpe-o.
Testando a aplicação
Isso ocorre porque um erro está sendo gerado na gravação (no servidor de banco de dados) e
nossa aplicação não está nos informando.
procedure TdmCadPedido.cdsPedidoReconcileError(
DataSet: TCustomClientDataSet; E: EReconcileError;
UpdateKind: TUpdateKind; var Action: TReconcileAction);
begin
MessageDlg(E.Message, mtError, [mbOk], 0);
end;
Este evento é gerado para cada registro que não pôde ser aplicado ao banco devido algum
erro ocorrido, entraremos em detalhes mais adiante.
...
implementation
uses
udmPrincipal,
Dialogs;
...
Podemos neste momento executar a aplicação, tentar incluir algum Pedido informando o
Código do Cliente, conseqüentemente o Nome do Cliente será atualizado e ao tentarmos
gravar, veremos o erro que está sendo gerado:
Ajustando o ProviderFlags
O erro está acontecendo porque faltou um passo a ser realizado, o ajuste dos ProviderFlags,
mas antes de fazermos isso, vamos entender o motivo do erro.
O Provider está gerando uma query de inclusão utilizando todos os campos que fizemos o
SELECT, portanto está sendo executada uma query da seguinte forma:
Perceba que o campo CLI_NOME está incluso na query, e isso não pode ocorrer, pois ele não
pertence à tabela de PEDIDO e sim à tabela de CLIENTE.
O que precisamos fazer é informar ao Provider para não incluir este campo na atualização.
Por padrão temos as opções pfInUpdate e pfInWhere ligadas nesta propriedade. O que
faremos é desligar a opção pfInUpdate para que o campo não seja incluso na cláusula
INSERT/UPDATE da query gerada pelo Provider, para isso, basta defini-la para False.
Testando a aplicação
- Se na inclusão o problema foi resolvido e na alteração o erro continua, significa que o ajuste
que fizemos funciona apenas para tirar o campo da cláusula INSERT e não da cláusula
UPDATE?
O erro até é pelo fato de estarmos alterando ao invés de inserindo, porém não está ligado
diretamente à cláusula UPDATE, mas sim à cláusula WHERE.
Quando modificamos nosso registro, o Provider está gerando uma query da seguinte forma:
Na cláusula SET terá apenas os campos que foram modificados e que estejam com a opção
pfInUpdate ligada na propriedade ProviderFlags. Em nosso caso, mesmo que o campo
CLI_NOME tenha sido modificado, ele não será incluso, pois está com a opção pfInUpdate
desligada.
Podemos perceber que o campo CLI_NOME está incluso na cláusula WHERE, e é isto que
está causando o erro. Precisamos informar ao Provider que este campo não poderá pertencer
também a esta cláusula.
Para isto, vamos seguir os mesmos passos que fizemos com a opção pfInUpdate, porém
agora desligaremos a opção pfInWhere.
Testando a aplicação
Podemos executar e fazer os testes de inclusão, alteração e exclusão que o erro não será mais
exibido.
Neste momento, nossa aplicação está funcionando perfeitamente, porém, temos que nos
atentar a uma propriedade importante existente no Provider que influenciará no controle de
concorrência de usuários atualizando o mesmo registro e ganho de performance nas
atualizações.
Como já explicamos, quando atualizamos nosso registro, internamente o Provider gera uma
Query de atualização onde usará todos os campos (que estiverem com a opção pfInWhere do
Provider ligada) na cláusula WHERE para poder localizar o registro.
Isso pode gerar uma queda de performance quando atualizarmos uma tabela com muitos
registros, pois é pouco provável que tenhamos um índice na tabela composto por todos os
campos utilizados na cláusula WHERE a fim de ganhar performance na atualização.
Número: 120
Data: 01/01/1900
Valor: 200,00
Código do Cliente: 5
O primeiro usuário faz a alteração no Pedido modificando apenas a data para 01/01/1920 e
logo em seguida grava, portanto o Provider irá gerar a seguinte query de atualização:
Enquanto isso, o segundo usuário estava com o registro aberto, ele ainda não sabe que houve
modificações no registro, somente saberá caso abra-o novamente ou execute um refresh.
Porém nada disto foi feito e o usuário alterou o campo Código do Cliente de 5 para 6 e gravou,
portanto a seguinte query será gerada:
Analisando esta query, veremos que a condição que está sendo montada é exatamente igual à
condição montada para o primeiro usuário, pois ambos abriram o registro na mesma situação.
Só que neste momento não existe mais o registro nesta condição WHERE .... AND PED_DATA
= 01/01/1900 ... o mesmo já foi alterado pelo primeiro usuário, a data foi modificada, portanto,
ao tentarmos gravar o registro, teremos a seguinte mensagem de erro:
A primeira idéia que surgiria seria de ‘travar’ o registro na edição. Quando trabalhamos com
TTable/Paradox, isso é feito de forma automática, porém o mesmo não ocorre em bancos de
dados Cliente/Servidor. Dependendo do banco, até podemos fazer isso, porém é um tanto
trabalhoso e não tão recomendado, pois teríamos que manter a transação aberta até que o
usuário finalize-a, particularmente recomendaria somente em casos de extrema necessidade.
Da forma que esta o projeto, o segundo usuário não poderia gravar o registro, até poderíamos
fazer o tratamento do erro, exibindo uma tela com os dados do registro atual, informando ao
usuário de que o mesmo já foi modificado e ali, permitir que se faça alguns ajustes para poder
atualizar o registro.
Uma alternativa interessante para contornarmos este problema é ajustar o Provider de modo
que ele utilize apenas o campo chave na cláusula WHERE, pois desta forma sempre o registro
será localizado, já que o campo chave jamais será modificado. O registro só não será
encontrado caso o mesmo tenha sido excluído.
Com esta alternativa, além de resolvermos esta questão, ganhamos performance, pois será
feito um WHERE somente no campo chave e já existe um índice para este campo, devido ao
fato de ser chave primária.
Outra questão importante é que, utilizando esta forma, sempre prevalecerá às alterações feitas
pelo último usuário, substituindo as já realizadas por outros usuários.
Colocando em prática
Testando a aplicação
Pesquisa por um Pedido, faça alguma alteração e ao gravar, veremos a seguinte mensagem de
erro:
Isto ocorre pois como havíamos comentando, precisamos informar ao Provider qual é a chave
para que ele possa utilizar na cláusula WHERE.
Definindo a chave
Testando a aplicação
Podemos testar a aplicação fazendo os testes de concorrência e perceberemos que o erro não
mais ocorrerá e conseqüentemente temos um ganho de performance na atualização pelo fato
de estarmos utilizando apenas o campo chave como meio de localização do registro.
O inconveniente deste modelo é que precisamos manualmente abrir o Detail após a abertura
do Master e chamar o método ApplyUpdates após termos executado no Master, o mesmo vale
para o método CancelUpdates.
Este modelo possui também o mesmo inconveniente descrito na primeira opção. Além disto,
temos sempre que ficar atentos aos filtros (clásula where do SQL), pois dependendo do filtro
aplicado na Query Master e Detail, qualquer mudança na Master poderá implicar em modificar
o filtro da Query Detail para que sempre trafegue somente os registros relativos ao Master.
- NestedDataSet
Este é a forma mais utilizada. Neste modelo definimos o relacionamento diretamente no
DataSet ao qual o Provider está ligado, ou seja, no servidor de aplicação quando trabalhamos
no modelo multicamadas, diferente do que ocorre nos outros modelos, onde o relacionamento
é definido no cliente (ClientDataSet).
Trabalhando desta forma, definimos no SQL detail uma condição para que somente os
registros relativos ao master sejam trafegados.
Para facilitar criaremos 2 cópias do projeto atual, onde na primeira cópia simularemos as 2
formas de relacionamento utilizando mastersource/masterfields e na segunda cópia,
utilizaremos o NestedDataset.
Então salve todo o projeto atual e faça uma cópia para as seguintes pastas:
- C:\CursoClientDataSet\projeto\src_md_mastersource
- C:\CursoClientDataSet\projeto\src_md_nesteddataset
Feito isso, teremos 3 versões do projeto, a primeira não será mais utilizada, continuará na
pasta src e trabalharemos nas outras cópias criadas.
TSQLQuery (dbExpress)
Name: qryPedItem
SQLConnection: dmPrincipal.SqlConnPrincipal
SQL:
SELECT
PED_NUMERO,
PROD_CODIGO,
PI_DESCRICAO,
PI_QTDE,
PI_VALUNIT,
PI_VALTOTAL
FROM
PEDITEM
Por enquanto não utilizaremos nenhuma condição, mas adiante faremos o ajuste para
trazermos somente os registros relativos ao master para reduzirmos o tráfego de informações.
Isto significa que nenhum registro será enviado, apenas o metadado, neste caso se
tentássemos abrir o cdsPedItem, seria exibida a seguinte mensagem de erro:
Para corrigirmos isto, basta ajustarmos a propriedade para seu valor original (-1), isto indica
que todos registros detalhes serão enviados.
Em seguida será exibida a tela para fazermos o relacionamento, defina-o da seguinte forma:
TDataSource
Name: dtsPedItem
DataSet: dmCadPedido.cdsPedItem
Precisamos abrir a tabela de Itens após a abertura da tabela de Pedidos, já que isso não é feito
automaticamente neste modelo, faremos isso no evento OnCreate do formulário e no botão
Pesquisar:
Da mesma forma que fizemos manualmente a abertura dos dados na tabela de Itens,
precisamos também executar manualmente o método ApplyUpdates após gravar ou excluir o
Pedido, portanto faremos este ajuste nos botões Gravar e Excluir:
Botão Gravar:
Botão Excluir:
dmCadPedido.cdsPedido.Delete;
Entendendo o código
Fazemos nesta ordem para não ocorrer erro de integridade, pois se aplicássemos primeiro o
registro master, o mesmo tentaria ser excluído do banco, e temos registros detalhes
dependentes, portanto, um erro de integridade seria gerado pelo servidor de banco de dados.
Testando a aplicação
Com o mesmo projeto, o que faremos agora é um ajuste na query dos registros detalhes para
que não sejam trafegados todos os registros, mas sim somente aqueles pertencentes ao
master.
Ajuste o script SQL do componente qryPedItem para que fique da seguinte forma:
SELECT
PED_NUMERO,
PROD_CODIGO,
PI_DESCRICAO,
PI_QTDE,
PI_VALUNIT,
PI_VALTOTAL
FROM
PEDITEM
WHERE
PED_NUMERO = :PED_NUMERO
DataType: dtInteger
ParamType: ptInput
Testando a aplicação
De modo geral não veremos diferença na aplicação, porém vale lembrar que agora estamos
restringindo o os registros detalhes, antes carregávamos todos os registros e aplicávamos um
filtro localmente, agora somente os registros relativos ao Pedido estão sendo trafegados, ou
seja, o filtro (WHERE) é aplicado no servidor. Se tivéssemos uma tabela de Itens grande,
perceberíamos a diferença no tempo de abertura desta tabela.
TSQLQuery (dbExpress)
Name: qryPedItem
DataSource: dtsPedido
SQLConneciton: dmPrincipal.SqlConnPrincipal
SQL:
SELECT
PED_NUMERO,
PROD_CODIGO,
PI_DESCRICAO,
PI_QTDE,
PI_VALUNIT,
PI_VALTOTAL
FROM
PEDITEM
WHERE
PED_NUMERO = :PED_NUMERO
Neste momento já percebemos uma diferença, não temos o Provider para o ClientDataSet
de Itens, isto ocorre mesmo, o Provider da tabela de Pedido será responsável por tudo.
Analisando o SQL da Query detalhe, percebemos que não há diferenças no que vimos até
agora, porém o parâmetro PED_NUMERO não será alimentado por nós, mas sim
automaticamente pela Query mestre (qryPedido), pois como dissemos, estamos fazendo um
relacionamento mestre/detalhe entre elas.
Seguindo as regras descritas acima, quando a query detalhe for aberta (isto é feito
automaticamente pela query master), ela perceberá que está trabalhando como detalhe, pois a
propriedade DataSource está alimentada, então verificará todos os parâmetros existentes e
para cada um, tentará procurar o campo com mesmo nome na query Master, achando,
alimentará o parâmetro com o valor do campo encontrado.
DataType: ftInteger
ParamType: ptInput
Pode parecer estranho, mas todos os registros detalhes serão enviados para o ClientDataSet
mestre em forma de um campo, é um campo do tipo TDataSetField, ou seja, é um DataSet,
para cada registro mestre, teremos este campo preenchido com todos os registros detalhes.
Depois de feito este relacionamento, teremos este campo disponível, para isso precisamos
atualizar o FieldsEditor do ClientDataSet Master.
Abra o FieldsEditor do cdsPedido e clique com o botão direito na janela, em seguida clique
em Add Fields...
O nome do campo possui o mesmo nome da query detalhe. Confirmando, ele estará disponível
na lista de campos, pronto para ser usado.
Utilizando o TDataSetField
Como este campo representa um DataSet, precisamos de algum componente que nos permita
manipular os dados contidos nele, então utilizaremos o próprio ClientDataSet, por isso temos
nosso cdsPedItem, que até o momento não havíamos utilizado, agora utilizaremos para esta
finalidade.
Para definirmos que o cdsPedItem manipulará este campo, precisamos associá-lo ao campo,
fazemos isso através da sua propriedade DataSetField. Acessando esta propriedade teremos
disponível um objeto chamado cdsPedidoqryPedItem que é o campo que acabamos de inserir
no FieldsEditor.
TDataSource
Name: dtsPedItem
DataSet: dmCadPedido.cdsPedItem
TDBGrid
Name: dbgrdItens
DataSource: dtsPedItem
Testando a aplicação
Faça o cadastro de um Pedido e grave, perceba que o mesmo foi aplicado com sucesso,
porém ao tentarmos alterar um registro detalhe e gravar o pedido, veremos a seguinte
mensagem de erro:
Isto ocorre por que o Provider está com a propriedade UpdateMode ajustada para
upWhereKeyOnly, como vimos, isto determina que somente os campos chaves irão para
cláusula WHERE. No componente qryPedido já havíamos definido quais são os campos
chaves através do ProviderFlags, agora precisamos fazer o mesmo para a query detalhe
(qryPedItem).
Isto ocorre pois quando pedimos para adicionar os campos no FieldsEditor, a query está sendo
aberta internamente, e por ela estar em um relacionamento mestre/detalhe, exige-se que a
query master esteja ativa, portanto, ative-a (qryPedido) definindo True para a propriedade
Active, siga os passos descritos anteriormente e em seguida, fecha-a, definindo False para a
propriedade Active.
Testando a aplicação
Podemos agora cadastrar e modificar os itens normalmente que o erro não mais será gerado,
pois agora o Provider já sabe quem são os campos chaves da tabela detalhe para poder
montar a query de atualização corretamente.
TSQLQuery (dbExpress)
Name: qryInfoProduto
SQLConnection: dmPrincipal.SqlConnPrincipal
SQL:
SELECT
PROD_DESCRICAO,
PROD_VALOR
FROM
PRODUTO
WHERE
PROD_CODIGO = :PROD_CODIGO
Neste SQL apenas estamos buscando a descrição e o valor de acordo com o Código do
Produto.
DataType: ftInteger
ParamType: ptInput
Abra o FieldsEditor do cdsPedItem, adicione todos os campos clicando com o botão direito do
mouse e acessando o item de menu Add all Fields.
Testando a aplicação
Podemos testar a aplicação e veremos que, ao digitar o código do produto nos itens, teremos a
descrição e o valor ajustado automaticamente.
Um exemplo de utilização
Em nosso banco de dados definimos que a chave primária da tabela de Itens é o Número de
Pedido + Código do Produto, portanto, não poderíamos permitir a inclusão de Produtos iguais
no mesmo Pedido. Poderíamos validar isso no momento da digitação do Código do Produto,
verificamos se o mesmo já existe nos Itens, se sim, barramos a inclusão, caso contrário,
deixamos prosseguir.
É possível contornar esse problema de várias formas, mas daria muito trabalho. Neste caso um
ClientDataSet Clonado resolve por completo nosso problema, pois clonaríamos a tabela de
Itens e após digitar o Código do Produto no ClientDataSet original, faríamos uma busca no
ClientDataSet clonado, no qual não estará em modo de edição, portanto não haverá problemas
ao fazer a busca.
Colocando em prática
Já que podemos Clonar somente após a abertura do ClientDataSet original, faremos o Clone
no evento AfterOpen do cdsPedItem:
qryInfoProduto.ParamByName('PROD_CODIGO').Value := Sender.Value;
qryInfoProduto.Open;
try
if not qryInfoProduto.IsEmpty then
begin
cdsPedItem.FieldByName('PI_DESCRICAO').Value :=
qryInfoProduto.FieldByName('PROD_DESCRICAO').Value;
cdsPedItem.FieldByName('PI_VALUNIT').Value :=
qryInfoProduto.FieldByName('PROD_VALOR').Value
end
else
raise Exception.Create('Produto não encontrado');
finally
qryInfoProduto.Close;
end;
end;
end;
Entendendo o código
Testando a aplicação
Ao tentarmos cadastrar itens com Códigos de Produtos iguais, seremos barrados com a
mensagem de erro, e notaremos que em nenhum momento a posição do registro atual é
modificada, pois a busca está sendo feita diretamente no Clone, não afetando o cursor original.
Para que nosso projeto fique mais completo, iremos agora implementar um código para que o
valor total do item seja calculado automaticamente.
Utilizaremos o evento OnValidate dos campos Valor Unitário e Quantidade para atualizarmos
o valor total do item.
...
private
procedure AtualizaValorTotalItem;
public
{ Public declarations }
end;
…
procedure TdmCadPedido.AtualizaValorTotalItem;
begin
cdsPedItem.FieldByName('PI_VALTOTAL').AsFloat :=
cdsPedItem.FieldByName('PI_VALUNIT').AsFloat *
cdsPedItem.FieldByName('PI_QTDE').AsInteger;
end;
Entendendo o código
Neste código, nosso único objetivo é atualizar o valor total com a multiplicação entre os campos
valor unitário e quantidade.
Executando o método
Testando a aplicação
Cadastre um Pedido incluindo itens e perceberemos que o Valor total do Item é atualizado
automaticamente após informarmos a quantidade e o valor unitário.
Campos Aggregate
É muito comum nas aplicações precisarmos totalizar colunas, obter médias, valor mínimo, valor
máximo, etc. Em instruções SQL’s podemos utilizar funções agregadas para obter este tipo de
resultado.
Podemos também fazer isso utilizando um recurso que o ClientDataSet nos disponibiliza,
chamado de AggregateField, ou seja, podemos criar um campo que representara um valor
agregado com base nos dados existentes no ClientDataSet.
Neste momento pode-se surgir à dúvida de qual opção é a mais recomendada. Para responder
esta questão, precisamos analisar a situação, podemos resumir da seguinte forma:
Por outro lado, se não temos as informações disponíveis no ClientDataSet, não valeria a pena
abri-lo com os dados e utilizar o AggregateField, estaríamos trafegando registros
desnecessariamente, neste caso, é muito mais recomendável executarmos uma instrução SQL
no servidor.
Em nosso projeto utilizaremos este recurso do ClientDataSet para podermos visualizar o valor
total de todos os itens do pedido aberto. O mais interessante é que o valor total será atualizado
automaticamente a cada inserção, modificação ou exclusão de algum item, sem precisarmos
codificar nenhuma linha de código.
Colocando em prática
Com o FieldsEditor aberto, clique com o botão direito do mouse e clique no item New field.
Name: VALORTOTAL
Field Type: Aggregate
Utilizaremos este campo como um qualquer, ligando em um DBText por exemplo para
visualizarmos o resultado, porém, antes precisamos fazer ajustes em algumas propriedades.
Active: True
Na propriedade Active definimos se o campo está ou não ativo, pois em determinados casos
podemos desativá-lo para evitar cálculos desnecessários.
Expression: SUM(PI_VALTOTAL)
Na propriedade Expression definimos a expressão de acordo com o tipo de resultado que
desejamos obter, utilizamos o SUM, pois precisamos da soma total dos itens. Poderíamos usar:
SUM, AVG, MAX e MIN. É possível também fazermos cálculos entre campos e depois
sumarizarmos, exemplo: SUM(CAMPO1 + CAMPO2).
Visible: True
Nesta propriedade determinamos que o campo será visível para ser utilizado nos DataControls.
Podemos notar que os campos agregados ficam em um quadro separado dos demais campos,
isto facilita em muito na manutenção.
Da mesma forma que ativamos um determinado campo agregado, devemos também ativar
este recurso no ClientDataSet.
Testando a aplicação
É interessante observarmos que o valor é atualizado a todo o momento, logo que gravamos o
item mais precisamente, porém isso não gera sobrecarga, pois internamente o ClientDataSet
recalcula apenas o que foi alterado.
Campos InternalCalc
Ganho de performance
Utilizando o evento OnCalcFields, podemos evitar que o campo InternalCalc seja calculado a
todo o momento, basta verificarmos o State do ClientDataSet, se estiver como dsInternalCalc,
significa que ele está processando esses campos, este é o momento ideal para ajustarmos seu
valor. Fazendo testes, percebemos que este estado ocorre no momento em que gravamos o
registro. Com base nisso, temos um número de recálculo inferior aos campos Calculateds, no
qual são recalculados a todo o momento, pois não podemos fazer a mesma comparação,
sendo assim temos uma perda de performance em casos onde o processo que o alimenta é
muito “pesado”.
Indexação
Pelo fato de estarem armazenados no ClientDataSet, podemos indexar ou definir um índice
que utilizam campos InternalCalc, diferente do que ocorre com os campos Calculated, que ao
tentarmos fazer isto, uma mensagem de erro é exibida alertando de que isto não é possível.
Colocando em prática
Utilizaremos este campo de forma bem simples, criaremos um campo do tipo InternalCalc para
representar a comissão do vendedor no Cadastro de Pedido.
Name: COMISSAO
Type: Float
Field type: InternalCalc
Perceba que ajustamos o Field type para InternalCalc, é assim que determinamos este tipo
de campo.
Para calcularmos este campo, utilizaremos o mesmo evento que já estamos acostumados,
portanto, insira o seguinte código no evento OnCalcFields do cdsPedido:
TLabel
Caption: Comissão
TDBEdit
DataSource: dtsPedido
DataField: COMISSAO
Testando a aplicação
Ao entrarmos no Cadastro de Pedidos e editarmos os dados, veremos que o campo esta sendo
calculado a todo o momento, por exemplo, basta modificarmos o valor do pedido e a comissão
já é calculada. Como havíamos comentado, podemos evitar isso para não sobrecarregar a
aplicação quando o cálculo é demorado, portanto, faremos o devido ajuste.
Entendendo o código
Testando a aplicação
Se realizarmos o teste agora, verificaremos que o campo será calculado somente após a
gravação, claro que, o mesmo também é calculado quando é aberto pela primeira vez, que é o
caso de fazermos a pesquisa e abrirmos o registro.
É interessante notarmos também que podemos alterar o valor deste campo livremente, porém
de nada adiantará, pois seu valor será sobreposto pelo cálculo que fazemos no evento
OnCalcFields.
Muitos bancos relacionais possuem este tipo de campo, porém, o Firebird até a versão 1.5 não
possui, neste caso utilizamos um novo conceito, chamado Generators.
Generators como o próprio nome já diz, são geradores, mais precisamente, geradores de valor,
não são ligados a nenhuma tabela e a nenhum campo, é apenas um objeto que criamos no
banco de dados responsável em armazenar um único valor. Incrementamos e retornamos seu
valor com uso de uma função que o servidor de banco de dados nos disponibiliza.
Colocando em prática
Criando o Generator
Com este código, apenas definimos um novo generator no banco de dados com o nome
GEN_PED_NUMERO.
Quando criamos um Generator, seu valor padrão é zero. Para ajustarmos, podemos utilizar o
seguinte script:
GEN_ID(nome_do_generator, valor_incrementar)
GEN_ID(GEN_PED_NUMERO, 1)
Perceba que no lugar do valor do Número do Pedido, utilizamos a função GEN_ID para que o
generator seja incrementado e o resultado seja passado para o campo.
Podemos executar esta instrução diversas vezes e os pedidos serão inseridos com a chave
incrementada.
Em nosso projeto não poderemos utilizar desta forma, pois não manipulamos a instrução
INSERT, como vimos, ela é gerada automaticamente pelo Provider. O que teremos que fazer é,
obter o valor do generator (através de um select) incrementado e em seguida, atribuir este valor
ao campo PED_NUMERO do ClientDataSet para que seja gerado o INSERT já com o Número
do Pedido definido.
SELECT
GEN_ID(GEN_PED_NUMERO,1) AS PED_NUMERO_NOVO
FROM
TABELA
Está técnica é muito interessante, podemos criar campos fictícios e atribuir um valor retornado
de funções, cálculo ou até mesmo um valor fixo. Experimente executar o seguinte script:
SELECT
10 AS TESTE
FROM
TABELA
Será exibido um campo chamado TESTE cujo valor sempre será 10. No script anterior,
trocamos esse valor por uma função.
Voltando ao script anterior, onde utilizamos a função GEN_ID, se executarmos nesta tabela
com 20 registros, a função será executada 20 vezes, conseqüentemente incrementará o
Generator 20 vezes, perceberemos este resultado nos registros que serão exibidos, onde cada
linha estará incrementada com um valor.
Podemos simular este teste com nossa tabela TIPOPEDIDO que contém 3 registros:
SELECT
GEN_ID(GEN_PED_NUMERO,1) AS PED_NUMERO_NOVO
FROM
TIPOPEDIDO
Ao executarmos este script, veremos sempre 3 linhas, já que a tabela possui 3 registros e cada
linha possui um valor diferente, pois como dissemos, a função GEN_ID está sendo executada
em cada linha, portanto retornará um valor diferente para cada uma.
Por este motivo, precisamos de uma tabela que contenha exatamente um registro.
No banco Oracle, por exemplo, existe uma tabela para este caso, chamada de DUAL. Já no
Firebird não temos, muitos utilizam a tabela RDB$DATABASE, que é uma tabela interna que
possui outros propósitos mas que contém apenas um registro. Particularmente acho mais
interessante ter uma tabela específica para isso, portanto, vamos criá-la:
Definimos o nome DUAL somente para seguir o modelo do Oracle, mas poderíamos utilizar
qualquer nome.
Precisamos inserir um registro nesta tabela, porém, antes execute um Commit no IBExpert já
que acabamos de criá-la. Em seguida execute o script abaixo:
O valor que atribuímos ao campo DUAL_ID não é importante, o que devemos nos atentar é na
quantidade de registros existentes nesta tabela, que sempre deverá ser um.
SELECT
GEN_ID(GEN_PED_NUMERO,1) PED_NUMERO_NOVO
FROM
DUAL
Cada vez que executarmos este SELECT, teremos um novo valor sendo exibido, ou seja, a
função GEN_ID está incrementando o valor e retornando ao SELECT. É isso que precisamos,
faremos uso disto para atribuirmos ao nosso campo chave no projeto.
A primeira grande dúvida que surge é em que local iremos obter o valor do Generator e aplicar
ao campo. Poderíamos utilizar o evento OnNewRecord do ClientDataSet, porém não é muito
recomendado, pois o usuário pode começar a inserir um registro e cancelar, neste caso
teríamos ‘gasto’ um Generator e não utilizado no banco de dados.
Outro evento seria o BeforePost, mas mesmo assim não é o mais adequado, poderíamos ter
algum erro na gravação do registro (ainda em memória) antes de ser enviado ao banco de
dados.
Portanto o que fazemos é preencher o campo chave com um valor fictício, no caso, um número
negativo, já que não teremos Pedidos com números deste tipo.
...
private
FSeqTmp: Integer;
procedure AtualizaValorTotalItem;
public
{ Public declarations }
end;
…
Entendendo o código
A cada novo registro a variável será decrementada e enviada ao campo Número do Pedido.
Neste caso não há problemas de o usuário cancelar a inclusão, pois neste momento apenas
utilizamos o seqüenciador negativo que é utilizado temporariamente.
Antes de codificarmos o próximo evento, prepararemos nossa query que buscará o valor do
generator incrementado, portanto inclua no DataModule uma SQLQuery:
TSQLQuery (dbExpress)
Name: qryGen
SQLConnection: dmPrincipal.SqlConnPrincipal
SQL:
SELECT
GEN_ID(GEN_PED_NUMERO,1) PED_NUMERO_NOVO
FROM
DUAL
Antes de implementarmos o evento, precisamos declarar mais uma variável global na seção
private chamada FNumPedido do tipo Integer que será utilizada e explicada a seguir.
private
FNumPedido: Integer;
FSeqTmp: Integer;
procedure AtualizaValorTotalItem;
public
{ Public declarations }
end;
Entendendo o código
O código parece ser complexo, mas explicando passo a passo entenderemos facilmente.
Primeiramente é importante sabermos que este evento será executado diversas vezes de
acordo com o número de operações realizadas no ClientDataSet. Por exemplo, se incluímos
um Pedido e um Item, este evento será executado 2 vezes, na ordem que o ClientDataSet
organiza para não haver erro de integridade no banco, portanto na primeira vez será para
inserção do Pedido e na segunda para inclusão do Item.
Analisando o código, verificamos se está sendo feito uma inclusão comparando UpdateMode
com ukInsert, em seguida, checamos se refere a Pedido comparando o SourceDs com
qryPedido (sempre devemos comparar com o DataSet do Provider e não o ClientDataSet), em
caso positivo, temos então que utilizar o generator, fazemos isso abrindo a query e guardando
o valor retornado na variável global. Depois atribuímos o valor obtido ao Delta na propriedade
NewValue do campo. O Delta representa os dados que serão enviados ao banco, portanto, é
nele que devemos fazer as modificações e sempre na propriedade NewValue do campo.
Nas linhas seguintes comparamos se estamos incluindo um item (em nosso exemplo será
verdadeira na segunda chamada deste evento), sendo verdadeiro então verificamos se o
Número do Pedido do item é negativo para saber se o mesmo pertence a um Pedido novo,
neste caso então atribuímos ao campo chave o valor da variável global que obtivemos na
primeira execução do evento.
As alterações que fizemos no Delta foram aplicadas ao banco de dados, porém o ClientDataSet
não visualiza essas mudanças, portanto, continuaremos com o Número Pedido negativo no
DBEdit, somente após fecharmos e abrirmos o ClientDataSet veremos as mudanças.
Para que as mudanças no Delta sejam refletidas, precisamos pedir ao Provider que propague-
as ao ClientDataSet, fazemos isso ajustando a propriedade Options do Provider ligando a
opção poPropagateChanges.
Testando a aplicação
Antes de testarmos, vamos limpar todos registros de pedidos e itens para não haver conflitos
de Números já existentes, portanto executaremos 2 scripts no banco de dados:
Talvez possa ser um incomodo ficar visualizando o Número do Pedido negativo no DBEdit,
podemos ajustá-lo visualmente, deixando em branco nestes casos, assim ele será exibido com
o valor real (positivo) após a gravação no banco de dados.
Entendendo o código
Apenas verificamos se o valor é menor que zero, sendo, atribuímos um valor em branco à
variável Text, caso contrário, alimentamos com o valor real.
Testando a aplicação
Fazendo a inserção do Pedido, veremos que o Número do Pedido começa em branco, e logo
após a gravação, temos o campo sendo exibido com o valor real.
...
implementation
uses
udmPrincipal;
...
TSQLQuery (dbExpress)
Name: qryTipoPedido
SQLConnection: dmPrincipal.SqlConnPrincipal
SQL:
SELECT
TP_CODIGO,
TP_DESCRICAO
FROM
TIPOPEDIDO
Implementando o Generator
Implementaremos agora o Generator, para que possamos ter a chave primária sendo
alimentada automaticamente, da mesma forma que fizemos no cadastro de Pedidos.
Devemos ajustar o valor inicial, pois já temos 3 registros na tabela, portanto, para não haver
conflitos de chave, defina-o com o valor 3:
TSQLQuery (dbExpress)
Name: qryGen
SQLConnection: dmPrincipal.SqlConnPrincipal
SQL:
SELECT
GEN_ID(GEN_TP_CODIGO,1) TP_CODIGO_NOVO
FROM
DUAL
Da mesma forma que fizemos no Cadastro de Pedidos, faremos aqui codificando o evento
BeforeUpdateRecord do Provider, porém perceberemos que o código é mais simples, pois
não temos tabela detalhe neste caso.
Feito isto, vamos agora codificar para termos o seqüenciador negativo, da mesma forma que
fizemos no Cadastro de Pedidos.
Declare uma variável do tipo Integer na seção private do DataModule chamada FSeqTmp.
...
private
FSeqTmp: Integer;
public
{ Public declarations }
end;
...
Criando o Formulário
...
implementation
uses
udmCadTipoPedido;
...
E no evento OnDestroy:
Gravar no Servidor:
Cancelar Atualizações:
...
implementation
uses
ufrmCadPedido,
ufrmCadTipoPedido;
...
Testando a aplicação
Neste modelo perceba que temos o botão Gravar no Servidor, isso significa que podemos
fazer as manutenções neste cadastro e ao final, aplicá-las para que possam ser gravadas no
banco de dados.
Neste momento nosso projeto contém apenas uma busca simples, onde pedimos o Número do
Pedido ao usuário através da chamada do método InputQuery. Implementaremos agora uma
busca avançada, teremos uma tela onde o usuário poderá informar o Número do Pedido ou
Período de Data de Cadastro dos Pedidos.
É interessante atentarmos a este tela, pois como dissemos, temos que seguir um novo conceito
quando trabalhamos com Cliente/Servidor, não abrimos todos os registros num DBGrid para o
usuário selecionar qual deseja editar por exemplo, teremos sempre uma busca parametrizada,
assim o usuário selecionará o registro e o abriremos especificamente para edição.
Colocando em prática
...
implementation
uses
udmPrincipal;
...
TSQLQuery (dbExpress)
Name: qryPedido
SQLConnection: dmPrincipal.SqlConnPrincipal
O próximo passo seria ajustarmos o SQL da Query, porém, aqui já temos um diferencial. Nossa
pesquisa será dinâmica, o usuário poderá pesquisar pelo Número do Pedido ou pela Data,
portanto, teremos um script SQL variável, teremos a cláusula WHERE diferenciada de acordo
com a pesquisa.
Criando o Formulário
...
implementation
uses
udmPesqPedido;
...
Nomeie os componentes na respectiva ordem para facilitar nosso acesso mas adiante:
Vale lembrar que os botões Ok e Cancelar devem estar com a propriedade Kind configurada:
Botão Ok: bkOk
Botão Cancelar: bkCancel
TDataSource
Name: dtsPedido
DataSet: dmPesqPedido.cdsPedido
DataSource: dtsPedido
ReadOnly: True
Ligamos a propriedade ReadOnly do DBGrid para não permitirmos qualquer tipo de alteração
nos dados.
OnCreate:
OnDestroy:
dmPesqPedido.cdsPedido.Close;
dmPesqPedido.cdsPedido.CommandText :=
'select ped_numero, ped_data, ped_valor from pedido where ' + sWhere;
dmPesqPedido.cdsPedido.Open;
end;
Entendo o código
Analisando o código, temos a montagem da cláusula WHERE sendo feito de acordo com os
campos informados e ao final atribuímos à propriedade CommandText do ClientDataSet o
SQL montado.
Esta propriedade permite que o cliente (ClientDataSet) substitua o SQL existente no DataSet
ao qual o Provider está ligado, isto é interessante, pois no modelo multicamadas o SQLQuery
não estaria no mesmo local que o ClientDataSet, não teríamos acesso ao componente da
mesma forma que temos ao ClientDataSet, portanto enviamos o SQL através do ClientDataSet.
Por padrão, se tentarmos utilizar esta propriedade, não teremos resultado, pois precisamos
configurar o Provider para aceitar o comando vindo através da propriedade CommandText do
ClientDataSet. Esta permissão é feita ligando a opção poAllowCommandText na propriedade
Options do Provider.
...
implementation
uses
ufrmCadPedido,
ufrmCadTipoPedido,
ufrmPesqPedido;
...
Testando a aplicação
O nosso próximo passo agora é fazer com que, ao dar um duplo clique no registro, seja aberta
a tela de cadastro com o respectivo registro.
O que faremos é criar um método no formulário de cadastro para que possamos instanciá-lo
passando como parâmetro a chave do Pedido, assim ele repassará para a query e
conseqüentemente teremos o respectivo Pedido aberto.
...
private
{ Private declarations }
public
class procedure AbreForm(NumPedido: Variant);
end;
...
dmCadPedido.cdsPedido.Close;
dmCadPedido.cdsPedido.FetchParams;
dmCadPedido.cdsPedido.Params.ParamByName('PED_NUMERO').Value :=
NumPedido;
dmCadPedido.cdsPedido.Open;
frmCadPedido.Show;
end;
Entendendo o código
A maioria das chamadas feitas no evento OnCreate do formulário está sendo executada agora
neste método, a diferença é que também estamos criando o formulário e alimentando o
parâmetro PED_NUMERO de acordo com o parâmetro NumPedido passado no método.
Precisamos agora ajustar o evento OnCreate do formulário, de modo que seja responsável
apenas pela criação do DataModule e abertura da tabela de Tipos de Pedido.
Entendendo o código
...
private
{ Private declarations }
public
class function Pesquisa(var NumPedido: Integer): Boolean;
end;
...
Entendendo o código
Nesse ponto o código somente prosseguirá após fecharmos o formulário, pois estamos
exibindo-o como Modal.
Depois comparamos o resultado com a constante mrOk, pois sendo verdadeiro, significa que o
usuário clicou no botão OK, neste caso retornamos True para a função e atribuímos ao
parâmetro NumPedido o valor da chave (campo PED_NUMERO) do registro selecionado. Ao
final destruímos o formulário com o método Release.
Entendo o código
Entendo o código
Podemos perceber o quanto ficou simples nosso código, tudo está centralizado nos métodos
Pesquisa e AbreForm dos formulários.
Na primeira linha exibimos a tela de pesquisa passando como parâmetro nossa variável local
para que seja alimentada com o número do Pedido pesquisado. Logo depois, caso a pesquisa
tenha sido confirmada, chamamos o método AbreForm passando a variável local como
parâmetro, pois assim abriremos o Formulário de Cadastro com o Número do Pedido
pesquisado.
...
uses
udmCadPedido,
ufrmPesqPedido;
...
Testando a aplicação
Colocando em prática
...
implementation
uses
udmPrincipal;
...
TSQLQuery (dbExpress)
Name: qryCliente
SQLConnection: dmPrincipal.SqlConnPrincipal
SQL:
SELECT
CLI_CODIGO,
CLI_NOME
FROM
CLIENTE
WHERE
CLI_NOME CONTAINING :CLI_NOME
DataType: ftString
ParamType: ptInput
Criando o Formulário
...
implementation
uses
udmPesqCliente;
...
Nomeie os componentes na respectiva ordem para facilitar nosso acesso mas adiante:
Edit: edtNome
Buttons: btnPesquisar, btnOk, btnCancelar
DBGrid: dbgrdClientes
Vale lembrar que os botões Ok e Cancelar devem estar com a propriedade Kind configurada:
Botão Ok: bkOk
Botão Cancelar: bkCancel
TDataSource
Name: dtsCliente
DataSet: dmPesqCliente.cdsCliente
OnCreate:
OnDestroy:
OnCloseQuery:
Precisamos agora codificar o evento OnDblClick do DBGrid para que a pesquisa também
possa ser confirmada dando um duplo clique no registro:
Criaremos agora nosso método que será responsável em abrir a tela de Pesquisa e retornar o
Código do Cliente selecionado.
Entendendo o código
Utilizamos o mesmo conceito visto na Pesquisa de Pedidos, ou seja, este método criará o
formulário de pesquisa e retornará o Código do Cliente selecionado.
Agora ajustaremos a tela de Pedidos para que possamos ter acesso a Pesquisa de Cliente.
...
implementation
uses
udmCadPedido,
ufrmPesqPedido,
ufrmPesqCliente;
...
Em seguida inclua um Botão ao lado do Nome do Cliente para ser utilizado na chamada da
Pesquisa de Clientes e nomeie-o como btnPesqCliente.
Entendendo o código
O único diferencial está na comparação que fizemos no início, pois o usuário poderia ter clicado
no botão de pesquisa do Cliente antes de colocar o Pedido em edição, neste caso, forçamos a
edição quando confirmado a pesquisa.
Testando a aplicação
Ordenando Registros
Índices padrões
São índices padrões criados e nomeados automaticamente pelo ClientDataSet e representam
ordenações específicas:
DEFAULT_ORDER: Representa a ordem original dos registros, da forma que foram abertos no
Provider.
Índices temporários
São definidos através da propriedade IndexFieldNames, onde informamos por quais campos
os registros serão ordenados. No caso de ser mais de um campo, separamos com ponto-e-
vírgula. Estes índices possuem uma limitação, só podemos utilizá-los em ordem ascendente,
para índices complexos, utilizamos os persistentes, onde podemos definir o tipo de ordenação,
case-insensitive entre outras opções.
São chamados de índices temporários pelo fato de não serem reaproveitáveis, são criados e
descartados automaticamente quando trocamos de campo. Exemplo: Definimos o campo
PED_NUMERO na propriedade IndexFieldNames, um índice será gerado com este campo. Em
seguida ajustamos para o campo PED_DATA, então o índice anterior é descartado e um novo
índice é criado com base neste campo. Quando voltarmos a definir o campo PED_NUMERO
novamente o índice será recriado, demandando praticamente o mesmo tempo de processo da
primeira vez, não havendo reaproveitamento.
Índices Persistentes
São definidos através da propriedade IndexDefs no qual podemos criá-los informando outras
características, tais como: Ascendente/Descendente, Primário, Case-Insensitive, etc.
Diferente dos índices temporários, eles são reaproveitáveis. Com base no exemplo citado
anteriormente, utilizando índices persistentes, ao voltarmos a utilizar o índice com base no
campo PED_NUMERO, que já foi processado uma vez, o tempo e os recursos gastos seriam
praticamente zero, pois ele é gerado uma única vez e mantido.
Entendendo o código
Vale lembrar que esta propriedade aceita informarmos mais de um campo, para isto basta
utilizarmos o caractere ponto-e-vírgula entre eles.
Testando a aplicação
Abrindo a tela de Pesquisa de Pedidos e aplicando uma busca, podemos clicar na coluna na
qual queremos indexar e a ordenação será feita instantaneamente, sem a necessidade de
acessar o servidor de banco de dados.
Como havíamos comentado, eles são definidos através da propriedade IndexDefs, porém não
iremos criá-los em tempo de projeto, apenas em tempo de execução, pois a ordenação é muito
dinâmica, temos além da variação dos campos, o tipo de ordenação (ascendente quando
clicado pela primeira vez na coluna e ascendente na segunda), portanto não valeria a pena
criar diversos índices deixando-os pronto para serem utilizados.
Para fins didáticos, apenas demonstraremos como seria a criação do índice em tempo de
projeto através da propriedade IndexDefs, e logo em seguida colocaremos em prática nossa
codificação para criação em tempo de execução.
Clicando com o botão direito do mouse, aparecerá um submenu com a opção Add. Clique
nesta opção e um novo item será adicionado neste quadro:
Depois de criado o índice, ele ficará disponível para utilizarmos na propriedade IndexName do
ClientDataSet:
Como havíamos comentando, não criaremos em tempo de projeto, mas sim em tempo de
execução, portanto, remova-o acessando novamente a propriedade IndexDefs.
dmPesqPedido.cdsPedido.IndexDefs.Update;
if dmPesqPedido.cdsPedido.IndexName = Column.FieldName + '_ASC' then
begin
sIndexName := Column.FieldName + '_DESC';
Options := [ixDescending];
end
else
begin
sIndexName := Column.FieldName + '_ASC';
Options := [];
end;
if dmPesqPedido.cdsPedido.IndexDefs.IndexOf(sIndexName) < 0 then
dmPesqPedido.cdsPedido.AddIndex(sIndexName, Column.FieldName,
Options);
dmPesqPedido.cdsPedido.IndexName := sIndexName;
end;
Entendendo o código
Comentamos a primeira linha pois era a forma que utilizávamos para índices temporários. Após
temos a seguinte seqüência: Executamos uma atualização na lista dos índices disponíveis, em
seguida verificamos se o campo do índice atual é o mesmo que está sendo ordenado e em
ordem Ascendente, neste caso ajustamos o nome e as opções do índice para Descendente, se
não, utilizamos o nome como Ascendente e as opções em branco, já que a ordenação padrão
é Ascendente.
Testando a aplicação
Abrindo a tela de Pesquisa, após termos os registros disponíveis, podemos clicar a primeira
vez na coluna e os registros serão ordenados em ordem Ascendente, clicando pela segunda
vez, em ordem Descendente.
Da mesma forma que aplicamos filtros nas TTables utilizando a propriedade Filter, evento
OnFilterRecord, Ranges, etc., aplicamos também ao ClientDataSet, a grande diferença está
que, nas TTables por exemplo, os filtros eram aplicados com base em todos os registros
existentes fisicamente na tabela, enquanto que no ClientDataSet, será aplicado apenas aos
registros que estão em memória.
Outro diferencial é que utilizando Ranges em TTables por exemplo, por ele depender de um
índice, somos obrigados a tê-lo fisicamente na tabela, com o ClientDataSet continuamos
dependendo do índice, porém não fisicamente, criamos através da propriedade IndexDefs ou
utilizamos a propriedade IndexFieldNames, como vimos anteriormente.
Com base nesses conceitos, concluímos que os registros podem ser restringidos em 2 etapas,
primeiramente no próprio SQL da Query utilizando a cláusula WHERE para evitarmos tráfego
na rede, e em seguida, localmente, no próprio ClientDataSet, depois de os registros já terem
sido trafegados e estarem em memória.
Aplicaremos este recurso de duas formas, utilizando a propriedade Filter que como vimos, não
dependemos de índice, e utilizando Ranges, que neste caso, teremos de criar o respectivo
índice.
Edit: edtFiltro
Button: btnAplicarFiltro
Entendendo o código
O código é bem simples, como havíamos comentado, usamos da mesma forma que aplicamos
nas TTables por exemplo, portanto, definimos a propriedade Filter com o conteúdo do Edit e
logo ligamos o filtro ativando a propriedade Filtered.
Testando a aplicação
Execute a aplicação, abra a tela de Cadastro de Tipos de Pedido e no campo de Filtro, digite
por exemplo:
TP_DESCRICAO = 'A*'
Ao clicarmos no botão Aplicar Filtro, os registros serão restringidos de acordo com o Filtro,
neste caso, apenas os registros cuja descrição comece com a letra A serão exibidos.
Vale lembra que o filtro está sendo aplicado localmente, em memória, nenhum acesso ao
servidor está sendo feito.
Como havíamos comentando, para utilizarmos este recurso, temos que ter um índice que
contenha os campos que utilizaremos no range, portanto, podemos utilizar a propriedade
IndexFieldNames ou IndexName.
Utilizaremos a segunda para fins didáticos e também por ter uma melhor performance quando
o número de registros é muito grande, portanto, precisaremos criar o respectivo índice para ser
utilizado na propriedade IndexName.
Colocando em prática
Em seguida será aberto o Editor de Índices, clique com o botão direito do mouse na janela e
clique no item Add.
Fields: TP_DESCRICAO
Name: IDX_TP_DESCRICAO
Entendendo o código
Na primeira linha estamos informando o índice que deverá ser utilizado pelo Range, e logo em
seguida aplicamos utilizando o método SetRange, onde no primeiro parâmetro informamos o
valor inicial e no segundo, o valor final.
Vale lembrar que o método SetRange foi utilizado apenas para fins didático, mas poderíamos
ter o mesmo resultado utilizando por exemplo, os métodos SetRangeStart, SetRangeEnd e
ApplyRange.
Testando a Aplicação
Podemos testar por exemplo, informando a letra A no primeiro campo e a letra B no segundo,
ao clicarmos no botão Aplicar Range, veremos que somente registros cuja descrição
estiverem neste intervalo serão exibidos.
Da mesma forma que podemos filtrar os registros em memória, podemos também localizar
utilizando os métodos que já conhecemos, tais como: Locate, FindKey, FindNearest entre
outros.
Segue-se o mesmo conceito de Filtros, a pesquisa é feita nos registros em memória, ou seja,
podemos dizer que também temos as 2 etapas de pesquisa, aplicamos o WHERE na Query
para trafegarmos somente os registros necessários, e depois de estarem em memória,
podemos fazer uma busca entre eles. Um caso típico é aquele onde o usuário faz uma busca
incremental por exemplo, conforme vai digitando, o cursor vai se posicionando no registro.
Simularemos este caso no Cadastro de Tipos Pedidos, teremos um campo para que o usuário
possa digitar a descrição e buscaremos o registro utilizando o método FindNearest.
Colocando em prática
Entendendo o código
Vale lembrar que neste caso, estamos utilizando a propriedade IndexFieldNames, mas
poderíamos também utilizar o índice que já temos criado para este campo, utilizando a
propriedade IndexName.
Testando a aplicação
O importante saber é que nesta busca, nenhum acesso físico ao banco de dados está sendo
feito, como dissemos, a busca é feita em memória, por isso temos o resultado de forma
instantânea.
O método Refresh que utilizamos nas TTables, também está disponível no ClientDataSet, e
além deste, temos um outro chamado RefreshRecord, utilizado para atualizar apenas o
registro atual, diferente do Refresh que atualiza todos os registros.
Refresh
Quando utilizamos o método Refresh, internamente o Provider executa um Fetch no banco de
dados para cada registro, ele verifica as mudanças ocorridas e logo atualiza o ClientDataSet
com as novas informações.
Chamar o método Refresh é muito mais rápido do que fecharmos e abrirmos o ClientDataSet,
pois desta forma, será executado todo processo de requisição dos dados e assim os registros
serão novamente trafegados pela rede, demandando um maior tempo.
A utilização deste método não permite que haja pendências no ClientDataSet, se tentar
executar e alguma atualização não tiver sido aplicada ainda ao banco de dados, uma
mensagem de erro será exibida e o método não será executado.
RefreshRecord
Este método atualiza apenas o registro atual diferente do Refresh que atualiza todos os
registros. Outro grande diferencial é que, neste caso, não é realizado um Fetch no banco, mas
sim um novo SELECT, porém baseado na chave primária para que apenas o registro atual
possa ser trafegado e atualizado.
Para que este método possa ser utilizado, precisamos ajustar o ProviderFlags do campo que
pertence à chave primária ligando a opção pfInKey, para que assim o Provider possa montar
corretamente o SELECT e extrair o registro do banco de dados.
Utilizando este método não será gerada uma exceção caso haja pendências no registro, porém
o mesmo não será atualizado, o log de alterações é mantido.
Testando a aplicação
Instancie duas vezes a aplicação abrindo a tela de Cadastro de Tipos de Pedido. Na primeira
instância, faça inclusões, alterações e exclusões de registros, em seguida aplique clicando no
botão Gravar no servidor. Ao voltarmos para segunda instância, veremos que os dados
permanecem os mesmos, então clique no botão Refresh e veja que as informações serão
atualizadas de acordo com as novas mudanças feitas no banco de dados, e melhor,
instantaneamente, pois como dissemos, esta sendo executado apenas um Fetch nos registros.
Testando a aplicação
Adicione todos os campos clicando com o botão direito em seguida clicando no item Add all
fields.
Testando a aplicação
Executando a aplicação e clicando no botão RefreshRecord, veremos que o erro não será
mais exibido.
CancelUpdates
Como já vimos, utilizamos este método no botão Cancelar do Cadastro de Pedidos. Sua
finalidade é de cancelar tudo que há pendente (não aplicado) no ClientDataSet.
RevertRecord
Desfaz as alterações ocorridas no registro atual, voltando ao seu estado original.
SavePoint
Não é um método e sim uma propriedade que nos permite desfazer as operações a partir de
um determinado ponto.
Colocando em prática
UndoLastChange:
Estamos passando True no parâmetro, indicando que o cursor deverá ser posicionando no
registro restaurado.
RevertRecord:
Como dissemos, SavePoint é uma propriedade do ClientDataSet. Ela é do tipo Integer e nos
retorna a situação atual do ClientDataSet. Guardamos o valor em uma variável global, em
seguida podemos fazer as alterações nos dados e ao final, se precisarmos, podemos voltar à
situação em que estava no momento que obtivemos o valor de SavePoint, atribuindo a esta
propriedade o valor da variável global. Vejamos sua utilização no botão abaixo.
SavePoint - Desfazer:
...
private
{ Private declarations }
FSavePoint: Integer;
public
end;
...
Testando a aplicação
Podemos abrir a tela de Cadastro de Tipos de Pedido, alterar os registros e verificar o efeito de
cada botão. O mais interessante é o SavePoint, pois podemos clicar para marcar o ponto,
fazer as mudanças e depois clicamos no botão Save Point – Desfazer para voltar ao estado
que marcamos.
Um caso muito comum de utilização é para tabelas auxiliares, utilizadas como Lookup, que
sofrem pouca manutenção e dependendo o caso, vale a pena além de mantê-la no servidor,
salvá-la localmente nas máquinas dos usuários, pois assim, quando fossemos utilizá-la em um
TDBLookupComboBox por exemplo, carregaríamos localmente a partir do arquivo, evitando
tráfego na rede.
Claro que devemos pensar muito bem antes de adotarmos esta solução, pois isso envolve
termos um controle de atualização das máquinas quando esta tabela fosse atualizada no
servidor.
Em nosso projeto aplicaremos este recurso com base nesta idéia, mas é importante sabermos
que existem muitos outros casos que podemos utilizar, por exemplo, quando trabalhamos com
WebServices, onde precisamos salvar e carregar os dados em formato XML.
Nossa idéia será ajustar a tela de Cadastro de Tipos de Pedido, permitindo também gravar os
dados localmente em arquivo, pois assim ajustaremos nosso DataModule de Pedidos para
carregar o ClientDataSet de Tipo de Pedidos a partir deste arquivo e não mais diretamente do
servidor.
Gravando o arquivo
Testando a aplicação
Lendo o Arquivo
Este método possui apenas um parâmetro, que indica o caminho do arquivo. Se deixarmos em
branco, ele utilizará o caminho especificado na propriedade FileName do ClientDataSet.
Como estaremos lendo os dados a partir de um arquivo, não precisaremos mais acessar o
servidor de banco de dados, portanto, podemos remover o Provider e a Query relativa ao Tipo
de Pedido. Portanto, limpe a propriedade ProviderName do cdsTipoPedido e remova os
componentes dspTipoPedido e qryTipoPedido.
Testando a aplicação
Este recurso é muito bem utilizado naqueles casos onde temos relatórios complexos, no qual
temos que unir informações de diversas tabelas e montar os dados em uma tabela temporária.
Trabalhando-se com Paradox, precisaríamos criá-las fisicamente, já com o ClientDataSet não é
necessário, definimos a estrutura da tabela no próprio ClientDataSet e alimentamos os dados
em memória, podendo ser utilizando normalmente em um relatório.
Para colocarmos este recurso em prática, criaremos uma tela onde o usuário informará um
período e alimentaremos o ClientDataSet com o valor total de vendas de cada dia dentro do
período informado. Obviamente que conseguiríamos este resultado de maneira simples através
de uma instrução SQL, porém faremos desta forma apenas para fins didáticos, pois assim
veremos a criação da estrutura e como alimentamos o ClientDataSet para podermos visualizá-
lo em um DBGrid por exemplo.
Colocando em prática
...
implementation
uses
udmPrincipal;
...
TSQLQuery (dbExpress)
Name: qryPedido
SQLConnection: dmPrincipal.SqlConnPrincipal
SQL:
SELECT
PED_DATA,
PED_VALOR
FROM
PEDIDO
WHERE
PED_DATA BETWEEN :PED_DATAINI AND :PED_DATAFIM
Nossa query será responsável apenas em obter a data e o valor dos pedidos dentro do
período. Criamos para isso dois parâmetros, data inicial e data final, portanto precisamos
ajustá-los. Clique na propriedade Params do componente qryPedido e ajuste-os da seguinte
forma:
DataType: ftDate
ParamType: ptInput
Com o FieldsEditor aberto, clique com o botão direito do mouse e acesse a opção NewField.
Name: DATA
Type: Date
FieldType: Data
Name: VALOR
Type: Float
FieldType: Data
Neste momento temos o ClientDataSet estruturado, precisamos agora implementar a rotina que
o alimentará.
private
{ Private declarations }
public
procedure ProcessaResumo(DataIni, DataFim: TDateTime);
end;
qryPedido.ParamByName('PED_DATAINI').Value := DataIni;
qryPedido.ParamByName('PED_DATAFIM').Value := DataFim;
qryPedido.Open;
try
while not qryPedido.Eof do
begin
if cdsResumo.Locate('DATA', qryPedido.FieldBYName('PED_DATA').Value, [])
then
cdsResumo.Edit
else
begin
cdsResumo.Append;
cdsResumo.FieldByName('DATA').Value :=
qryPedido.FieldByName('PED_DATA').Value;
end;
cdsResumo.FieldByName('VALOR').AsFloat :=
cdsResumo.FieldByName('VALOR').AsFloat +
qryPedido.FieldByName('PED_VALOR').AsFloat;
cdsResumo.Post;
qryPedido.Next;
end;
finally
qryPedido.Close;
end;
end;
Entendendo o código
O código é um pouco extenso, mas não temos muito o que comentar, é pura programação do
dia a dia., apenas fizemos um loop na query e alimentamos o ClientDataSet na seqüência.
Perceba que não utilizamos o método Open ou a propriedade Active para abrir o ClientDataSet,
utilizamos o método CreateDataSet, que é responsável em criar o DataSet com sua estrutura e
em seguida abrí-lo.
Criando o Formulário
...
implementation
uses
udmResumoDiario;
...
Adicione um DataSource:
...
implementation
uses
ufrmCadPedido,
ufrmCadTipoPedido,
ufrmPesqPedido,
ufrmResumoDiario;
...
Testando a aplicação
Podemos testar a aplicação cadastrando primeiramente alguns Pedidos com datas comuns, em
seguida, entrando na tela de Resumo Diário, veremos os dados processados, gravados no
ClientDataSet que criamos somente em memória, podemos inclusive editá-los normalmente
pelo DBGrid.
Vale lembrar que muito dos os recursos que vimos até agora, poderiam ser aplicados a este
ClientDataSet, como por exemplo, ordenação dos registros, filtros, busca, salvar em arquivos,
etc.
Transações
Trabalhar com transações não é algo específico do ClientDataSet ou DBExpress, fazemos isso
em qualquer Engine de acesso, inclusive no BDE utilizando Paradox, dependemos apenas do
banco de dados suportar, a grande maioria suporta.
Para quem não conhece pode parecer um termo estranho, mas o conceito é bem interessante.
Supondo que temos de executar diversas rotinas de atualizações no banco de dados. Por
exemplo, realizamos uma venda e precisamos executar 3 processos:
- Baixar Estoque
- Gerar Nota
- Gerar Duplicatas
Neste caso, ao invés de fazermos um trabalho ‘braçal’ de desfazer tudo o que foi feito caso
algum erro tenha ocorrido, optamos em trabalhar com transações. Iniciamos uma transação,
fazemos todas as modificações no banco de dados e ao final aplicamos (commit) ou
descartamos (rollback) as alterações, tudo com base na transação iniciada.
Quando iniciamos uma transação e fazemos alterações no banco de dados, só serão gravadas
efetivamente se confirmarmos com um Commit, caso contrário, tudo o que foi feito será
descartado.
Este processo nos obriga a atualizar 2 tabelas: Itens de Pedido e Pedido. Temos que excluir
primeiro os Itens e depois os Pedidos, e tudo deve ser executados com sucesso, não
poderemos excluir os itens e deixar de excluir os Pedidos pelo fato de algum erro ter ocorrido,
se isso acontecer, devemos desfazer tudo.
...
implementation
uses
udmPrincipal;
...
TSQLQuery (dbExpress)
Name: qryExcluiPedido
SQLConnection: dmPrincipal.SqlConnPrincipal
SQL:
DELETE
FROM
PEDIDO
WHERE
CLI_CODIGO = :CLI_CODIGO
TSQLQuery (dbExpress)
Name: qryExcluiItem
SQLConnection: dmPrincipal.SqlConnPrincipal
SQL:
DELETE FROM
PEDITEM
WHERE
EXISTS (SELECT PED_NUMERO FROM PEDIDO WHERE PED_NUMERO =
PEDITEM.PED_NUMERO AND CLI_CODIGO = :CLI_CODIGO)
DataModule de Transação
DataType: ftInteger
ParamType: ptInput
...
private
{ Private declarations }
public
procedure ExecutaProcessoComSQLQuery(Codigo: Integer);
end;
...
Precisamos declarar a unit DBxpress na cláusula uses do DataModule para termos disponível
o tipo TTransactionDesc que utilizamos na declaração da variável.
Entendendo o código
Criando o Formulário
...
implementation
uses
udmTransacao;
...
Formulário de Transação
OnCreate:
OnDestroy:
OnClose:
procedure TfrmTransacao.FormClose(Sender: TObject;
var Action: TCloseAction);
begin
Action := caFree;
frmTransacao := nil;
end;
...
implementation
uses
ufrmCadPedido,
ufrmCadTipoPedido,
ufrmPesqPedido,
ufrmResumoDiario,
ufrmTransacao;
...
Testando a aplicação
Para simplificar, simularemos o erro no próprio código, gerando uma exceção com o método
raise.
qryExcluiPedido.ParamByName('CLI_CODIGO').Value := Codigo;
qryExcluiPedido.ExecSQL;
dmPrincipal.SQLConnPrincipal.Commit(TD);
except
dmPrincipal.SQLConnPrincipal.Rollback(TD);
raise;
end;
end;
Entendendo o código
Na linha que adicionamos o raise, simulamos como se a primeira Query tivesse sido executada
com sucesso e um erro ocorresse na execução da segunda query (qryPedido), portanto o
método Commit não será executado, pois o código passará para o bloco except executando o
Rollback e as atualizações serão desfeitas, neste caso a exclusão dos itens.
Testando a aplicação
Abra a tela de Transação e informe um Código de Cliente que contenha Pedidos e Itens, ao
clicarmos no botão ExecutarComSqlQuery, o processo será executado e podemos depois
checar que o Pedido e os Itens não foram excluídos, justamente pelo fato de termos gerado a
exceção na transação.
O Provider realmente já faz esse trabalho, porém em nosso caso teríamos 2 conjuntos dos
componentes ClientDataSet/Provider/Query, um para excluir Pedidos e outro para Itens,
portanto, teríamos 2 ClientDataSet. Neste caso chamaríamos o método ApplyUpdates em
ambos, logo teríamos a seguinte ordem de transações iniciadas e encerradas
automaticamente:
Perceba que para cada chamada ao método ApplyUpdates temos uma transação sendo
iniciada e encerrada, portanto, se algum erro ocorrer na 2.a chamada, será desfeito apenas o
que foi feito no 2.o ClientDataSet, mas o que feito no 1.o não seria possível, pois a transação já
foi encerrada.
Perceba que apenas uma transação foi aberta e encerrada manualmente, portanto, havendo
algum erro no 2.o ClientDataSet, podemos cancelar a transação e os dados de ambos os
ClientDataSets serão desfeitos.
Colocando em prática
TSQLQuery (dbExpress)
Name: qryPedido
SQLConnection: dmPrincipal.SqlConnPrincipal
SQL:
SELECT
PED_NUMERO
FROM
PEDIDO
WHERE
CLI_CODIGO = :CLI_CODIGO
TSQLQuery (dbExpress)
Name: qryPedidoItem
SQLConnection: dmPrincipal.SqlConnPrincipal
SQL:
SELECT
PED_NUMERO,
PROD_CODIGO
FROM
PEDITEM
WHERE
EXISTS (SELECT PED_NUMERO FROM PEDIDO WHERE PED_NUMERO =
PEDITEM.PED_NUMERO AND CLI_CODIGO = :CLI_CODIGO)
DataType: ftInteger
ParamType: ptInput
private
{ Private declarations }
public
procedure ExecutaProcessoComSQLQuery(Codigo: Integer);
procedure ExecutaProcessoComCDS(Codigo: Integer);
end;
if (cdsPedidoItem.ApplyUpdates(0) = 0) and
(cdsPedido.ApplyUpdates(0) = 0) then
dmPrincipal.SQLConnPrincipal.Commit(TD)
else
dmPrincipal.SQLConnPrincipal.Rollback(TD);
except
dmPrincipal.SQLConnPrincipal.Rollback(TD);
raise;
end;
end;
Entendendo o Código
A transação é iniciada e encerrada da mesma forma que fizemos no modelo anterior, o único
detalhe que devemos nos atentar é a forma que executamos o método ApplyUpdates nos
ClientDataSets para depois sim executar o Commit. Perceba que verificamos se o retorno do
método ApplyUpdates de ambos são iguais a zero, pois desta forma temos a certeza de que
tudo foi aplicado com sucesso, se algum erro ocorresse, não seria gerada uma exceção, a
função retornaria um valor diferente de zero e conseqüentemente nossa comparação seria
False, logo executaríamos o método RollBack para desfazer o que foi aplicado.
Poderíamos ter ajustado o código para executar o método CancelUpdates em caso de algum
erro para limpar as pendências do ClientDataSet, neste caso não se faz necessário, não
teremos essas pendências em uma segunda execução, pois fechamos o ClientDataSet caso o
mesmo esteja aberto.
Testando a aplicação
O mesmo tipo de teste que fizemos utilizando SQLQuery poderia ser feito neste, gerando uma
exceção e percebendo que os dados são desfeitos da mesma forma.
Decidir qual evento utilizar dependerá muito do caso, se precisarmos fazer algo antes da
atualização, utilizamos o evento BeforeUpdateRecord, se for após, utilizamos o evento
AfterUpdateRecord.
É importante sabermos que, quando esses eventos são disparados, uma transação já foi
iniciada pelo Provider automaticamente (caso não tenha sido iniciado manualmente), como
vimos anteriormente, portanto, se executarmos querys (utilizando a mesma conexão dentro
destes eventos) para atualizarmos outras tabelas por exemplo, tudo estará sendo feito na
mesma transação, portanto temos a garantia de que tudo será aplicado com sucesso ou tudo
será descartado.
Não permitiremos a exclusão física de um Pedido, quando o mesmo for excluído, definiremos
apenas uma data da exclusão (campo PED_DATAEXCLUSAO) no registro e anularemos o
processo de exclusão, portanto, neste caso utilizaremos o evento BeforeUpdateRecord, pois
somente nele que podemos anular uma atualização no banco.
Colocando em prática
TSQLQuery (dbExpress)
Name: qryAtualizaDataModificacao
SQLConnection: dmPrincipal.SQLConnPrincipal
SQL:
UPDATE
PEDIDO
SET
PED_DATAMODIFICACAO = :PED_DATAMODIFICACAO
WHERE
PED_NUMERO = :PED_NUMERO
TSQLQuery (dbExpress)
Name: qryAtualizaDataExclusao
SQLConnection: dmPrincipal.SQLConnPrincipal
SQL:
UPDATE
PEDIDO
SET
PED_DATAEXCLUSAO = :PED_DATAEXCLUSAO
WHERE
PED_NUMERO = :PED_NUMERO
PED_DATAMODIFICACAO
DataType: ftDate
ParamType: ptInput
PED_NUMERO
DataType: ftInteger
ParamType: ptInput
PED_DATAEXCLUSAO
DataType: ftDate
ParamType: ptInput
PED_NUMERO
DataType: ftInteger
ParamType: ptInput
Entendendo o código
O que fizemos foi, verificamos se a operação que está sendo realizada é uma exclusão, sendo,
checamos se refere a Pedido, neste caso, executamos nossa query que atualizará o campo
Data de Exclusão do Pedido e logo em seguida ligamos a variável Applied, desta forma
indicamos ao Provider que a atualização já foi aplicada, ou seja, ele não executará a
exclusão do Pedido.
No caso estar modificando um Pedido e apenas excluir um item, a tabela de Pedidos não
estará vazia, portanto neste caso a exclusão do item ocorre normalmente.
Entendendo o código
Percebemos que o código neste evento é mais simples, apenas verificamos se a operação está
sendo realizada no Pedido e se é uma modificação comprando o UpdateKind com ukModify.
Sendo verdadeiro, em seguida excutamos nossa query que atualizará o campo Data de
Modificação do Pedido.
O que precisamos observar é que utilizamos a propriedade OldValue do Delta para obter o
número do Pedido, isto foi necessário pois não houve mudanças neste campo, portanto a
propriedade Value estará definida como NULL neste evento.
Testando a aplicação
Quando testarmos a exclusão, veremos que o Pedido realmente será removido da tela de
cadastro, porém ele permanecerá fisicamente no banco e com uma data de exclusão definida.
Podemos comprovar isso utilizando o IBExpert.
Claro que se pesquisarmos o Pedido pela aplicação, ainda estará disponível, pois não fizemos
nenhuma condição para que sejam restringidos os Pedidos excluídos, bastaria adicionar uma
condição na pesquisa do tipo: ...AND PED_DATAEXCLUSAO IS NULL.
Podemos ter algum caso no qual precisamos fazer um SELECT em mais de uma tabela
unindo-as e deixando disponíveis os dados para o usuário fazer as devidas manutenções.
Neste caso, quando chamarmos o método ApplyUpdates, o Provider fará a atualização na
primeira tabela da cláusula FROM, porém pode não ser exatamente a tabela que desejamos
atualizar.
Por este motivo o Provider nos disponibiliza uma forma alterarmos o NOME da tabela que será
atualizada, fazemos isso através do seu evento OnGetTableName.
Em nosso projeto aplicaremos este recurso de forma simples, teremos uma tela exibindo os
Itens e os dados do Pedido na mesma tabela fazendo um JOIN entre elas e o usuário poderá
fazer as alterações nos campos dos Itens. Na atualização, veremos que o Provider tentará
aplicar as mudanças à tabela de Pedidos, pois será a primeira tabela que colocaremos na
cláusula FROM, em seguida ajustaremos de forma que atualize a tabela de Itens.
Colocando em prática
...
implementation
uses
udmPrincipal;
...
TSQLQuery (dbExpress)
Name: qryPedidoItens
SQLConnection: dmPrincipal.SqlConnPrincipal
SQL:
SELECT
PED.PED_NUMERO,
PED.PED_DATA,
PED.CLI_CODIGO,
PI.PROD_CODIGO,
PI.PI_DESCRICAO,
PI.PI_VALUNIT,
PI.PI_QTDE,
PI.PI_VALTOTAL
FROM
PEDIDO PED
INNER JOIN PEDITEM PI ON PI.PED_NUMERO = PED.PED_NUMERO
DataModule MultiplasTabelas
Para visualizarmos o erro que o Provider irá gerar na atualização, implemente o evento
OnReconcileError do cdsPedidoItens da seguinte forma:
procedure TdmMultiTabelas.cdsPedidoItensReconcileError(
DataSet: TCustomClientDataSet; E: EReconcileError;
UpdateKind: TUpdateKind; var Action: TReconcileAction);
begin
MessageDlg(E.Message, mtError, [mbOk], 0);
end;
Criando o Formulário
...
implementation
uses
udmMultiTabelas;
...
Formulário MultiTabelas
Adicione um DataSource:
TDataSource
Name: dtsPedidoItens
DataSet: dmMultiTabelas.cdsPedidoItens
Aplicar:
Cancelar:
OnCreate:
OnDestroy:
OnClose:
...
implementation
uses
ufrmCadPedido,
ufrmCadTipoPedido,
ufrmPesqPedido,
ufrmResumoDiario,
ufrmTransacao,
ufrmMultiTabelas;
...
Testando a aplicação
Execute a aplicação e entre no formulário que criamos. Teremos disponíveis os dados dos
Itens juntamente com os dados do Pedido, portanto, tente alterar por exemplo, a descrição de
algum item e em seguida, clicar no botão Aplicar para gravar fisicamente no banco de dados.
A seguinte mensagem de erro será exibida:
Isso ocorre justamente pelo fato de o Provider estar tentando atualizar a tabela PEDIDO, e
nesta tabela não temos mesmo o campo PI_DESCRICAO, portanto, precisamos ajustá-lo de
forma atualize a tabela PEDITEM.
Como havíamos comentado, para definirmos o nome da tabela a ser atualizada, utilizamos o
evento OnGetTableName do Provider, que é disparado quando o mesmo precisa saber o
nome da tabela para poder montar a query de atualização, portanto, este será o evento que
trataremos.
Entendendo o código
Quando este evento for disparado, a variável TableName estará com o nome da tabela
PEDIDO, portanto, trocamos definindo para PEDITEM para que assim o Provider possa utilizá-
la na montagem da query de atualização.
O parâmetro DataSet identifica o respectivo DataSet ao qual o Provider está ligado, no caso o
qryPedidoItens.
Testando a aplicação
Ao simularmos o mesmo tipo de teste que fizemos, teremos agora uma nova mensagem de
erro:
Isto está acontecendo devido ao problema que já estudamos anteriormente, a cláusula WHERE
que o Provider está montando contém todos os campos, inclusive os campos da tabela de
Pedidos, é isso que está gerando o problema, pois pedimos agora para atualizar a tabela
PEDITEM e nela não temos os campos do Pedido.
Faremos o ajuste indicando ao Provider para utilizar somente os campos chave na cláusula
WHERE, ao invés de desligar campo a campo a opção pfInWhere do ProviderFlags.
Testando aplicação
Podemos simular o mesmo teste alterando a descrição do item e gravando, a tabela será
atualizada com sucesso.
Claro que se tentarmos alterar os campos relativos à tabela de Pedido, uma mensagem de erro
seria exibida quando tentássemos gravar no banco, neste caso poderíamos barrar definindo os
campos do Pedido como ReadOnly, ou se fosse necessário atualizá-los, usaríamos o evento
AfterUpdateRecord do Provider para executar uma query de atualização na tabela de Pedidos.
O Delphi possui um formulário padrão já montado especificamente para este recurso, onde
nele visualizamos os dados do registro que está sendo atualizado, o motivo do erro e optamos
pela ação que tomaremos com o respectivo erro.
Colocando em prática
É interessante analisar o código do formulário para entendermos como são feitos os controles
internos, preenchimento do grid com as informações, etc..
Aplicaremos este recurso no Cadastro de Tipos Pedido, pois será mais fácil para entendermos
e visualizarmos o resultado da reconciliação, já que lá podemos aplicar uma atualização em
múltiplos registros.
...
implementation
uses
udmPrincipal,
ufrmReconcileError;
...
procedure TdmCadPedido.cdsPedidoReconcileError(
DataSet: TCustomClientDataSet; E: EReconcileError;
UpdateKind: TUpdateKind; var Action: TReconcileAction);
begin
Action := HandleReconcileError(DataSet, UpdateKind, E);
end;
Entendendo o código
A linha de código é muito simples, apenas atribuímos à variável Action a ação selecionada
pelo usuário no formulário, assim o ClientDataSet prosseguirá fazendo o devido tratamento de
acordo com esta ação.
Testando a aplicação
Para testarmos a reconciliação, temos que ajustar a chamada do ApplyUpdates, pois estamos
executando com parâmetro Zero, e como nosso objetivo é simular e tratar os erros, então
vamos informar ao método que não limite a quantidade de erros, portanto, passaremos o valor -
1 como parâmetro.
Para verificarmos o efeito, simularemos a típica situação onde o usuário tentará remover um
registro já excluído por outro usuário.
Abra duas instâncias da aplicação deixando aberto o Cadastro de Tipos de Pedidos, certifique-
se de que temos pelos menos 2 registros para realizarmos os testes.
Na primeira instância, exclua os dois registros e logo em seguida aplique a atualização clicando
no botão Gravar no Servidor.
O mais interessante é observarmos que ela será exibida duas vezes, pois dois erros foram
gerados, já que tentamos excluir dois registros inexistentes.
Clicando em Skip pulamos a atualização do registro, porém o mesmo ficará em cache e será
aplicado na próxima atualização.
Clicando em Correct, o ClientDataSet ajustará o registro com as novas mudanças feitas nos
campos.
Independente da opção, após confirmarmos, novamente a tela será exibida, agora com o erro
da exclusão do segundo registro.
Neste exemplo, pata obtermos um resultado satisfatório, poderíamos optar pela ação Cancel e
após a chamada do método ApplyUpdates, poderíamos chamar o método Refresh. Desta
forma eliminaríamos do cache as atualizações que não foram aplicadas e teríamos o
ClientDataSet atualizado com as novas modificações feitas no banco de dados.
Utilizando BDE, podemos fazer este monitoramento com o uso do SQL Monitor, que é um
utilitário que acompanha o Delphi. Já com dbExpress, a forma de monitoramento não é feita
através de um utilitário e sim de um componente específico para isso, o TSQLMonitor, que
intercepta as mensagens que ocorrem entre o componente SqlConnection (ao qual está
ligado) e o Banco de Dados.
Este componente nos permite acessar as mensagens monitoradas através da sua propriedade
TraceList, além disto temos o recurso de gravá-las em arquivo automaticamente a cada
mensagem interceptada, isto é feito ligando sua propriedade AutoSave e especificando o
caminho do arquivo na propriedade FileName.
Colocando em prática
TSQLMonitor (dbExpress)
Name: SqlMonitor
AutoSave: True
FileName: c:\sqlmonitor.txt
SQLConnection: SqlConnPrincipal
Propriedades utilizadas
AutoSave
Por padrão, as mensagens monitoradas são adicionadas à propriedade TraceList, porém,
ligando esta propriedade, determinamos também que elas deverão ser gravadas
automaticamente em arquivo, definido na propriedade FileName.
FileName
Path do arquivo onde as mensagens monitoradas serão gravadas.
SQLConnection
Componente SQLConnection a ser monitorado.
Para que a monitoração de mensagens seja ativada, devemos ligar a propriedade Active do
componente, portanto faremos isso no evento OnCreate do DataModule:
Vale lembrar que não é recomendado deixarmos o monitoramento ativo sempre na aplicação,
fazemos isso somente quando necessário, assim evitamos consumos de recursos
desnecessários.
Testando a aplicação
Para checarmos as mensagens que estão sendo monitoradas e gravadas, podemos por
exemplo, abrir o Cadastro de Pedidos, pois o Provider executa instruções SQL no banco para
extração dos dados, portanto, após sua abertura, checamos o arquivo c:\sqlmonitor.txt e
conferimos as mensagens monitoradas.
Vale lembrar que as mensagens monitoradas poderiam também ser visualizadas através da
propriedade TraceList do componente SQLMonitor.
Distribuindo a aplicação
A distribuição da aplicação é feita de forma bem simples, não precisamos de todos aqueles
disquetes que precisávamos antes para instalação do BDE.
Para rodar a aplicação, basta estar com essas dlls no mesmo diretório do executável ou no
diretório do Windows. Em alguns casos, ao executar o aplicativo uma mensagem de erro é
exbida dizendo que não foi possível carregar a Midas.dll. Para resolver este problema, basta
registrá-la, para isso, copie-a para o diretório do Windows e execute a seguinte linha de
comando:
regsvr32 midas.dll
Uma alternativa para não utilizar as DLL’s seria incluir as respectivas units no projeto:
DbExpint e MidasLib.