Um Mecanismo de Cache de Dados para DataSnap

Hoje pela manhã, durante o Delphi Academy, discutimos sobre cache de dados para DataSnap. Basicamente o cenário onde você tem tabelas que sofrem nenhuma ou poucas alterações ao longo do período, as quais você não precisa refazer a consulta a todo tempo, para todos os usuários.

Estou disponibilizando o exemplo abaixo, mas em um resumo, temos o seguinte:

  • Em uma aplicação DataSnap REST, adicionar um DataModule e configurar seu LifeCycle para Server, no ServerContainer. Com isso, ele terá uma única instância durante o ciclo de vida da aplicação
  • Neste DataModule, coloque um MemTable (ou Clientdataset se preferir) para cada tabela que deseja manter em cache
  • Defina uma conexão ao banco, e métodos (ou componentes para a execução das queries) para executar as consultas e carregar as tabelas em memória
  • Adicione um TFDEventAlerter e faça as devidas configurações: http://docwiki.embarcadero.com/Libraries/Tokyo/en/FireDAC.Comp.Client.TFDEventAlerter
  • Crie triggers de INSERT/UPDATE/DELETE que fará o “POST_EVENT” com o nome do evento alertando que a tabela sofreu alguma alteração. Este mesmo mecanismo está disponível para a maioria dos banco de dados: http://docwiki.embarcadero.com/RADStudio/Tokyo/en/Database_Alerts_(FireDAC)
  • No evento do OnAlert do  TFDEventAlerter, faça o refresh das tabelas e atualização dos datasets em memória

Com isso, você tem um mecanismo seguro e prático para manter as tabelas em memória sempre atualizadas.

Screen Shot 2017-05-30 at 17.34.14Screen Shot 2017-05-30 at 17.34.21

Os exemplos atualizados estão nestes links:

 

Multi-tier com Delphi XE6 e FireDAC JSON Reflection

Introdução

Durante o Delphi Tour prometemos falar mais sobre o novo suporte a Reflection no FireDAC para o desenvolvimento de aplicações Multi-tier.

Este novo suporte foi introduzido ainda na versão XE5 Update 2, como você pode observar neste post introdutório do Marco Cantu.

Vamos detalhar um pouco mais este framework e também mostrar como adicionar compressão de dados a este cenário, ideal para conexões “não muito boas” (leia-se conexõe 3G…).

Este demo pretende ilustrar um guia inicial de boas práticas com FireDAC Reflection. Ele também será utilizado em posts futuros mostrando deployment, inclusive para Apache:

Visão Geral do Projeto Exemplo

O exemplo base que utilizaremos aqui encontra-se disponível nos demos do Delphi em “C:UsersPublicDocumentsEmbarcaderoStudio14.0SamplesObject PascalDataSnapFireDACJSONReflect”, mas vamos disponibilizar uma cópia atualizada contendo as adições deste post.

Esta é a estrutura do projeto exemplo:

A aplicação server é criada através do wizard “DataSnap WebBroker Application”. Para o exemplo estamos utilizando uma aplicação “Stand Alone” VCL (a partir do XE6 pode ser também FireMonkey), porém você ainda tem as opções Apache e IIS, além de uma aplicação do tipo “console”.

Nosso servidor vai acessar uma base exemplo do Interbase (EMPLOYEE). Serão basicamente três consultas SQL, as quais serão “exportadas” via JSON para a aplicação cliente. Abaixo você pode observar a conexão FireDAC (FDConnection) e as três consultas (FDQuery). Caso não conheça FireDAC, este webinar pode lhe ajudar: http://forms.embarcadero.com/LA14Q1BRBDEApplicationstotheFuture

Conforme comentado acima, os datasets resultantes serão exportados como JSON para a aplicação cliente. Abaixo segue o código exemplo de uma das funções criadas no servidor, todas as demais seguem o mesmo padrão de codificação:

function TServerMethods1.GetDepartmentNames: TFDJSONDataSets;
begin
 FDQueryDepartmentNames.Active := False;

 Result := TFDJSONDataSets.Create;
 TFDJSONDataSetsWriter.ListAdd(Result, FDQueryDepartmentNames);
end;

Observe que você não precisa se preocupar em converter o DataSet para JSON manualmente, a classe auxiliar “TFDJSONDataSetsWriter” possui um método (Add) que vai automaticamente serializar o DataSet em formato JSON e adicioná-lo ao Result, que por sua vez trata-se de um TFDJSONDataSets (basicamente uma lista de DataSets em formato JSON). Todas estas novas classes estão declaradas na unit “Data.FireDACJSONReflect”.

