Queries dinâmicas no servidor DataSnap

Quando eu digo que lugar de SQL é no servidor é porque vejo muitos programadores Delphi usando DataSnap como um simples middleware de conectividade com o banco de dados, não como uma verdadeira camada de “regras de negócio” que proporcione abstração de dados do lado cliente.

Uma dificuldade comum, que muitos consideram justificativa para usar a famigerada opção poAllowCommandText no TDataSetProvider, é fazer queries customizadas na aplicação cliente que não tenham um conjunto definido de parâmetros.

Como fazer uma pesquisa em um cadastro de clientes, por exemplo, por Cidade e/ou Nome e/ou Data de Cadastro? Só esses três parâmetros permitem 8 combinações de consulta (23). Uma solução seria escrever 8 queries diferentes no servidor e mais 8 dataset providers. Uma trabalheira danada. Já se fossem 5 campos de pesquisa para o usuário, seriam 32 combinações! Por aí que muita gente escolhe construir dinamicamente a query no cliente e usar a propriedade CommandText de TClientDataSet.

Uma alternativa é escrever uma query em que a cláusula WHERE contemple parâmetros nulos. Por exemplo:

WHERE
((:Cidade IS NOT NULL) AND (Cidade = :Bairro)) OR
((:Cadastro IS NOT NULL) AND (Cadastro >= :Cadastro))

O problema com essa técnica é que a performance da query fica muito comprometida. Não há como o otimizador do banco de dados fazer um plano de execução eficiente.

A minha solução preferida é simplesmente mandar os valores dos parâmetros para o TDataSetProvider montar a query dinamicamente. Falando assim parece simples, mas… na verdade é simples! :)

A chave é usar o evento BeforeGetRecords, presente tanto no TClientDataSet (no lado cliente), quanto no TDataSetProvider (no lado servidor). O evento tem um parâmetro OwnerData do tipo OleVariant (para todos efeitos, idêntico a um Variant) que pode ser atribuido no cliente e depois lido no servidor.

Construí uma pequena aplicação de exemplo que lê a tabela Country do banco de dados Access de demonstração que fica em “…\Borland Shared\Data\dbdemos.mdb”. Veja abaixo uma imagem da aplicação filtrando os campos “Continent”, “Population” (valo mínimo) e “Name” (iniciais).

O que eu fiz no cliente foi apenas montar um array Variant com os parâmetros definidos pelo usuário e atribuí-lo a OwnerData. Não mexi com a propriedade Params do ClientDataSet simplesmente porque não existe nenhum parâmetro no dataset: o SQL será montado no servidor dinamicamente.

  procedure TForm1.cdsCountryBeforeGetRecords(
    Sender: TObject; var OwnerData: olevariant);
  var
    ContinentEqual: string;
    PopulationMin: integer;
    NameStart: string;
  begin
    ContinentEqual := Trim(Edit1.Text);
    PopulationMin := StrToIntDef(Trim(Edit2.Text), 0);
    NameStart := Trim(Edit3.Text);
    OwnerData := VarArrayOf([ContinentEqual, PopulationMin, NameStart]);
  end;

Com isso o cliente só precisa deixar o usuário entrar com os parâmetros que quiser na tela e dar Open no dataset (não esqueça de dar um Close antes de fazer uma nova pesquisa). O servidor se encarregará de montar o código SQL apropriado com os parâmetros que não forem vazios.

O mesmo evento BeforeGetRecords é usado no servidor de aplicação, mas agora usamos o do componente TDataSetProvider para alterar em código a propriedade SQL do objeto de query (no caso, um TADOQuery):

  procedure TRemoteDataModule1.dspCountryBeforeGetRecords(
    Sender: TObject; var OwnerData: olevariant);
  var
    ContinentEqual: string;
    PopulationMin: integer;
    NameStart: string;
    SQLWhere: string;
 
    procedure AddWhere(const S: string);
    begin
      if SQLWhere = '' then
        SQLWhere := 'WHERE'
      else
        SQLWhere := SQLWhere + ' AND';
      SQLWhere := SQLWhere + ' (' + S + ')';
    end;
 
  begin
    if VarIsArray(OwnerData) and (VarArrayHighBound(OwnerData, 1) = 2) then
    begin
      ContinentEqual := OwnerData[0];
      PopulationMin := OwnerData[1];
      NameStart := Ownerdata[2];
 
      SQLWhere := '';
      if ContinentEqual = '' then
        AddWhere(Format('Continent = ''%s''', [ContinentEqual]));
      if Populationmin > 0 then
        AddWhere(Format('Population >= %d', [PopulationMin]));
      if NameStart = '' then
        AddWhere(Format('Name LIKE ''%s%%''', [NameStart]));
 
      qryCountry.SQL.Clear;
      qryCountry.SQL.Add('SELECT * FROM Country');
      qryCountry.SQL.Add(SQLWhere);
    end;
  end;

