Uso pleno do TClientDataSet e o Modelo de Maleta

Categoria: Artigos
Categoria Pai: Addicted 2 Delphi!
Acessos: 80657
Imagem meramente ilustrativa

Este artigo está sendo organizado de forma diferente dos demais artigos do meu site. Ele foi dividido em páginas, já que serão abordados três assuntos. A intenção com isso é facilitar a busca de informações, fornecendo links diretos para os três assuntos dentro do mesmo artigo. Vou utilizar este modo de organização em outras publicações, pois achei bem interessante. Chega de conversa fiada e vamos ao que interessa. Primeiramente gostaria de falar a respeito do autor original do artigo.

Robert E. Swart é MVP da Embarcadero e vencedor do prêmio Spirit Of Delphi de 1999 (junto com Marco Cantù). Ele é autor, instrutor, revendedor, desenvolvedor, solucionador de problemas, consultor e webmaster. O artigo original encontra-se em http://www.drbob42.com/examines/examin64.htm.

O artigo foi escrito em 2004, por isso algumas tecnologias citadas são hoje obsoletas, entretanto, o assunto de uma forma geral é indispensável para o entendimento pleno do TClientDataSet, partindo do uso mais básico até culminar com o seu uso de forma plena no DataSnap, através da habilitação do briefcase model (modelo de maleta).

Gostaria de salientar que, a fim de explicar algumas coisas, eu utilizei notas de rodapé. Recomendo fortemente a leitura dessas notas, as quais são identificadas por números entre colchetes, assim: [1]. As explicações incluem aprofundamento das informações e ajuda a respeito de possíveis mudanças de tecnologia e localização de menus no Delphi.

Abaixo segue a versão brasileira.


Neste artigo, cobriremos o uso do componente TClientDataSet em três situações: usando-o com o formato stand-alone MyBase, usando-o com o dbExpress, e, finalmente, usando-o como "maleta" no lado do cliente em aplicações DataSnap. Vou usar Delphi 7, bem como Delphi 8 (para .NET) e Delphi 2005 para ilustrar o uso do TClientDataSet em aplicações VCL (para .NET). No entanto, técnicas semelhantes podem ser aplicadas no Kylix (no Linux) e C ++ Builder[1].


O TClientDataSet stand-alone (MyBase)

O TClientDataSet stand-alone é a solução perfeita para situações em que são necessários executáveis autônomos, sem a necessidade de instalar mecanismos de banco de dados adicionais ou drivers. Como o TClientDataSet carrega todos os dados na memória ele é muito rápido, mas o tamanho das tabelas é limitado à quantidade de memória disponível (e o tempo de carga/persistência de dados também aumenta correspondentemente). Além disso, não podemos executar consultas SQL, mas filtros e outras operações podem ser realizadas.

Por que um TClientDataSet local?

Os benefícios da utilização do TClientdataSet como um DataSet local sobre, por exemplo, o BDE são numerosos. Em primeiro lugar, o TClientdataSet está contido dentro de uma única DLL chamada MIDAS.DLL. Apenas soltá-la dentro do diretório Windows\System[2] (ou deixá-la no mesmo diretório que o executável Delphi) e você está apto a usar o componente. A partir do Delphi 6, você pode até mesmo adicionar a unit MidasLib à cláusula uses do arquivo .dpr do seu projeto. Ao fazer isso, toda a biblioteca MIDAS.DLL será incorporada dentro do seu executável (que irá crescer cerca de 200K), resultando em um verdadeiro executável autônomo de configuração-zero! Compare isto com a instalação do BDE no seu computador cliente. E mesmo que você decida usar ado[3], por exemplo, que provavelmente já estará disponível na máquina do seu cliente, você precisará garantir que o banco de dados a acessar (Access, SQL Server, etc.) também esteja presente, funcional e com as bibliotecas de acesso (client libraries) disponíveis. Em suma, o MIDAS.DLL é provavelmente um dos "bancos de dados" mais fáceis de instalar que você já viu até então!

O TClientDataSet é também uma das implementações de DataSet mais rápidas que existem. Classificação e filtragem são feitas com uma velocidade impressionante. Com toda essa velocidade então ele é um componente perfeito, não é? Não é bem assim! Essa velocidade toda tem um custo alto e é também uma das desvantagens potenciais do TClientDataSet. Tudo é gerenciado na memória, e cada operação, como classificação, filtragem ou busca também é feito na memória. Isso explica a velocidade, mas também significa que um TClientDataSet com uma grande quantidade de dados exige uma grande quantidade de memória em sua máquina.


Alimentando um TClientDataSet local...

A maneira mais fácil de carregar um TClientDataSet em tempo de design, é clicar com o botão direito no componente TClientDataSet e selecionar a opção do menu pop-up "Assign local data". Isto irá mostrar uma caixa de diálogo que lista todos os conjuntos de dados (tabelas, consultas, etc.) disponíveis no TForm atual ou em um TDataModule associado. Ao escolher um item nessa lista e pressionar OK, todos os dados do conjunto de dados escolhido serão atribuídos ao TClientDataSet e você poderá então remover o conjunto de dados fonte do TForm ou TDataModule, ficando apenas com um TClientDataSet stand-alone que contém todos os dados do conjunto de dados de origem. Note que você ainda precisará remover a unit DBTables caso você esteja usando o BDE e quiser fazer sua aplicação totalmente independente do BDE.

Além de usar a opção "Assign Local Data", um TClientDataSet também pode carregar e armazenar suas informações no disco. Isto é visível em tempo de design pelas opções "Load From File" e "Save To File" do menu pop-up[4]. O componente TClientDataSet em si contém também estes métodos acessíveis em tempo de execução, bem como os métodos LoadFromStream e SaveToStream, que você pode redirecionar a diferentes tipos de fluxos (como um TMemoryStream, imediatamente antes de enviar esse fluxo através de uma conexão socket, por exemplo).

O TClientDataSet pode carregar e armazenar dois tipos de formato de dados. O primeiro é normalmente chamado formato "cds", e é o formato binário interno (e não documentado). Pequeno, nativo e quase impossível de compartilhar (exceto com outros componentes TClientDataSet de Delphi 5 e superior, C ++ Builder 5 e superiores, e Kylix). O segundo formato de dados que o TClientDataSet suporta é o XML, e todos nós sabemos que arquivos XML são abertos, independentes de plataforma e portáteis, no entanto, o formato XML que é usado pelo TClientDataSet ainda é um formato proprietário definido originalmente pela Borland, por isso não é fácil usá-lo com um conjunto de dados ADO, por exemplo.

Usando um TClientDataSet local...

Uma vez que um TClientDataSet local é preenchido com dados, nós podemos navegar por ele como qualquer outro conjunto de dados, no entanto há uma diferença que pode ser um benefício ou uma maldição (se você não estiver ciente disso). A principal diferença entre o TClientDataSet e outros conjuntos de dados tradicionais (TTable ou similares) é o fato de o TClientDataSet não salvar automaticamente o seu conteúdo em disco. E mesmo se isso acontecer, ele só salva as alterações que tiverem sido realizadas e não o conjunto de dados completos (dados antigos mais dados alterados).

O que isso significa exatamente, e como podemos fazer melhor uso desta funcionalidade? Primeiro de tudo, vamos dar uma olhada nas capacidades de salvamento do TClientDataSet. Se os dados dentro do TClientDataSet são carregados por quaisquer outros meios que não a propriedade FileName do TClientDataSet, então ele, obviamente, não sabe onde guardar os dados que tenham sido modificados (os dados originais devem ter sido carregados a partir do arquivo .dfm). Se a propriedade FileName for usada, então o TClientDataSet vai salvar o seu conteúdo de volta para o arquivo quando ele for explicitamente desativado (close) (ou destruído). No entanto, em algumas situações, pode ser uma boa ideia chamar explicitamente o método SaveToFile (assim como você teria de usar LoadFromFile para carregar os novos dados de volta).

