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

Como é de conhecimento da comunidade o FireDac não tem suporte completo ao Firebird3, já que o lançamento do FB3 veio depois do lançamento do Berlin.

Quando se trabalha com Package (novidade no FB3) não é possível escolher na IDE qual o procedimento a executar no componente TFDStoredProc.
Uma forma de fazer isto é escrevendo um editor (delphi way) para auxiliar a propriedade StoredProcName…


unit Data.fireStoredProcEditor;

interface

uses
  SysUtils, Classes, DesignIntf, DesignEditors, DB;

type
  TFireStoredProcNames = class(TStringProperty)
  private
    procedure GetValues(Proc: TGetStrProc); override;

  public
    function GetAttributes: TPropertyAttributes; override;
  end;

procedure Register;

implementation

uses FireDAC.Comp.Client, FireDAC.Phys.Intf;

procedure Register;
begin
  RegisterPropertyEditor(TypeInfo(string), TFDCustomStoredProc,
    'StoredProcName', TFireStoredProcNames);
end;

{ TFireStoredProcNames }

function TFireStoredProcNames.GetAttributes: TPropertyAttributes;
begin
  result := [paValueList];
end;

procedure TFireStoredProcNames.GetValues(Proc: TGetStrProc);
var
  DB: TFDCustomStoredProc;
  qry: TFDQuery;
  eh3:boolean;
  oMetaIntf: IFDPhysConnectionMetadata;
  function iff(b:boolean;t,f:string):string;
  begin
    if b then result := t else result := f;
  end;
begin
  if (GetComponent(0).InheritsFrom(TFDCustomStoredProc)) then
  begin
    DB := TFDCustomStoredProc(GetComponent(0));
    if assigned(DB.Connection) then
    begin
      if (DB.Connection.DriverName = 'FB') then
      begin
          oMetaIntf := DB.Connection.ConnectionMetaDataIntf;
          eh3 := oMetaIntf.ServerVersion.ToString[1]='3';
          qry := TFDQuery.create(nil);
          try
            qry.Connection := DB.Connection;
            qry.SQL.Text := 'select rdb$procedure_name sName from rdb$procedures ';
            if eh3 then
               qry.SQL.Text := qry.SQL.Text+ iff(db.PackageName<>'', ' where rdb$package_name = ' + QuotedStr(DB.PackageName.ToUpper),' where rdb$package_name is null ');
            qry.Open;
            with qry do
              while eof = false do
              begin
                Proc(fieldByName('sName').asString);
                next;
              end;
          finally
            qry.Free;
          end;
      end
      else
        inherited;
    end;
  end
  else
    inherited;

end;

end.

Exemplo de uma package no FB3: DateUtils Package

Criando um Packege no Delphi para a Integração
Para integrar o novo editor é necessário criar um novo projeto Package no Delphi e incluir o código do editor.

// exemplo do projeto do Package (mínimo)
package FireEditores;
{$R *.res}
requires
  DesignIDE;
contains
  Data.fireStoredProcEditor in 'Data.fireStoredProcEditor.pas';
end.

Share Button

Construindo o Servidor RESTServer com exemplos… retornar um TDataset em um JSONObject X “Reflection”.

Para iniciar utilizei o modelo criado pelo delphi: new -> Datasnap REST Application. Criado o projeto alterei a unit abaixo:

GIT com fontes para as dependências


unit ServerMethodsUnit1;

interface

uses System.SysUtils, System.Classes, System.Json,
  Data.FireDACJSONReflect,
  Datasnap.DSServer, Datasnap.DSAuth, Datasnap.DSProviderDataModuleAdapter,
  FireDAC.Stan.StorageJSON, FireDAC.Stan.StorageBin;

