Por que eu devo ler este artigo:Aplicações que utilizam a relação N x N são denominadas do tipo mestre-detalhe. Nesse tipo de aplicação a exibição dos dados contidos na tabela detalhe é dependente da tabela mestre, ou seja, a seleção de dados na tabela mestre faz com que os dados exibidos na tabela detalhe sejam atualizados automaticamente.

Essas aplicações são as mais comuns e largamente utilizadas em sistemas comerciais, uma vez que podem trazer agilidade e economia de tempo nas telas de consulta. Elas podem ser construídas utilizando ou não os recursos de cache, sendo que essa segunda abordagem apresenta uma série de vantagens e funcionalidades adicionais que podem aumentar o controle sobre os dados nesse tipo de aplicação, como o envio de registros em lote para serem gravados e um controle de erro mais eficaz. Desta forma, é importante o conhecimento sobre como construir aplicações mestre-detalhe utilizando a engine de acesso a dados padrão do Delphi atualmente, o FireDAC.

Quando se pensa no desenvolvimento de software seguindo a metodologia cliente e servidor, basicamente podemos utilizar dois métodos principais no que diz respeito à manipulação dos registros. O primeiro é o mais comum e ocorre quando os registros são enviados um a um para a base de dados, ou seja, a cada clique em algum botão específico é executado um comando insert, update ou delete. Por outro lado, o segundo método é utilizar conceitos de cache. Nesse caso, as atualizações são armazenadas em memória até que o usuário decida aplicá-las no servidor em uma operação em lote (batch), que geralmente ocorre em uma única transação. A segunda abordagem é bastante conhecida dos desenvolvedores Delphi, sendo que esses mecanismos são geralmente implementados utilizando dbExpress em conjunto com os componentes TClientDataSet e TDataSetProvider.

Conforme mencionado anteriormente, sem a utilização de cache os dados são enviados um por um diretamente para o banco de dados, fato que não permite a revisão dos dados por parte dos usuários ou então a gravação em lote. Portanto, a utilização desse recurso apresenta uma série de vantagens, das quais podemos citar a visualização e possibilidade de modificação dos dados antes de serem gravados e, principalmente, a facilidade de salvar os dados em um arquivo local, o que permite trabalhar com o sistema desconectado. Nesse contexto, a conexão ao servidor seria necessária somente na gravação final de todos os registros.

Apesar de possuir essas vantagens, um problema desse tipo de aplicação é que os dados podem “envelhecer” rapidamente. Imagine que dois usuários acessem um mesmo conjunto de registros e que o primeiro abra uma janela e deixe o sistema inativo por algum tempo. Enquanto isso, o segundo usuário realiza várias alterações e as grava no servidor. Neste contexto, o primeiro usuário trabalharia com uma cópia inválida dos dados, o que aumentaria o risco de sobrescrever os registros e até mesmo invalidar as modificações aplicadas pelo primeiro usuário. De acordo com isso, podemos perceber que aplicações desta natureza necessitam de um maior controle para evitar inconsistência dos dados.

Baseado nisso, o objetivo do presente artigo é mostrar como construir uma aplicação mestre-detalhe utilizando o Delphi 10.1 Berlin, a engine FireDAC de acesso a dados e o SQL Server como o sistema gerenciador de banco de dados. Será mostrado como criar a base de dados e utilizar duas formas para a implementação da janela mestre-detalhe: a primeira sem a utilização dos recursos de cache e a segunda fazendo uso desse recurso. Serão também discutidas algumas vantagens de utilizar cache, sendo mostrados também vários recursos adicionais que o uso desse mecanismo possibilita. As próximas seções mostram o desenvolvimento completo do software.

Criando a base de dados

O primeiro passo é a criação da base de dados que representará nosso exemplo de aplicação mestre-detalhe. Para isso, a Listagem 1 apresenta os comandos SQL para a definição da base de dados e das tabelas no SQL Server. Na linha 01 encontra-se o comando para a criação da base de dados, enquanto que entre as linhas 04 e 11 estão os comandos para a criação da tabela de clientes e de produtos. Como pode ser observado, essas duas tabelas armazenam somente um identificador (chave primária e autoincremento — identity) e um nome.