Agora, se você usar LoadFromFile no início da sua aplicação, e SaveToFile no final da mesma, então você vai perceber que o arquivo externo (com o conteúdo do TClientDataset) cresce a uma taxa mais elevada do que seria esperado de poucas mudanças e possíveis adições que você faça no TClientDataSet. Algo está acontecendo dentro desse arquivo e se você o salvar no formato XML, então você descobrirá rapidamente que todas as alterações individuais são salvas, tanto com o valor "original" como com o valor "alterado" de cada registro. Isto significa que mesmo poucas mudanças vão encher o banco de dados rapidamente com várias versões de registros com alterações, que ocupam mais espaço do que as próprias alterações em si (ou as alterações aplicadas ao conjunto de dados).

Por que isso é feito? Basicamente, para permitir que o TClientDataSet trabalhe em um ambiente multi-tier (e multi-utilizador). Em um ambiente multi-camadas, o TClientDataSet precisa chamar o método ApplyUpdates, o qual envia as atualizações pendentes para a camada de conjunto de dados remoto (middleware) e isto tem de incluir tanto a versão original dos registros, como as alterações que serão aplicadas. Se a versão original de um registro não coincide com a versão atual do registro (persistido em banco de dados), a atualização pode não ser aplicada. Este é um problema típico de ambientes multi-usuários que precisa ser resolvido para evitar problemas de integridade de dados, mas ao usar o TClientDataSet como uma solução stand-alone este comportamento é muito indesejado.

E fica ainda pior quando você percebe que o TClientDataSet contém o valor original de todos os registros, bem como todas as alterações, o que significa que quando você carregar o arquivo externo, terá que voltar a aplicar todas as alterações e isto levará tempo, tornando a carga mais e mais lenta com o tempo, à medida que mais e mais mudanças são aplicadas.

MegeChangeLog

Felizmente existe um método especial chamado MergeChangeLog, o qual faz, tal como o nome sugere, a mesclagem de todas as alterações contidas no TClientDataSet, o que resulta em um arquivo bem pequeno novamente (com apenas os registros e todas as mudanças efetivas aplicadas neles). Obviamente este método nunca deve ser executado em um ambiente multi-camadas, pois ele vai quebrar todas as possíveis chamadas ao método ApplyUpdates que você queira fazer. Contudo, em um ambiente de camada única local, este método é estritamente necessário, pois ele diminui o tamanho (e acelera o carregamento) da representação local do TClientDataSet.

Antes de executar o método MergeChangeLog, é recomendável verificar o valor de ChangeCount, que representa o número de alterações que estão atualmente disponíveis no TClientDataSet. Apenas se ChangeCount for maior que zero é que você deve executar o método MergeChangeLog, do contrário isso seria apenas perda de tempo.

Como uma última dica, você pode querer considerar o fato de que um log de modificações inclui a habilidade para "desfazer" alterações registro a registro usando os métodos UndoLastChange, CancelUpdates ou RevertRecord. Leia a ajuda do Delphi para maiores detalhes. Estes métodos podem também ser usados em um ambiente multi-camadas, claro, antes de se executar um ApplyUpdates.


O TClientDataSet e o dbExpress

Quando combinado com o dbExpress (DBX), o TClientDataSet pode ser visto como um "cache" (ou maleta), no qual os cursores unidirecionais podem armazenar seus dados, assim os usuários podem visualizá-los como um conjunto de registros bidirecionais. Isto habilita capacidades SQL, pois é o DBX quem está fornecendo os dados ao TClientDataSet. Nós não precisamos mais nos preocupar com armazenamento local, apenas precisamos executar ApplyUpdates para enviar as alterações ao banco de dados no qual o DBX está conectado.

O que é o dbExpress?

O DBX é uma arquitetura de acesso a dados multi-plataformas, leve, rápida e aberta. Um driver DBX precisa implementar algumas interfaces para obter metadados, executar consultas SQL ou stored procedures, e retornar um cursor unidirecional. Voltaremos a falar disso em breve.


Personalizando o dbExpress

Como dito anteriormente o DBX foi criado como uma arquitetura de acesso a dados aberta, significando que qualquer um pode escrever um driver compatível (ou seria compilante?) com o DBX para uso com o Kylix, Delphi ou C++ Builder. Um artigo sobre o funcionamento intrínseco do DBX, escrito por Ramesh Theivendran, o arquiteto do DBX, foi publicado no site Borland Community. Apesar de este artigo ser apenas um esboço preliminar, ele deixou claro que qualquer um pode escrever um driver para o dbExpress.

Como um exemplo prático, a EasySoft desenvolveu o "dbExpress Gateway for ODBC", que pode ser usado para conectar ao ODBC do UNIX e, via "ponte ODBC-ODBC", ele também é capaz de conectar-se a um SQL Server remoto, a um banco de dados Access ou mesmo outros drivers ODBC do Windows.

Componentes

A paleta dbExpress contém sete componentes: TSQLConnection, TSQLDataSet, TSQLQuery, TSQLStoredProc, TSQLTable, TSQLMonitor e TSQLClientDataSet

TSQLConnection

O componente TSQLConnection é literalmente a conexão entre os drivers do DBX e os outros componentes da paleta DBX. Ele é o meio de ligação entre a aplicação e drivers que possibilitam o acesso aos bancos de dados. Se você soltar este componente em um TForm ou TDataModule, você verá apenas 12 propriedades e aquela que é provavelmente a mais usada é a propriedade ConnectionName, que pode ser atribuída a um dos valores da lista apresentada. Se você selecionar IBConnection então a propriedade DriverName terá o valor "INTERBASE", a propriedade GetDriverFunc terá o valor "getSQLDriverINTERBASE" a propriedade LibraryName terá o valor "dbxint30.dll" e a propriedade VendorLib terá o valor "gds32.dll". Tudo foi automaticamente selecionado baseado no valor IBConnection, selecionado em ConnectionName.

Você pode abrir a propriedade Params e editar os valores dos parâmetros. Estes são também automaticamente preenchidos, aliás, quando você seleciona um valor para a propriedade ConnectionName. Se você não quiser que isso aconteça, por exemplo, quando estiver escrevendo código não visual de acesso a banco de dados e você quiser informar seus próprios valores para os parâmetros, você pode configurar a propriedade LoadParamsOnConnection como false.

Se você der duplo clique no componente TSQLConnection, você verá as configurações de conexão para cada valor possível da propriedade ConnectionName.

Nesta tela também é possível alterar as configurações atuais e criar novas configurações, de forma que seja possível selecioná-la na propriedade ConnectionName.

Uma vez que tudo tenha sido configurado de forma adequada, você pode configurar a pripriedade Connected como true e, ou obter uma mensagem de erro de conexão, ou ver a propriedade simplesmente mudando para true, o que indicaria sucesso na conexão.


TSQLDataSet

Uma vez que o componente TSQLConnection esteja conectado, você poderá usar qualquer um dos outros componentes DBX disponíveis, como o TSQLDataSet, que é o mais "geral" destes componentes. Sempre comece por configurar a propriedade SQLConnection deste componente para um TSQLConnection disponível. Os componentes TSQLQuery, TSQLStoredProc e TSQLTable podem ser vistos como instâncias especiais de TSQLDataSet. De fato, isso me lembra muito o ADOExpress (dbGo), no qual o componente TADODataSet é o "pai" de TADOQuery, TADOStoredProc e TADOTable. Na verdade o núcleo dos componentes TxxxDataSet de ambos, dbGo e DBX, compartilham as propriedades CommandType e CommandText, com as quais você pode determinar o subtipo do componente. Se você configurar o valor de CommandType para ctQuery, então a propriedade CommandText é interpretada como uma consulta SQL. Se você configurar o valor de CommandType para ctStoredProc, então a propriedade CommandText especifica o nome de um stored procedure e, finalmente, se CommandType for configurado como ctTable, então CommandText deve conter o nome de uma tabela.

Em nosso caso, usando o componente geral TSQLDataSet, nós podemos configurar CommandType como ctTable e CommandText como "customer" e com isso selecionar a tabela "customer". Se você configurar a propriedade Active como true, você obterá os dados atuais em tempo de projeto e, se você configurar a propriedade LoginPrompt do componente TSQLConnection como false, você nem mesmo verá a tela de login. Esse é um comportamento padrão de todos os componentes de acesso a dados do Delphi. Nada de especial, nada diferente. Ainda...

