Share Button

Trabalhar com RECORD é mais fácil administrar memória quando comparado com classes.

Como cada variável RECORD ocupa um endereço diferente com os dados e controla a sua retirada da memória com mais facilidade, prefiro usar RECORD para fazer CACHE de dados.

Por outro lado, temos vários acessos ao banco de dados que possuem parâmetros que estão nestes RECORD (alguém lembrará um CRUD). Uma solução é utilizar RTTI para ler os valores dos parâmetros que estão armazenados no RECORD e passar os valores para os parâmetros (TParams).

function TALQuery.FillParams<T>(rec: T): TALQuery;
var
  LList: TJsonValuesList;  // unit System.uJson
  LPair:TJsonPair;
  i:integer;
  prm:TParamBase;
begin
  result := self;
  LList := TJsonValuesList.Create();    // unit System.uJson
  try
    TJsonValue.GetRecordList<T>(LList, rec); // carrega os valores do RECORD em um LIST
    for I := 0 to params.count-1 do
         begin
            prm:= params[i];
            LPair := LList.names[ prm.Name ];
            if assigned(LPair) then
              case prm.DataType of
                 ftSmallint,ftInteger:
                   prm.AsInteger := LPair.JsonValue.GetValue<integer>;
                 ftFloat:
                   prm.AsFloat := LPair.JsonValue.GetValue<double>;
                 ftCurrency:
                   prm.AsCurrency := LPair.JsonValue.GetValue<Currency>;
                 ftDateTime,ftDate,ftTime :
                   prm.asDateTime := LPair.JsonValue.getValue<TDateTime>;
               else
                 prm.Value := LPair.JsonValue.getValue<string>;
              end;
         end;
  finally
    LList.free;
  end;
end;


/* aplicando */


Type
    TPedidoRecord = record
      ...
      pedido:integer;
      filial:integer;
      data:TDatetime;
    end;


var qry:   TMinhaQuery;
    rec : TPedidoRecord;
begin
    ....
    qry.fillParams<TPedidoRecord>(rec);
    ...
end;

 

Dependência: System.uJson

 

Share Button

Quando estava preparando os exemplos de código para o artigo sobre ForIn para FireDAC, lembrei de um artigo do meu amigo Marcos Douglas publicado no Object Pascal Programming que discorre sobre a programação Imperativa ou Estruturada.

Na Ciência da Computação, programação imperativa é um paradigma de programação que descreve a computação como ações, enunciados ou comandos que mudam o estado (variáveis) de um programa. Muito parecido com o comportamento imperativo das linguagens naturais que expressam ordens, programas imperativos são uma sequência de comandos para o computador executar. (Wikipedia)

O artigo traz como objetivo discutir o fim do FreeAndNil propondo a utilização de Interface na programação PASCAL. Extraindo o conceito é possível escrever código utilizando diretamente interface o que demostra o poder da linguagem frente aos novos paradigmas.

Reescrevendo TFDQuery com interface, podemos fazer:

   TQueryIntf.New(FDConnection1)
      .Table('sigcad a')
      .FieldNames('codigo,nome')
      .Where('codigo between :codigo_de and :codigo_ate')
      .ParamValue('codigo_de', 1)
      .ParamValue('codigo_ate',5)
      .open
      .DoQuery( procedure (ds:TDataset)
          begin
            memo1.Lines.Add( 'Carregou: '+intToStr(ds.RecordCount) )
          end);

 

Share Button

Recurso de loops com ForIn não estão disponível para os componentes TDataSet o que não permite usar:

var fld:TFields;
begin
    for fld in FQuery1 do
     begin
         memo1.lines.add( fld.fieldByName('nome').asString );
     end;
end;

Um objeto que queira fazer uso de ForIn deve implementar a interface IEnumerator.
Fazendo uma adaptação útil para a classe TFDQuery do FireDAC é possível escrever os seguintes métodos para atender a condição do ForIn:

    function GetEnumerator: IQuery;
    function GetCurrent: TFields;
    property Current:TFields read GetCurrent;
    function MoveNext: boolean;
    procedure Reset;

Código Fonte no Git

A propriedade de colunas da tabela já implementa Enumerator o que permite usar:

   for f in query1.fields do
       begin
          memo1.Lines.add( f.FieldName );
       end;