Entre as linhas 12 e 16 encontra-se o script para a definição da tabela de vendas, que, além da chave primária autoincremento, possui a data da venda e uma chave estrangeira para a tabela de clientes. Por fim, a partir da linha 17 se localiza o script de criação da tabela venda_produtos, que armazenará os produtos existentes em cada venda e que caracteriza a relação mestre-detalhe deste exemplo. Essa tabela contém uma chave primária autoincremento, a quantidade de produtos comprados e duas chaves estrangeiras: uma relacionada à venda e outra ao produto.

  01 create database mestre_detalhe;
  02 go
  03 use mestre_detalhe;
  04 create table clientes (
  05   idcliente int not null primary key identity,
  06   nome varchar(50) not null
  07 );
  08 create table produtos (
  09   idproduto int not null primary key identity,
  10   nome varchar(50) not null
  11 );
  12 create table vendas (
  13   idvenda int not null primary key identity,
  14   idcliente int not null foreign key references clientes (idcliente),
  15   data_venda date not null
  16 );
  17 create table venda_produtos (
  18   idvenda_produtos int not null primary key identity,
  19   idvenda int not null foreign key references vendas (idvenda),
  20   idproduto int not null foreign key references produtos (idproduto),
  21   quantidade int not null
  22 );
Listagem 1. Scripts de criação da base de dados

O código da Listagem 2 apresenta os scripts em SQL para inserção de dois clientes (linhas 01 e 02), dois produtos (linhas 03 e 04), duas vendas (linhas 05 e 06) e três produtos nas vendas (linhas 07 a 09). A primeira venda possui somente um produto cadastrado, enquanto que a segunda contém dois. É importante salientar que, pelo fato dos campos chave primária serem autoincremento, não é necessário passar seus valores na cláusula values dos comandos insert.

  01 insert into clientes (nome) values ('Ana');
  02 insert into clientes (nome) values ('Pedro');
  03 insert into produtos (nome) values ('Suco');
  04 insert into produtos (nome) values ('Pão');
  05 insert into vendas (idcliente, data_venda) values (1, '2017-01-01');
  06 insert into vendas (idcliente, data_venda) values (2, '2017-01-02');
  07 insert into venda_produtos (idvenda, idproduto, quantidade) values (1, 1, 5);
  08 insert into venda_produtos (idvenda, idproduto, quantidade) values (2, 1, 3);
  09 insert into venda_produtos (idvenda, idproduto, quantidade) values (2, 2, 2);
Listagem 2. Scripts para inserção de dados

Definindo o acesso aos dados

O próximo passo é criar uma nova aplicação do tipo VCL no Delphi (File > New > VCL Forms Application – Delphi), salvar o formulário gerado pela IDE como PrincipalFrm e nomeá-lo (pela propriedade name) como frmPrincipal. Esse tipo de aplicação utiliza a biblioteca Visual Component Library (VCL) e diz respeito a softwares que funcionarão somente no Windows, diferentemente de aplicações criadas com o FireMonkey, que são multiplataforma.

Todos os componentes de acesso aos dados serão centralizados em um Data Module. Para criá-lo deve-se escolher a opção de menu File > New > Other > Delphi Files > Data Module, salvá-lo com o nome ConexaoDtm e nomeá-lo, através de suas propriedades, como dtmConexao. A Figura 1 apresenta os componentes que serão utilizados, sendo: um TFDConnection (cnnConexao), dois TFDQuery (qryVendas e qryVendaProdutos), um TFDMemTable (memCache), três TDataSource (dsVendas, dsVendaProdutos e dsCache) e um TFDSchemaAdapter (adapter).

Data Module com os componentes de acesso aos dados
Figura 1. Data Module com os componentes de acesso aos dados

A primeira configuração que faremos é a conexão do sistema com a base de dados por meio do componente cnnConexao. Basta dar um duplo clique nesse componente para que a janela FireDAC Connection Editor seja aberta, na qual as seguintes propriedades devem ser configuradas:

  • Driver ID: MSSQL
  • Database: mestre_detalhe
  • Server: local
  • OSAuthent: Yes

As três últimas propriedades são relativas ao nome da base de dados, à localização do servidor e à forma de autenticação, respectivamente. Note que não foi necessário configurar o nome do usuário e senha de acesso à base de dados visto que neste exemplo estamos utilizando a segurança integrada do SQL Server, ou seja, são usadas as mesmas credenciais de login do sistema operacional.

A qryVendas e a qryVendaProdutos têm a função de extrair os dados da base de dados e efetivamente vincular as duas tabelas para que o relacionamento mestre-detalhe seja possível (ambas devem estar ligadas ao cnnConexao por meio da propriedade Connection). A primeira query deve ter o seguinte comando na propriedade SQL, o qual somente extrai todos os registros da tabela mestre (vendas):

select idvenda, idcliente, data_venda from vendas

Por outro lado, a segunda query deve ter o script a seguir configurado em sua propriedade SQL:

select idvenda_produtos, idvenda, idproduto, 
quantidade from venda_produtos where idvenda = :idvenda

