Raportointityö toimii hienosti vuoden ajan. Se rakentaa työkirjan, täyttää taulukon kyselyn palauttamilla tiedoilla ja tallentaa sen. Sitten asiakas, jolla on viiden vuoden historia, pyytää täydellistä vientiä, rivimäärä ylittää miljoonan ja prosessi kaatuu "out of memory" -virheeseen kauan ennen kuin tiedosto saavuttaa levyn. Koodissa ei ollut mitään vikaa. Se piti koko työkirjaa RAM-muistissa voidakseen sarjallistaa sen lopuksi, ja sen vaatima muisti kasvoi samaan tahtiin sen rivimäärän kanssa, joka sen pyydettiin kirjoittamaan
Ratkaisu ei ole isompi kone. Se on erilainen kirjoitusmalli. HotXLS:n striimaava suorakirjoittaja (streaming direct writer) tuottaa OOXML-paketin vähitellen rivien saapuessa, joten sen käyttämä muisti ei riipu siitä, kuinka monta riviä kirjoitat. Se on kirjoituspuolen vastine striimaavalle lukijalle: missä lukija käy läpi valtavan taulukon rakentamatta solupuuta, kirjoittaja tuottaa sellaisen, myöskään rakentamatta solupuuta
Miksi normaali tallennuspolku kasvaa datan mukana
Tavallinen TXLSXWorkbook-polku rakentaa ensin täydellisen objektimallin. Jokainen solu arvoineen, tyyppeineen ja tyyliviitteineen elää objektina muistissa, kunnes kutsut tallennusta, jolloin koko puu sarjallistetaan pakettiin. Tämä malli on oikea, kun haluat lukea taulukon, muokata sitä, laskea uudelleen ja kirjoittaa sen takaisin, koska satunnaissaanti mihin tahansa soluun on juuri sitä, mitä muokkaaminen tarvitsee. Se on väärä malli, kun kaadat rivejä yhteen suuntaan etkä koskaan katso taaksepäin, koska maksat jokaisen rivin pitämisestä muistissa ilman mitään hyötyä. Miljoona riviä objekteja on miljoona riviä objekteja riippumatta siitä, palaatko niihin koskaan vai et
Striimaava kirjoittaja poistaa puun. Heti kun solu on kirjoitettu, siitä tulee tavuja laskentataulukon osaan, ja nämä tavut luovutetaan zip-tulosteelle. Laskentataulukon virta on ainoa puskuri, joka kasvaa, ja se kasvaa tulostepuolella, ei elävinä Delphi-objekteina keossa. Muistissa pysyy kiinteä määrä kirjanpitoa: taulukoiden nimet, muutama lippu, nykyinen rivinumero, solulaskuri. Tämä joukko ei muutu rivin yksi ja rivin kymmenen miljoonan välillä
Jaettujen merkkijonojen taulukko on ansa, ja sisäkkäiset merkkijonot (inline strings) ovat tie ulos
Useimmat striimaavat XLSX-kirjoittajat pärjäävät hyvin, kunnes ne kohtaavat tekstiä. OOXML-muoto tallentaa merkkijonot normaalisti jaettujen merkkijonojen taulukkoon (shared-string table): jokainen erillinen merkkijono kirjoitetaan kerran erilliseen osaan, ja jokainen solu, joka sisältää kyseisen merkkijonon, kantaa mukanaan indeksiä taulukkoon tekstin sijaan. Se on hyvä tilanoptimointi tiedostoille, jotka ovat täynnä toistuvia otsikoita, ja se on oletus, jota standardi tallennuspolku käyttää. Ongelma striimaavalle kirjoittajalle on julma. Deduplikointia varten taulukon on pysyttävä muistissa koko työn ajan, koska mikä tahansa vielä tuleva rivi voi toistaa jo kirjoitetulla rivillä olevan merkkijonon, ja vain täydellinen muistissa oleva kartta nähdyistä merkkijonoista voi määrittää oikean indeksin. Joten ainoa rakenne, jota striimaava kirjoittaja ei voi striimata, on juuri se rakenne, jonka on tarkoitus tehdä tiedostosta pieni. Tekstipainotteinen data kumoaa striimauksen, jota tulit hakemaan
Suorakirjoittaja sivuuttaa taulukon kokonaan. Merkkijonot kirjoitetaan sisäkkäin (inline), t="inlineStr"-soluina, joiden teksti istuu suoraan solun sisällä <is><t>-elementin kera. Kertyvää taulukkoa ei ole eikä muistissa pidettävää karttaa nähdyistä merkkijonoista, joten tekstisarakkeet eivät kuluta enempää muistia kuin numeerisetkaan sarakkeet. Kompromissi on selkeä ja sen ilmaiseminen on vaivan arvoista. Sisäkkäiset merkkijonot toistavat saman tekstin kaikkialla missä se esiintyy, joten tiedosto, jossa on monia identtisiä otsikoita, on levyllä suurempi kuin jaettuja merkkijonoja käyttävä vastaava. Kulutat tiedostokokoa ostaaksesi vakiomuistin. Yhden läpikäynnin viennissä tämä on oikea puoli kaupassa, ja zip-pakkaus imee suurimman osan toistoista joka tapauksessa ulosmenomatkalla
Tyylitaulukko saapuu lopuksi, varustettuna yhdellä päivämäärämuodolla
Tyylit aiheuttavat saman jännitteen kuin merkkijonot. Työkirja viittaa muotoiluunsa tyyliosan kautta, eikä striimaava kirjoittaja voi pitää kasvavaa tyylipalettia synkronoituna jo huuhdeltujen solujen kanssa. Suorakirjoittaja vastaa tähän pitämällä tyylitaulukon pienenä ja kiinteänä ja lähettämällä sen suljettaessa etukäteen tekemisen sijaan. Yksi oletussolumuoto kattaa tavalliset solut. Yksi päivämäärän numeromuoto kattaa päivämäärät, ja se on rekisteröity muotokoodilla yyyy-mm-dd tunnettuun sijaintiin solumuotoluettelossa
Tämä päivämäärämuoto on syy, miksi WriteDateTime on olemassa omana kutsunaan. Excelillä ei ole natiivia päivämäärätyyppiä; päivämäärä on numero, jolla on yllään päivämäärämuoto. WriteDateTime kirjoittaa arvon tavallisena sarjanumerona ja merkitsee solun tällä yhdellä päivämäärätyylillä, jotta laskentataulukko näyttää sen päivämääränä viisinumeroisen kokonaisluvun sijaan. Sen kirjoittamalla sarjanumerolla on väliä edestakaisessa matkassa. Se tallentaa TDateTime-arvon suoraan vuoden 1900 päivämääräjärjestelmän alla, mikä on sama käytäntö, jota tavallinen TXLSXWorkbook-tallennuspolku käyttää. Koska molemmat polut ovat yhtä mieltä sarjanumerosta, striimaavan kirjoittajan tuottama tiedosto lukee takaisin HotXLS-lukijan kautta ja avautuu Excelissä päivämäärillä, jotka vastaavat tarkoitustasi, ilman yhdellä heittävää virhettä tai aikakausi-yllätystä kirjoittajan ja lukijan välillä
Järjestys on pakollinen, koska tavut ovat jo menneet
Striimaus ostaa muistiprofiilinsa yhdellä säännöllä, jota sinun on noudatettava. Tuloste lähetetään sitä mukaa kun menet eteenpäin eikä siihen voi palata, joten kaikki on kirjoitettava siinä järjestyksessä kuin se esiintyy tiedostossa. Rivin sisällä solut kulkevat nousevassa sarakejärjestyksessä. Taulukon sisällä rivit kulkevat nousevassa järjestyksessä. Ei ole puskuria, jonka avulla kirjoittaja voisi lajitella solujasi jälkikäteen, koska hetki sitten sulkemasi rivi on jo tavuina zip-virrassa eikä se ole enää saavutettavissa. Jos annat sille sarakkeen 5 ja sitten sarakkeen 2 samalla rivillä, tuloste on väärin muodostettu, koska kirjoittaja yksinkertaisesti lähettää sen mitä annat sille, siinä järjestyksessä kuin sen sille annat
Riviohjelmointirajapinnassa on pieni mukavuus yleistä tapausta varten. AddRow ottaa 1-pohjaisen rivi-indeksin, mutta 0:n välittäminen tarkoittaa, että otetaan seuraava rivi edellisen jälkeen, joten peräkkäisen täytön ei tarvitse seurata ja välittää kasvavaa laskuria. Jokainen AddRow sulkee sitä edeltävän rivin, ja jokainen AddSheet sulkee sitä edeltävän taulukon, joten et koskaan nimenomaisesti lopeta riviä tai taulukkoa. Aloitat seuraavan ja kirjoittaja viimeistelee avoimen rakenteen puolestasi
Escapointi hoidetaan siellä missä teksti saapuu XML:ään
Kaikesta kirjoittamastasi tekstistä tulee osa XML-asiakirjaa, joten viisi ennalta määritettyä XML-entiteettiä on escapoitava, tai paketti on virheellinen heti, kun arvo sisältää et-merkin tai kulmasulkeen. Kirjoittaja escapoi &, <, >, " ja ' puolestasi sekä sisäkkäisessä merkkijonotekstissä että kaavatekstissä, niissä kahdessa paikassa, joissa kutsujan toimittamat merkit päätyvät merkintäkielen (markup) sisään. Välität raa'an WideString-merkkijonon ja kirjoittaja tekee siitä turvallisen. Tuotenimi, kuten Smith & Co <Ltd> tai kaava, joka viittaa lainausmerkeissä olevaan taulukon nimeen, tulee ulos oikein muodostettuna XML:nä ilman, että sinun tarvitsee tehdä mitään escapointia
Elinkaari, ja miksi Destroy sulkee silti
Paketin viimeistely on se, mikä kirjoittaa työkirjaosan, tyyliosan, sisältötyyppi- ja suhdeosat ja lopuksi zip-keskushakemiston. Tämä työ tapahtuu Close-kutsussa. Paketti, jota ei koskaan suljeta, on epätäydellinen zip, jota mikään taulukkolaskentaohjelma ei avaa, joten sulkeminen ei ole vapaaehtoista siivousta, se on vaihe, joka tekee tiedostosta kelvollisen. Suojautuakseen unohdetulta Close-kutsulta virhepolussa, Destroy suorittaa best-effort -sulkemisen, jos paketti on yhä auki, joten kirjoittajan vapauttaminen ei vuoda taustalla olevaa zip-objektia edes silloin, kun poikkeus ohitti eksplisiittisen kutsun. Luotettava malli on silti tavallinen Delphi-malli: kirjoita try-lohkon sisällä, kutsu Close ja vapauta finally-lohkossa
Suuren taulukon striimaus alusta loppuun
Työn muoto on: aloita, lisää taulukko, kaada rivejä, sulje. Alla oleva esimerkki kirjoittaa otsikkorivin ja sen jälkeen pitkän sarjan tyypitettyjä datarivejä, yhdistellen merkkijonoja, numeroita, kaavan ilman välimuistissa olevaa tulosta ja päivämäärän. Sen käyttämä muisti kymmenelle riville ja kymmenelle miljoonalle riville on sama, koska jokainen solu lähtee zip-virtaan heti kun se on kirjoitettu
uses
lxDirectWrite;
procedure StreamReport(const Path: string; RowCount: Integer);
var
W: TXLSDirectWriter;
I: Integer;
begin
W := TXLSDirectWriter.Create;
try
W.BeginFile(Path);
W.AddSheet('Sales');
// Header row, written in ascending column order
W.AddRow(1);
W.WriteString(1, 'Item');
W.WriteString(2, 'Qty');
W.WriteString(3, 'Price');
W.WriteString(4, 'Total');
W.WriteString(5, 'Date');
// Data rows; pass 0 to AddRow to take the next row automatically
for I := 1 to RowCount do
begin
W.AddRow(0);
W.WriteString(1, 'Item ' + IntToStr(I));
W.WriteNumber(2, I);
W.WriteNumber(3, 1.5 + (I mod 10));
W.WriteFormula(4, Format('B%d*C%d', [I + 1, I + 1]));
W.WriteDateTime(5, EncodeDate(2026, 1, 1) + I);
end;
W.Close; // finalises the package
finally
W.Free;
end;
end;
Toinen taulukko on yksinkertaisesti uusi AddSheet-kutsu ennen kuin jatkat, ja kirjoittaja sulkee ensimmäisen taulukon avatessaan toisen. Totuusarvoliput käyttävät WriteBoolean-kutsua, joka kirjoittaa tyypitetyn boolean-solun sen sijaan, että kirjoittaisi tekstin "True". Jos haluat vahvistaa, että tiedosto on ehjä ja matkaa edestakaisin virheettömästi, ominaisuus CellCount raportoi, kuinka monta solua kirjoitettiin, ja tuloksen lukemisen takaisin striimaavalla lukijalla pitäisi raportoida sama yhteissumma
// A second sheet of typed flags after the data sheet above
W.AddSheet('Flags');
W.AddRow(1);
W.WriteString(1, 'Name');
W.WriteString(2, 'Active');
W.AddRow(0);
W.WriteString(1, 'alpha');
W.WriteBoolean(2, True);
WriteLn(Format('wrote %d cells', [W.CellCount]));
Virtaan kirjoittaminen tiedoston sijaan tapahtuu samalla koodilla, mutta BeginFile-kutsun tilalla käytetään kutsua BeginStream. Tämä antaa palvelimelle mahdollisuuden lähettää työkirja HTTP-vastaukseen tai muistivirtaan ilman tilapäistä tiedostoa levyllä. Kirjoittaja ei omista sille välitettyä virtaa, joten säilytät sen elinkaaren hallinnan itselläsi
Kun kyseessä on palvelimen päätepiste (endpoint), joka rakentaa työkirjoja pyynnöstä, artikkelin striimaavat kirjoitukset palvelimelle ja erätöille mallit näyttävät, kuinka tämä kytketään pyynnönkäsittelijään ja ajastettuun vientiin. Kun kysymys on erittäin suurten työkirjojen laajemmistakin kustannuksista, sekä lukemisen että kirjoittamisen osalta, suurten työkirjojen suorituskyky Delphissä kattaa sen, mihin aika ja muisti todella kuluvat. Striimaava suorakirjoittaja toimitetaan osana HotXLS-komponenttia Delphille ja C++Builderille, yhdessä täydellisten luku-, muokkaus- ja tallennusohjelmointirajapintojen kanssa, joita käsitellään muualla tässä blogissa