Share Button

Sim…. replicação para firebird… não tem ? tem sim… só trabalhar um pouco – nem tudo é como visão de brigadeiro..

A replicação consistem manter DUAS ou mais tabelas com conteúdo iguais – de tal forma que se obtém o mesmo resultado em qualquer uma das duas bases de dados.

Existe dois lados na replicação – um que tem os dados que precisam ser levados para o outro (publisher-aquele que publica) – o outro que recebe os dados publicados pelo servidor (subscriptor-aquele que se candidata a receber os dados). Por último a combinação de ambos em um só – ele é tanto publisher como subscriptor da mesma tabela;


Fase do desenvolvimento

  1. Preparar o Publisher para publicar dados para replicação (banco de origem);
  2. Preparar o Subscriptor para receber os dados publicados (banco de destino);
  3. Codificar os componente que monitoram as alterações do Publisher;
  4. Codificar a transferências dos dados da origem para o destino;

 

A título ilustrativo, vou adotar a tabela de PEDIDOs como base

Considere a seguinte tabela:


create table  PEDIDOS  (dcto varchar(10), 
                        data Date, 
                        cliente integer, 
                        total numeric(15,4));


Preparando o banco de origem em firebird

Como base para a replicação, vou criar uma coluna nova na tabela de pedidos na origem para indicar a chave de referência para a replicação – para fugir dos tipos auto-incrementos (já escrevi como fazer isto aqui no blog) vou usar uma coluna para identificar a chave de replicação que receberá uma representação texto para um GUID

alter table PEDIDOS add GID varchar(38);

[OFF] antes que alguém questione… não dá para usar auto-incremento em razão de atender a pré-condição de que o dado dever ser o mesmo nos dois banco de dados (traduzindo – não podem ser diferentes)

A coluna GID vai receber um valor por “trigger” toda vez que for incluída na tabela

SET TERM ^ ;
ALTER TRIGGER REPL_PEDIDOS_GID ACTIVE
BEFORE INSERT OR UPDATE POSITION 0
AS 
begin 
  /* Trigger Utilizado para Replicacao */ 
  if ((new.gid is null)) then 
     new.gid =  UUID_TO_CHAR( gen_uuid() ); 
end^
SET TERM ; ^

[OFF] como se nota inclui na trigger o UPDATE – fiz isto para pegar linhas antigas que ainda não foram replicadas em nenhum processo anterior – caso ocorre uma replicação parcial;

Até aqui já temos a marcação de chave na tabela de origem para localizar as linhas que serão publicadas para replicação. Agora vamos criar uma tabela de controle de publicação das replicações

CREATE TABLE REPL_ITENS
  (
    TABELA Varchar(128),   // recebe o nome da tabela alvo
    GID Varchar(38),       // recebe o GID da tabela alvo 
    TIPO Char(1),          // I-insert U-update D-delete
    DATA Date,             // data e hora da alteração
    ID integer,            // sequencial interna
    SESSION_ID integer     // sessão da transação que criou - um luxo
  );
CREATE INDEX REPL_ITENSDATA ON REPL_ITENS (DATA);
CREATE INDEX REPL_ITENSGID ON REPL_ITENS (GID);
CREATE INDEX REPL_ITENSID ON REPL_ITENS (ID);
CREATE INDEX REPL_ITENSTABELA ON REPL_ITENS (TABELA);
GRANT DELETE, INSERT, REFERENCES, SELECT, UPDATE
ON REPL_ITENS TO SYSDBA WITH GRANT OPTION;

Vamos precisar de um GENERATOR para popular a coluna ID da tabela e uma trigger para popular com o valor do GENERATOR. ver também

CREATE GENERATOR REPL_ITENS_GEN_ID;
SET TERM ^ ;
CREATE TRIGGER REPL_ITENS_ID FOR REPL_ITENS ACTIVE
BEFORE INSERT POSITION 0
AS 
begin /* Replicacao Storeware */ 
  new.id = gen_id(REPL_ITENS_GEN_ID,1); 
  new.session_id = rdb$get_context('SYSTEM','SESSION_ID'); 
  new.data = cast('now' as date); 
end^
SET TERM ; ^

[OFF] para este caso não usei o GUID – vamos precisar garantir uma sequência na tabela que seja em ordem crescente – isto vai facilitar a codificação a frente.

 

