Usando o Zeos – ZQuery

O componente chamado TZQuery presente na paleta do Zeos é basicamente para executar queries que retornam valores para um TDataset ou ResultSet como preferir chamar. Daí componentes que contenham a propriedade Datasource e Dataset tais como DBGrid, DBEdit, DBNavigator,… se alimentam dele.

Uma capacidade muto interessante do TZQuery é que o mesmo é capaz de fazer o streamming ou paginação de dados conforme o uso, isto é importante porque ao trazer uma quantidade muito grande de dados para estação cliente, os recursos do cliente podem ser exauridos e talvez parar de responder. Mas usando o recurso de paginação, o cliente requisita os registros que precisará ver sob demanda. Imagine um DBGrid e imagine uma pesquisa contendo mil registros, porém o DBGrid mostra apenas 50 registros por vez, isto significará que a mesmo que você precisará ir para o registro #51 para a query vá até o servido e pegue mais 50 registros. Claro que essa habilidade é configurável através de métodos como FetchRow ou FetchALL.

Há um componente muito similar chamado ZReadOnlyQuery, a única diferença é que não tem como usá-la para modificar os dados. Nem mesmo suporte a TUpdateSQL ela possui. Por não ter como usá-la para modificar o banco, pense nela como um componente mais leve para ser usaodo em consultas, relatórios, exportação de dados e afins.

Vamos a uma explicação sobre cada propriedade do componente ZQuery:

Active

Tipo “true” ou “false”. Quando passa de falso para verdadeiro, a sentença SQL que estiver na propriedade “SQL” é executada e se tudo ocorrer bem então Active passa a ser “true”, se falhar então permanece em “false”. Algo sugerido antes de passar de false para true é testar se o mesmo não esta ativo e se o estiver então fechá-lo, além de liberar memória, as outras propriedades não serão influenciadas por mudanças com o dataset aberto, como se faz isso? simples:

if ZQuery1.Active then
  ZQuery1.Close;

AutoCalcFields

Tipo “true” ou “false”. Quando verdadeiro indica que quando o Dataset for aberto haverá processamento de TFields definidos como “calculated” (“calculado” daqui em diante) no evento OnCalcFields. O calculo ocorre também quando o foco do dataset muda para o campo “calculado”. Se falso, tais TFields mesmo que marcados como “calculado” não serão processados:

Campos Calculated existem apenas na aplicação e são calculados no evento OnCalcFields

CachedUpdates

Tipo “true” ou “false”. Os dados serão armazenados localmente pelo programa e não serão enviados para o banco de dados até que o método ApplyUpdates seja invocado junto com o CommitUpdates para confirmar as operações ou CancelUpdates para desfazê-las.

Connection

Tipo ZConnection. Toda query precisa estar assinalada a uma conexão, no caso do componente Zeos, uma conexão é também uma TIL(Transaction Isolation Layer). A partir do Zeos8 há um componente ZTransaction que corrige essa dificuldade e podemos ter uma query assinalada a uma conexão e um tipo de TIL em particular.

DataSource

Tipo TDatasource. Similar as propriedades LinkedFields, MasterFields e MasterSource para formulários que exibem dados do tipo Mestre/Detalhe, mas com uma única diferença, cria dinamicamente a query detalhe enquanto navega-se no datasource Mestre(apenas Datasource neste contexto). Por exemplo:

select * from tdetalhe where campo1_tdetalhe =:campo2_tmestre

É mais vantajoso do que usar LinkedFields, MasterFields e MasterSource porque estes funcionam apenas quando os dados já foram trazidos localmente e estão com o dataset ativo, enquanto usando a propriedade Datasource, uma nova query será gerada toda vez que a posição do cursor no datasource(mestre) se alterar.

FetchRow

Tipo Inteiro. Determina a quantidade de registros que serão trazidos por vez para o lado clente, isso é basicamente uma paginação de dados automática. Por exemplo, se for informado 50 então apenas 50 registros serão trazidos para o lado cliente por vez. Isso minimiza o impacto que haveria se trouxesse todos os registros de uma única vez. Se usar zero, o sistema trará todos os registros e não haverá paginação e será uma consulta mais lenta e consumindo mais recursos. Se o valor for -1, o sistema arbitrará uma quantidade que julgar suficiente conforme a área disponível para exibi-las.