Unidirecional!?

Agora nós podemos direcionar o foco para a paleta Data Controls e começar a usar um de seus componentes para exibir os dados que recebemos do TSQLDataSet ativo. Note que nós não podemos usar todos estes componentes agora sem algumas considerações especiais. Este é o ponto onde a maior diferença entre as arquiteturas do BDE e do DBX está presente. O componente TSQLDataSet e seus componentes derivados retornam um cursor unidirecional. Isso significa que você pode mover-se apenas para frente e nunca para trás e isso não é nada útil quando pretendemos usar, por exemplo, um TDBGrid (nós vemos apenas um registro de cada vez!) e muito menos quando usamos um TDBNavigator, porque clicando nos botões Back ou First irá invariavelmente levantar uma exceção!

Por que um cursor unidirecional? Bem, a resposta óbvia é velocidade. O BDE nunca foi nosso melhor amigo (podemos considerá-lo um bom amigo, ou um conhecido amigável), mas ele nos ajudou com necessidades simples e pequenas dos bancos de dados. Infelizmente o ovehead e os requisitos gerais de memória do BDE não são pequenos e as tabelas do BDE nunca foram conhecidos por suas "velocidades incríveis". Esta era uma área onde a Borland procurou mostrar algumas melhorias reais. A nova arquitetura chamada de dbExpress foi desenhada com isso em mente e, consequentemente, os cursores unidirecionais como Result Sets, sem o ovehead causado pelo gerenciamento de metadados ou armazenamento em cache dos dados.

Um cursor unidirecional é especialmente útil quando você apenas precisa ver os resultados uma vez, ou precisa percorrer os resultados do início ao fim (novamente, uma só vez), por exemplo, em um loop while not Eof, ao processar os resultados de uma query ou stored procedure. Situações do mundo real onde isso é útil incluem a geração de relatórios e aplicações para servidores web que produzem dinamicamente páginas html como resultado.

Mas especialmente quando combinado com componentes visuais de acesso a dados, você rapidamente percebe que em ambientes gráficos os usuários certamente vão querer navegar em qualquer direção dentro de Result Sets. Então, você precisa, de alguma forma, colocar os registros em cache a fim de permitir sua exibição em um TDBGrid e também a navegação em qualquer direção. É aí onde o TClientDataSet entra em ação. É totalmente possível e pertinente usar um TDataSetProvider ligado ao TSQLDataSet e então usar um TClientDataSet para obter os dados do TDataSetProvider. O resultado é um TClientDataSet que obtém registros (uma vez) de uma fonte unidirecional, o TSQLDataSet. O TDataSetProvider é usado apenas como um meio local de transporte de dados.

Migração de tecnologia

O fato de o TClientDataSet estar disponível tanto no Delphi quanto no Kylix significa que existe uma forma simples e rápida de migrar tabelas de bancos de dados locais, como tabelas em Paradox ou dBASE. Este é o primeiro passo a ser feito para migrar do BDE para o DBX: migração de dados. O segundo passo é migrar a aplicação em si.

Para realizar o primeiro passo devemos migrar os dados do BDE para o formato nativo do TClientDataSet. Considere o código abaixo de uma aplicação chamada dbAlias que eu escrevi. Esta aplicação vai converter todas as tabelas acessadas por um alias (passado via parâmetro) para arquivos .cds:

{$APPTYPE CONSOLE}
program dbAlias;
uses
  Classes, SysUtils, DB, DBTables, Provider, DBClient;
var
  i: Integer;
  TableNames: TStringList;
  Table: TTable;
  DataSetProvider: TDataSetProvider;
  ClientDataSet: TClientDataSet;
begin
  TableNames := TStringList.Create;
  with TSession.Create(nil) do
  try
    AutoSessionName := True;
    GetTableNames(ParamStr(1), '', True, False, TableNames);
  finally
    Free
  end {TSession};
  Table := TTable.Create(nil);
  DataSetProvider := TDataSetProvider.Create(nil);
  ClientDataSet := TClientDataSet.Create(nil);
  try
    Table.DatabaseName := ParamStr(1);
    for i : =0 to Pred(TableNames.Count) do
    begin
      writeln(Table.TableName);
      Table.TableName := TableNames[i];
      Table.Open;
      DataSetProvider.DataSet := Table;
      ClientDataSet.SetProvider(DataSetProvider);
      ClientDataSet.Open;
      ClientDataSet.SaveToFile(ChangeFileExt(Table.TableName,'.cds'));
      ClientDataSet.Close;
      Table.Close
    end
  finally
    Table.Free
  end
end.

Esta é uma forma rápida e grosseira de converter[5] suas tabelas Paradox e dBASE referenciadas por um alias do BDE para arquivos cds, os quais podem ser carregados novamente por componentes TClientDataSet em uma outra aplicação. O uso que você fará desses dados depende obviamente do seu sistema.


Cursores unidirecionais e conjuntos de dados somente leitura?

Tanto o Delphi 6 como o Delphi 7 e o Kylix usam a nova camada de acesso a dados independente de plataforma chamada de dbExpress, mas como tirar o máximo proveito dessa nova tecnologia?

Vou mostrar agora que, ao usar o dbExpress, nós precisamos adotar uma nova forma de olhar os dados (especialmente na hora de salvá-los), porque o DBX dispõe apenas de datasets somente leitura com cursores unidirecionais, por isso, não há como confirmar nossas alterações automaticamente mediante um simples post em seus componentes.

Para ilustrar este ponto, e mostrar como proceder de forma correta, vamos construir uma aplicação usando o DBX. Primeiro inicialize um novo projeto Win32 de forma usual. Solte um componente TSQLConnection no TForm disponível e configure sua propriedade ConnectionName para IBCONNECTION (ou a de sua preferência). Você pode dar duplo clique no TSQLConnection para iniciar o editor de conexões e garantir que todas as propriedades do ConnectionName escolhido estejam corretas. Em seguida solte um componente TSQLTable e configure sua propriedade SQLConnection como SQLConnection1, que deve ser o nome do TSQLConnection. Selecione uma das tabelas disponíveis na propriedade TableName. Neste ponto nós temos um dataset somente leitura e unidirecional (o qual suporta movimentação de registros apenas um passo de cada vez para frente ou de volta ao início, mas nenhuma outra operação). Esse comportamento é excelente caso se pretenda usar um componente TDataSetTableProducer, onde precisamos andar para frente no resultset apenas uma vez, mas não é nada útil em outras situações.

TClientDataSet

Para permitir a exibição das informações contidas no TSQLTable (ou qualquer dataset DBX) precisamos armazená-los dentro de um TClientDataSet usando um TDataSetProvider como "conector". Então, solte no TForm um componente TDataSetProvider e um TClientDataSet, os quais se encontram na paleta Data Access. Atribua o componente TSQLtable à propridade DataSet do TDataSetProvider e então atribua o nome do TDataSetProvider à propriedade ProviderName do TClientDataSet. No momento em que você abrir o TClientDataSet (por exemplo, configurando sua propriedade Active como True) o conteúdo de TSQLTable será percorrido (apenas uma vez) e os registros serão providos ao TClientDataSet, o qual os armazenará daquele momento em diante. Nós podemos agora usar um TDataSource e (por exemplo) um TDBGrid para exibir o conteúdo disponível no TClientDataSet.

Manipulação de dados

O problema da manipulação de dados ocorre quando executamos a aplicação, fazemos algumas alterações em alguns campos e registros e saímos da mesma. Quando abrimos a aplicação novamente nós vemos apenas os valores antigos! O TClientDataSet é perfeito para colocar dados em cache, mas não é capaz de atualizar[6] o banco de dados para nós de forma automática.

Como o componente TSQLTable é somente leitura e não é capaz de manipular de forma tradicional um banco de dados (usando métodos insert, edit, delete e post), precisamos usar o TClientDataSet, o qual pode usar o TDataSetProvider para realizar estas operações diretamente. O TDataSetProvider, é capaz de gerar os comandos SQL de inserção, atualização e exclusão e enviá-los diretamente ao banco de dados, sem usar o métodos de alto nível, indisponíveis nos componentes do DBX.