Para fechar, vamos ensinar a tabela PEDIDOS como publicar as suas alterações:

SET TERM ^ ;
CREATE TRIGGER REPL_PEDIDOS_REG FOR PEDIDOS ACTIVE
AFTER INSERT OR UPDATE OR DELETE POSITION 0
AS 
begin 
  /* Replicacao Storeware */ 
  in autonomous transaction do 
  begin 
   if (inserting) then 
     insert into repl_itens ( tabela,gid,tipo) 
            values('PEDIDOS',new.gid,'I'); 
   if (updating) then 
     insert into repl_itens ( tabela,gid,tipo) 
            values('PEDIDOS',new.gid,'U'); 
   if (deleting) then 
     insert into repl_itens ( tabela,gid,tipo) 
            values('PEDIDOS',old.gid,'D'); 
  end 
end 
^
SET TERM ; 

 

Preparando o banco de dados de destino

No banco de dados destino temos a mesma tabela de pedidos (o ideal que possua a mesma estrutura).

Considerando que durante a replicação dos dados ocorrerão momentos em que haverá UPDATEs e DELETEs para fazer – será necessário ter uma chave correspondente para a alteração do registro. Esta chave não pode ser controlado localmente pelo banco, ela precisa ser uma chave que tanto o banco de origem como o banco de destino sejam cooperativos. A este requisito a chave GID que recebe um GUID atente perfeitamente.

alter table PEDIDOS add GID varchar(38);

A “trigger” para gerenciar a coluna GID segue o mesmo comportamento do banco de origem.

SET TERM ^ ;
ALTER TRIGGER REPL_PEDIDOS_GID ACTIVE
BEFORE INSERT OR UPDATE POSITION 0
AS 
begin 
  /* Trigger Utilizado para Replicacao */ 
  if ((new.gid is null)) then 
     new.gid =  UUID_TO_CHAR( gen_uuid() ); 
end^
SET TERM ; ^

[OFF] Note que a trigger checa se o GID é null – isto é importante pois quem vai gerar a chave para o GID é o banco que criou o registro (onde ele nasceu) e durante toda a sua vida precisa receber o mesmo valor nos dois banco de dados.

Agora já temos tudo que precisamos no banco de dados para controlar o que será replicado. Passemos a construir os códigos que vão fazer a replicação em si.


Codificando o select para monitorar as alterações na Origem

O select na origem, ou no publisher, será feito considerando que queremos somente as linhas alteradas para serem transferidas para o banco de destino.

Para isto precisamos fazer controle de quais linhas foram e quais linhas ainda não foram para o destino. Para isto foi que usei um GENERATOR para a tabela de controle   REPL_ITEMS->id

Uma vez executado a replicação precisamos guardar qual foi o ultimo ID utilizado para ser utilizado na próxima chamada:

  select a.*, b.tipo repl_tipo from PEDIDOS a, REPL_ITEMS b
  where  a.gid = b.gid and b.id>:id
  order by b.id

Para tabelas com muitas linhas, pode ser interessante usar um derivação incluindo o número de linhas a sincronizar a cada chamada:

  select first 1000 a.*, b.tipo repl_tipo from PEDIDOS a, REPL_ITEMS b
  where  a.gid = b.gid and b.id>:id
  order by b.id

Codificando a transferência dos dados para o destino

Chegou o momento de enviar o dados da Origem para o Destino….
Dependendo da coluna REPL_TIPO que vem do Select na Origem:
I – Faz um INSERT na tabela de destino;
U – Faz um UPDATE;
D – Faz um DELETE;

Esteja preparado para tratar algumas exceções:

  • quando for fazer um UPDATE e o registro ainda não se encontra no destino, precisando fazer o INSERT para iniciar o registro;
  • quando for fazer DELETE de uma registro que não existe;
  • fazer INSERT de registro que já tem chave primária idêntica no destino;
  • se esta inciando a sincronização de uma tabela que já existe, sinalizar a tabela de eventos com os dados já existentes;

 


 

 

Projeto no Git

 

 

 

Share Button