Quando paginação de dados está envolvida podemos usar o método FetchALL para trazer todos os registros para a estação cliente. Alguns usam FetchALL para poder usar a propriedade RecordCount que retorna a quantidade de registros, porém essa não é uma boa ideia porque todos os registros serão lidos, uma tarefa muito pesada para apenas saber quantos registros há. O ideal é um SELECT COUNT(*) por cima da query adjacente para obter quantidade de registros, imagine a query:

Select * from clientes where razao_social like  "a%"

Como saber quantos registros a query acima retornou sem ter de usar um FetchALL? Usamos uma query derivativa, veja:

Select count(*) from (
  Select * from clientes where razao_social  like "a%"
)

Assim será perfeitamente possível criar uma função genérica e reutilizá-la sobre qualquer query. E se você se preocupou que algum campo será lido por causa da menção às colunas, o gerenciador de banco de dados tem inteligência suficiente para entender que nossa ação é saber a quantidade de registro e não os dados em si.

Filter e Filtered

Filter é do tipo String, enquanto Filtered é “true” ou “false”. Filter é basicamente uma expressão que determinará os resultados que estarão visíveis dentro dum TDataset quando Filtered=true. Se estiver paginando dados, todos os dados serão resgatados para a aplicação do filtro. Exemplo de filtros são “nome like ‘*santos*'” ou “cancelado=true”.

Importante: Se estiver usando paginação de dados, ao usar Filter e Filtered, todos os registros serão trazidos para o lado cliente, por isso, se puder usar WHERE em seu select será mais performático. Use Filtros quando todos os registros de que precisa já estão no lado cliente, é o caso de quando populamos uma tabela temporária para ser exibida em tela e depois processada.

IndexFieldNames

Nomes dos campos pelo qual manterá ordenação. Os nomes podem estar separados por ponto-e-virgula, por exemplo:

nome;escolaridade

Também é possível usar os sufixos “asc” ou “desc” para que a ordenação seja ascendente ou descendente:

nome desc

É bom usar essa propriedade com um índice atrelado a tabela, caso contrário, os dados terão de ser ordenados em tempo de execução e isso diminuirá significativamente a performance. Um exemplo prático de usá-lo é no evento onTilteClick de um DBGrid, veja o exemplo:

procedure TfmPrincipal.DBgrid1TitleClick(Column: TColumn);
begin
  if SameText(zquery1.IndexFieldNames,Column.FieldName+' DESC') then
    zquery1.IndexFieldNames:=Column.FieldName+' ASC'
  else
    zquery1.IndexFieldNames:=Column.FieldName+' DESC';
end; 

As vezes também é necessário indicar que o título da coluna é clicável para ordenação, uma maneira de fazer isso é mudar a aparencia do cursor quando passa o ponteiro do mouse por cima delas, isso pode ser conseguido usando o evento OnMouseMove do DBGrid, veja o exemplo:

procedure TfmPrincipal.DBGrid1MouseMove(Sender: TObject;
  Shift: TShiftState; X, Y: Integer);
var
  Col,
  Row : Integer;
  InTitle:Boolean;
begin
  Col := DBGrid1.MouseCoord(X, Y).X;
  Row := DBGrid1.MouseCoord(X, Y).Y;
  InTitle:=(Row=0);
  if InTitle then
    DBGrid1.Cursor:=crHandPoint
  else
    DBGrid1.Cursor:=crDefault;
end;

Importante: Se estiver usando paginação de dados, ao usar IndexFieldNames, todos os registros serão trazidos para o lado cliente.

LinkedFields, MasterFields e MasterSource

Geralmente são usados em conjunto para exibição de dados do tipo Mestre<->Detalhe. Por exemplo, a tabela clientes e tabela pedidos, você indica em pedidos que a MasterSource será a tabela de clientes, MasterFields será ID_CLIENTE que liga as duas tabelas e LinkedFields é automaticamente ajustada para dizer que ID_CLIENTE de uma tabela refere-se ID_CLIENTE de outra tabela. Geralmente usamos nomes de campos iguais em ambas, mas se a modelagem for diferente, não há problemas desde que o nomes dos campos sejam apontados corretamente em LinkedFields.

Options