type
{$METHODINFO ON}
  TServerMethods1 = class(TDataModule)
    FDStanStorageJSONLink1: TFDStanStorageJSONLink;
    FDStanStorageBinLink1: TFDStanStorageBinLink;
  private
    { Private declarations }
  public
    { Public declarations }
    // original criado pelo DELPHI
    function EchoString(Value: string): string;
    function ReverseString(Value: string): string;

    // passa o codigo do cliente e retorna os dados do cliente
    function GetCliente(codigo: int64): TJSONValue;  // exemplo retornando um JSONObject
    function GetCliente2(codigo: int64): TFDJSONDataSets;  // usando Reflection

    // retorna na mesma resposta CLIENTE + ITENS
    function GetNotaFiscal(ANumero: integer): TFDJSONDataSets;

  end;
{$METHODINFO OFF}

implementation

{$R *.dfm}

uses System.StrUtils, FireDAC.ObjectDataSet, Data.db.helper;

function TServerMethods1.EchoString(Value: string): string;
begin
  Result := Value;
end;

function TServerMethods1.ReverseString(Value: string): string;
begin
  Result := System.StrUtils.ReverseString(Value);
end;

Criando as classes a serem carregadas no TDataset usando RTTI:

type
  TClientes = class
  private
    FCodigo: int64;
    FNome: string;
    FCidade: string;
    FDebitos: double;
    FEndereco: string;
    FEstado: String;
    procedure SetCidade(const Value: string);
    procedure SetCodigo(const Value: int64);
    procedure SetDebitos(const Value: double);
    procedure SetEndereco(const Value: string);
    procedure SetEstado(const Value: String);
    procedure SetNome(const Value: string);
  public
    property codigo: int64 read FCodigo write SetCodigo;
    property Nome: string read FNome write SetNome;
    property Cidade: string read FCidade write SetCidade;
    property Estado: String read FEstado write SetEstado;
    property Endereco: string read FEndereco write SetEndereco;
    property Debitos: double read FDebitos write SetDebitos;
  end;

  TNotaFiscalItens = class
  private
    FPreco: double;
    FCodigo: string;
    FQtde: double;
    FNome: string;
    procedure SetCodigo(const Value: string);
    procedure SetNome(const Value: string);
    procedure SetPreco(const Value: double);
    procedure SetQtde(const Value: double);
  public
    property codigo: string read FCodigo write SetCodigo;
    property Nome: string read FNome write SetNome;
    property Qtde: double read FQtde write SetQtde;
    property Preco: double read FPreco write SetPreco;
  end;

Quando o cliente fizer chamada para GetCliente irá passar o código do cliente, internamente o servidor irá procurar no banco de dados e retornar um JSONObject com os dados do cliente. Este processo permite retornar uma informação mais genérica, importante quando os clientes que irão consumir tenham múltiplas linguagens ou múltiplos desenvolvedores que nem sempre serão Delphianos.


function TServerMethods1.GetCliente(codigo: int64): TJSONValue;
// TFDJSONDataSets;
var
  ds: TObjectDataSet;
  cli: TClientes;
begin
  // buscar os dados no banco de dados com codigo passado pelo cliente...

  // resposta para o cliente;

  // meus dados no firedac
  // usei um ObjectDataset somento para não precisar criar uma conexão e um query
  ds := TObjectDataSet.Create(self, TClientes);  // usa RTTI para mapear coluna do TDataset
  try
    ds.Open;
    ds.append;
    with ds do
    begin
      FieldByName('codigo').Value := 1;
      FieldByName('nome').Value := 'Embarcadero SA';
      FieldByName('Endereco').Value := 'Rua...xxxx...,10';
      FieldByName('Cidade').Value := 'Sao Paulo';
      FieldByName('Estado').Value := 'SP';
      FieldByName('Debitos').Value := 100000.12;
    end;
    ds.Post;

    // retorna um JsonObject
    Result := TJSONObject.ParseJSONValue(ds.ToJson);
  finally
    ds.Free;
  end;
end;

Se os clientes que irão consumir o serviço forem sempre RestClient de Delphi, então retornar um objeto mais preparado para a estrutura do RestClient pode ser um opção. Retornando um objetos TFDJSONDataSets aumenta os dados a serem retornados ao cliente mas pode facilitar a implementação para os cliente Delphi.