Por padrão o FireDAC mantem ativo (true) a propriedade SilentMode. Nesta condição o FireDAC mantem o ciclo de mensagem com o windows para apresentar a ampulheta de processando. Evidentemente, informar o usuário sobre o andamento do processamento pode ser um ponto importante para não ficar com a impressão que tudo travou, no entanto, isto tem um custo no tempo de resposta da operação.

A questão pode se mostrar bastante crítica quando aninha chamada ao banco durante a navegação do usuário em um GRID com os dados… Imagina fazendo um select a cada vez que muda um linha no GRID. Desligar o SilentMode pode ter impacto positivo considerável no resultado… não ficará com aquela ampulheta gastando tempo durante a navegação… tenta ainda teclar varias vez a tecla para baixo em um GRID onde a cada pressionar de tecla chama um SELECT no banco de dados… notará o quanto é significativo o recurso.

No FireDAC há varias formas de acessar esta propriedade, desde acesso mais global como o que ocorre com o componente  TFDManager até acesso mais localizado como em um TFDQuery…

Toda rotina que não precisa informar o usuário, deveria ser desligado para melhorar a resposta e liberar o firedac de ficar despachando mensagem para o Windows.

query1.ResourceOptions.SilentMode := True;

Share Button

Já notou que tem algumas coisas que são diferentes de um banco de dados para outro…

Ex: Um UpperCase se escreve diferente dependendo do banco de dados que você irá utilizar;

Esquece…  o FireDAC resolve quase tudo para você…..

 

Escreve:

Select   {uCase(Nome)} from clientes     ///o FireDAC irá converter para UpperCase o nome do cliente;

Select   {LCase(Nome)} from clientes     ///o FireDAC irá converter para LowerCase o nome do cliente;

 

Ver no manual do FireDAC: Preprocessing Macros

Share Button

Em outro “post” escrevi sobre obter o código do cliente gerado automático pelo banco e como retornar o seu valor usando “returning”.

Há situações que é necessário confirmar se uma operação foi aceita pelo banco (sem erro), e que de fator a linha foi inserida, alterada ou excluída do banco de dados.

Depois de submeter um comando de Insert, Update ou Delete para o banco pode-se adotar algumas estratégias para saber se houve sucesso:

  1. usar um Try/Exception para capturar uma exceção. Se o comando retornou uma exceção significa que o banco de dados criticou o comando. De outro lado há comandos que mesmo não retornando nenhum exceção NÃO garante que  conseguiu fazer… ex: envia um update e não encontra o registro – não gera exceção, mas também não fez;
  2. usar a propriedade  RowsAffected  para ler quantas linhas foram afetadas pelo último comando executado.
RowsAffected = 0    ->  não efetuou nenhuma alteração;

RowsAffected = -1   -> quando a operação não suporta ou o banco de dados não suporta obter retorno;

RowsAffected > 0     -> número de linhas que foram alteradas;

 

Exceção:   No MS-SQLServer há situações que o retorno é -1  em decorrência deste recurso ser desligado em triggers ou procedure quando omite o comando:  SET NOCOUNT ON

 

Exemplo:

Query1.sql.text := ‘update clientes set fone = ‘xxxx-xxxx’ where codigo=1′;

Query1.execSql;

if Query1.RowsAffected>0 then

showMessage(‘Sucesso’);

 

Share Button

A API do firedac traz um componente que encapsula o nbackup do firebird o que facilita
personalizar o controle de backups. TFDFBNBackup.

Exemplo Nivel 1:

TNBackup.ExecuteNBackup(‘localhost’,’c:\dados\meubanco.fdb’,’sysdba’,’masterkey’,1,’c:\backup\b
ackup2.nbk’);

  • Segestão de como utilizar NIVEL (level):
    a) fazer backup FULL Nivel 0 para um intervalo de período (semanal);
    b) fazer backup Nivel 1, diário;
    c) fazer backup Nivel 2 para backup a cada hora.

Código base:

uses
FireDAC.Phys.IBWrapper,FireDAC.Phys.FB,FireDAC.Phys.FBDef,FireDAC.Comp.UI,FireDAC.Phys;

type

TNBackup = record
   private
     class function GetNBackup(AHost, ABanco, AUser, APass: string;
        ANivel: integer; ADestino: String): TFDFBNBackup;static;
     class function ExecuteNBackup(AHost, ABanco, AUser, APass: string;
        ANivel: integer; ADestino: String): boolean;static;