Trata-se de um set de possibilidades, Sendo:

  • doAlignMaxRequiredWideStringFieldSize: o alinhamento usará o tamanho máximo da definição do campo como referencia
  • doAlwaysDetailResync: Alguns bancos tem numero de linhas virtuais, e as vezes programadores usam essas linhas como referencias em visualizações mestre/detalhe. Aqui temos a possibilidade de alterar o rowno na tabela mestre e recarregar totalmente as tabelas de detalhes.
  • doCachedLobs: Alguns drivers, como o Firebird, suportam o carregamento de LOBs (BLOBs e CLOBs) sob demanda. Esses drivers geralmente só carregam LOBs do servidor quando são lidos e os fecham se não forem mais necessários. A configuração doCachedLobs armazenará em cache esses BLOBs e CLOBs no lado do cliente. Drivers que não suportam o carregamento atrasado de LOBs ignorarão essa opção e sempre armazenarão em cache LOBs no lado do cliente.
  • doCalcDefaults: Deve ser ativado por padrão. Os Zeos tentará carregar e manipular os valores padrão que sua definição de tabela contém para os campos. Se desligado, a ausência de um valor poderá gerar uma exceção.
  • doDontSortOnPost: Não mude a ordem das linhas exibidas quando fizer o “Post”.
  • doNoAlignDisplayWidth: Não alinhe pela largura do titulo do campo(DisplayWidth)
  • doOemTranslate: Usar ou não conversão para OEM.
  • doPreferPrepared: Autopreparar ou não as queries. Se as queries tem parâmetros, mas foi esquecida a preparação, a partir da segunda execução faz o preparo automaticamente.
  • doSmartOpen: Normalmente TZQuery e TZReadOnlyQuery irão gerar uma exceção se você tentar usar o método Open em instruções que não retornam dados. Isso pode acontecer ao usar INSERT, UPDATE e DELETE. Com essa opção definida, o conjunto de dados não gerará uma exceção, mas abrirá um conjunto de dados vazio com zero linhas e zero colunas.
  • doUpdateMasterFirst: Isso significa que todas as tabelas de detalhes são registradas no mestre e vice-versa. Eu determino qual é o mestre e esta tabela é atualizada primeiro. Em seguida, todas as tabelas de detalhes serão atualizadas. Isso é muito útil se você tiver restrições.

ParamChar

Parametros são aquelas ocorrências de variaveis dinamicas precedidas geralmente por “:”, exemplo:

select * from clientes where id_cliente=:p_id_cliente

Pois bem, :p_id_cliente é um parâmetro e os “:” é o ParamChar. Um problema a ser evitado é achar que parâmetros são como macro-substituições, uma especie de Search/Replace dentro de queries, pois não são. Parâmetros são formas de identificar para o servidor SGBD a possibilidade de repetição de uma mesma sentença SQL, daí ele passará a se lembrar do calculo de menor custo para a operação e não precisará recalculá-lo toda vez. Um ponto importante é que uma vez “preparada” qualquer query de mesma assinatura se beneficiará do calculo previamente realizado para qualquer usuário que estiver conectado e não apenas quem fez a primeira preparação, deveras, mesmo que quem preparou da primeira vez tenha sua conexão terminada a query ainda será mantida pelo tempo que o servidor considerar necessário.

A propriedade ParamChar existe porque nem sempre é desejável que parâmetros sejam identificados por “:”, por que não? Porque podem haver sentenças onde “:” deve ser tratado de outro jeito, por exemplo, no FirebirdSQL dependendo de como você cria um psql “:etiqueta” pode ser um parâmetro ou uma referencia a uma variável local e daí temos uma ambiguidade, ex:

declare variable id_cliente int;
begin
  (...)
  for select id_cliente from clientes where id_cliente=1001 into :id_cliente;

No exemplo acima, id_cliente é um campo da tabela, contudo :id_cliente é excepcionalmente uma variável local de mesmo nome. Situações assim acontecem em banco de dados que tenham suporte a psql e note que não são parâmetros, mas o Zeos não saberá como distinguir, a menos que o psql seja escrito de uma maneira diferente ou que deixe de usar “:” como indicador de parâmetro para usar outro caractere diferente.

No FirebirdSQL, em algumas situações como a descrita acima é necessário trocar ParamChar, ex:

execute block(p_id_cliente int=~p_id_cliente, p_cancelar boolean=~p_cancelar) returns (error_msg varchar(255));
begin
  error_msg='';
  if (:p_id_cliente&lt;=0) then
     error_msg='Codigo de cliente invalido';
  (...) 
  suspend;