// retorna um objeto FireDAC JSON  Reflection
// isto estabelece uma boa integração entre aplicativos que usam FireDAC nos clientes.
// se o cliente não for FireDAC... talves seja interessante pensar em retornar um
//   formato mais generico

function TServerMethods1.GetCliente2(codigo: int64): TFDJSONDataSets;
var
  ds: TObjectDataSet;
  cli: TClientes;
begin
  // buscar os dados no banco de dados com codigo passado pelo cliente...

  // resposta para o cliente;
  Result := TFDJSONDataSets.Create;
  // meus dados no firedac
  // usei um ObjectDataset somento para não precisar criar uma conexão e um query
  ds := TObjectDataSet.Create(nil, TClientes);
  try
    ds.Open;
    ds.append;
    with ds do
    begin
      FieldByName('codigo').Value := 1;
      FieldByName('nome').Value := 'Embarcadero SA';
      FieldByName('Endereco').Value := 'Rua...xxxx...,10';
      FieldByName('Cidade').Value := 'Sao Paulo';
      FieldByName('Estado').Value := 'SP';
      FieldByName('Debitos').Value := 100000.12;
    end;
    ds.Post;

    TFDJSONDataSetsWriter.ListAdd(Result, 'CLIENTE', ds);
  finally
    // ds.Free;   -- eh destruido pelo Writer
  end;
end;

Para retornar mais de uma estrutura de informações no mesma chamada, pode-se utilizar TFDJSONDataSets que permite como no exemplo retornar os dados do Cliente e também os dados dos itens da nota fiscal (no exemplo) no retorno da mesma chamada.


// retornando mais de uma tabala na mesma consulta
// retorna  CLIENTE  e ITENS da nota fiscal
function TServerMethods1.GetNotaFiscal(ANumero: integer): TFDJSONDataSets;
var
  ds: TObjectDataSet;
  cli: TClientes;
  dsItens: TObjectDataSet;
  itens: TNotaFiscalItens;
begin
  // buscar os dados no banco de dados com codigo passado pelo cliente...

  // resposta para o cliente;
  Result := TFDJSONDataSets.Create;
  // meus dados no firedac
  // usei um ObjectDataset somento para não precisar criar uma conexão e um query
  ds := TObjectDataSet.Create(nil, TClientes);
  try
    ds.Open;
    ds.append;
    with ds do
    begin
      FieldByName('codigo').Value := 1;
      FieldByName('nome').Value := 'Embarcadero SA';
      FieldByName('Endereco').Value := 'Rua...xxxx...,10';
      FieldByName('Cidade').Value := 'Sao Paulo';
      FieldByName('Estado').Value := 'SP';
      FieldByName('Debitos').Value := 100000.12;
    end;
    ds.Post;

    TFDJSONDataSetsWriter.ListAdd(Result, 'CLIENTE', ds);

    // usa RTTI para mapear o objeto para TFDMemTable
    dsItens := TObjectDataSet.Create(nil, TNotaFiscalItens);
    dsItens.Open;
    itens := TNotaFiscalItens.Create;
    with itens do
    begin
      codigo := '000001';
      Nome := 'Margarina';
      Qtde := 2;
      Preco := 8.5;
    end;
    dsItens.ObjectList.Add(itens);  // adiciona o objeto ao ObjectDataSet

    with itens do
    begin
      codigo := '000002';
      Nome := 'Pao Frances';
      Qtde := 1;
      Preco := 15;
    end;
    dsItens.ObjectList.Add(itens);

    TFDJSONDataSetsWriter.ListAdd(Result, 'ITENS', dsItens);

  finally
    // ds.Free;   -- eh destruido pelo Writer
  end;
end;

.....
end.

Share Button

Clone de ObjectDataset-JEDI para FireDAC….

Encontra-se no GIT o código para carregar um TObject ou TClass diretamente em um Dataset.
A implementação faz uso de RTTI para ler as propriedades do objeto e mapeia os tipo para as colunas do TFDMemTable do FireDAC.

