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”
Deixe uma Resposta

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.
Boa dica! Continue postando sobre 3 camadas que me interessa muito!
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
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!
É 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
Gostei da dica, faltam dicas de 3 camadas
se tiver alguma coisa de Web Service estou querendo aprender mais tambem
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.
Jhosef, muito obrigado! Fico feliz em contribuir com a comunidade Delphi. Um abraço!
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..
Parabens, muito boa sua dica. Sou iniciante em 3 camadas e o conteudo da dica foi de grande ajuda.
Obrigado
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..
ola… pq nao usar o filter do clientdataset.. utilizo, é bem simples e me da otimas formas de filtro… inclusive composta, like, etc.
OK… massa! so nao acho correto implementação dentro de eventosssss spCountryBeforeGetRecords , cdsCountryBeforeGetRecords…parece liguagem estruturada.
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?