Esse comando é bastante similar ao anterior e é responsável por trazer todos os registros da tabela detalhe (venda_produtos). A principal diferença está no fato de existir um comando where no final do script, que de fato realiza o relacionamento entre as tabelas mestre e detalhe. Note que para essa cláusula é utilizado o campo idvenda da tabela venda_produtos, o qual representa a chave estrangeira que faz a ligação com a tabela mestre. Outro fato importante é a existência de um parâmetro (:idvenda), que deve possuir exatamente o mesmo nome do campo chave primária da tabela mestre, caso contrário o relacionamento automático entre as tabelas pode não ocorrer de maneira adequada. Não há necessidade de preenchimento manual via código desse parâmetro, pois o Delphi identificará automaticamente o relacionamento e preencherá o parâmetro em tempo de execução de acordo com os registros que forem selecionados pelo usuário na interface gráfica do sistema (a ser construída na sequência).

Com relação aos dois componentes TDataSource (dsVendas e dsVendaProdutos), ambos devem ter suas propriedades DataSet setadas para qryVendas e qryVendaProdutos, respectivamente. Essa ligação tem o objetivo de possibilitar acesso à ligação mestre-detalhe, bem como permitir que os dados sejam exibidos em tela posteriormente. Além da associação entre os componentes por meio de SQL, é necessário ainda configurar as seguintes propriedade para a qryVendaProdutos:

  • MasterSource: dsVendas
  • MasterFields: idvenda

Essas duas propriedades estão efetivamente ligadas ao relacionamento entre os componentes e é por meio delas que o Delphi entenderá o parâmetro idvenda e de fato operacionalizará a ligação entre as tabelas. Adicionalmente, deve-se clicar com o botão direito nas queries, acessar a opção Fields Editor e escolher Add All Fields para que todos os campos sejam adicionados.

Com relação aos outros componentes presentes na Figura 1, a única configuração que precisa ser feita é para o DataSource dsCache, que deve ter sua propriedade DataSet alterada para memCache. Mais detalhes sobre esses componentes serão apresentados na sequência.

Criando a janela mestre-detalhe sem cache

A Figura 2 apresenta a interface gráfica que usaremos para realizar os testes em nossa aplicação, a qual possui uma TToolBar (tblFuncoes) com sete TButtons (btnVerAlteracoes, btnUltimaAlteracao, btnReverter, btnCancelar, btnCriarSavePoint, btnRestaurarSavePoint e btnGravar). Logo abaixo, existem três TDBGrids (grdVendas, grdVendaProdutos e grdCache), os quais exibirão respectivamente: as vendas, os produtos somente das vendas selecionadas na primeira grade e os dados que estão armazenados no cache. Os dois primeiros dizem respeito ao relacionamento mestre-detalhe, enquanto que a terceira grade será utilizada para entendermos as funcionalidades dos processos de cache no Delphi. Essas funcionalidades serão explanadas por meio das funções apresentadas nos botões.

É necessário, agora, vincular os DBGrids aos seus respectivos DataSources presentes no Data Module. Para isso, deve-se primeiramente seguir a opção File > Use Unit e escolher o arquivo ConexaoDtm.pas para que a respectiva unit seja adicionada na cláusula uses do formulário, possibilitando acesso aos componentes. Por fim, cada grade necessita ter sua propriedade DataSource configurada de modo que esteja ligada às tabelas correspondentes.

Interface gráfica mestre-detalhe
Figura 2. Interface gráfica mestre-detalhe

A partir desse momento a aplicação já pode ser executada, bastando que a propriedade Active das duas queries esteja configurada com o valor True. Neste exemplo não estamos utilizados botões para inclusão, alteração e exclusão de registros. Dessa forma, para inclusão deve-se somente pressionar a seta para baixo do teclado e para exclusão, o conjunto das teclas CTRL + DELETE.

A Figura 2 apresenta a simulação da inserção de uma nova venda com dois produtos, podendo-se observar que, após gravada, a nova venda recebeu o identificador 1004, que corresponde ao campo autoincremento/chave primária do tipo identity utilizado pelo SQL Server. É importante salientar que neste exemplo o valor 1004 refere-se ao próximo identificador disponível nesta base de dados, podendo variar de base para base de acordo com a quantidade de registros. Esse mecanismo de autoincremento é gerenciado pelo próprio banco de dados e o programador não tem muito controle sobre os valores gerados.

