[Delphi] Afinal, qual é a melhor forma de copiar registros de um DataSet?

[Delphi] Afinal, qual é a melhor forma de copiar registros de um DataSet?

Saudações, leitores!
O artigo de hoje traz uma dúvida relativamente comum. Eventualmente, por conta das regras de negócio do cliente ou uma migração de dados, surge a necessidade de copiar vários registros de um DataSet para outro. Neste momento, uma das nossas maiores preocupações é a performance dessa operação, concordam? Confira, neste artigo, algumas formas de realizar essa cópia e a apresentação de dois ótimos recursos que o FireDAC nos oferece para essa finalidade.

 

Algumas vezes recebo e-mails com a seguinte dúvida:

André, preciso copiar vários registros entre dois DataSets. Qual a melhor forma? Executar um loop e copiar cada campo através do FieldByName?

O motivo que causa essa dúvida, a princípio, está relacionado ao requisito de desempenho da rotina. Os programadores buscam a melhor forma de codificação para reduzir, ao máximo, o tempo dispendido pela operação, afinal, quando nos referimos a estruturas de repetição, devemos ter cautela nas instruções dentro da iteração para não comprometer a experiência do usuário.
Por conta disso, como forma de contribuição para a comunidade Delphi, fiz questão de abordar este assunto no blog.
Para que o artigo fique em uma estrutura didática, cada solução foi dividida em diferentes seções, ordenadas por recomendação em ordem crescente. Vamos lá!

 

1) Executar um loop no DataSet, copiando os valores com FieldByName
A primeira forma é utilizar um loop para percorrer os registros do DataSet, copiando os valores de cada campo através do método FieldByName:

DataSetOrigem.First;
while not DataSetOrigem.Eof do
begin
  DataSetDestino.Append;
 
  DataSetDestino.FieldByName('Campo1').Value := DataSetOrigem.FieldByName('Campo1').Value;
  DataSetDestino.FieldByName('Campo2').Value := DataSetOrigem.FieldByName('Campo2').Value;
  {...}
  DataSetDestino.FieldByName('CampoN').Value := DataSetOrigem.FieldByName('CampoN').Value;
 
  DataSetDestino.Post;
 
  DataSetOrigem.Next;
end;

Há alguns anos, foram publicados alguns artigos na internet com orientações para evitar o uso do FieldByName, já que, até então, este método realizava um loop em uma lista de objetos do DataSet para encontrar o campo desejado. Porém, desde o Delphi Seattle, a lista que armazena os Fields do DataSet foi substituída pelo TDictionary, que possui uma performance evidentemente melhor. Apenas para título de conhecimento, essa alteração pode ser encontrada na classe TFields:

FDict: TDictionary<string, TField>;

Portanto, não se preocupe mais com desempenho ao utilizar o FieldByName. 🙂

 

2) Executar um loop no DataSet, copiando os valores com variáveis TField
Mesmo assim, se você prefere evitar o uso do FieldByName, existe a opção de criar variáveis do tipo TField e apontá-las para os campos do DataSet antes de iniciar as iterações:

var
  Campo1: TField;
  Campo2: TField;
  {...}
  CampoN: TField;
begin
  Campo1 := DataSetDestino.FieldByName('Campo1');
  Campo2 := DataSetDestino.FieldByName('Campo2');
  {...}
  CampoN := DataSetDestino.FieldByName('CampoN');
 
  DataSetOrigem.First;
  while not DataSetOrigem.Eof do
  begin
    DataSetDestino.Append;
 
    Campo1.Value := DataSetOrigem.FieldByName('Campo1').Value;
    Campo2.Value := DataSetOrigem.FieldByName('Campo2').Value;
    {...}
    CampoN.Value := DataSetOrigem.FieldByName('CampoN').Value;
 
    DataSetDestino.Post;
 
    DataSetOrigem.Next;
  end;  
end;

O mesmo pode ser feito para os campos de origem, mas, neste caso, teríamos o dobro de variáveis do tipo TField.

 