end

Note que a assinatura (onde estão os parâmetros de entrada e saída) contêm ~p_id_cliente e ~p_cancelar, por que fiz assim? Para que pudêssemos indicar “~” como ParamChar e o Zeos entender que eles são os parâmetros e o :p_id_cliente mais abaixo fosse interpretado como parte do psql e o Zeos não toque nele.

IMPORTANTE: Evite o “?”(ponto de interrogação) como ParamChar, pois ele é reservado para outra coisa e não funcionaria adequadamente.

ParamCheck

Tipo “true” ou “false”. Se “true” a lista de parâmetros é esvaziada e os parametros são populados com base na SQL informada no momento em que o método ParamByName ou Params é invocado. Se o valor for “false” a lista não será conferida com o SQL e portanto a lista de parâmetros estará sempre intacta, mas você terá de fornecer os parâmetros(propriedade Params) manualmente.

Params

Tipo Lista. Em associação com as propriedades ParamCheck e SQL, mantêm uma lista de parametros quie poderão ser usados. Existem os métodos .ParamByName(‘p_nome’) e Params[n] que permitem acessar o parâmetro e seu valor. Ex:

ShowMessage('Quantidade de parametros: '+IntToStr(zqupdate.Params.Count)); 
zquery1.parambyname('p_id_cliente').AsInteger:=col_ID_CLIENTE;
zquery1.parambyname('p_NOME_ALTERNATIVO').AsString:=col_NOME_ALTERNATIVO;
zquery1.parambyname('p_END_CIDADE').AsString:=col_END_CIDADE;
zquery1.parambyname('p_END_UF').AsString:=col_END_UF;
zquery1.parambyname('p_STATUS').AsString:=col_STATUS;
zquery1.Params[0].AsInteger:=col_ID_CLIENTE;
zquery1.Params[1].AsString:=col_NOME_ALTERNATIVO;
zquery1.Params[2].AsString:=col_END_CIDADE;
zquery1.Params[3].AsString:=col_END_UF;
zquery1.Params[4].AsString:=col_STATUS;   

IMPORTANTE: Acessar ou mudar parâmetros pelo seu índice é bem mais rápido, mas só faça isso se estiver numa operação de lote muito demorada porque acessar seus valores usando o nome do parâmetro tem a vantagem de evitar erros como mudar a ordem dos parâmetros.

Properties

Uma lista de parâmetros para estabelecer comportamentos com a base de dados, usar properties aqui significa que os comportamentos que eu desejo é apenas para a query e não para a conexão inteira. Por exemplo, pode usar properties para estabelecer um timeout para sua query. Por exemplo, no firebird podemos:

SET STATEMENT TIMEOUT 30 SECONDS;
UPDATE CLIENTES SET STATUS='C' WHERE (...)

Contudo, o comando SET poderia ser suprimido se eu acrescentasse como parâmetro:

isc_req_stmt_timeout=30

Porque ter uma propriedade assim já que podemos fazê-lo numa mesma sentença? Porque alguns bancos não permitem mais que uma instruções no mesmo statement então teria de executar o comando set numa execução e sua sentença SQL na seguinte. Então dependendo do banco de dados que for usar com o Zeos você pode economizar statement com Properties.

ReadOnly

Tipo “true” ou “false”. Quando “true”, os dados poderão ser lidos, mas nunca alterados. Vale ressaltar que se uma query será do inicio ao fim sempre read only então seria melhor usar o componente TZQueryReadOnly onde apesar do mesmo objetivo, nunca poderá mudar seu estado para realizar alterações ou ser usado em conjunto com TZUpdateSQL.

Sequence e SequenceField

É usado para obter uma sequencia numérica do objeto do tipo SEQUENCES no banco de dados. Até o Firebird2.5 usava-se este tipo de dados para obter sequencias que geralmente eram usadas como autoincremento dentro das tabelas. A partir da versão 3.0, o FirebirdSQL ganhou o tipo “autoincremento” para tabelas e não carece mais de usar SEQUENCES para este fim, mas você ainda pode usar um SEQUENCES para outras finalidades.

ShowRecordTypes

Permite exibir uma coluna virtual que indica o tipo de operação é efetuada com o registro antes do CommitUpdates, a saber:

  • usUnmodified: Não modificado
  • usModified: Modificado
  • usInserted: Inserido
  • usDeleted: Excluído