Note também que no segundo registro da segunda grade está presente o valor -3, que indica a chave primária temporária até que o valor seja gravado em definitivo. O FireDAC utiliza essa estratégia de valores decrementais negativos para evitar conflito com possíveis restrições de integridade; por exemplo, se forem inseridos cinco registros, os valores estarão no intervalo de -1 até -5. É também possível observar na segunda grade que a chave estrangeira idvenda foi preenchida automaticamente com o valor da chave primária da tabela mestre para efetivamente concretizar o relacionamento entre ambas.

Nota: A própria engine do FireDAC detecta automaticamente os campos que foram definidos como autoincremento na base de dados, independente do sistema gerenciador de banco de dados utilizado, desde que esse seja suportado (consulte a seção Links para mais informações sobre os outros SGBDs suportados). Abrindo o Fields Editor de qualquer uma das queries utilizadas (botão direito no componente) e clicando no campo chave primária, pode-se notar que a propriedade ServerAutoIncrement já vem marcada como True, o que indica que o próprio servidor é responsável por gerenciar o incremento dos campos e que o FireDAC faz a interface entre o SGBD e a aplicação. Esse tipo de campo é tratado de maneira diferente de acordo com o banco de dados, por exemplo: no Firebird e Interbase são utilizados Generators e Triggers, no Informix e PostgreSQL são os campos do tipo serial, no MySQL é usado o tipo auto_increment e no Sybase e SQL Server é o tipo identity. A vantagem deste recurso do FireDAC é que a engine consegue manipular esses tipos de campos por mais que eles apresentem maneiras diferentes de serem construídos.
Nota: No que diz respeito a utilização de campos do tipo autoincremento, esses podem ser definidos diretamente no banco de dados ou então na própria aplicação. A abordagem recomendada é a primeira, pois ela é de responsabilidade direta do SGBD e está fora das transações da aplicação. Em algumas situações, não é uma boa prática manter um contador na aplicação porque ele está em uma transação e os outros usuários não terão acesso até que ela seja “comitada”. Essa abordagem garante o controle de colisão, evitando que vários usuários recebam códigos repetidos.

Com isso, sem nenhuma linha de código é possível construir uma interface totalmente funcional. Porém, antes de cadastrar um novo produto a venda precisa ser gravada, ou seja, a chave primária da venda precisa ser gerada para que ela seja copiada para a tabela detalhe (utilizados os recursos de post do TFDQuery). Isso pode ser observado novamente na Figura 2, na qual é possível notar que o valor 1004 foi gerado na primeira grade para depois ser copiado para a segunda.

Esse tipo de comportamento pode ser útil e adequado em várias situações, todavia, em alguns casos pode gerar alguns problemas. Para exemplificar, consideremos o exemplo de um sistema on-line de nota fiscal, no qual o cabeçalho da nota (tabela mestre) necessita ser enviado para o servidor junto com todos os produtos que foram comprados (tabela detalhe). Da forma que o sistema está implementado, até agora isso não seria possível, pois primeiramente o cabeçalho seria enviado para o servidor e depois cada um dos produtos individualmente. Caso ocorresse algum problema de conexão, a nota fiscal poderia ficar gravada no servidor somente com o cabeçalho ou então com menos produtos do que efetivamente teria.

Criando a janela mestre-detalhe com cache

Para resolver a questão elencada anteriormente, serão apresentados na sequência os recursos de cache updates, os quais permitirão que os dados sejam salvos localmente por padrão e enviados em lote para o servidor somente após um comando realizado pelo usuário. Esse recurso é muito semelhante ao utilizado em aplicações dbExpress, as quais utilizam TClientDataSet e TDataSetProvider, porém aqui serão utilizados os recursos da engine FireDAC para acesso a dados.

Para podermos trabalhar com dados em cache, na query mestre qryVendas a única propriedade que precisa ser configurada é a CachedUpdates, que passa a conter o valor True. De forma similar, na query detalhe (qryVendaProdutos) essa mesma propriedade deve ser mudada, além de setar para True a propriedade FetchOptions > DetailCascade e preencher IndexFieldNames com o valor idvenda, que equivale ao campo de chave estrangeira. A propriedade DetailCascade, por sua vez, controla a propagação de um conjunto de dados mestre para um conjunto de dados de detalhe, devendo ser utilizada quando está sendo aplicado o modo de atualização de cache centralizado, que será visto adiante com o componente TFDSchemaAdapter.

A Figura 3 apresenta a inserção de uma venda e dois produtos, na qual é possível observar que o campo autoincremento utilizado tanto no mestre quanto no detalhe é uma chave primária temporária com o valor -1, o que indica que os dados ainda não foram gravados. Para efeitos de comparação, observe novamente que na Figura 2 os registros detalhes receberam o valor 1004, que diz respeito ao campo autoincremento gravado antes dos detalhes serem inseridos.

Inserção com cache updates
Figura 3. Inserção com cache updates

Desta forma, é necessário fazer uso do comando ApplyUpdates para que os três novos registros da Figura 3 sejam gravados em conjunto na base de dados em um único lote. Os componentes do tipo TFDQuery disponibilizam esse método, porém utilizaremos o TFDSchemaAdapter (adapter) (BOX 1) localizado no Data Module pelo fato de ele facilitar o gerenciamento da gravação. Para isso, as duas queries (qryVendas e qryVendaProdutos) devem ter sua propriedade SchemaAdapter configuradas para o componente correspondente: adapter.

BOX 1: O componente TFDSchemaAdapter é utilizado para gerenciar a gravação centralizada quando o recurso de cache updates é utilizado. Esse recurso oferece a facilidade de utilizar somente um comando ApplyUpdates quando vários DataSets estão presentes na aplicação, servindo como um armazenamento central para os registros e suas alterações em vários conjuntos de dados.

Depois dos registros terem sido gravados é necessário fazer uma limpeza no cache — o código apresentado na Listagem 3 apresenta a definição de um procedimento que realiza essa tarefa. Na linha 02 foi definida a assinatura do método na seção private da unit ConexaoDtm, sendo que para ter sua implementação gerada deve-se pressionar o conjunto de teclas CTRL + SHIFT + C. Nas linhas 06 e 07 é executado o método CommitUpdates para as duas queries, que tem a função de limpar o log de alterações e marcar todos os registros com o status não modificado. Esse método é comumente invocado depois de uma chamada com sucesso ao ApplyUpdates.

  01 private
  02   procedure LimpaCache(Sender: TObject);
  03 public
  04 procedure TdtmConexao.LimpaCache(Sender: TObject);
  05 begin
  06   qryVendas.CommitUpdates;
  07   qryVendaProdutos.CommitUpdates;
  08 end;
Listagem 3. Método para limpar o cache das queries

A Listagem 4 apresenta a codificação que deve ser feita no evento OnCreate do Data Module. Na linha 03 o evento AfterApplyUpdate do SchemaAdapter recebe o método LimpaCache, o que garante que toda vez que uma gravação acontecer o cache será limpo logo após (caso essa limpeza do cache não seja efetuada, a cada clique no botão gravar os registros serão duplicados). Adicionalmente, nas linhas 04 e 05 as duas queries são abertas para que não seja necessário manter a propriedade Active = True em modo de design.

  01 procedure TdtmConexao.DataModuleCreate(Sender: TObject);
  02 begin
  03   adapter.AfterApplyUpdate := LimpaCache;
  04   qryVendas.Open();
  05   qryVendaProdutos.Open();
  06 end;
Listagem 4. Evento OnCreate do Data Module

Por fim, a Listagem 5 apresenta o código que deve ser digitado no evento OnClick do botão gravar (btnGravar) do formulário principal. Na linha 03 é invocado o método ApplyUpdates do adapter, gravando tanto os registros da tabela mestre quanto da tabela detalhe utilizando uma única linha de código. Conforme mencionado anteriormente, o uso do componente SchemaAdapter centraliza a gravação de registros de várias tabelas, fato que facilita o controle de erros e principalmente evita que alguns detalhes sejam inseridos e outros não.

Ainda na linha 03, o valor 0 indica que nenhum erro pode ocorrer em toda a gravação, ou seja, caso estejamos incluindo uma venda com dez produtos e ocorra erro somente em um deles, toda a operação será cancelada. Esse recurso garante a integridade dos dados se levarmos em consideração o exemplo da gravação de uma nota fiscal completa explanado anteriormente.

  01 procedure TfrmPrincipal.btnGravarClick(Sender: TObject);
  02 begin
  03   dtmConexao.adapter.ApplyUpdates(0);
  04 end;
Listagem 5. Gravação dos dados com SchemaAdapter e ApplyUpdates

Para uma melhor visualização e entendimento de como o cache no FireDAC funciona, vamos agora fazer uso do componente TFDMemTable. Esse componente será responsável por armazenar e exibir na terceira grade do formulário (grdCache) todas as modificações feitas em memória na tabela mestre antes de serem aplicadas na base de dados.

Para isso, a Listagem 6 apresenta o código que deve ser implementado no evento FormActivate do formulário principal. Na linha 03 é executado o método CloneCursor do memory table, passando como parâmetro a query mestre (qryVendas). Em resumo, esse código cria um clone do cursor da qryVendas para o memCache, de modo que o novo cursor aponte para os mesmos registros do cursor da qryVendas.