3) Executar um loop nos Fields para evitar a repetição de código
A terceira opção é usar um loop dentro de um loop. O primeiro itera os registros e o segundo itera os Fields do DataSet com uma instrução FOR-IN. Dessa forma, evitamos a necessidade de escrever uma linha para cada campo, tornando-se um benefício quando os DataSets possuem dezenas de campos a serem copiados:

var
  Field: TField;
begin
  DataSetOrigem.First;
  while not DataSetOrigem.Eof do
  begin
    DataSetDestino.Append;
 
    for Field in DataSetOrigem.Fields do
      DataSetDestino.Fields[Field.Index].Value := Field.Value;
 
    DataSetDestino.Post;
 
    DataSetOrigem.Next;
  end;
end;

Vale ressaltar que essa opção só é viável quando os dois DataSets possuem a mesma estrutura de Fields, inclusive na mesma ordem. Caso contrário, os valores de alguns campos no DataSet de destino poderão ficar inconsistentes, já que foram copiados de campos diferentes.

 

4) FireDAC: usando o método CopyRecord
Além da enorme quantidade de vantagens proporcionadas, a tecnologia FireDAC também trouxe o método CopyRecord, disponível para copiar todos os valores do registro atual de um DataSet. Acompanhe:

DataSetOrigem.First;
while not DataSetOrigem.Eof do
begin
  DataSetDestino.Append;
  DataSetDestino.CopyRecord(DataSetOrigem);
  DataSetDestino.Post;
 
  DataSetOrigem.Next;
end;

Não precisamos utilizar o FieldByName ou variáveis TField, sem contar que, claro, o código fica bem mais limpo. Além disso, o próprio método se encarrega de copiar apenas os campos correspondentes, caso a estrutura dos DataSets não seja equivalente.

 

5) FireDAC: o poderoso método CopyDataSet
Se eu disser que podemos executar essa cópia com apenas uma instrução, vocês acreditariam? Pois bem, o método CopyDataSet copia todos os valores de todos os registros de um DataSet, respeitando a mesma característica de campos correspondentes:

DataSetDestino.CopyDataSet(DataSetOrigem);

Um grande diferencial deste método é a performance. Em um teste rápido, usando um DataSet com 4 campos e 5.000 registros, o loop com CopyRecord levou 6.58 segundos, enquanto o CopyDataSet demorou 0.04.

 

Menos de 1 segundo?!
Exato, meu amigo! Migre já para o FireDAC! 🙂

 

Fico por aqui, pessoal.
Qualquer dúvida, observação ou contribuição, não hesite em deixar um comentário.
Abraço!


 

