Desde o Access 2010, o Access oferece suporte ao tipo de dados Anexos que, à primeira vista, parece um recurso conveniente para armazenar pequenas imagens ou arquivos. No entanto, uma pesquisa rápida no Google geralmente mostrará que é melhor evitá-los. Tudo isso se resume ao fato de que um tipo de dados Attachments é na verdade um Multi-Valued Field (MVF), e estes vêm com vários problemas. Por um lado, você não poderá usar consultas para inserir ou atualizar vários registros de uma só vez. De fato, quaisquer tabelas que contenham esse tipo de dados forçam você a fazer muito código e, por esse motivo, evitamos usar esses tipos de dados normalmente.
No entanto, há um problema. Adoramos usar a galeria de imagens e temas, os quais dependem de uma tabela de sistema,
MSysResources
que infelizmente usa os tipos de dados de anexo. Isso criou um problema para gerenciar recursos em nossa biblioteca padrão porque queremos usar o MSysResources
mas não podemos facilmente atualizá-los ou inseri-los em massa. O tipo de dados do anexo (assim como os MVFs) força você a usar a programação “linha por linha agonizante” ao lidar com um campo MVF, é um twofer com o campo Attachments porque você teria que usar o
LoadFromFile ou SaveToFile
métodos. A Microsoft tem um artigo com exemplos sobre esses métodos. Assim, você deve interagir com o sistema de arquivos ao adicionar novos registros. Nem sempre desejável em todas as situações. Agora, se estamos copiando de uma tabela para outra, podemos evitar saltar sobre o sistema de arquivos fazendo algo como:
Dim SourceParentRs As DAO.Recordset2
Dim SourceChildRs As DAO.Recordset2
Dim TargetParentRs As DAO.Recordset2
Dim TargetChildRs As DAO.Recordset2
Dim SourceField As DAO.Field2
Set SourceParentRs = db.OpenRecordset("TableWithAttachmentField", dbOpenDynaset)
Set TargetParentRs = db.OpenRecordset("AnotherTableWithAttachmentField", dbOpenDynaset, dbAppendOnly)
Do Until SourceParentRs.EOF
TargetParentRs.AddNew
For Each SourceField In SourceParentRs.Fields
If SourceField.Type <> dbAttachment Then
TargetParentRs.Fields(SourceField.Name).Value = SourceField.Value
End If
Next
TargetParentRs.Update 'Must save record first before can edit MVF fields
TargetParentRs.Bookmark = TargetParentRs.LastModified
Set SourceChildRs = SourceParentRs.Fields("Data").Value
Set TargetChildRs = TargetParentRs.Fields("Data").Value
Do Until SourcechildRs.EOF
TargetChildRs.AddNew
Const ChunkSize As Long = 32768
Dim TotalSize As Long
Dim Offset As Long
TotalSize = SourceChildRs.Fields("FileData").FieldSize
Offset = TotalSize Mod ChunkSize
TargetChildRs.Fields("FileData").AppendChunk(SourceChildRs.GetChunk(0, Offset)
Do Until Offset > TotalSize
TargetChildRs.Fields("FileData").AppendChunk(SourceChildRs.GetChunk(Offset, ChunkSize)
Offset = Offset + ChunkSize
Loop
TargetChildRs.Update
SourceChildRs.MoveNext
Loop
TargetParentRs.Update
SourceParentRs.MoveNext
Loop
Santo looping, batman! Isso é muito código, tudo apenas para copiar anexos de uma tabela para outra. Mesmo que não saltemos sobre o sistema de arquivos, também é muito lento. Em nossa experiência, uma tabela com 1.000 registros contendo um único anexo pode levar minutos apenas para processar. Agora, isso é bastante desproporcional quando você considera o tamanho. A mesa com os acessórios não é tão grande. Na verdade, vamos fazer um experimento. Vamos ver o que acontece se eu copiar e colar via folha de dados:
Portanto, copiar e colar é praticamente instantâneo. Obviamente, o código usado para colar não é o mesmo código que usaríamos no VBA. No entanto, acreditamos muito que, se pudermos fazer isso de forma interativa, também podemos fazê-lo em VBA. Podemos replicar a velocidade da colagem interativa no VBA? A resposta é sim, nós podemos!
Acelere com …. XML?
Surpreendentemente, o método que fornece a maneira mais rápida de copiar dados, incluindo anexos, é por meio de arquivos XML. Admito que não busco arquivos XML, exceto como solução alternativa para limitações. Em média, os arquivos XML são relativamente lentos para outros formatos de arquivo, mas, neste caso, o XML tem uma grande vantagem; ele não tem nenhum problema em descrever MVFs. Vamos criar um arquivo XML e investigar os recursos que obtemos com a importação/exportação de um arquivo XML.
Após a caixa de diálogo do assistente de exportação usual para definir o caminho para salvar o arquivo XML, obteremos uma caixa de diálogo como esta:
Se clicarmos no botão “More Option…”, obteremos esta caixa de diálogo:
A partir desse diálogo, vemos mais algumas pistas sobre o que é possível; nomeadamente:
- Temos a opção de exportar a tabela inteira ou apenas um subconjunto da tabela aplicando um filtro
- Podemos transformar a saída XML.
- Podemos descrever o esquema além do conteúdo da tabela.
Acho que é melhor incorporar o esquema; o padrão é exportá-lo, mas como um arquivo separado. No entanto, isso pode ser propenso a erros e eles podem esquecer de incluir o arquivo XSD com o arquivo XML. Isso pode ser alterado por meio da guia de esquema mostrada:
Vamos terminar de exportá-lo e dar uma olhada rápida nos dados do arquivo XML resultante.
Observe que os anexos são descritos dentro do Data
o conteúdo da subárvore e do arquivo é codificado em base 64. Vamos tentar importar o arquivo XML. Depois de passar pelo assistente de importação, obteremos esta caixa de diálogo:
Observe as seguintes características:
- Assim como na exportação, temos a opção de transformar o XML.
- Podemos controlar se importamos a estrutura, os dados ou ambos
Se terminarmos de importar o arquivo XML, descobriremos que é tão rápido quanto a operação de copiar e colar que fizemos.
Agora sabemos que há um caminho melhor para copiar vários registros com anexos. Mas nesta situação, queremos fazer isso de forma programática, em vez de interativamente. Podemos fazer a mesma coisa que acabamos de fazer? Novamente a resposta é sim. Existem várias maneiras de fazer a mesma coisa, mas acho que o método mais fácil é usar os 3 novos métodos que foram adicionados ao Application
objeto desde o Access 2010:
ExportXML
método
TransformXML
método
ImportXML
método
Observe que o ExportXML
O método suporta a exportação de vários objetos. No entanto, como o objetivo aqui é poder copiar ou atualizar em massa os registros de uma tabela com campos de anexo, o melhor tipo de objeto para usarmos é uma consulta salva. Com uma consulta salva, podemos controlar quais linhas devem ser inseridas ou atualizadas e também podemos moldar a saída. Se você observar o design do esquema do MSysResources
tabela abaixo:
Há um problema em potencial. Sempre que usamos temas ou imagens, referenciamos o item pelo nome, não pelo ID. No entanto, o Nome
coluna não é exclusiva e não é a chave primária da tabela. Portanto, quando adicionamos ou atualizamos registros, queremos corresponder no Nome
coluna, não o Id
coluna. Isso significa que, quando exportamos, provavelmente não devemos incluir o Id
coluna e devemos exportar apenas a lista exclusiva do Nome
para garantir que os recursos não vão repentinamente de “Open.png” para “Close.png” ou algo bobo.
Em seguida, criaremos uma consulta para atuar como a origem dos registros que queremos importar para o MSysResources
tabela. Vamos começar com este SQL apenas para demonstrar a filtragem para um subconjunto de registros:
SELECT e.Data, e.Extension, e.Name, e.Type
FROM Example AS e
WHERE e.Name In ("blue","red","green");
Vamos salvá-lo como qryResourcesExport
. Podemos então escrever código VBA para exportar XML:
Application.ExportXML _
ObjectType:=acExportQuery, _
DataSource:="qryResourcesExport", _
DataTarget:="C:\Path\to\Resources.xml", _
OtherFlags:=acEmbedSchema
Isso emula a exportação que originalmente fizemos interativamente.
No entanto, se importarmos o XML resultante, teremos apenas a opção de anexar dados a uma tabela existente. Não podemos controlar em qual tabela ele será anexado; ele irá encontrar uma tabela ou tabela de consulta com o mesmo nome (por exemplo, qryResourcesExport
e anexar registros a essa consulta. Se a consulta for atualizável, não há problema e ela será inserida no Exemplo
em que a consulta se baseia. Mas e se a consulta de origem que usamos não for atualizável ou não existir? Em ambos os casos, não poderíamos importar o arquivo XML como está. Ele pode falhar ao importar ou acabar criando uma nova tabela chamada qryResourcesExport
o que não nos ajuda. E o caso de copiar dados de Exemplo
para MSysResources
? Não queremos anexar dados ao Exemplo
tabela.
É aí que o TransformXML
método vem para resgatar. Uma discussão completa sobre como escrever uma transformação XML está além do escopo, mas você poderá encontrar amplos recursos sobre como escrever uma folha de estilo XSLT para descrever a transformação. Existem várias ferramentas online que você pode usar para validar seu XSLT também. Aqui está um. Para o caso simples em que queremos apenas controlar em qual tabela o arquivo XML deve anexar os registros, você pode começar com este arquivo XSLT. Você pode então executar o seguinte código VBA:
Application.TransformXML _
DataSource:="C:\Path\to\Resources.xml", _
TransformSource:="C:\Path\to\ResourcesTransform.xslt", _
OutputTarget:="C:\Path\to\Resources.xml", _
WellFormedXMLOutput:=True, _
ScriptOption:=acEnableScript
Podemos substituir o arquivo XML original pelo arquivo XML transformado, que agora será inserido no MSysResources
tabela em vez de em (possivelmente consulta/tabela inexistente) qryResourcesExport
.
Em seguida, precisamos lidar com as atualizações. Porque na verdade estamos anexando novos registros, e o MSysResources
table não tem nenhuma restrição nos nomes duplicados, precisamos garantir que todos os registros existentes com os mesmos nomes sejam excluídos primeiro. Isso pode ser feito escrevendo uma consulta equivalente assim:
DELETE FROM MSysResources AS r
WHERE r.Name In ("blue","red","green");
em seguida, execute-o primeiro antes de executar o código VBA:
Application.ImportXML DataSource:="C:\Path\to\Resources.xml", ImportOptions:=acAppendData
Como o arquivo XML foi transformado, o ImportXML
O método agora inserirá os dados no MSysResources
tabela em vez da consulta original que usamos com o ExportXML
método. Especificamos que ele deve anexar dados em uma tabela existente. No entanto, se a tabela não existir, ela será criada.
E com isso, conseguimos uma atualização/inserção em massa da tabela com um campo de anexo que é muito mais rápido em comparação com o código VBA original do conjunto de registros e do conjunto de registros filho. Espero que ajude! Além disso, se precisar de ajuda com o desenvolvimento de aplicativos Access, não hesite em nos contatar!