Quando esse método é invocado todos os registros serão exibidos na tabela em memória, porém o que nos interessa neste momento é a visualização somente de alterações, inserções e exclusões no cache. Desta forma, na linha 04 esse filtro é feito por meio da propriedade FilterChanges. O comando rtModified é responsável pelos registros alterados após a busca ou após a última operação de um CommitUpdates ou CancelUpdates. Já o rtInserted mostra os novos registros, ainda não lançados no banco de dados e adicionados depois que o conjunto de dados foi aberto ou após a última operação CommitUpdates. O comando rtDeleted mostra os registros que foram excluídos na aplicação, mas não do banco de dados. Ainda é possível utilizar o rtUnmodified, que, por sua vez, mostra os registros inalterados, e também o rtHasErrors, cuja função é verificar os registros que possuem erros associados após as operações de ApplyUpdates.

  01 procedure TfrmPrincipal.FormActivate(Sender: TObject);
  02 begin
  03   dtmConexao.memCache.CloneCursor(dtmConexao.qryVendas, true);
  04   dtmConexao.memCache.FilterChanges := [rtModified, rtInserted, rtDeleted];
  05 end;
Listagem 6. Evento FormActivate do formulário principal
Nota: O componente TFDMemTable é um DataSet em memória e é utilizado para gerenciar registros no lado cliente da aplicação e também para trocar dados com uma base de dados. Tem funcionalidades semelhantes ao TClientDataSet, mas segue o modelo Cache Updates. Ou seja, quando o valor da propriedade CachedUpdates for True, o TFDMemTable irá gravar as alterações no log de atualizações. Para que essas atualizações sejam gravadas no banco de dados é necessário utilizar o comando ApplyUpdates e, em seguida, para mesclar o log de alterações, chamar o comando CommitUpdates. Esse componente é comumente utilizado em aplicações móveis que empregam a tecnologia DataSnap.

O objetivo é que a tabela em memória exiba um status sobre que tipo de operação ocorreu em cada registro, ou seja, se foi uma alteração, inserção ou exclusão. Essa informação ficará armazenada em um campo calculado. Para criá-lo, deve-se acessar o Fields Editor da tabela em memória (memCache) clicando com o botão direito no componente e depois escolher a opção New Field (botão direito também). A Figura 4 apresenta a janela correspondente, na qual foi definido o campo status, do tipo String, com tamanho 20 e do tipo calculado (Calculated). Note que também estão presentes os campos idvenda, idcliente e data_venda, que devem ser copiados do Fields Editor da qryVendas e colados no Fields Editor da tabela em memória.

Criação de campo calculado
Figura 4. Criação de campo calculado

Um último passo para que possamos visualizar as alterações de cache é programar o evento CalcFields da tabela em memória (memCache) conforme o mostrado na Listagem 7. Na linha 03 é verificado se o status de atualização do DataSet é de alteração (usModified). Caso positivo o campo status recém-criado recebe o valor Modificado, que será exibido no formulário. O mesmo processo segue no restante do código, ou seja, na linha 05 é feita a verificação de inserções e na linha 07 de exclusões.

  01 procedure TdtmConexao.memCacheCalcFields(DataSet: TDataSet);
  02 begin
  03   if DataSet.UpdateStatus = usModified then
  04     DataSet.FieldByName('status').AsString := 'Modificado'
  05   else if DataSet.UpdateStatus = usInserted then
  06     DataSet.FieldByName('status').AsString := 'Inserido'
  07   else if DataSet.UpdateStatus = usDeleted then
  08     DataSet.FieldByName('status').AsString := 'Excluído';
  09 end;
Listagem 7. Evento CalcFields para exibir o status

A Figura 5 apresenta uma simulação do armazenamento do cache na tabela em memória, podendo ser visto na parte inferior da figura. O primeiro registro (idvenda = 1) teve a data alterada e, por isso, foi mostrado com o valor Modificado no status, enquanto que o segundo (idvenda = 2) foi apagado e seu status mostra o valor Excluído (a venda recém-excluída é mostrada somente na grade inferior). Por fim, um novo registro foi inserido na grade superior (idvenda = -1) e é exibido com o status Inserido. Com essa funcionalidade, é possível observar e controlar em tempo real todas as operações que são executadas em memória para que, assim, tratamentos ou correções específicas possam ser realizadas quando necessário antes dos dados serem efetivamente gravados.

Status dos registros
Figura 5. Status dos registros