22 comentários

  1. O que exatamente o CopyDataSet faz nos bastidores? Estou sem muita paciência para investigar. Poderia dizer?

    1. Olá, Carlos!
      O CopyDataSet faz uma operação similar à atribuição da propriedade Data, mas com algumas diferenças. Entre elas, como já apresentei no artigo, o método copia apenas os campos correspondentes, ao invés de sobrescrever a estrutura do DataSet de destino, como acontece na atribuição do Data. Além disso, o CopyDataSet não mantém as “versões” dos registros de origem, ou seja, não copia as informações de que os registros foram inseridos, removidos ou alterados.
      Não abordei no artigo, mas o CopyDataSet aceita um conjunto de opções como segundo parâmetro. É possível, por exemplo, copiar a estrutura do DataSet de origem antes da operação, e/ou também copiar índices, campos agregados e constraints, conforme a necessidade do desenvolvedor.
      No link abaixo, do Wiki da Embarcadero, há mais detalhes deste método:

      http://docwiki.embarcadero.com/Libraries/Berlin/en/FireDAC.Comp.DataSet.TFDDataSet.CopyDataSet

      Abraço!

    1. Olá, Ramon!
      Pretendo postar um artigo exclusivo sobre os Options do FireDAC, mas, em resumo, essas são as opções disponíveis:

      • coStructure: copia a estrutura do DataSet de origem;
      • coIndexesCopy: copia os índices do DataSet de origem. O inverso é coIndexesReset;
      • coAggregatesCopy: copia os campos agregados (Aggregates) do DataSet de origem. O inverso é coAggregatesReset;
      • coConstraintsCopy: copia as constraints do DataSet de origem. O inverso é coConstraintsReset;
      • coRestart: move o cursor para o primeiro registro do DataSet de destino após a cópia (padrão);
      • coAppend: adiciona cada registro do DataSet de origem no DataSet de destino (padrão);
      • coEdit: localiza o registro no DataSet de destino para editá-lo;
      • coDelete: caso um registro tenha sido removido no DataSet de origem, localiza o registro correspondente no DataSet de destino para também removê-lo;
      • coRefresh: alterar os registros somente no cache local, diferente do coEdit.

      Para mais detalhes, acesse:

      FireDAC CopyDataSet at Embarcadero Wiki

      Abraço!

  2. Opa André!

    É possível fazer um ApplyUpdates com o DataSet de destino após o CopyDataSet?

    Tipo:

    FDQueryDestino.EmptyDataSet;
    FDQueryDestino.CopyDataSet(FDQueryOrigem);
    FDQueryDestino.ApplyUpdates(0);
    1. Boa noite, Wanderson!
      Não cheguei a fazer a teste, mas, pela proposta da função, é como se os registros estivessem sido inseridos um a um no DataSet de destino, portanto, o ApplyUpdates deveria funcionar.

      Abraço!

  3. Eu testei e funciona perfeitamente… eu estava tentando fazer de um SGDB pra Outro mas estava dando problemas e achei que poderia ser uma limitação…

    1. Opa, bom saber, Wanderson!
      Obrigado por ter feito o teste! 🙂

  4. Essas funções não aparacem no componente do ClientDataSet, a unica função disponivel é a “CopyFields”.
    estou usando o delphi Xe7

    1. Olá, Murilo, tudo certo?
      Algumas funções estão disponíveis apenas nos componentes do FireDAC.

      Abraço!

  5. Bom dia, André.
    Ótima postagem. Ajudou muito.
    Estou fazendo uma aplicação para celular no Delphi XE8 e estou com três problemas.
    O primeiro é o tamanho do aplicativo no celular que esta em 117 MB. Tem alguma forma de diminuir o tamanho dele?
    O segundo problema é o tempo de abertura dos forms quando clico nos botões no forme principal. Estou usando FDquery, e tanto tabelas com 50 registros ou 10 registros levam o mesmo tempo para abrir.
    O terceiro problema é quando abro um form dentro de outro usando uma aba de apoio. Funciona bem, mas para voltar demora muito ou muitas vezes fecha o aplicativo.

    Por exemplo, quando abro o form carteira no form alunos com o código abaixo:

    procedure TfrmCadAlunos.SpbCarteirasClick(Sender: TObject);
    var
      LayoutBase : TComponent;
    begin
      frmPrincipal.TipoCart:=1;
      Application.CreateForm(TfrmCadCarteiras, frmCadCarteiras);
      LayoutBase := frmCadCarteiras.FindComponent('lytBase');
      If Assigned(LayoutBase) then
       Begin
         lytApoio.AddObject(TLayout(LayoutBase));
       End;
      MudarAba(TabItemApoio, Sender);
      frmCadCarteiras.TipobtnCart:=1;
      frmPrincipal.Fl_Nav        :=False;
    end;

    Porém a volta no form caretiras que fica muito lento

    procedure TfrmCadCarteiras.spbVoltarClick(Sender: TObject);
    begin
      //Carrego proximo form
      AbrirForm(TfrmCadAlunos);
      MudarAba(TbItemApoio, Sender);
    end;

    Segue método “AbrirForm”:

    procedure TfrmCadCarteiras.AbrirForm(AFormClass: TComponentClass);
    Var LayoutBase : TComponent;
    begin
      //frmPrincipal.Fl_Nav:=False;
       If Assigned(FActiveForm) then
       Begin
         If FActiveForm.ClassType=AFormClass then
          Begin
            Exit
          End
          Else
          Begin
            FActiveForm.DisposeOf; //Não use Free
            FActiveForm :=nil;
          End;
       End;
       Application.CreateForm(AFormClass, FActiveForm);
       LayoutBase := FActiveForm.FindComponent('lytBase');
       //Encontra o Layoutbase no form a ser exibido para adicionar ao frmPrincipal
       If Assigned(LayoutBase) then
        Begin
          LytApoio.AddObject(TLayout(LayoutBase));
        End;
    end;
    1. Olá, Judeir, tudo bem?
      Encaminhei a sua dúvida para o MVP Landerson Gomes, que é especialista em desenvolvimento mobile com Delphi.
      Assim que ele responder, envio um e-mail para você, ok?

      Abraço!

  6. Opa, André e o:

    CloneCursor(Source: TCustomClientDataSet; Reset: Boolean; KeepSettings: Boolean = False); virtual;

    Também não faz a mesma coisa entre DataSets?

    1. Olá, Manoel, tudo bem?
      Não, o CloneCursor tem uma função diferente. Ao clonar um DataSet, você mantém uma referência ao DataSet original. Dessa forma, se você inserir, alterar ou excluir um registro no clone, a operação é automaticamente refletida no DataSet que foi clonado. Em outras palavras, os dois DataSets compartilham o mesmo conteúdo.
      Já nos métodos publicados neste artigo, os dados são efetivamente copiados para outro DataSet, tornando-os independente um do outro.

      Espero ter esclarecido a dúvida!
      Abraço!

  7. Boa Noite André,

    Infelizmente o CopyRecord não tem as mesmas opções do CopyDataSet. Preciso copiar um registo apenas para outro DataSet (TFDMemTable), incluindo os campos calculados. Tem uma saída para isso? Estou tendendo a usar o método “3) Executar um loop nos Fields para evitar a repetição de código”

    1. Olá, Bruno, tudo bem?
      Peço desculpas pela demora para respondê-lo.
      Você tem razão. O CopyRecord não possui parâmetros para cópia de campos calculados como no CopyDataSet. Para confirmar isso, entrei na documentação da Embarcadero e este item está bem explícito: “The field in the Self dataset is not calculated.”
      Acredito que, neste caso, a solução realmente é utilizar a abordagem do loop nos Fields, como você mencionou.

      Grande abraço!

  8. Boa Tarde, André.
    Eu sempre vejo seus tutoriais e queria uma ajuda. To começando no Delphi eu uso o Delphi 10.1;
    Eu tenho um DBGrid ligado a uma DataSet com um campo calculado de nome ” Ativo”. Criei uns CheckBox para marcar true ou false. Você têm algum tutorial que eu veja como varrer esse DataSet e pegar os campos marcados como True? Desde já meu muito obrigado.
    Abraços.

    1. Olá, Maurício, tudo bem?
      Para identificar os registros que foram marcados, você pode percorrer o DataSet (com uma instrução while), verificando se o campo referente ao CheckBox está com o valor “True” (ou “S”), basicamente dessa forma:

      while not DataSet.Eof do
      begin
        // Verifica se está marcado
        if DataSet.FieldByName('Marcado').AsString = 'S' then
          ShowMessage('Este registro foi marcado!');
      
        DataSet.Next; // Move para o próximo registro
      end;

      Porém, este procedimento será refletido na DBGrid, ou seja, para cada iteração do while, o cursor na DBGrid será alterado, prejudicando um pouco a performance. Como solução, você pode clonar o DataSet original (e trabalhar com o clone), ou executar um DisableControls antes de iniciar o while.

      Abraço!

  9. Mais um vez obrigado, foi de grande ajuda, mas você disse que perderia em performance. Você teria alguma outra ideia de fazer de outra forma?

    1. Olá, Maurício.
      Acredito que a melhor forma é clonar o DataSet que está ligado na DBGrid e filtrar os registros que estão marcados:

      var
        Clone: TClientDataSet;
      begin
        Clone := TClientDataSet.Create(nil);
        try
          Clone.CloneCursor(DataSetGrid, True);
          Clone.Filter := 'Marcado = ' + QuotedStr('S');
          Clone.Filtered := True;
          Clone.First;
      
          // percorre apenas os registros marcados
          while not Clone.EOF do
          begin
            { ... }
          end;
        finally
          Clone.Free;
        end;
      end;

      Como apenas os registros marcados serão percorridos, a performance fica bem melhor.
      Abraço!

Deixe uma resposta

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *