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.
Introdução
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
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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 a título de conhecimento, essa alteração pode ser encontrada na classe TFields
:
1 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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:
1 2 3 4 5 6 7 8 9 |
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:
1 |
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!
O que exatamente o CopyDataSet faz nos bastidores? Estou sem muita paciência para investigar. Poderia dizer?
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!
Poderia explicar as Options do CopyDataSet?
Olá, Ramon!
Pretendo postar um artigo exclusivo sobre os Options do FireDAC, mas, em resumo, essas são as opções disponíveis:
Para mais detalhes, acesse:
FireDAC CopyDataSet at Embarcadero Wiki
Abraço!
Opa André!
É possível fazer um ApplyUpdates com o DataSet de destino após o CopyDataSet?
Tipo:
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!
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…
Opa, bom saber, Wanderson!
Obrigado por ter feito o teste! 🙂
Essas funções não aparacem no componente do ClientDataSet, a unica função disponivel é a “CopyFields”.
estou usando o delphi Xe7
Olá, Murilo, tudo certo?
Algumas funções estão disponíveis apenas nos componentes do FireDAC.
Abraço!
Muito bom o artigo, Vlw!
Legal, Júnior! Abraço!
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:
Porém a volta no form caretiras que fica muito lento
Segue método “AbrirForm”:
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!
Opa, André e o:
Também não faz a mesma coisa entre DataSets?
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!
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”
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 noCopyDataSet
. 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!
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.
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:
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!
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?
Olá, Maurício.
Acredito que a melhor forma é clonar o DataSet que está ligado na DBGrid e filtrar os registros que estão marcados:
Como apenas os registros marcados serão percorridos, a performance fica bem melhor.
Abraço!
Minha situação é um pouco complexa. Com CopyDataSet não funcionou.
Preciso copiar os dados do DataSet para um DataSet desconectado de banco de dados… irá ficar em memória.
Resolvi clonando para um TFDMemTable utilizando CloneCursor.
Olá, Lindemberg!
O
CopyDataSet
não exige conexão com o banco de dados. O método copia os dados que estão em memória em um DataSet para outro DataSet também em memória. A conexão com o banco de dados ocorre apenas se houver umApplyUpdates
durante ou após a operação. No seu caso deveria funcionar!Abraço!
Parceiro, será que minha necessidade tem solução?
Precisava criar uma cópia do DataSet do FDMemTable para retorno de uma função, tipo:
Ou algo similar que retorne em um DataSet o conteúdo.
Tem como fazer?
Olá, José Soares!
Sim, é possível, e o código fica da forma como você mencionou mesmo. No exemplo abaixo, a função cria uma cópia do DataSet recebido por parâmetro:
Abraço!
Bom dia André.
Tenho Meu Form Saída de produtos. Nele tenhos 2 tabelas: uma saida_cab e a outra saida_item no caso (Mestre Detalhe).
Porém preciso que ele copie apenas o registro que está mostrando no form e inserir um novo registro com os dados do que eu copiei (menos a chave primária) que será outra! Tipo duplicar o registro de saída , porque todo mês esses mesmo registro vai ser feito!
Então então vem a pergunta!? Tem como ele fazer este tipo de processo? ou existe outro método?
Abraços.
Olá, Roberto, boa tarde! Desculpe-me pela demora.
Você pode usar o método
CopyRecord
, apresentado no artigo, e após a cópia, alterar o valor do campo referente a chave primária. Veja o exemplo:Em seguida, você pode fazer o mesmo com os itens.
Abraço!
Muito obrigado André !
André, preciso conectar no banco e ler uns dados e jogar num grid somente para consulta e encerrar a conexao, como faço pra não perder a visualização? tentei o copydataset mas nao consegui manipular a fdquery. vlw
Olá, Cleiton, boa tarde!
Não entendi muito bem o “não perder a visualização”. Você quis dizer que a aplicação fica travada enquanto a consulta dos dados é realizada?
Fico no aguardo.
Att,
Olá, usei o memtable e deu certo, vlw
Certo, Cleiton!
Olá André, fiz o uso do
DataSetDestino.Data := DataSetOrigem.Data
e doDataSetDestino.CopyDataSet(DataSetOrigem)
.Na primeira vez que executa, realmente fica muito rápido, mas se continuar na tela e realizar uma nova pesquisa (no caso uso para trazer os dados do banco para tela), fica muito lento.
Sempre dou um
DataSetDestino.EmptyDataSet
antes de rodar a rotina, mas fica lento depois da primeira vez que executa.Projeto para Mobile.
Bom dia, Rodrigo!
Que estranho. Não deveria ficar lento ao repetir a operação, já que você limpa o DataSet antes (EmptyDataSet).
Suspeito que outra rotina está sendo executada durante essa operação.
Mesmo assim, faça um teste: ao invés de limpar o DataSet, feche ele:
Se ainda assim continuar lento, entre em contato comigo pelo e-mail [email protected].
Olá André
Uma dúvida estou tentando utilizar o CopyDataSet para copiar os dados de um DataSet para outro, mas se tem um campo que é DateTime e esta null no DataSet de origem , acontece este erro:
Para resolver meu problema eu fiz a copia utilizando o FieldByName (1º exemplo) e tratando se estava null os campos de Data, mas conhece alguma outra solução mais prática?
Obrigado
Sergio
Olá, Sergio!
Vou entrar em contato com você.
Abraço!
Bom dia André. Testei o CopyDataset e realmente é mais rápido e muito while, só que eu tava querendo acompanhar o processo, tipo mostrar o processo em no Gauge ou ProgressBar. Nesse código meu são 12 mil registros… será que tem como?
Olá, Jhonlemon!
O método CopyDataSet não possui um mecanismo de CallBack para devolver quantos registros já foram copiados.
A minha sugestão, neste caso, é exibir uma tela de espera (com uma Thread, por exemplo), enquanto a operação é realizada.
Abraço!
Bom Dia, André!
Parabéns, pelo conteúdo.
Estou precisando criar uma forma do usuário subir ou descer um registro, criando uma forma de ordenação manual, tipo invertendo registros, não achei uma forma simples de fazer, tem alguma idéia?
Desde já Agradeço!
Bom dia, Sergio, tubo bem?
Para que os registros sejam ordenados, você vai precisar de um campo no DataSet que indique essa ordenação e então usá-lo na propriedade
IndexFieldNames
.Dessa forma, para “descer” um registro, incremente o valor desse campo. Para “subir”, decremente o valor.
Veja um exemplo prático abaixo. Suponha que temos esses 3 registros com suas respectivas ordenações:
Para “subir” o Registro C, basta controlar o valor do campo “Ordem”, decrementando-o para que fique dessa forma:
Porém, para isso, é necessário que você refaça a ordenação dos outros registros também. No exemplo acima, não só o registro C foi alterado, como também os registros A e B.
E, só ressaltando: lembre-se de indicar esse campo na propriedade
IndexFieldNames
:Espero ter ajudado. Abraço!
Eu poderia fazer por exemplo, a cópia de um dataset de uma conexão para outra, para “replicar” os dados?
Ex: LFDQueryTarget.CopyDataSet(LFDQuerySource) ?
Olá, Herlon, tudo bem?
Não fiz esse teste, porém, como os dados são copiados em memória, eu acredito que poderá funcionar, desde que o banco de dados de destino seja uma cópia fiel do banco de dados de origem, já que inclusive as chaves primárias são copiadas de um DataSet para o outro.
Abraço!
Boa tarde André tudo bem?
Usando o cloneCursor os valores de OldValue e NewValue não são alimentados?
Olá, Elpidio!
Já estamos nos falando por email. Abraço!
Olá, estou tentando fazer com mestre detalhe e esta acontecedo que quando faço o filtro de apenas um registro (exemplo buscando por um código específico), mostra corretamente, mas quando busco, por exemplo, vendas de um cliente específico, mostra apenas do detalhe da primeira venda os demais detalhes não aparecem, alguma dica ou um exemplo com TFDMemTable utilizando mestre detalhe?
Olá, Hilton, tudo bem?
Para que pudesse lhe dar uma resposta mais assertiva, eu teria que analisar o código. No entanto, o meu diagnóstico inicial seria:
1) A SQL está incorreta (trazendo apenas os detalhes da primeira venda)
2) O DataSet detalhe está sempre filtrado (Filter) pela primeira venda
3) Algum evento do formulário está interferindo na apresentação dos resultados
O caso mais comum é o 2º, Hilton. Veja como está a propriedade Filter do seu DataSet detalhe.
Caso não resolva, envei um email para [email protected].
Abraço!
Olá, tenho uma tela de cadastro basico onde uso FDQuery e um DataSource apenas, gostaria de testar, caso clique acidentalmente no botao sair, se houve alterações no registro e disparar um aviso de “houve alterações no registro, deseja salvar?”, existe alguma propriedade do FDQuery que me permita verificar as alterações?
obs. não uso ClientDataSet.
Olá, Humberto. Primeiramente, peço desculpas pela demora.
Você pode fazer essa verificação da seguinte forma:
Explicando: a propriedade
State
indica o estado atual da Query. Se estiver emdsEditModes
, significa que a Query está em modo de inserção (Append/Insert) ou edição (Edit). Já a propriedade ChangeCount aponta a quantidade de alterações que foram realizadas na Query, ou seja, que já foram salvas em memória (Post), mas ainda não persistidas no banco de dados.Espero que ajude, Humberto.
Abração!
Bom dia.
Como faço para converter o resultado gerado por TApolloQuery para TFDQuery?
Temos um sistema legado que ainda usa DBF e embora seja possível fazer a consulta usando o Firedac, ele precisa de um Driver ODBC para acesso a DBF e em algumas máquinas com Windows 10 ou superior não tem esse driver e dá erro, por isso, compramos um pacote da Apollo. Eu gostaria de fazer essa conversão pois a maioria das funções da nossa API retornam objeto do tipo TFDQuery.
Olá, bom dia, Adson.
Infelizmente nunca trabalhei com TApolloQuery. Na verdade, nem sabia que ele existia, rsrs.
Bom, essa conversão depende do formato que o component eTApolloQuery produz ao ser executado. Se for um formato diferente do TFDQuery (e provavelmente é, certo?), você terá que fazer a conversão “manualmente”, ou seja, percorrer o TApolloQuery e inserir os registros no componente TFDQuery. Entendo que isso pode prejudicar um pouco a performance e parece trabalhoso mas, como esses dois não se “conversam” de forma nativa, não vejo outra alternativa, ao menos que o componente TApolloQuery tenha algum método ou propriedade que permita a leitura dos dados de uma forma que o TFDQuery aceite.
Abraço!
André, descobri aqui e é muito simples !!!! rs…
result := TFDQuery(qryDBF); // qryDBF : TApolloQuery é convertido para TFDQuery
** Programando e aprendendo **
Opa, é fácil assim? Que bom! rsrs
Obrigado pelo retorno!
Bom dia André!
Estou tentando usar o CopyDataSet, mas estou recebendo o erro ‘Abstract Error’
O codigo que estou utilizando é esse:
procedure TFormConfigPedido.FormCreate(Sender: TObject);
var LDataSource: TDataSource;
LDataSet: TDataSet;
begin
FController := TControllerGeral.New;
LDataSource := TDataSource.Create(nil);
LDataSet := TDataSet.Create(nil);
LDataSource.DataSet := LDataSet;
FDataSetPreferenciaPedido := TFDDataSet.Create(nil);
FController.Preferencia.FindPedido(LDataSource);
FDataSetPreferenciaPedido.CopyDataSet(LDataSet, [coRestart, coAppend, coStructure]);
end;
Consegue me ajudar por favor?
Olá, Pedro.
Seu código parece ser estar correto. Você tem certeza que o problema acontece na linha do CopyDataSet?
Notei que você faz operações nesse código com outros objetos, por isso fiquei na dúvida. Para ter certeza, execute a aplicação em modo Debug (depuração), adicione um breakpoint nesse método e acompanhe a execução de cada linha.
Se realmente for no CopyDataSet, experimente remover os parâmetros adicionais gradativamente (coRestart, depois coAppend e depois coStructure).
Abraço!
Olá André,
Muito obrigado pela tua resposta!
O erro acontecia sim no momento do CopyDataSet mas o problema era a assinatura do objeto, ele estava com TDataSet , então eu fiz o seguinte para corrigir:
// Replicar DataSet
FDataSourcePreferenciaGeral := TFDMemTable.Create(nil);
FDataSourcePreferenciaGeral.CloneCursor(FController.Preferencia._DataSource.DataSet as TFDDataSet);
FDataSourcePreferenciaGeral.CopyDataSet(FController.Preferencia._DataSource.DataSet as TFDDataSet, [coRestart, coAppend, coStructure]);
Essa é uma parte mais enxuta do código, mas funcionou certinho.
Obrigado pelo retorno, Pedro.
Não tinha reparado que um dos objetos era de uma classe diferente. Mistério resolvido! 🙂
Abraço!