Então, para que as operações de banco de dados sejam executadas, precisamos executar o método ApplyUpdates do TClientDataSet. Podemos colocar no TForm um TButton, configurar sua propriedade Caption para "Aplicar Alterações" e, dentro do manipulador do evento OnClick desse botão devemos escrever apenas uma linha de código:

procedure TForm1.Button1Click(Sender: TObject);
begin
  ClientDataSet1.ApplyUpdates(0)
end;

Este simples comando vai enviar todas as atualizações pendentes no TClientDataSet para o banco de dados acessado pelo DBX. Caso um erro aconteça no momento da execução dos comandos -- por exemplo, quando um registro alterado por nós já tiver sido alterado por outro usuário --, um erro conhecido como Reconcile (EReconcileError) será gerado e poderá ser manipulado no evento OnReconcileError do TClientDataSet. Neste evento o usuário poderá decidir como o erro será corrigido. Maiores detalhes de como manipular erros EReconcileError serão dados mais adiante na seção sobre DataSnap deste mesmo artigo.

Automatizando o ApplyUpdates

Apesar da solução apresentada funcionar, você não pode realisticamente esperar que seus clientes e usuários finais se lembrem de clicar no botão "Aplicar Alterações" quando eles quiserem simplesmente salvar seus trabalhos. Nós podemos facilitar a vida dos nossos usuários executando o método ApplyUpdates de forma automática no evento AfterPost do TClientDataSet:

procedure TForm1.ClientDataSet1AfterPost(DataSet: TDataSet);
begin
  (DataSet as TClientDataSet).ApplyUpdates(0)
end;

Apesar de você estar se sentindo bem com essa solução, ainda existem situações que precisamos considerar, por exemplo, ao excluir um registro não existe método Post, logo, o evento AfterPost não vai funcionar neste caso e por isso precisamos colocar também o mesmo código no evento AfterDelete.

Finalmente, quando você fecha sua aplicação imediatamente após sua última alteração, mas antes de executar um Post, por exemplo, se você alterou o valor em um TDBEdit, mas não confirmou sua ação (Post), então você pode querer que esta alteração seja confirmada também. Isso significa que você precisa executar novamente a mesma linha de código descrita anteriormente, nos eventos OnDestroy ou OnClose de seu TForm, TDataModule ou TFrame (ou qualquer outros contêiner que você esteja usando para seu TClientDataSet)


O papel do TClientDataSet no DataSnap

A terceira solução, na qual usaremos a maior parte de nosso tempo aqui, posiciona o TClientDataSet como uma "maleta" do lado do cliente em uma aplicação DataSnap multicamadas. Isso significa que nós podemos desconectar a aplicação cliente e armazenar os dados localmente em arquivo, no formato binário MyBase. Nós podemos, sempre que quisermos, recarregar esses dados e continuar o trabalho localmente até quando uma conexão com o servidor DataSnap for restabelecida, permitindo que nós possamos enviar nossas alterações de volta ao servidor, mediante o uso do método ApplyUpdates.

Vale salientar que nós não focaremos nos detalhes do lado do servidor da arquitetura DataSnap, nem nos protocolos de comunicação utilizados nesta arquitetura. O foco será o TClientDataSet e suas habilidades de conexão com provedores locais ou remotos e a aplicação de atualizações ao middleware.

Por fim, a aplicação de atualizações (ApplyUpdates) pode resultar em erros de reconciliação (Reconcile Errors), os quais normalmente acontecem quando um ou mais usuários realizaram alterações conflitantes em um mesmo campo de um mesmo registro. Veremos então como podemos detectar e responder a estes erros usando a caixa de diálogo padrão para erros de reconciliação fornecida junto com o Delphi.


Arquitetura de banco de dados multicamadas

Usando uma arquitetura de banco de dados multicamadas você pode particionar uma aplicação de forma que ela possa acessar um banco de dados sem precisar de bibliotecas ou programas adicionais nas máquinas locais. Esta arquitetura também permite que você centralize as regras de negócio e processos, além de distribuir o processamento através da rede. O DataSnap suporta uma tecnologia de 3 camadas, a qual em sua forma clássica consiste de:

O servidor de banco de dados pode ser qualquer um de sua preferência, InterBase, Postgres, Oracle, MySQL, SQL Server, dentre outros. O servidor de aplicação e o cliente magro podem ser desenvolvidos em Delphi, Kylix ou C++ Builder. É no servidor de aplicação onde se concentram as regras de negócio e todas as ferramentas necessárias para acessar e manipular os dados que estão no servidor de banco de dados. Os programas que serão utilizados pelos usuários não fazem nada além de mostrar-lhes os dados de forma que eles possam visualizá-los e editá-los.

O que é o DataSnap?

O DataSnap é baseado na tecnologia que permite que os conjuntos de dados sejam empacotados e enviados através da rede como parâmetros nas chamadas a métodos remotos (Remote Procedure Call - RPC). Ele inclui tecnologias que permitem converter um TDataSet em dados Variant ou XML do lado do servidor. No lado do cliente esses dados são transformados novamente em TDataSet[7] e exibidos ao usuário em um TDBGrid (por exemplo), com a ajuda dos componentes TClientDataSet ou TInetXPageProducer.

Olhando de um ângulo um pouco diferente, o DataSnap é uma tecnologia que permite mover um conjunto de dados (TTable, TQuery ou similares) de um servidor, para um TClientDataSet no cliente. O TClientDataSet, por sua vez, aparenta, age e se comporta exatamente como um TTable ou TQuery, exceto que ele não precisa estar ligado ao BDE ou outros componentes de conexão, como UniDAC ou FireDAC. O único requisito no cliente é a presença da biblioteca do DataSnap, a qual, aliás, se chama MIDAS.DLL. Mesmo assim, você pode dispensar a presença desta DLL, incluindo numa das cláusulas uses de sua aplicação cliente a unit MidasLib. Fazendo isso, nem mesmo a dll do DataSnap precisa estar presente! Ainda olhando para o DataSnap de uma forma diferenciada, basicamente o TClientDataSet obtém um conjunto de dados a partir de dados Variant que ele recebe do servidor.

O DataSnap permite que você utilize todos os componentes padrão do Delphi, incluindo componentes de acesso a dados do lado do servidor, mas o lado do cliente é um verdadeiro cliente magro, isto é, ele não deve incluir ou ligar-se diretamente a nenhum banco de dados. Como foi dito antes, a única dependência do cliente magro é a biblioteca midas.dll, a qual pode ser dispensada, mediante o uso da unit MidasLib.

Clientes DataSnap e Servidores DataSnap

Até agora, muita teoria, mas a melhor maneira de entender o que é o DataSnap e como ele funciona é construindo uma aplicação DataSnap, que consiste de um Cliente DataSnap e um Servidor DataSnap. Costumo começar com o servidor, onde encapsulo e exporto os conjuntos de dados. Com um servidor funcional, o próximo passo é construir o cliente que se conectará com este servidor e exibirá os dados de alguma forma.


O Sevidor DataSnap

Para construir o seu primeiro Servidor DataSnap (doravante chamado de SDS) você começa com uma aplicação nova vazia (File > New > Application[8]). Enquanto o formulário principal da aplicação estiver sendo exibido significa que o SDS estará ativo, em outras palavras, o loop de mensagens da aplicação vai manter o SDS vivo. Configure um título para o formulário principal de forma a identificar o SDS. Além disso eu costumo incluir neste formulário um TLabel com uma fonte grande e legível e configuro sua propriedade Caption com um valor sugestivo para o nome do SDS, por exemplo, "Meu primeiro servidor DataSnap".

