Siden Access 2010 har Access understøttet vedhæftede datatyper, som på overfladen virker som en praktisk funktion til lagring af små billeder eller filer. En hurtig google-søgning vil dog som regel vise, at de bedst undgås. Alt dette bunder i, at en Attachments-datatype faktisk er et Multi-Valued Field (MVF), og disse kommer med flere problemer. For det første ville du ikke være i stand til at bruge forespørgsler til at indsætte eller opdatere flere poster på én gang. Faktisk tvinger alle tabeller, der indeholder en sådan datatype, dig til at lave en masse kode, og alene af den grund undgår vi at bruge sådanne datatyper normalt.
Der er dog et problem. Vi elsker at bruge billedgalleriet og temaerne, som begge afhænger af en systemtabel, MSysResources
som desværre bruger de vedhæftede datatyper. Dette har skabt et problem med at administrere ressourcer i vores standardbibliotek, fordi vi ønsker at bruge MSysResources
men vi kan ikke nemt opdatere eller indsætte dem i bulk.
Vedhæftningsdatatypen (såvel som MVF'er) tvinger dig til at bruge "række-for-lidende-række"-programmering, når du har at gøre med et MVF-felt, det er en twofer med vedhæftede filer, fordi du skal bruge LoadFromFile eller
SaveToFile
metoder. Microsoft har en artikel med eksempler om disse metoder. Derfor skal du interagere med filsystemet, når du tilføjer nye poster. Ikke altid ønskværdigt i alle situationer. Nu, hvis vi kopierer fra en tabel til en anden tabel, kan vi undgå at hoppe over filsystemet ved at gøre noget som:
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
Hellige sløjfe, batman! Det er en masse kode, alt sammen bare for at kopiere vedhæftede filer fra en tabel til en anden. Selvom vi ikke hopper over filsystemet, er det også meget langsomt. Det er vores erfaring, at en tabel med 1000 poster, der indeholder en enkelt vedhæftet fil, kan tage minutter bare at bearbejde. Nu er dette ret overdimensioneret, når man tænker på størrelsen. Bordet med vedhæftede filer er ikke så stort. Faktisk, lad os lave et eksperiment. Lad os se, hvad der sker, hvis jeg kopierer og indsætter via dataark:
Så kopiering og indsættelse sker næsten øjeblikkeligt. Det er klart, at koden, der bruges til at indsætte, ikke er den samme kode, som vi ville bruge i VBA. Men vi er meget overbeviste om, at hvis vi kan gøre det interaktivt, kan vi også gøre det i VBA. Kan vi gentage hastigheden af interaktiv indsættelse i VBA? Svaret viser sig at være ja, det kan vi!
Fremskynd med …. XML?
Overraskende nok er den metode, der giver den hurtigste måde at kopiere data på, inklusive vedhæftede filer, via XML-filer. Jeg vil indrømme, at jeg ikke rækker ud efter XML-filer undtagen som en løsning på begrænsninger. I gennemsnit er XML-filer relativt langsomme i forhold til andre filformater, men i dette tilfælde har XML en stor fordel; det har ingen problemer med at beskrive MVF'er. Lad os oprette en XML-fil og undersøge de muligheder, vi får ved at importere/eksportere en XML-fil.
Efter den sædvanlige eksportguide-dialog for at indstille stien til at gemme XML-filen, får vi en dialog som denne:
Hvis vi derefter klikker på knappen "Flere valgmuligheder...", får vi denne dialog i stedet:
Fra denne dialog ser vi få flere ledetråde om, hvad der er muligt; nemlig:
- Vi har mulighed for at eksportere hele tabellen eller kun en delmængde af tabellen ved at anvende et filter
- Vi kan transformere XML-outputtet.
- Vi kan beskrive skemaet ud over indholdet af tabellen.
Jeg finder, at det er bedst at indlejre skemaet; standarden er at eksportere den, men som en separat fil. Det kan dog være udsat for fejl, og de kan glemme at inkludere XSD-filen med XML-filen. Dette kan ændres via skemafanen vist:
Lad os afslutte eksporten og tage et hurtigt kig på den resulterende XML-fils data.
Bemærk, at de vedhæftede filer er beskrevet i Data
undertræ og filindhold er base-64-kodet. Lad os prøve at importere XML-filen. Efter at have gennemgået importguiden får vi denne dialogboks:
Bemærk følgende funktioner:
- Som med eksport, har vi mulighed for at transformere XML.
- Vi kan kontrollere, om strukturen, dataene eller begge dele skal importeres
Hvis vi derefter afslutter importen af XML-filen, finder vi ud af, at den er lige så hurtig som den copy'n'paste-operation, vi gjorde.
Vi ved nu, at der er en bedre vej til at kopiere flere poster med vedhæftede filer. Men i denne situation ønsker vi at gøre dette programmatisk i stedet for interaktivt. Kan vi gøre det samme, som vi lige har gjort? Igen er svaret ja. Der er flere måder at gøre det samme på, men jeg tror, at den nemmeste metode er at bruge de 3 nye metoder, der blev tilføjet til applikationen
objekt siden Access 2010:
EksporterXML
metodeTransformXML
metodeImporterXML
metode
Bemærk, at ExportXML
metoden understøtter eksport fra forskellige objekter. Men fordi formålet her er at være i stand til at kopiere eller opdatere en masse poster i en tabel med vedhæftede felter, er den bedste objekttype for os at bruge en gemt forespørgsel. Med en gemt forespørgsel kan vi styre, hvilke rækker der skal indsættes eller opdateres, og vi kan også forme outputtet. Hvis du ser på skemadesignet af MSysResources
tabel nedenfor:
Der er et potentielt problem. Når vi bruger temaer eller billeder, henviser vi til varen ved navn, ikke ved id. Men Navn
kolonnen er ikke unik og er ikke den primære nøgle i tabellen. Derfor, når vi tilføjer eller opdaterer poster, ønsker vi at matche på Navn
kolonne, ikke Id
kolonne. Det betyder, at når vi eksporterer, bør vi sandsynligvis ikke inkludere Id
kolonne, og vi bør kun eksportere den unikke liste over Navn
for at sikre, at ressourcerne ikke pludselig går fra "Open.png" til "Close.png" eller noget fjollet.
Vi opretter derefter en forespørgsel, der skal fungere som kilden til de poster, vi vil importere til MSysResources
bord. Lad os starte med denne SQL bare for at demonstrere filtreringen ned til en undergruppe af poster:
SELECT e.Data, e.Extension, e.Name, e.Type FROM Example AS e WHERE e.Name In ("blue","red","green");
Vi gemmer den som qryResourcesExport
. Vi kan derefter skrive VBA-kode for at eksportere XML:
Application.ExportXML _ ObjectType:=acExportQuery, _ DataSource:="qryResourcesExport", _ DataTarget:="C:\Path\to\Resources.xml", _ OtherFlags:=acEmbedSchema
Dette emulerer den eksport, vi oprindeligt udførte interaktivt.
Men hvis vi derefter importerer den resulterende XML, har vi kun mulighed for at tilføje data til en eksisterende tabel. Vi kan ikke kontrollere, hvilken tabel den vil føjes til; den vil finde en tabel eller forespørgselstabel med samme navn (f.eks. qryResourcesExport
og tilføje poster til den forespørgsel. Hvis forespørgslen kan opdateres, er der ikke noget problem, og den indsættes i Eksempel
som forespørgslen er baseret på. Men hvad hvis den kildeforespørgsel, vi bruger, ikke kan opdateres eller måske ikke eksisterer? I begge tilfælde ville vi ikke være i stand til at importere XML-filen, som den er. Det kan enten mislykkes med at importere eller ende med at skabe en ny tabel med navnet qryResourcesExport
som ikke hjælper os. Og hvad med tilfældet med kopiering af data fra Eksempel
til MSysResources
? Vi ønsker ikke at tilføje data til Eksemplet
tabel.
Det er her TransformXML
metoden kommer til undsætning. En fuldstændig diskussion om, hvordan man skriver en XML-transformation, er uden for rækkevidden, men du burde være i stand til at finde rigelige ressourcer til, hvordan man skriver et XSLT-stylesheet til at beskrive transformationen. Der er flere online værktøjer, du kan bruge til at validere din XSLT også. Her er en. For det simple tilfælde, hvor vi blot vil kontrollere, hvilken tabel XML-filen skal tilføje posterne til, kan du komme i gang med denne XSLT-fil. Du kan derefter køre følgende VBA-kode:
Application.TransformXML _ DataSource:="C:\Path\to\Resources.xml", _ TransformSource:="C:\Path\to\ResourcesTransform.xslt", _ OutputTarget:="C:\Path\to\Resources.xml", _ WellFormedXMLOutput:=True, _ ScriptOption:=acEnableScript
Vi kan erstatte den originale XML-fil med den transformerede XML-fil, som nu indsættes i MSysResources
tabel i stedet for ind i (muligvis ikke-eksisterende forespørgsel/tabel) qryResourcesExport
.
Så skal vi håndtere opdateringerne. Fordi vi faktisk tilføjer nye poster og MSysResources
tabel ikke har nogen begrænsninger på de dubletnavne, skal vi sikre, at alle eksisterende poster med samme navne først slettes. Dette kan opnås ved at skrive en tilsvarende forespørgsel som sådan:
DELETE FROM MSysResources AS r WHERE r.Name In ("blue","red","green");
derefter køre det først, før du kører VBA-koden:
Application.ImportXML DataSource:="C:\Path\to\Resources.xml", ImportOptions:=acAppendData
Fordi XML-filen blev transformeret, er ImportXML
metoden vil nu indsætte dataene i MSysResources
tabel i stedet for den oprindelige forespørgsel, som vi brugte med ExportXML
metode. Vi specificerer, at den skal tilføje data til en eksisterende tabel. Men hvis tabellen ikke eksisterer, vil den blive oprettet.
Og dermed har vi opnået en masseopdatering/indsættelse af tabellen med et vedhæftningsfelt, som er meget hurtigere sammenlignet med den originale recordset-and-child-recordset VBA-kode. Håber det hjælper! Hvis du har brug for hjælp til at udvikle Access-applikationer, er du også velkommen til at kontakte os!