O Firedac no Delphi tem algo similar quando estamos usando Cached Updates, usamos para entender num conjunto de dados, o que será excluído, modificado ou inserido antes do CommitUpdates. Não há documentação farta em como mostrar essas operações no Zeos, mas presume-se algo similar ao Firedac com cached updates, por exemplo, se “usDeleted” estiver ligado neste set e CachedUpdates estiver ativo então será possível ver todos os registros excluídos.

SortedFields

Lista de campos para ordenação, geralmente usado em conjunto com a propriedade SortType. Muitas vezes é mais prático usar SortedFields e SortType do que IndexFieldNames na programação, veja o exemplo abaixo onde ao clicar no titulo de uma coluna no DBGrid haverá uma ordenação pelo campo seja ascendente ou descendente se clicar nele uma segunda vez:

procedure TFormUsuarios.gridUsuarioTitleClick(Column: TColumn);
begin
  if tUsuario.SortType &lt;&gt; stAscending Then
  begin
    tUsuario.SortedFields:= Column.FieldName;
    DataModuleMain.tUsuario.SortType := stDescending;
  end
  else
  begin
    tUsuario.SortedFields:= Column.FieldName;
    tUsuario.SortType := stAscending;
  end;   
end;

SQL

Tipo String. É a sentença SQL propriamente dita. Use o ParamChar “:” quando desejar usar parâmetros ou referenciar campos noutro resultset especificado em MasterSource.

UpdateMode

Set de Valores. Com o CachedUpdates (ou RequestLive com ZTable) ativo você pode alterar os valores locais e eles serão enviados ao servidor após o ApplyUpdates. Para que isso funcione, o sistema gerará querys dinamicamente para aplicar essas atualizações e ele tem basicamente dois métodos onde poderá localizar os registros originais que precisam ser atualizados:

  • umUpdateAll: Usa todos os campos, modificados ou não. Como se percebe é anti-performático.
  • umUpdateChanged(default): Atualiza apenas os campos que foram modificados, neste caso você precisa ter uma primary key(PK) para ele saber gerar adequadamente a clausula where do SQL. Esse método é importante também para caso duas ou mais pessoas alterem o mesmo registro na rede, porém cada um deles gerará um update apenas dos campos que cada um modificou mantendo um mix de atualizações, não sobrepondo valores inalterados.

UpdateObject

Referencia um TZUpdateSQL. Com um TZUpdateSQL em seu sistema você pode construir seu INSERT/UPDATE/DELETE de forma que uma alteração visual de um registros possa ser aplicado do jeito que você deseja. É muito mais prático do que escrever INSERT/UPDATE/DELETE na unha.

WhereMode

Set de Valores. Quando essas queries de INSERT/UPDATE/DELETE são criadas dinamicamente, será necessário localizar corretamente os registros originais que sofrerão a ação.

  • wmWhereAll: Todos os campos serão referenciados na clausula WHERE do SQL para localizar o valor original. Anti-performático. Só faz sentido essa opção em dados não “normatizados” ou “bagunçados”.
  • wmWhereKeyOnly: Usa apenas a primary-key como referencia.

Conclusão

O componente TZQuery é o mais usado dentro de um programa. Embora todas as explicações mencionados aqui sejam para o Zeos, muitas outras suítes tem propriedades bastante similares. Então, se dominá-los no Zeos, você leva este conhecimento também para outras frameworks de acesso a dados.

Série de vídeos

Siga a playlist abaixo e veja o uso das propriedades na prática:

Zeos com Lazarus e Delphi: ZQuery (Introdução)
Vamos esmiuçar: Connection, Active(Open/Close) e FetchRow e algumas dicas importantes sobre paginação de dados.
Zeos com Lazarus e Delphi: ZQuery(AutoCalcFields)
Zeos com Lazarus e Delphi: ZQuery(CachedUpdates)
Zeos com Lazarus e Delphi: ZQuery(Datasource)
Zeos com Lazarus e Delphi: ZQuery(Filter e Filtered)
Zeos com Lazarus e Delphi: ZQuery(IndexFieldNames)
Zeos com Lazarus e Delphi: ZQuery(LinkedFields, MasterFields,MasterSource)
Zeos com Lazarus e Delphi: ZQuery(Options)

Parte #14: Zeos com Lazarus e Delphi: ZQuery(ParamChar, Params)