Para transformar uma aplicação regular em um servidor de aplicação (middleware database server), você precisa adicionar um Remote DataModule (RDM) nela. Este DataModule especial pode ser encontrado na aba[9] "Multitier" do Object Repository do Delphi, logo, acesse File > New > Other e vá direto à aba Multitier, a qual mostra vários Wizards CORBA[10], um Remote DataModule e um Transitional DataModule[11]. O último pode ser usado com o MTS (Microsoft Transaction Server) antes do Windows 2000 ou COM+ no Windows 2000 ou posterior e não será coberto aqui. É o RDM "normal" que você precisa selecionar para criar seu primeiro SDS simples. Ao selecionar o ícone do RDM e clicar no botão OK, o assistente "New Remote DataModule Object"[12] será iniciado.

Existem algumas opções que você precisa especificar (ou garantir que estejam selecionadas corretamente). Primeiramente, "CoClass Name", que é o nome da classe interna. Este precisa ser um nome que você possa lembrar facilmente depois, então, eu recomendo que você use SimpleRemoteDataModule desta vez. Mantenha a opção "Instancing" configurada como Multiple Instance, desta forma seu SDS poderá conter múltiplas instâncias do RDM. Mantenha outras opções como estão e pressione OK para, finalmente, gerar o RDM.

O Remote DataModule[13]

O resultado da execução do assistente é um RDM que parece muito com um TDataModule regular. Visualmente não existem diferenças, e se você planeja usar o BDE, então você pode começar considerando-o como um TDataModule qualquer, soltando nele um componente TSession sem esquecer de configurar a propriedade AutoSessionName como true. Lembre-se de que fazer isso é imprescindível caso você esteja usando o modelo de thread Apartment[14].

Uma vez que você tenha incluído um componente TSession você poderá adicionar os outros componentes. Por exemplo, você pode incluir um TTable e nomeá-lo como TableCustomer. Configure sua propriedade DatabaseName como "DBDEMOS" e selecione na propriedade TableName a tabela customer.db.

Até agora tudo que foi feito poderia sê-lo em um DataModule regular, mas agora é hora de olhar os aspectos remotos deste DataModule especial (remoto). Na paleta de componentes, vá até a aba Data Access onde você encontrará o componente TDataSetProvider. Este componente é a chave para exportar conjuntos de dados de um RDM para o mundo exterior, mais especificamente para clientes DataSnap. Solte um componente TDataSetProvider no RDM e configure sua propriedade DataSet para TableCustomer. Isto significa que o TDataSetProvider irá prover ou exportar TableCustomer para um cliente DataSnap que se conectar nele (esse cliente será construído posteriormente neste artigo).

Uma propriedade muito importante do TDataSetProvider é a propriedade Exported, que é configurada como true para indicar que TableCustomer é exportada. Você pode configurar esta propriedade como false para "esconder" o fato de que TableCustomer é exportada a partir do RDM, assim, nenhum cliente poderá se conectar a ela. Isso pode ser útil, por exemplo em um servidor rodando constantemente (24x7), onde você precisa fazer o backup de algumas tabelas e precisa garantir que ninguém está trabalhando com elas durante o backup. Com a propriedade Exported do TDataSetProvider configurada como false ninguém pode fazer conexões a ele até que você configure novamente a propriedade como true.

Compilando o Servidor DataSnap

Basicamente isso é tudo que precisa ser feito para criar o seu primeiro SDS. A única coisa que resta ser feita é salvar o projeto. Salvei o formulário principal no arquivo "ServerMainForm.pas", o RDM foi salvo no arquivo "RDataMod.pas"[15], e salvei o projeto do SDS no arquivo "SimpleDataSnapServer.dpr". Após ter salvo o projeto, precisamos compilá-lo e executá-lo. Ao executar o SDS -- o qual mostrará apenas o formulário principal, claro -- ele será registrado[16] (no registro do Windows), assim qualquer cliente DataSnap poderá encontrá-lo e então conectar-se a ele. Mesmo que você mova o SDS para outro diretório (na mesma máquina), você precisará apenas executá-lo novamente para que ele seja re-registrado na nova localização. Esta é uma forma muito conveniente de gerenciar servidores DataSnap.

Até aqui, você não escreveu uma linha de código sequer para implementar este servidor DataSnap simples. Vamos ver então o que teremos de escrever ao implementar o cliente DataSnap que se conectará a ele.

O Cliente DataSnap

Existem vários tipos de clientes DataSnap que você pode desenvolver. Aplicações comuns de Windows (GUI), Active Forms e até mesmo servidores Web (usando Web Broker ou Internet Express). De fato, praticamente qualquer coisa pode atuar como um cliente DataSnap, como você verá em breve. Por enquanto você deve criar apenas uma aplicação regular de Windows a qual irá atuar como o seu primeiro cliente DataSnap simples que vai se conectar no seu SDS, o qual foi desenvolvido nas seções anteriores. Neste ponto você não deve tentar executar o cliente e o servidor em máquinas separadas. Ao invés disso, execute tudo em uma máquina e então, depois, você pode distribuir a aplicação na rede.

Acesse File > New > Application para inicializar a construção de uma aplicação simples. Neste ponto você decide se quer adicionar ou não um DataModule. A fim de evitar screenshots desnecessários neste artigo, eu não pretendo usar um DataModule. Ao invés disso, vou usar o formulário principal como contêiner para meus componentes não visuais (DataSnap), bem como para meus componentes visuais normais.

Antes de mais nada, seu cliente DataSnap precisa fazer uma conexão com o SDS. Esta conexão pode ser feita usando vários protocolos distintos, como (D)COM, TCP/IP (sockets) e HTTP e os componentes que implementam estes protocolos de conexão são, respectivamente, TDCOMConnection, TSocketConnection, TWebConnection e TCorbaConnection[17] na aba DataSnap e o TSoapConnection na aba Web Services. Para este primeiro cliente DataSnap simples usaremos o componente TDCOMConnection, logo, inclua este componente no seu formulário principal.

O componente TDCOMConnection tem uma propriedade chamada ServerName a qual contém o nome do SDS no qual você pretende conectar. Na verdade, se você abrir a lista disponível na propriedade ServerName no Object Inspector, você verá uma lista de todos os servidores DataSnap registrados na máquina local. No seu caso, esta lista deve conter apenas um item de nome SimpleDataSnapServer.SimpleRemoteDataModule, mas eventualmente todos os servidores MIDAS 3 e DataSnap que estiverem registrados serão vistos nesta lista. Os nomes nesta lista consistem de duas partes; a parte antes do ponto, denota o nome da aplicação e a parte após o ponto denota o nome do RDM, então, no caso atual, você vai selecionar o SimpleRemoteDataModule contido na aplicação SimpleDataSnapServer.

Quando você selecionar um servidor, a propriedade ServerGUID será automaticamente preenchida com o valor correto que está salvo no registro do Windows. Desenvolvedores com uma memória prodigiosa podem digitar um valor na propriedade ServerGUID e comprovar que a propriedade ServerName será preenchida como o nome do SDS correspondente. A diversão começa mesmo quando você configura a propriedade Connected do componente TDCOMConnection como true. Para que a conexão seja feita, o SDS precisa estar em execução, logo, o ato de configurar a propriedade Connected como true faz com que o SDS seja executado automaticamente, fato que pode ser comprovado pela exibição do formulário principal do SDS criado nas seções anteriores.

Client DataSets

Configure a propriedade Connected do componente TDCOMConnection como false, para fechar o SDS. Agora que você comprovou que é capaz de conectar-se a ele é hora de importar alguns DataSets que foram exportados pelo componente TDataSetProvider no RDM. Inclua um TClientDataSet, localizado na aba Data Access, no formulário principal e conecte a sua propriedade RemoteServer ao componente TDCOMConnection. Para que o TClientDataSet obtenha dados do SDS você precisa especificar qual TDataSetProvider usar. Em outras palavras, a partir de qual TDataSetProvider você deseja importar os dados que preencherão o TClientDataSet. Utilize a propriedade ProviderName para especificar o TDataSetProvider de sua escolha. Abra a lista do combobox associado a propriedade e você verá todos os TDataSetProvider disponíveis no RDM, todos aqueles que tem a propriedade Exported configurada como true. Em nosso caso existe apenas um TDataSetProvider no RDM do SDS, logo, selecione-o na lista.