end;

class function TNBackup.ExecuteNBackup(AHost, ABanco, AUser, APass: string;
     ANivel: integer; ADestino: String): boolean;
begin
   result := false;
   with TNBackup.GetNBackup(Ahost,ABanco,AUser,APass,ANivel,ADestino) do
   try
      Backup; // gerar backup.
      result := true;
   finally
      free;
   end;
end;

class function TNBackup.GetNBackup(AHost, ABanco, AUser, APass: string;
    ANivel: integer; ADestino: String): TFDFBNBackup;
var
nBackup:TFDFBNBackup;
FDGUIxWaitCursorX: TFDGUIxWaitCursor;
FDPhysFBDriverLinkX: TFDPhysFBDriverLink;
begin
     result:=TFDFBNBackup.create(nil);
     try
        FDGUIxWaitCursorX:= TFDGUIxWaitCursor.Create(result);
        FDPhysFBDriverLinkX:= TFDPhysFBDriverLink.Create(result);
        with result do
        begin
           Level := ANivel;
           host := AHost;
           username := AUser;
           password := APass;
           protocol := ipTCPIP;
           Database := ABanco;
           backupfile := ADestino;
           DriverLink := FDPhysFBDriverLinkX;
        end;
   finally
      // liberar a instancia no metodo chamador
   end;
end;

Share Button

Lembra quantas vezes você precisou fazer um Loop em um Dataset para fazer uma soma, uma
contagem ou qualquer outra coisa…
Não gosto de fazer de novo algo que já fiz antes… Pensando nisto passei a usar “anonimous
method” do delphi para executar para mim os trechos repetitivos dos loops…
Veja como ficou.

type
TDatasetHelper = class helper for TDataset
public
   procedure DoLoopEvent(AEvent: TProc&lt;TDataset&gt;); overload;
end;

procedure TForm34.execute;
var total:Double;
begin
   // abere o Dataset com os dados.
   alQuery1.sql.Text := 'select codigo, total valor from sigcaut1 where data&gt;=:data';
   alQuery1.ParamByName('data').AsDateTime := strTodate('01/01/2016');
   alQuery1.Open;
   // fazer um loop para somar o total, usando metodos anonimos;
   total := 0;
   alQuery1.DoLoopEvent( procedure( ds:TDataset)
   begin
     total := total + ds.FieldByName('valor').AsFloat; // executa o loop
     end);
   showMessage( FloatTOStr(total) ); // mostra o total da soma obtida no loop
end;


procedure TDatasetHelper.DoLoopEvent(AEvent: TProc;TDataset;);
var
book: TBookMark;
begin
    book := GetBookmark;
    try
      DisableControls;
      first;
      while eof = false do
      begin
         AEvent(self);
         next;
      end;
    finally
    GotoBookmark(book);
    FreeBookmark(book);
    EnableControls;
  end;
end;

Código Original

Share Button

Executar uma query em segundo plano (em paralelo) não …é difícil de fazer, o seu controle é que
pode ser mais complexo.
Para executar em segundo plano basta:

TThread.CreateAnonymousThread(procedure
begin
   ALQuery1.sql.Text := '....';
   ALQuery1.Open;
end).Start;

ou

TThread.CreateAnonymousThread(
   procedure
   var i:integer;
   begin
      for I := 0 to 10 do
      begin
         // faz alguma coisa...
         AlQuery1.execSQL;
      end;
    end).Start;

Um pensamento simplista é você criar uma conexão para cada QUERY em separado. Se você tem
um CONNECTION isolado para UMA QUERY, então é possível executá-la em paralelo dentro de
uma nova Thread;

Algumas idéias onde pode utilizar o processo em paralelo:
– quando precisa registrar um log em uma tabela;
– se for possível adiantar um SELECT que será utilizado mais a frente;
– se precisa rodar um loop que não tem dependência com os próximos passos da sequencia do
código;
– quando precisa fazer um somatório de dados na tabela para mostrar o seu valor na janela…. e
liberar o usuário para continuar fazendo outras coisas.
As vezes você pode por um SELECT em paralelo e disparar outra sequencia de código… e mais
adiante aguardar o primeiro SELECT concluir… para depois então continuar…. Este é assunto para
outro POST para tratar de TTASK.