Por tratar-se de uma lista de DataSets, você pode retornar em um mesmo método diversos DataSets, como por exemplo:

function TServerMethods1.GetDepartmentEmployees(
  const AID: string): TFDJSONDataSets;
begin
  FDQueryDepartmentEmployees.Active := False;
  FDQueryDepartmentEmployees.Params[0].Value := AID;

  FDQueryDepartment.Active := False;
  FDQueryDepartment.Params[0].Value := AID;

  Result := TFDJSONDataSets.Create;

  TFDJSONDataSetsWriter.ListAdd(Result, sDepartment, FDQueryDepartment);
  TFDJSONDataSetsWriter.ListAdd(Result, sEmployees, FDQueryDepartmentEmployees);
end;

Outro método que gostaria de comentar é o responsável por aplicar as alterações retornadas pelo “client” diretamente no banco de dados:

procedure TServerMethods1.ApplyChangesDepartmentEmployees(
  const ADeltaList: TFDJSONDeltas);
var
  LApply: IFDJSONDeltasApplyUpdates;
begin
  LApply := TFDJSONDeltasApplyUpdates.Create(ADeltaList);

  LApply.ApplyUpdates(sDepartment, FDQueryDepartment.Command);

  if LApply.Errors.Count = 0 then
    LApply.ApplyUpdates(sEmployees, FDQueryDepartmentEmployees.Command);

  if LApply.Errors.Count > 0 then
    raise Exception.Create(LApply.Errors.Strings.Text);
end;

A aplicação cliente envia um “TFDJSONDeltas” para o server. Basicamente temos que instanciar um “IFDJSONDeltasApplyUpdates” e aplicar as alterações nos Datasets originais através do método “ApplyUpdates”.

Para a aplicação cliente, qualquer plataforma pode ser utilizada. Ou seja, o código do exemplo se aplica tanto a VCL (Windows 32/64), bem como ao FireMonkey (Windows, OS X, iOS e Android)!

Uma vez criada sua aplicação cliente, você deve utilizar o wizard “DataSnap REST Client Module” para gerar o código do proxy que conterá as chamadas para sua aplicação servidora. Em outras palavras, você cria uma aplicação na plataforma desejada (desktop ou mobile) e utiliza o wizard acima para gerar o código necessário para conectá-la a aplicação servidora.

Estamos utilizando Visual LiveBindings para efetuar a ligação dos controles visuais. Por esta razão é necessário definir os fields dos FDMemTables para então fazer a ligação em tempo de design. Neste caso, o tipo de dado dos campos não é importante, mas o nome deve ser exatamente o mesmo retornado pelo servidor.

Vamos entender agora como fazer a chamada para a aplicação servidora, abaixo temos o código que retorna a lista de departamentos e preenche o TListView “ListViewDepartments”:

procedure TDepartmentsClientForm.ButtonDepartmentsClick(Sender: TObject);
begin
  GetDepartmentNames;
end;
procedure TDepartmentsClientForm.GetDepartmentNames;
var
  LDataSetList: TFDJSONDataSets;
begin
  try
    LDataSetList := ClientModule2.ServerMethods1Client.GetDepartmentNames();
    UpdateDepartmentNames(LDataSetList);
  except
    on E: TDSRestProtocolException do
      HandleRestException(ClientModule2.DSRestConnection1, 'Get Departments error', E)
    else
      raise;
  end;
end;
procedure TDepartmentsClientForm.UpdateDepartmentNames(const ADataSetList: TFDJSONDataSets);
begin
  FDMemTableDepartments.Active  := False;
  FDMemTableDepartments.AppendData(
    TFDJSONDataSetsReader.GetListValue(ADataSetList, 0));
end;

Como podem observar, o método “GetDepartmentNames” faz uma chamada ao servidor (através da classe proxy gerada pelo wizard “DataSnap REST Client Module”) retornando um “TFDJSONDataSets”, ou seja, uma lista de datasets em formato JSON. Resta então, utilizando a classe auxiliar “TFDJSONDataSetsReader”, reverter o processo de serialização, adicionando o resultando em um FDMemTable. O TFDMemTable pode ser comparado a um TClientDataSet, isto é, um dataset em memória. Os dados armazenados em um TFDMemTable podem ser alterados, e retornados ao servidor para atualização no banco de dados, fazendo-se uma chamada ao método correspondente:

procedure TDepartmentsClientForm.ButtonApplyUpdatesClick(Sender: TObject);
begin
  ApplyUpdates;