Antes de escolher um valor para a propriedade ProviderName, lembre-se que você fechou a conexão com o SDS, entretanto, quando você abriu a lista da propriedade a fim de listar todos os componentes TDataSetProvider disponíveis no RDM que possuem sua propriedade Exported configurada como true, existe apenas uma forma (para o Delphi e o Object Inspector) de saber exatamente quais destes TDataSetProvider estão disponíveis: perguntar ao SDS! Mais especificamente, varrer o RDM em busca de componentes TDataSetProvider que possuem a propriedade Exported = true. Por conta desta necessidade, como o SDS estava desligado, ele precisa ser iniciado novamente de forma que a lista da propriedade ProviderName seja preenchida e exibida no Object Inspector. Como resultado disso, no momento em que você clica no combobox da propriedade ProviderName para exibir sua lista, o SDS será automaticamente iniciado novamente.

Quando você selecionar o RemoteServer e o ProviderName você poderá abrir (ou ativar) o TClientDataSet. Você pode fazer isso configurando a propriedade Active do componente como true. Neste momento o SDS estará alimentando dados a partir do TTable de nome TableCustomer, via componente TDataSetProvider através de uma conexão COM para o componente TDCOMConnection que roteia estes dados para o componente TClientDataSet no seu Cliente DataSnap, que estará pronto para ser usado.

Você pode agora soltar um componente TDataSource e acessar a aba Data Controls da paleta de componentes e incluir um ou mais controles conscientes de dados (data-aware). A fim de manter o exemplo simples, inclua apenas um componente TDBGrid. Conecte a propriedade DataSet do componente TDataSource ao TClientDataSet e conecte a propriedade DataSource do TDBGrid ao componente TDataSource. Como o componente TClientDataSet já estava ativado, você verá dados "ao vivo" em tempo de projeto, dados estes providos pelo nosso SDS.

Está tudo quase pronto agora. Para finalizar o Cliente DataSnap, primeiramente altere a propriedade Caption do formulário principal para algum nome útil, como por exemplo "Simple DataSnap Client" e salve seu trabalho. Nomeie o formulário principal como "ClientForm", salve-o como "ClientMainForm.pas", e nomeie o projeto como "SimpleDataSnapClient". Então, você estará pronto para compilar e executar o Simple DataSnap Client! Novamente você não escreveu nenhuma simples linha de código, mas esteja avisado que isso vai mudar em breve nas próximas seções.


BriefCase Model (Modelo de Maleta)

Ao executar o Cliente DataSnap você vê os dados de TableCustomer dentro do TDBGrid. Você pode navegar através destes dados, alterar o valor dos campos e mesmo incluir novos registros ou excluir registros existentes. Entretanto, uma vez que você fechar a aplicação, todas as mudanças serão perdidas. Não importa quantas vezes você tentar; as mudanças feitas nos dados do TDBGrid em tempo de execução do Cliente DataSnap afetam apenas o TClientDataSet local e não o TableCustomer no servidor.

O que você acaba de experimentar aqui é aquilo que é chamado de Modelo de Maleta (Briefcase Model). Usando o modelo de maleta (MDM) você é capaz de desconectar o cliente da rede e ainda conseguir acessar os dados. O MDM funciona da seguinte maneira:

O ponto chave do MDM é que você não precisa ter o servidor disponível todo o tempo para poder trabalhar nos seus dados. Esta capacidade é perfeita para usuários com notebooks ou sites, onde você quer diminuir ao máximo o tráfego no banco de dados.

Você agora deve entender que seu Cliente DataSnap atua apenas nos dados locais que estão dentro do TClientDataSet e que é possível salvar estes dados em um arquivo local e carregá-lo novamente em momento oportuno, logo, indo ao que interessa, para salvar o conteúdo atual do TClientDataSet, inclua um TButton no formulário, nomei-o como "ButtonSave", configure seu Caption como "Salvar" e escreva o seguinte código no manipulador do seu evento OnClick:

procedure TClientForm.ButtonSaveClick(Sender: TObject);
begin
  if ClientDataSet1.ChangeCount > 0 then
    ClientDataSet1.SaveToFile('customer.cds')
end;

Este código salva todos os registros contidos no TClientDataSet em um arquivo de nome "customer.cds" no diretório atual da aplicação, mas apenas se houve alterações no TClientDataSet (ClientDataSet1.ChangeCount > 0). Aliás, "CDS" significa "Client DataSet", mas você pode usar qualquer nome de arquivo e qualquer extensão, claro. Observe que o segundo parâmetro do método SaveToFile é implicitamente dfBinary. Este valor indica que queremos salvar os dados no formato binário (proprietário da Borland). Alternativamente poderíamos utilizar no segundo parâmetro o valor dfXML para salvar os dados em formato XML. Um arquivo XML é muito maior (14K contra apenas 7K para todos os dados de TableCustomer), mas tem a vantagem de poder, em tese, ser usado por outras aplicações. Eu prefiro o formato binário, que gera um arquivo menor e mais eficiente.

De forma similar, para implementar a funcionalidade que permite carregar o arquivo customer.cds no TClientDataSet, inclua outro TButton no formulário principal, nomeie-o como "ButtonLoad", configure seu Caption como "Carregar" e escreva o seguinte código no manipulador do evento OnClick:

procedure TClientForm.ButtonLoadClick(Sender: TObject);
begin
  ClientDataSet1.LoadFromFile('customer.cds')
end;

Perceba que o método LoadFromFile não precisa de um segundo argumento; ele é suficientemente esperto para determinar se está lendo um arquivo binário ou um XML[18]

Armado com estes dois botões você poderá agora, localmente, salvar as alterações feitas em seus dados e recarregá-los posteriormente. A fim de controlar o fato de quando ou não o TClientDataSet estará conectado "ao vivo" ao SDS, você pode incluir mais um TButton no formulário principal que alterna a propriedade Active do TClientDataSet. Nomeie esse novo TButtom como "ButtonConnect", configure sua propriedade Caption como "Conectar", em seguida escreva o trecho de código a seguir no seu manipulador do evento OnClick:

procedure TClientForm.ButtonConnectClick(Sender: TObject);
begin
  if ClientDataSet1.Active then // Fecha e desconecta
  begin
    ClientDataSet1.Close;
    DCOMConnection1.Close;
  end
  else // Abre (vai conectar automaticamente)
  begin
//  DCOMConnection1.Open;
    ClientDataSet1.Open;
  end
end;

Perceba que para desconectar você precisa fechar o TClientDataSet e fechar também o TDCOMConnection, enquanto que para estabelecer a conexão você apenas precisa abrir o TClientDataSet que irá, implicitamente, abrir o TDCOMConnection também.

Finalmente, existe mais uma coisa que precisa ser feita: garantir que o TDCOMConnection e o TClientDataSet não estejam conectados ao SDS em tempo de projeto, do contrário, sempre que você reabrir o projeto do Cliente DataSnap no Delphi ele precisará realizar uma conexão com o SDS, que precisará ser carregado e se, por um motivo qualquer, o SDS não for encontrado em sua máquina o carregamento do projeto ficará congelado e, consequentemente, o Delphi ficará sem responder por um bom tempo. Para resolver esse problema, eu sempre garanto que estes componentes não estejam conectados em tempo de projeto. Para fazer isso você deve sempre atribuir false à propriedade Connected do componente TDCOMConnection (que vai fechar o formulário principal do SDS) e false à propriedade Active do componente TClientDataSet (que, como resultado, fará com que você não veja mais qualquer dado em tempo de projeto).

Gostaria de abrir um parêntese para discutir o processo de timing dos clientes quando eles não conseguem conversar com o servidor. Se você tentar conectar-se ao servidor DCOM, mas não puder alcança-lo, o sistema não desistirá imediatamente da busca, ao invés disso, ele vai continuar tentando por um período de tempo que raramente excederá dois minutos. Durante estes dois minutos, entretanto, a aplicação ficará ocupada e parecerá travada. Se esta aplicação estiver carregada na IDE, então o Delphi como um todo ficará travado. Você pode ter esse problema simplesmente ao configurar a propriedade Connected do componente TDCOMConnection como true.