Para apresentar um controle mais detalhado dos erros que podem ocorrer no processo de gravação, a Listagem 8 apresenta o código que deve ser adicionado no evento OnClick do botão btnGravar (em substituição da codificação apresentada na Listagem 5). Na linha 03 é declarada a variável AErros, que tem a função de armazenar a quantidade de erros que ocorreram no processo de gravação dos dados (linha 05). Note que foi utilizado o valor -1 na linha 05, que indica que o FireDAC deve tentar gravar todos os registros que conseguir, independentemente de serem gerados erros, enquanto que o uso do valor 0 na Listagem 5 quer dizer que nenhum erro é permitido. Vale citar também que se for passado como parâmetro um inteiro positivo, o número de erros permitido será igual a esse parâmetro. Na linha 06 é verificado se a quantidade de erros é maior do que zero, e caso positivo o filtro da tabela em memória é alterado para o valor rtHasErrors (linha 07), ou seja, serão mostrados na última grade os registros que foram recusados pelo servidor e que devem ser revisados pelo usuário. Por fim, na linha 08 o cache da query de vendas é limpo.

  01 procedure TfrmPrincipal.btnGravarClick(Sender: TObject);
  02 var
  03   AErros: Integer;
  04 begin
  05   AErros := dtmConexao.adapter.ApplyUpdates(-1);
  06   if AErros > 0 then
  07     dtmConexao.memCache.FilterChanges := [rtHasErrors];
  08   dtmConexao.qryVendas.CommitUpdates;
  09 end;
Listagem 8. Gravação dos dados com tratamento de erros

Outras funcionalidades

Nesta seção veremos outros recursos bastante úteis quando trabalhamos com cache, que são: visualização dos valores modificados, reversão de alterações, cancelamento de todas as modificações e uso de save points. Cada uma dessas funções corresponde a um botão do formulário.

A primeira função que será abordada é a visualização dos novos e antigos valores de determinados campos. A Listagem 9 apresenta o código que deve ser implementado no evento OnClick do botão btnVerAlteracoes. Conforme pode ser observado na linha 03, o valor antigo é dado pela propriedade OldValue, enquanto que o valor que o usuário modificou é dado pela propriedade Value.

É importante frisar que esse código está mostrando alterações somente no campo idcliente. Para testá-lo, basta trocar o valor desse campo, manter o registro selecionado e clicar no botão correspondente. Esse recurso pode ser interessante em aplicações nas quais é necessário realizar validações em registros que estão sendo alterados, por exemplo em um controle de estoque. Nesse cenário, uma validação pode ser executada para verificar se a nova quantidade informada pelo usuário está de acordo com a quantidade de produtos existentes no estoque, assim evitando que seja vendida uma quantidade maior do que a efetivamente disponível. De forma similar, caso o usuário diminua a quantidade, pode-se incrementar os valores no estoque.

  01 procedure TfrmPrincipal.btnVerAlteracoesClick(Sender: TObject);
  02 begin
  03   ShowMessage('Valor antigo: ' 
       + IntToStr(dtmConexao.qryVendas.FieldByName('idcliente').OldValue) +
      ' Valor novo: ' 
      + IntToStr(dtmConexao.qryVendas.FieldByName('idcliente').Value));
  04 end;
Listagem 9. Visualização dos valores modificados

Entre as linhas 01 e 04 da Listagem 10 encontra-se o código que deve ser implementado no evento OnClick do botão btnUltimaAlteracao, enquanto que entre as linhas 05 e 08 está o código que corresponde ao evento OnClick do botão btnReverter. O método UndoLastChange da linha 03, como o próprio nome sugere, tem o objetivo de reverter as últimas alterações realizadas. Supondo-se que três registros sejam alterados, se o botão for clicado três vezes cada um dos cliques reverterá os dados somente de um único registro, muito semelhante ao comando desfazer CTRL + Z comumente utilizado em processadores de texto, por exemplo. Já o método RevertRecord (linha 07) desfaz as alterações somente no último registro modificado, ou seja, se três registros forem alterados somente o último voltará aos valores originais. Podemos resumir a diferença entre os dois comandos da seguinte forma: o primeiro permite a restauração de vários registros (um por clique), enquanto que o segundo possibilita a restauração somente da última modificação.

  01 procedure TfrmPrincipal.btnUltimaAlteracaoClick(Sender: TObject);
  02 begin
  03   dtmConexao.qryVendas.UndoLastChange(True);
  04 end;
  05 procedure TfrmPrincipal.btnReverterClick(Sender: TObject);
  06 begin
  07   dtmConexao.qryVendas.RevertRecord;
  08 end;
Listagem 10. Comandos UndoLastChange e RevertRecord