end;

function TDepartmentsClientForm.GetDeltas: TFDJSONDeltas;
begin
  Result := TFDJSONDeltas.Create;
  TFDJSONDeltasWriter.ListAdd(Result, sEmployees, FDMemTableEmployee);
  TFDJSONDeltasWriter.ListAdd(Result, sDepartment, FDMemTableDepartment);
end;

procedure TDepartmentsClientForm.ApplyUpdates;
var
  LDeltaList: TFDJSONDeltas;
begin
  LDeltaList := GetDeltas;
  try
    ClientModule2.ServerMethods1Client.ApplyChangesDepartmentEmployees(LDeltaList);
  except
    on E: TDSRestProtocolException do
      HandleRestException(ClientModule2.DSRestConnection1, 'Apply Updates error', E)
    else
      raise;
  end;
end;

Otimizando a Transferência dos Dados

Para aplicações onde todas as camadas (server e client) estão desenvolvidas em Delphi ou C++ Builder, podemos ainda aplicar uma compressão de dados para otimizar seu tempo de transferência. Obviamente, a diferença somente será perceptível em um ambiente com alguma restrição de banda (3G por exemplo).

Tanto o Delphi, quanto o C++ Builder, trazem uma implementação da ZLib, biblioteca para compressão de dados: http://docwiki.embarcadero.com/Libraries/XE6/en/System.ZLib. O mais interessante é que nossa implementação da ZLib está disponível para todas as plataformas suportadas, ou seja, você pode compactar uma informação em seu servidor Windows e descompactar em sua aplicação mobile iOS ou Android!

Você pode facilmente adicionar a compressão de dados em seus métodos JSON utilizando os “class methods” que estamos disponibilizando abaixo, os quais tornam o uso da ZLib bastante simples:

unit DSSupportClasses;

interface

uses System.Classes, System.ZLib, System.SysUtils;

type
  TDSSupportZLib = class(TObject)
  private
    { private declarations }
  protected
    { protected declarations }
  public
    { public declarations }
    class function ZCompressString(aText: string): TBytes;
    class function ZDecompressString(aText: TBytes): string;
  end;

implementation

{ TDSSupport }

class function TDSSupportZLib.ZCompressString(aText: string): TBytes;
var
  strInput: TBytesStream;
  strOutput: TBytesStream;
  Zipper: TZCompressionStream;
begin
  SetLength(Result, 0);
  strInput := TBytesStream.Create(TEncoding.UTF8.GetBytes(aText));
  strOutput := TBytesStream.Create;
  try
    Zipper := TZCompressionStream.Create(TCompressionLevel.clMax, strOutput);
    try
      Zipper.CopyFrom(strInput, strInput.size);
    finally
      Zipper.Free;
    end;
    Result := Copy(strOutput.Bytes, 0, strOutput.size);
  finally
    strInput.Free;
    strOutput.Free;
  end;
end;

class function TDSSupportZLib.ZDecompressString(aText: TBytes): string;
var
  strInput: TBytesStream;
  strOutput: TBytesStream;
  UnZipper: TZDecompressionStream;
begin
  Result := '';
  strInput := TBytesStream.Create(aText);
  strOutput := TBytesStream.Create;
  try
    UnZipper := TZDecompressionStream.Create(strInput);
    try
      try
        strOutput.CopyFrom(UnZipper, 0);
      except
        on E: Exception do
        begin
          raise Exception.Create('Error Message: ' + E.Message);
        end;
      end;
    finally
      UnZipper.Free;
    end;
    Result := TEncoding.UTF8.GetString(strOutput.Bytes, 0, strOutput.size);
  finally
    strInput.Free;
    strOutput.Free;
  end;
end;

end.

Porém, nenhuma implementação adicional será necessária em seu projeto. O que fizemos foi modificar a unit “Data.FireDACJSONReflect”, onde estão implementadas as classes base do FireDAC JSON Reflection, adicionando a compressão/descompressão nos métodos de serialização JSON. Assim, basta adicionar esta versão da unit a seu projeto e fazer um “Build”. Compressão/descompressão adicionadas!

Downloads e referências

EMBARCADERO CONFERENCE BRASIL!!!

Aproveitando a ocasião, já estamos preparando a conferência deste ano, e a página para submissão de palestras já está no ar! Se você tem algo interessante para mostrar, em Delphi e C++ Builder, ou ainda um caso de sucesso utilizando nossas ferramentas, estamos esperando por sua inscrição:

http://www.embarcaderoconference.com.br/palestrantes/