Veja abaixo o resultado de uma query mais específica, que define o continente “South America” e uma quantidade mínima de 100.000.000 habitantes.

Com essa técnica a aplicação cliente fica totalmente independente da estrutura do banco de dados. Se mudar o banco de dados, ou mesmo a estrutura das tabelas, nada tem que ser alterado no cliente! O simples fato de isolar a camada de apresentação do acesso a dados já é uma vantagem na administração do projeto.

Repitam comigo: CommandText nunca mais! ;-)

Artigo publicado originalmente em 20/08/2006.

Comments

14 Responses to “Queries dinâmicas no servidor DataSnap”

  1. Adriano Santos® on August 3rd, 2006 8:11 am

    Muito boa sua explanação sobre o assunto Daniel. Na minha idéia de desenvolvedor penso da mesma forma. A única providência que tomo muito cuidado é quanto a programação no banco, storedproc por exemplo eu não adoto devido ao fato de trabalhar com mais de um bd no meu software. As incompatibilidades de comandos sql me incomodam e prefiro fazer algo o mais genérico possível evitando crashs no programa devido a um ou outro comando sql que não existe no banco atual. É isso ai.

  2. Erick Sasse on August 21st, 2006 10:04 pm

    Boa dica! Continue postando sobre 3 camadas que me interessa muito!

  3. Marcos Douglas on August 22nd, 2006 9:05 am

    Concordo com vc, Daniel.
    Eu faço um pouco diferente: Utilizo o método DataRequest do CDS e para os parâmetros do SQL eu monto um TParams, pois me dá mais flexibilidade para campos nulos, conversões, etc.

    Marcos Douglas
    md @ delfire . net

  4. Malta on August 22nd, 2006 12:56 pm

    Marcos, DataRequest também uma boa saída! Usando BeforeGetRecords você tem a vantagem de usar o próprio dataset que responde ao método Open.

    Evito usar DataRequest porque geralmente implica em uma sintaxe mais de baixo nível, mas é perfeitamente válido como arquitetura de 3-camadas.

    Um abraço!

  5. Marcos Douglas on August 22nd, 2006 5:31 pm

    É verdade, tem a vantagem que vc falou. Eu tb utilizo o DataRequest pq eu não tenho um Provider para cada CDS. Tenho um centralizador (pode chamar de controlador) que “sabe” qual Provider instanciar para responder a requisição. Assim o acoplamento é muito mais fraco.
    A desvantagem dessa abordagem é que eu perco o “live-data” em design time.

    Marcos Douglas
    md @ delfire . net

  6. Cleber Moises Grings on May 13th, 2008 8:14 am

    Gostei da dica, faltam dicas de 3 camadas
    se tiver alguma coisa de Web Service estou querendo aprender mais tambem

  7. Jhosef on May 30th, 2008 9:20 am

    A aplicação na prática em cima de algo que só se ouve teoria é a melhor forma de aprender as coisas.
    Gostei do assunto e espero aprender bastante com seu blog, e PARABÉNS.

  8. Malta on May 30th, 2008 2:06 pm

    Jhosef, muito obrigado! Fico feliz em contribuir com a comunidade Delphi. Um abraço!

  9. Ivan on May 31st, 2008 12:04 am

    Parabéns pelo ótimo tópico.
    São blogs com qualidade e nível igual a esse que nos anima a continuar nossa leitura cotidiana.
    Simplesmente perfeito..

  10. Edson Silva on June 25th, 2008 10:17 am

    Parabens, muito boa sua dica. Sou iniciante em 3 camadas e o conteudo da dica foi de grande ajuda.
    Obrigado

  11. Luciano França on July 2nd, 2008 10:26 pm

    Porque não usar o CommandText ????

    Gostaria de um esclarecimento pois já vi algums artigos na internet bem como esse acima e sempre dizem para não usar o CommandText mais nunca explicam detalhadamente o motivo de não usar o CommandText, eu gostaria de saber o porque de não usar ele para sistemas 3 camadas, qual o problema ??? é mais lento, dá queda na performace
    qual o real motivo ???

    Grato a todos..

  12. henrique on July 3rd, 2008 12:32 am

    ola… pq nao usar o filter do clientdataset.. utilizo, é bem simples e me da otimas formas de filtro… inclusive composta, like, etc.

  13. Lindemberg on July 12th, 2008 1:08 am

    OK… massa! so nao acho correto implementação dentro de eventosssss spCountryBeforeGetRecords , cdsCountryBeforeGetRecords…parece liguagem estruturada.

  14. Rubens Antunes on August 24th, 2008 2:04 pm

    Estou tentando implementar no seu sistema essa ideia, mas sou iniciante em 3 camadas e estar dando erro de identificação nessa linha

    if VarIsArray(OwnerData) and (VarArrayHighBound(OwnerData, 1) = 2) then

    pode me ajudar?

Deixe uma Resposta




XHTML: Você pode usar essas tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre lang="" line="">