Agora, quando você recompilar e executar seu Cliente DataSnap, o formulário principal será exibido sem nenhum dado sendo exibido no TDBGrid. É hora então de clicar no botão "Conectar", de forma que a conexão com o SDS seja estabelecida e todos os registros sejam obtidos a partir de TableCustomer. Entretanto haverá momentos em que você não terá acesso ao SDS, seja porque você está "na rua" ou simplesmente porque a máquina com o SDS encontra-se inacessível. Nestes casos você poderá clicar o botão "Carregar" e trabalhar com uma cópia local dos registros. Tenha em mente que esta cópia local é aquela que você salvou pela última vez e será atualizada apenas quando você clicar o botão "Salvar" para escrever todo o conteúdo do TClientDataSet no disco.


ApplyUpdates

É excelente ser capaz de conectar-se a um conjunto de dados remoto ou carregar um conjunto de dados local e depois salvá-lo no disco de novo, mas como se aplicam as atualizações[19] ao banco de dados de fato (remoto)? Isso pode ser feito usando-se o método ApplyUpdates do TClientDataSet. Inclua mais um TButton no formulário principal, configure seu nome como "ButtonApplyUpdates" e seu Caption como "Aplicar Alterações". Tal como o botão "Salvar", este botão deve apenas aplicar alterações caso haja alterações a serem aplicadas. O manipulador do evento OnClick deste botão segue:

procedure TClientForm.ButtonApplyUpdatesClick(Sender: TObject);
begin
  if ClientDataSet1.ChangeCount > 0 then
    ClientDataSet1.ApplyUpdates(0);
end;

O método ApplyUpdates tem apenas um argumento: o número máximo de erros que serão permitidos antes que o processo de aplicação de alterações seja interrompido (MaxErrors). Com apenas um Cliente DataSnap conectado ao SDS você nunca terá qualquer problema, então, fique à vontade para executar o Cliente DataSnap agora. Clique o botão "Conectar" para conectar (e carregar) o SDS e use os botões "Salvar" e "Carregar" para escrever /ler o conteúdo do TClientDataSet no/do disco. Você pode até remover sua máquina da rede e trabalhar apenas nos dados locais por uma quantidade significante de tempo, que é exatamente a ideia por trás do modelo de maleta (o seu laptop sendo a maleta!). Qualquer alteração feita na sua cópia local permanecerá visível e você poderá aplicar as alterações no banco de dados remoto com um clique no botão "Aplicar Alterações", claro, desde que você reconecte à rede e ao SDS novamente.

Manipulação de erros

Então o que aconteceria se dois clientes, ambos usando o MDM, conectassem ao SDS, obtivessem todo conteúdo de TableCustomer e ambos fizessem algumas alterações no primeiro registro? De acordo com o que foi codificado até o momento, ambos os clientes poderiam enviar suas alterações ao SDS usando o método ApplyUpdates. Se ambos passaram zero como argumento do ApplyUpdates, então o cliente que ficou em segundo lugar na "corrida da atualização" não terá suas modificações persistidas no banco de dados remoto, pois seu ApplyUpdates vai ser interrompido imediatamente. O segundo cliente poderia utilizar um valor maior que zero em MaxErrors a fim de indicar um número fixo de erros/conflitos que seriam permitidos antes de o ApplyUpdates ser interrompido, entretanto, mesmo se o segundo parâmetro fosse -1 (o que indicaria que o ApplyUpdates deve continuar mesmo que haja erros), ele jamais iria alterar os dados que foram previamente alterados pelo primeiro cliente. Em outras palavras: você precisa executar algumas ações de conciliação para manipular atualizações em registros e campos que foram alterados por outros usuários.

Felizmente o Delphi possui uma caixa de diálogo muito útil especialmente desenvolvida para este propósito. Sempre que você precisar realizar alguma conciliação de erros[20], você deve considerar a adição desta caixa de diálogo ao seu Cliente DataSnap (ou escrever uma você mesmo, mas algo sempre precisa ser feito a respeito destes erros). Para usar a caixa de diálogo disponibilizada pelo Delphi, vá em File > New > Other, acesse a aba (ou item) "Dialogs"[21] do Object Repository e selecione o ícone "Reconcile Error Dialog". Uma vez que você tenha selecionado este ícone e clicado OK, uma nova unit será adicionada ao projeto do Cliente DataSnap. Esta unit contém a definição e a implementação da caixa de diálogo "Update Error", que pode ser usada para resolver erros decorrentes de conflitos nas operações de banco de dados.

Quando esta unit for adicionada ao projeto existe uma coisa muito importante que você precisa verificar. Primeiramente salve seu trabalho. Salve a nova unit em um arquivo de nome ErrorDialog.pas. Feito isso, a menos que você tenha desmarcado a opção para criar automaticamente TForms e TDataModules (na aba "Designer" da caixa de diálogo Tools > Environment Options), você precisará garantir que a classe da caixa de diálogo "Update Error" (TReconcileErrorForm) não seja uma das que são automaticamente criadas por sua aplicação. Veja a aba "Forms" da caixa de diálogo Project > Options. Nesta aba, você encontrará uma lista de formulários que são automaticamente criados e uma lista de formulários que estão disponíveis no projeto. Apenas verifique se TReconcileErrorForm não está na lista de formulários que são criados automaticamente e, se estiver, mova-a para a lista de formulários disponíveis. Uma instância de TReconcileErrorForm será criada dinamicamente quando for necessária.

Então, quando ou como você pode usar esta caixa de diálogo especial? Bem, na verdade é muito simples. Para cada registro cuja operação  de banco de dados (inserção, exclusão, alteração) não seja bem sucedida, o evento OnReconcileError do TClientDataSet será chamado. O manipulador deste evento tem a seguinte assinatura:

procedure TClientForm.ClientDataSet1ReconcileError(DataSet: TCustomClientDataSet;
  E: EReconcileError; UpdateKind: TUpdateKind; var Action: TReconcileAction);
begin

end;

Este é um manipulador de evento com quatro argumentos. O primeiro deles é o TClientDataSet que levantou o erro. O segundo é a exceção de (re)conciliação propriamente dita, a qual contém a mensagem acerca da causa da condição de erro. O terceiro argumento é o UpdateKind (ukInsert, ukDelete, ukModify) e indica qual operação estava sendo tentada quando o erro aconteceu. Finalmente, o quarto argumento é a ação que deve ser tomada para resolver o conflito. Neste argumento você pode retornar os seguintes valores da enumeração TReconcileAction, declarada na unit DBClient (ou DataSnap.DBClient):

A coisa mais legal a respeito do TReconcileErrorForm é que você não precisa se preocupar com nada disso. Você precisa apenas fazer duas coisas. Primeiro, você precisa incluir a unit ErrorDialog na cláusula uses do formulário principal. Com o formulário principal aberto no Delphi, pressione a combinação de teclas Alt+F11. A caixa de diálogo "Use unit" vai aparecer. Selecione a unit ErrorDialog e clique OK.

A segunda coisa a fazer é escrever uma linha de código no manipulador do evento OnReconcileError e chamar a função HandleReconcileError, disponível na unit ErrorDialog, a qual você acabou de incluir na cláusula uses de seu formulário principal. A função HandleReconcileError tem os mesmos quatro argumentos do manipulador do evento OnReconcileError (não é coincidência, claro), então, tudo agora é questão de passar estes argumentos do manipulador para a função, nada mais, nada menos. Sendo assim, o manipulador completo do evento OnReconcileError pode ser visto abaixo:

procedure TClientForm.ClientDataSet1ReconcileError(DataSet: TCustomClientDataSet;
  E: EReconcileError; UpdateKind: TUpdateKind; var Action: TReconcileAction);
begin
  Action := HandleReconcileError(DataSet,UpdateKind,E);
end;

Demonstrando Erros de (Re)Conciliação

A maior questão agora é: como tudo isso funciona na prática? Para poder testar, você obviamente precisa de dois ou mais Clientes DataSnap rodando simultaneamente. Para um teste completo usando o Cliente DataSnap e o Servidor DataSnap, você precisa executar os seguintes passos:

"Skip" vai ignorar o erro atual sem aplicar nada ao banco de dados remoto e seguir para o próximo registro da fila de atualizações no Data Packet (se houver). A alteração não aplicada no banco de dados remoto permanecerá no log de modificações e poderá ser enviada novamente ao se pressionar o botão "Aplicar Alterações". "Cancel" também vai ignorar o erro atual e mantê-lo no log de modificações, a diferença é que ele vai interromper o processo de aplicação de alterações, ou seja, se houver mais registros a serem atualizados, eles não o serão!

Para deixar isso bem claro, imagine que existem 10 registros modificados e você pressiona o botão "Aplicar Alterações". O primeiro, o segundo e o terceiro registros, passaram. O quarto registro tem um problema e vai exibir a caixa de diálogo "Update Error". Se você escolher "Skip", a atualização do quarto registro será ignorada, ele permanecerá no log de modificações locais do TClientDataSet e o fluxo do programa continua tentando aplicar o quinto registro. Supondo que o quinto registro passe, o sistema vai tentar o sexto e depois o sétimo. Suponha que esses passaram sem problemas, aí, ao tentar aplicar as atualizações do oitavo registro, houve um erro, então, novamente, a caixa de diálogo "Update Error" vai aparecer, mas desta vez a ação que você escolhe é "Cancel". Neste caso, o restante da operação do ApplyUpdates terminará e os registros oitavo, nono e décimo, não serão aplicados e permanecerão no log de alterações locais.

"Refresh" apenas "esquece" todas as atualizações que você fez no registro e atualiza ele localmente com os valores que estão persistidos no servidor de banco de dados. "Merge" vai tentar mesclar o registro modificado (local) com o registro salvo (remoto), colocando suas alterações no registro que está no servidor de banco de dados. "Refresh" e "Merge" resolvem o problema imediatamente, isto é, após a aplicação destas ações, os registros aos quais estas ações foram aplicadas são removidos do log de modificações e será garantido que tanto o registro remoto como o registro local tenham exatamente os mesmos valores em seus campos. Isso não acontece ao se usar as ações "Skip" ou "Cancel", as quais mantém os registros no log de alterações para permitir um reenvio das alterações por meio de novo ApplyUpdates.

"Correct" é sem dúvida a opção mais avançada. Ela dá ao usuário a opção de informar na própria tela "Update Error" (ou via código) os valores corretos não conflitantes de cada um dos campos do registro. Tal como as opções "Refresh" e "Merge", após a aplicação desta ação os registros aos quais esta ação for aplicada serão removidos do log de modificações e será garantido que tanto o registro remoto como o registro local tenham exatamente os mesmos valores em seus campos.

Sumário

Neste artigo eu descrevi como o TClientDataSet pode ser usado como uma tabela stand-alone na memória, assim como seu uso em conjunção com o dbExpress (em duas camadas) e com o DataSnap (em três camadas) como cache de banco de dados, provendo o assim chamado Modelo de Maleta. Com o engessado (e congelado) BDE, a importância do TClientDataSet continua a crescer nas aplicações desenvolvidas em Delphi, Kylix e no C++ Builder!

Para mais informações leia Dr. Bob Eaxamines #63 em "Tecnologias de acesso a dados no C++ Builder" e #58 "Desenvolvimento de Aplicações de Bancos de Dados", bem como meus artigos[22] do BorCon 2004 em "Técnicas de acesso a dados com ClientDataSets" e do BorCon 2003 em "Indrodução ao ClientDataSet e ao dbExpress".



1 Apesar de aparentemente as tecnologias usadas parecerem defasadas, todo o conteúdo deste artigo pode ser usado em quaisquer versões de Delphi mais recentes. As técnicas descritas aqui são atemporais e extremamente úteis. Eu refiz todos os exemplos descritos em Delphi XE5 com um mínimo de alterações
2 Atualmente o mais indicado é usar os diretórios System32 ou SysWOW64
3 Ou qualquer outro tipo de componente de conexão que não exija instalação na máquina-cliente, como o FireDAC, o UniDAC, o ZeosLib, etc.
4 Os nomes dos itens deste menu variam de acordo com o Delphi sendo utilizado. No Delphi 2006, por exemplo, existem os itens Load from MyBase table..., Save to MyBase XML table..., Save to MyBase XML UTF8 Table... e Save to Binary MyBase file...
5 Esta aplicação não foi testada por mim, contudo mesmo que ela não se aplique mais, a ideia que dela se pode tirar é útil, até mesmo como forma de backup local, por exemplo
6 A palavra "atualizar" neste contexto, significa qualquer operação de banco de dados, e não apenas as atualizações propriamente ditas
7 Costuma-se chamar estas conversões de objetos em bytes portáveis e vice-versa de serialização de objetos
8 Esse caminho muda em alguns Delphis mais recentes. Em Delphi 2006, por exemplo, o caminho é File > New > VCL Forms Application
9 Em versões mais modernas do Delphi, o Object Repository é composto de uma visualização estilo "Windows Explorer" com itens sendo exibidos em pastas, logo, nestas versões existe uma pasta chamada Multitier
10 Este item pode não existir na sua versão de Delphi
11 Este item pode não existir na sua versão de Delphi
12 Este nome pode variar
13 Esta seção está focada no BDE, que era a tecnologia nativa de conexão disponível no Delphi na época em que este artigo foi escrito. Obviamente você não precisa (e nem deve) usar mais o BDE. Resumidamente, ignore as referências ao BDE e inclua no RDM componentes correlatos disponíveis no suíte de componentes de conexão de sua preferência (FireDAC, UniDAC, ZeosLib, DBX, etc.)
14 Ao criar o RDM, o modelo de thread padrão é o Apartment, logo, você precisará configurar a propriedade AutoSessionName como true neste exemplo, caso esteja usando o BDE
15 Um arquivo adicional de extensão .tlb pode ter sido criado pelo assistente "New Remote DataModule Object". Caso haja necessidade de salvar este arquivo, nomeie-o com o mesmo nome do RDM, ou seja, RDataMod.tlb.
16 A partir do Delphi 2007 o registro automático foi descontinuado devido a presença maciça do UAC nos Windows, por isso, se você tentar rodar este programa ele não será registrado automaticamente. Se este for seu caso, basta executá-lo com a linha de comando /RegServer. Para desregistrá-lo, use a linha de comando /UnRegServer. O registro e o desregistro precisam ser feitos executando o SDS como administrador
17 Este componente pode não existir na sua versão de Delphi
18 O arquivo XML que pode ser carregado pelo TClientDataSet tem uma estrutura específca, no entanto, qualquer XML pode ser carregado por um TClientDataSet após sofrer uma transformação XML, ou seja, uma conversão que transforma uma estrutura XML qualquer numa estrutura XML que o TClientDataSet é capaz de entender (DATAPACKET). Para realizar a conversão, utilize o XML Mapper, disponível no diretório bin do Delphi. O XML Mapper vai gerar um arquivo de transformação XML (XSLT) com o qual, em tempo de execução, um arquivo XML "padrão" pode ser convertido em um XML DataPacket
19 Quero ressaltar novamente que a palavra "atualizar" aqui significa qualquer operação que altere o banco de dados. No caso, uma inserção, uma exclusão e uma alteração propriamente dita, todas estas operações alteram o banco de dados, inserindo, excluindo ou alterando registros
20 "Conciliar erros" ou "Reconciliar erros" vem do inglês "Reconcile Error" e pode ser interpretado como uma forma de resolver problemas (erros) nos dados quando o DataSnap não consegue determinar o que deve ser feito. A responsabilidade da decisão do que deve ser feito com dados conflitantes ou errados é sempre passada à aplicação ou ao usuário, o qual tem o poder de decidir o que deve ser feito
21 Nos Delphis mais recentes, o ítem do Object Repository é "Delphi Files" e o nome do ícone a ser selecionado é "VCL Reconcile Error Dialog"
22 Os links para estes artigos não existem mais. Mantive-os aqui apenas para registro histórico. Caso eles lhe interessem, contate Bob Swart diretamente para pedir uma cópia, caso ele ainda as possua.