Exemplo de uso:


// Exemplo de uma classe para mapear no Dataset
  TMinhaClasse = class
  private
    ....
  published
    property Nome: string read FNome write SetNome;
    property id: integer read Fid write Setid;
    property valor: double read Fvalor write Setvalor;
    property Data: TDatetime read Fdata write Setdata;
    property Dinheiro: Currency read FDinheiro write SetDinheiro;
    property Cliente: double read FCliente write SetCliente;
  end;

.....
uses FireDac.ObjectDataSet;
.....
// iniciando o Dataset com o link da Classe...
var ods:TObjectDataset;
begin
  ods := TObjectDataset.Create(self, TMinhaClasse);
  //DataSource1.DataSet := ods; // associa o Dataset a um Datasource
  ods.Open;
end;

Código Fontes no GIT

Lidando com uma lista externa

A classe mantem uma lista interna com os objetos publicando pela propriedade  TObjectDataset.ObjectList,  se você já possui uma lista externa pode utilizar os métodos:

procedure LoadFromList( AList:TList );   // carrega a lista externa para o TDataset
procedure SaveToList( AList:TList );        //  preeche a lista externa com os dados do TDataset;

Emprestando o exemplo do amigo (Marlon)

   TCliente;
   var
     cli : TCliente;
     ods : TObjectDataSet;
   begin
     cli := TCliente.Create;
     cli.ID := 1;
     cli.Nome := ''Marlon;
     cli.Salario := 123.45;
     cli.Dt_Nasc := Date;
     ods := TObjectDataSet.Create(Self, TCliente);
     ods.Open;
     DataSource1.DataSet := ods;

     // pode acrescentar o registro no Dataset;
     ods.append;
     ods.ObjectToField(cli);   // não precisa fazer free... a lista interna faz a liberação de memoria.
     ods.post;

     // outra opção seria:
     ods.append;
     ods.FieldByname('id').asInteger := 1;
     ods.FieldByName('nome').asString := 'Marlon';
     ...
     ods.post;

 

Contribuição do  Marlon


unit Unit56;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, FireDac.ObjectDataSet,
  FireDAC.Stan.Intf, FireDAC.Stan.Option, FireDAC.Stan.Param,
  FireDAC.Stan.Error, FireDAC.DatS, FireDAC.Phys.Intf, FireDAC.DApt.Intf,
  Data.DB, FireDAC.Comp.DataSet, FireDAC.Comp.Client, Vcl.Mask, Vcl.DBCtrls,
  Vcl.StdCtrls, Vcl.ExtCtrls, Vcl.Grids, Vcl.DBGrids;

type

  TCliente = class
  private
    FDt_Nasc: TDate;
    FSalario: Currency;
    FID: Integer;
    FNome: String;
    procedure SetDt_Nasc(const Value: TDate);
    procedure SetID(const Value: Integer);
    procedure SetNome(const Value: String);
    procedure SetSalario(const Value: Currency);
  public
    property ID : Integer read FID write SetID;
    property Nome : String read FNome write SetNome;
    property Salario : Currency read FSalario write SetSalario;
    property Dt_Nasc : TDate read FDt_Nasc write SetDt_Nasc;
  end;


  TForm56 = class(TForm)
    Edit1: TEdit;
    Edit2: TEdit;
    Edit3: TEdit;
    Edit4: TEdit;
    Button1: TButton;
    DBEdit1: TDBEdit;
    DBEdit2: TDBEdit;
    DBEdit3: TDBEdit;
    DBEdit4: TDBEdit;
    DataSource1: TDataSource;
    DBGrid1: TDBGrid;
    DBNavigator1: TDBNavigator;
    Button2: TButton;
    procedure Button1Click(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure Button2Click(Sender: TObject);
  private
    ods : TObjectDataSet;
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form56: TForm56;

implementation

{$R *.dfm}

{ TCliente }

procedure TCliente.SetDt_Nasc(const Value: TDate);
begin
  FDt_Nasc := Value;
end;

procedure TCliente.SetID(const Value: Integer);
begin
  FID := Value;
end;

procedure TCliente.SetNome(const Value: String);
begin
  FNome := Value;
end;

procedure TCliente.SetSalario(const Value: Currency);
begin
  FSalario := Value;
end;

procedure TForm56.Button1Click(Sender: TObject);
var
  cli : TCliente;
begin
  cli := TCliente.Create;

  ods.FieldToObject(cli);

  Edit1.Text := IntToStr(cli.ID);
  Edit2.Text := cli.Nome;
  Edit3.Text := CurrToStr(cli.Salario);
  Edit4.Text := DateToStr(cli.Dt_Nasc);

end;

procedure TForm56.Button2Click(Sender: TObject);
var
  cli : TCliente;
begin
  cli := TCliente.Create;

  cli.ID := StrToIntDef(Edit1.Text, 0);
  cli.Nome := Edit2.Text;
  cli.Salario := StrToCurrDef(Edit3.Text,0);
  cli.Dt_Nasc := StrToDateDef(Edit4.Text, Date);

  ods.Insert;
  ods.ObjectToField(cli);
  ods.Post;

end;

procedure TForm56.FormCreate(Sender: TObject);
begin
  ods := TObjectDataSet.Create(Self, TCliente);
  ods.Open;
  DataSource1.DataSet := ods;
end;

end.

 

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

Este artigo foi revisão – acesse o link aqui

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 &nbsp;PEDIDOS &nbsp;(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&nbsp;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 ; 

Parte 2


Share Button

Algumas vezes já me declarei com pouca disposição em escrever uma infinidade de linhas de código para fazer coisas simples.
Depois de ver outros artigos tratando sobre a geração de chave de relacionamento em “Master x Detail” e ver códigos de colegas montando engenhocas para obter a chave gerada pelo banco de dados (auto incremento), vou arriscar por pimenta nesta conversa.

Contexto: É fato que é muito comum gerar colunas de auto incremento no banco de dados para criar o relacionamento entre tabelas. Dependendo do banco de dados isto é efetuado por tipagem da coluna direto na tabela como auto incremento (ex: mysql) ou em outros casos se usa triggers para gerar a sequência (ex: Firebird); Num ou outra situação ocorre a mesma coisa, o identificador é gerado pelo servidor sem conhecimento do aplicativo.
Neste contexto é possível obter a chave gerada diretamente na mesma instrução de INSERT como ocorre no Firebird ou ainda, através de recurso da API de acesso ao banco como existe no FIREDAC.

Como fazer com comando direto no Firebird

No FireDAC há a possibilidade de escrever diretamente por comandos para a API do FireDAC e instruí-lo a retornar através de “preprocessing”:

    insert into fb_pedido 
                         (codigo,qtde) 
                    value( :codigo, :qtde)  
                    returning id_pedido {into :id_pedido}

Documentação Embarcadero

Proposta para simplificar:
Já há algum tempo que as linguagens incluem um gerador de identificador único para múltiplos propósito – falando de GUID – que gera uma sequência única por máquina a cada chamada (ligado ao relógio).
Então a idéia é criar na tabela pedido uma coluna que recebe a nova chave única que servirá para o relacionamento do pedido (Master & Detail):
Firebird:

     // GID - coluna para receber o GUID (texto)
     alter table tb_pedido add GID varchar(38) not null;

usando o delphi, gerar localmente o GID e incluí-lo no insert.

     var gid:string;
     ...
     gid := GuidToString( TGuid.NewGuid );   //(SysUtils) chave de relacionamento do pedido
     ...
     insert into tb_pedido
                          ( gid, codigo, qtde) 
                    values( :gid, :codigo, :qtde)


Uma vez gerado a chave GID (GUID ID), os itens passam a receber a mesma informação para indicar o relacionamento entre as tabelas… ou seja, não precisamos daqueles “auto incrementos” que as vezes traz uma lista de códigos associados.

 

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;