Uma das vantagens de um fórum de programação é observar as dúvidas mais frequentes dos usuários e tentar ajudá-los de uma forma mais prática. Dessa vez, notei que muitos desenvolvedores têm dificuldades em compreender, criar e manipular tabelas temporárias no Delphi utilizando ClientDataSet. Além de ser um recurso muito útil, trabalhar com tabelas temporárias não exige conhecimentos avançados de programação.
Preparado pra mais um pequeno tutorial sobre desenvolvimento?
Introdução
Também conhecida como “tabela virtual”, uma tabela temporária é capaz de armazenar registros em memória sem a necessidade de estar conectada a um banco dados, diferentes das tabelas físicas, que armazenam os dados em disco.
Os registros da tabela temporária são apagados automaticamente quando a aplicação é encerrada ou quando se utiliza um comando próprio pra isso, no qual veremos no próximo artigo. Um grande recurso da tabela temporária é permitir manipular estes dados em memória, como inclusões, alterações, exclusões e filtros, tal como podemos fazer com tabelas físicas no banco de dados.
As vantagens dependem do tipo de projeto que você está desenvolvendo. Tabelas temporárias, em linhas gerais, são úteis para trabalhar com dados locais na aplicação de forma dinâmica. Por exemplo, em um ambiente master/detail, uma tabela temporária pode armazenar os registros filhos antes mesmo do registro mestre ser gravado no banco de dados.
Cenário de exemplo
Imagine que estamos desenvolvendo um sistema para controle de vendas.
No nosso banco de dados, teremos as seguintes tabelas relacionadas à venda:
1 2 3 4 5 6 7 8 9 10 11 12 |
-- Tabela VENDAS COD_VENDA DATA COD_CLIENTE TOTAL -- Tabela ITENS COD_VENDA (chave estrangeira referenciando a tabela VENDAS) COD_PRODUTO QTDE VALOR TOTAL |
Agora, suponha que iremos gravar uma nova venda no sistema com os seguintes dados:
1 2 3 4 |
COD_VENDA = 1 DATA = 15/02/2013 COD_CLIENTE = 20 TOTAL = ? |
Você deve concordar que, para informar o valor total, é necessário adicionar os itens, ou seja, precisamos somá-los para calcular o valor total da venda, não é?
Por outro lado, para adicionar os itens precisamos do código da venda, já que as duas tabelas são relacionadas por uma chave estrangeira:
(ITENS.COD_VENDA = VENDAS.COD_VENDA).
Agora raciocine comigo: se tentarmos adicionar um item sem o código da venda ou com o código de uma venda que (ainda) não existe, receberemos o erro de “violação de chave estrangeira”, visto que, na verdade, tentaríamos adicionar um registro filho sem a existência do registro pai.
Pois bem, e para obtermos o código da venda, precisamos primeiro gravá-la no banco de dados, certo? Mas espere aí… conforme vimos acima, não temos o valor total pra gravar a venda!
E então entramos em um impasse:
- Não podemos gravar primeiro a venda, pois não temos os itens
- Não podemos gravar primeiro os itens, pois não temos a venda
E agora? Já sei! Vamos gravar a venda sem o valor total!
Muitos desenvolvedores utilizam essa “técnica”, se é que podemos chamá-la assim. Aplicando à nossa realidade, gravamos a venda sem o valor total, apenas para obter o código da venda. Dessa forma, podemos gravar os itens da venda normalmente, e após gravá-los, calculamos o valor total e alteramos o registro da venda, atualizando o total. Em um contexto procedimental, ficaria dessa forma:
- Gravar a venda sem o valor total;
- Obter o código da venda gravada;
- Gravar os itens utilizando o código da venda obtido;
- Somar os itens para calcular o total;
- Editar a venda e atualizar o valor total.
Observe que para cada venda será necessário fazer duas operações na tabela de vendas: uma de inserção e outra para alteração. Eu, particularmente, não sou de acordo com esse procedimento.
Em um exemplo, imagine que gravamos a venda, mas ao começar a adicionar os itens, a energia é cortada ou o usuário decide fechar a tela por algum motivo. Obviamente o registro ficará incompleto, já que teremos uma venda sem itens gravada no banco de dados. Tecnicamente, será um registro mestre sem registros filhos.
No caso da energia acabar, o problema pode ainda se agravar um pouco mais. Ao reiniciar o sistema, o usuário irá gerar uma nova venda, visto que, aos olhos do usuário, a outra venda não foi gravada. No fim da história haverá 2 vendas idênticas no sistema: uma sem itens (perdida) e outra válida.
Mas neste caso é só excluir essa venda incompleta, não é?
Sim, pode ser. Mas para isso, você terá que implementar um controle eficiente no seu sistema para identificar esses registros problemáticos. E também, lembre-se que cada vez que isso ocorrer, o código da venda será perdido (“pulado”) no banco de dados.
Aí prepare-se para receber algumas ligações do usuário perguntando:
“Por quê o sistema pulou o número da venda?”
“A venda nº X sumiu do sistema…”
“Existe uma venda aqui sem itens!”
Tabelas Temporárias
Amigos, todos os problemas citados acima podem ser resolvidos utilizando tabelas temporárias! Durante a inclusão da venda, ao invés de gravarmos os itens no banco de dados, iremos gravá-los em uma tabela temporária. Se a energia acabar, nada será afetado, já que nenhuma alteração foi feita no banco de dados.
Este será o nosso procedimento:
- Inserir os itens na tabela temporária;
- Ao gravar a venda, os itens da tabela temporária serão “copiados” para a tabela física, tudo em um mesmo método.
Desta maneira, iremos reduzir bastante a possibilidade de uma venda ficar incompleta, visto que a gravação da venda e os itens da venda acontecerão praticamente ao mesmo tempo. E o melhor: durante a inclusão da venda não faremos nenhuma operação no banco de dados, somente na gravação.
Legal! Como faço pra criar uma tabela temporária?
Pessoal, como essa parte conceitual de tabelas temporárias ficou um pouco extensa, vou fazer um suspense e deixar a parte prática para o próximo artigo, ok?
Boa semana pra vocês e até logo!
Confira os outros artigos:
Tabela temporária com ClientDataSet – Conceito
Tabela temporária com ClientDataSet – Prática
Tabela temporária com ClientDataSet – Final
A utilização de TClientDataSets até a versão 5 do Delphi, se não estou enganado, exigia a cópia da midas.dll para a pasta System32 do Windows e precisava ser distribuída com a aplicação. A partir da versão 6, a Borland criou a midaslib, que eliminava a exigência desta dll.
Bem lembrado, Marcos! Inclusive até as versões atuais do Delphi exigem a distribuição dessa DLL, ao menos que a MidasLib seja declarada na seção uses.
Muito bom o artigo, entendi perfeitamente os conceitos e o porque o uso delas e de suma eficacia, o post com a parte pratica já foi feito? irá me ajudar muito estou em uma desses impasses, obrigado abraços.
Olá, Gustavo. A parte prática já está pronta sim. Clique neste link. Obrigado!
Irei estudá-los, Muito Obrigado por compartilhar seu conhecimento conosco.
Excelente! Parabéns pela didática! Explicando desse jeito é impossível não entender e aprender!
Olá, Hugo! Muito obrigado pelo feedback! Fiquei contente com o seu comentário!
Abraço!
Fantástico, era isso que eu precisava , agora vou ler o próximo artigo , Muito Obrigado André
Isso aí, André! Os três artigos lhe ajudarão a entender o funcionamento de tabelas temporárias! Abraço!
Olá André. Somente um esclarecimento… a pergunta é básica: A tabela VENDAS tem como PK COD_VENDA, certo? A tabela ITENS possui algum outro tipo de chave, além da FK COD_VENDA? Estou usando o IBExpert para criar as mesmas. Grato.
Olá, Mario, tudo bem?
Na tabela de itens, normalmente a chave primária é composta pelo código da venda (COD_VENDA) e pelo código do produto (COD_PRODUTO). Isso implica, claro, que a mesma venda não poderá ter produtos repetidos.
Caso essa repetição seja necessária, pode-se substituir o código do produto por um sequencial. O importante é manter a chave composta.
Abraço!
Ola André.
O uso de tabelas temporárias realmente me interessa bastante.
As reclamações do cliente que vc colocou eu já recebi algumas, minha solução, antes de gerar um novo, uma rotina verifica se existe um com valor=0 e abre a primeira ocorrência, gravo os pedidos e itens direto no BD por causa de outra reclamação, ‘Acabou a energia e eu perdi todo orçamento com mais de 50 itens’, quando a energia retorna volta nele.
Mas tenho aplicação imediata para as tabelas temporárias.
Obrigado, até mais.
Legal, Gerson!
A única condição para trabalhar com gravação no banco de dados durante a inserção (sem usar tabelas temporárias) é utilizar “recursos controlados”. Se há 50 usuários em uma rede e um deles inicia uma nova inserção, o ID desse registro (seja um pedido, orçamento ou venda) deve ser “reservado” para este usuário, de forma que, caso acabe a energia, ele possa recuperá-lo.
O risco dessa técnica é que, se ele iniciar uma nova inserção (assumindo que a antiga foi perdida), surgirão registros órfãos no banco de dados. Além disso, o ID desse registro será “pulado”.
Ola, André.
Vou detalhar um pouco, deu trabalho corrigir estes detalhes que citou.
Eu isolo os terminais e não os usuários como alguns fazem. Isso evita o travamento por um usuário estar logado em outro terminal. O usuário (vendedor) pode usar qualquer terminal disponível e até criar um novo pedido enquanto outro vendedor está separando itens, sem problemas. Aliás isso é uma prática normal, tenho em um cliente 12 vendedores para 5 terminais.
O isolamento dos terminais é uma parte, quando finaliza um pedido, outro é gerado para este terminal (exclusivo para ele). A sequência é garantida por generators, mas antes de gerar um novo verifico se o terminal tem pedidos com valor igual a 0. Se tem, abre o mais antigo e não gera outro, tudo com Stored Procedures. Existe uma tecla que gera um novo sem verificação, para o caso de usar um terminal com um pedido já iniciado. um vendedor pode criar vários novos em branco, mas a procedure que verifica antes de criar um novo se encarrega de utilizar eles.
Nenhum pedido pode ser deletado. Até a liquidação pode ser alterado, depois não.
Tenho clientes que estão alcançando 2 milhões de pedidos, no máximo 15 terminais simultâneos. Roda direitinho, mas quando implantei essas funções houveram reclamação sobre o número e data não serem coerentes e datas recentes com numeração antiga.
Opa, Gerson, agora entendi como funciona a sua aplicação! Legal!
O uso de Generators é muito importante para evitar a duplicação de chaves e algumas vezes é ignorado. Muitos apostam no incremento do ID (Último ID + 1), aumentando o risco de comprometer a integridade dos dados.
O seu método para verificar se existe um pedido em aberto é bem interessante! É como uma “recuperação” de um registro pendente de inserção. Obrigado por compartilhar!
Abraço!
Olá, eu tentei usar o incremento no programa, mas não dá certo mesmo, porque no espaço de tempo entre verificar e gravar, vários podem ter pego o mesmo valor. Por isso trato todo o processo em stored procedures e triggers.
No princípio realmente foi para recuperação, agora evita o erro.
Abraço!
Obrigado André, me ajudou muito cara. Comecei a trabalhar e aqui na empresa tem muitas telas que utilizam ClientDataSet e confesso que estava ficando perdido e sem entender o motivo. Artigo muito esclarecedor, obrigado!
Olá, Josimar!
Que bom! Fico feliz por saber que o artigo foi útil!
Na verdade, esse artigo já está bastante obsoleto (foi publicado em 2013). Desde então, o
TClientDataSet
recebeu várias modificações. Assim que oportuno, vou atualizar essa série de artigos.Abraço!
Olá André
Como usar um select em uma tabela temporária, pesquisei na net achei muita coisa que não deram certo.
Estou gravando em um clientdataset informação que vem de várias tabelas, depois preciso dar um select para organizar para montar um relatório.
Olá, Evaldo!
É possível executar intruções SQLs apenas em queries.
No caso de tabelas temporárias, você pode utilizar filtros através da propriedade Filter do componente TClientDataSet. Essa propriedade permite que você aplique condições para filtrar os dados, similar à cláusula WHERE da linguagem SQL.
Abraço!