Outro comando utilizado para cancelar alterações é o CancelUpdates, que restaura as modificações em todos os registros que ainda não foram gravados, diferentemente dos dois métodos anteriores, que fazem a restauração somente de um registro por vez. A Listagem 11 apresenta o código correspondente (linha 03), que deve ser implementado no evento OnClick do botão btnCancelar. Essa opção é particularmente útil quando várias alterações estão sendo feitas nos itens de uma nota fiscal, por exemplo, e, por alguma razão, todas as alterações devem ser canceladas ao mesmo tempo.

  01 procedure TfrmPrincipal.btnCancelarClick(Sender: TObject);
  02 begin
  03   dtmConexao.qryVendas.CancelUpdates;
  04 end;
Listagem 11. Cancelamento de todas as alterações

O último exemplo a ser abordado sobre as funcionalidades que podem ser implementadas com cache updates é a utilização de save points, que são pontos de “marcação” que o usuário pode escolher durante a manipulação dos registros, podendo restaurar todos os dados a partir do ponto marcado. Por exemplo, se um usuário cria um save point e faz a modificação de cinco registros, no momento em que pedir para restaurar o save point o FireDAC restaurará exatamente esses cinco registros que foram “marcados” no início.

Esse recurso é útil em situações nas quais o usuário necessita manipular registros, mas não há certeza sobre os novos dados informados. Desta forma, o usuário poderá adicionar um save point quando iniciar as alterações e, caso algum erro aconteça, basta restaurar a partir do ponto marcado. A Listagem 12 apresenta a codificação necessária para implementar esse recurso, sendo que na linha 02 é declarada a variável global FSavePoint, na seção private do formulário, a qual será utilizada para realizar o controle do ponto inicial.

Na linha 05 encontra-se o código para a criação do save point, que deve ser programado no evento OnClick do botão btnCriarSavePoint(linha 03). Similarmente, na linha 09 está o código responsável pela restauração dos dados, que deve ser implementado no evento OnClick do botão btnRestaurarSavePoint. Conforme pode ser observado na listagem, o código é bastante simples e são utilizadas somente duas linhas: na linha 05, a variável FSavePoint recebe a propriedade SavePoint da query para realizar a marcação. Por outro lado, na linha 09 ocorre o inverso, ou seja, a propriedade SavePoint recebe o valor da variável inteira, o que indica que os dados devem ser restaurados para o momento em que o save point foi criado.

  01 private
  02   FSavePoint: Integer;
  03 procedure TfrmPrincipal.btnCriarSavePointClick(Sender: TObject);
  04 begin
  05   FSavePoint := dtmConexao.qryVendas.SavePoint
  06 end;
  07 procedure TfrmPrincipal.btnRestaurarSavePointClick(Sender: TObject);
  08 begin
  09   dtmConexao.qryVendas.SavePoint := FSavePoint;
  10 end;
Listagem 12. Criação e restauração de save point

Com isso, este aplicativo mestre-detalhe já está funcional, e com vários recursos importantes e comumente utilizados em aplicações comerciais implementados. Conforme pudemos observar, a utilização dos recursos de cache em aplicações mestre-detalhe agrega uma série de recursos adicionais e formas diferenciadas de controle dos dados.

Muitos desenvolvedores, principalmente os que estão habituados com a engine dbExpress de acesso a dados, usualmente têm dúvida de como utilizar o FireDAC com recursos de cache semelhantes ao que já é bem conhecido da comunidade Delphi com a utilização de TClientDataSet em conjunto com o TDataSetProvider. Conforme visto neste artigo, o próprio componente TFDQuery já possui todos os recursos necessários para a construção deste tipo de sistema.

Além do conteúdo abordado nos tópicos anteriores, algumas melhorias neste aplicativo são necessárias, como o controle mais apurado de erros e, principalmente, a gerência da concorrência para evitar que dados desatualizados sejam sobrescritos. Esse último recurso pode ser implementado por meio de um status nos registros, de modo que esse campo possa ser utilizado para gerenciar se o registro pode ou não ser aberto caso algum usuário já esteja realizando modificações.

Outra dúvida bastante comum é como trabalhar com atualizações utilizando comandos “SQL complexos”, que são aqueles que apresentam muitos joins com outras tabelas. Para isso, uma alternativa é a utilização do componente TFDUpdateSQL, no qual podem ser definidas instruções SQL personalizadas. Por fim, um último comentário: caso sejam necessárias mais tabelas de detalhe na aplicação, o mesmo procedimento deve ser seguido, ou seja, com o FireDAC é possível trabalhar com múltiplas tabelas mestre-detalhe.