Zadatak izvještavanja radi posve u redu čitavih godinu dana. Gradi radnu knjigu, ispunjava list sa svime onime što upit vraća i zatim to sprema. Tada kupac s pet godina povijesti zatraži potpuni izvoz (export), broj redaka premaši milijun, a proces se sruši s greškom zbog nedostatka memorije davno prije nego što datoteka uopće dosegne disk. Ništa tu zapravo nije bilo krivo s kodom. On je naime cijelu radnu knjigu samo držao u RAM memoriji kako bi je na samom kraju mogao serijalizirati, te je onda u tom pogledu memorija koju je trebao rasla u korak s brojem redaka koji se od njega uopće zatražio da ih zapiše
Rješenje se ne krije u većem stroju. Riječ je uistinu tek o drugačijem modelu pisanja. Izravni pisač sa strujanjem (streaming direct writer) unutar HotXLS-a emitira OOXML paket postepeno dok redovi pristižu, stoga iskoristiva memorija ne ovisi o tome koliko točno redaka pritom pišete. To je samo pandan sa same strane pisanja prema čitaču sa strujanjem (streaming reader): ondje gdje naime čitač hoda po ogromnom radnom listu potpuno bez onog uobičajenog kreiranja stabla ćelija, pisač na jednak način proizvodi sve isto posve izbjegavajući bilo kakvu takvu izgradnju samog stabla ćelije (cell tree)
Zašto uobičajeni put spremanja raste skupa s podacima
Uobičajeni put alata TXLSXWorkbook prvenstveno uvijek gradi puni model objekata. Svaka ćelija tu, sa svojom pratećom vrijednošću, zatim zadanom vrstom i odgovarajućom referencom na stil, živi isključivo kao nekakav obični objekt unutar memorije sve do trenutka kada vi ne prozovete spremanje, a u tom istom trenutku cjelokupno spomenuto stablo biva potom serijalizirano izravno u taj paket. Ta vrsta navedenog modela upravo je onaj pravi odabir onda kada zbilja želite samo isčitavati nekakav list, redovno ga uređivati, pa opet iznova preračunavati i pritom zatim natrag upisivati, s obzirom na to da je navedeni nasumični pristup (random access) upravo apsolutno sve do čega je redovnom uređivanju uistinu posve stalo i nasušno zapravo potrebno. S druge pak strane takav izabran model uveliko predstavlja itekako posve krivi odabir u onom datom trenu kada izričito sve redove ustvari isključivo sasvim redovito nezaustavljivo istačete u jednom istom jedinstvenom i posve zadanom smjeru ne obazirući se apsolutno više nigdje nazad, prvenstveno radi logičnog razloga pošto u tom specifičnom trenutku sasvim uobičajeno takozvano prisutnost i uskladištenje tog istog objekta itekako sasvim bezrazložno redovno plaćate a usput potpuno bez nekakve logične prednosti. Milijun redaka prepunih objekata u konačnici i jest naime milijun redaka objekata, bez obzira na to hoćete li im se u konačnici doista uopće ikada ikako iznova zbilja više i vraćati natrag
Pisač za strujanje (streaming writer) uklanja ovo stablo. Čim se izabrana ćelija zapiše, ona redovno u dijelu tog radnog lista jednostavno isključivo postaje običan navedeni iskazani skup bajtova a ti se isti pridruženi zapisi potom slobodno sasvim i u potpunosti posve logično uručuju prema sasvim prepoznatljivom redovnom spomenutom predviđenom izlazu kao i ZIP formatu. Navedeni tok radnog lista raste isključivo na izlaznoj strani, a ne u obliku živih Delphi objekata na hrpi (heap). Ono što doista ostaje prisutno samo je uobičajeno uistinu fiksna količina knjigovodstvene obveze: nadasve imena radnih listova, zatim usput nekoliko zastavica, trenutni redni broj retka i jedinstveno brojač samih ćelija. Takav skup apsolutno i isključivo ne mijenja se uopće naravno ni između prvog i desetmilijuntog retka
Tablica dijeljenih nizova znakova jest zamka, a ugrađeni nizovi (inline strings) tu su rješenje
Većina XLSX pisača (writers) sa strujanjem funkcionira relativno dobro sasvim dok ne naiđe na tekst. OOXML format po svome uobičajenom obrascu stringove sprema u tablicu dijeljenih nizova znakova: svaki zaseban string zapisuje se po jedanput u odvojeni dio, nakon čega svaka ćelija koja drži dotični string nosi sa sobom umjesto teksta samo indeks usmjeren natrag ravno prema tablici. To predstavlja izvrsnu optimizaciju prostora za datoteke ispunjene ponavljajućim oznakama, i to je zasigurno zadana (default) opcija koju standardni put spremanja i koristi. No, problem je s druge strane za pisač sa strujanjem iznimno brutalan. Za uspješno dedupliciranje tablica mora i dalje ostati prisutna u memoriji za cijeli posao, budući da bilo koji redak koji tek treba stići može slobodno ponoviti string iz retka koji je već uistinu zapisan, pa stoga jedino samo potpuna mapa ranije uočenih stringova zbilja može dodijeliti ispravan indeks. Tako ispada da ona jedina struktura koju pisač sa strujanjem doista uopće ne može strujati (stream) jest upravo ta ista struktura koja bi ustvari naoko trebala samu datoteku učiniti manjom. Masovni tekstualni podaci samim time nadasve apsolutno poražavaju strujanje po koje ste uopće i došli
Izravni pisač izbjegava tablicu u potpunosti. Stringovi se zapisuju unutar teksta (inline) kao ćelije vrste t="inlineStr" čiji tekst počiva izravno unutar ćelije elementom <is><t>. Nema tablice koju biste trebali akumulirati, a niti mape viđenih stringova koju biste zadržali, tako da tekstualni stupci ne koštaju ništa više memorije nego oni brojevni. Takva zamjena (trade) izričito je nedvosmislena i svakako je valja jasno javno iskazati. Ugrađeni (inline) stringovi iznova ponavljaju posve isti sadržaj gdjegod se on pojavi, stoga je datoteka s hrpom istih identičnih oznaka bitno veća na samom disku od svog ekvivalenta sa tablicom dijeljenih nizova znakova (shared-string). Svjesno trošite veličinu datoteke ne biste li tako kupili u zamjenu konstantnu memoriju. Za jednokratni (one-pass) izvoz to svakako i jest prava strana tog dogovora, dok istovremeno ZIP kompresija ionako pri izlasku i sama po sebi apsorbira veliki dio tog uočenog ponavljanja
Tablica stilova pristiže pri samom kraju, i to sa samo jednim jedinim datumskim formatom
Stilovi donose potpuno jednaku problematiku i napetost baš poput samih stringova. Radna knjiga referencira svoja oblikovanja (formatting) putem dijela za stilove, a pisač sa strujanjem ne može održavati rastuću paletu stilova ukorak s ćelijama koje je on već isprao i izbacio (flushed). Izravni pisač na ovo sasvim mirno odgovara držeći tablicu stilova prilično malom i posve fiksnom, izbacujući je stoga tek prilikom krajnjeg zatvaranja, umjesto naravno da to odradi unaprijed. Taj jedan zadani (default) format pokriva obične ćelije. Jedan datumski brojčani format pokriva datume, a pritom biva registriran sa sasvim formatnim kodom obrasca yyyy-mm-dd na jednoj već otprije potpuno poznatoj poziciji unutar popisa formata samih ćelija
Taj datumski format zapravo je i pravi razlog zašto navedeni poziv oblika WriteDateTime i postoji kao vlastiti izabran poziv. Excel nema nikakav domaći (native) tip datuma; datum je zapravo ustvari broj odjeven unutar datumskog formata. Alat WriteDateTime ispisuje zadanu vrijednost u obliku sasvim običnog serijskog broja te posve slobodno označava zadanu ćeliju isključivo uz pomoć onog jednog datumskog stila, tako da je proračunska tablica bez puno muke samo jednostavno renderira (renders) kao odabrani i očekivani datum umjesto pukog peteroznamenkastog cijelog broja (integer). Serijski broj koji takav alat ispisuje iznimno je važan za zaokruženi povratak (round-tripping). Zadržava TDateTime vrijednost isključivo ravno pod uzorom na onaj odabrani i isporučeni uobičajeni datumski sustav iz nimalo sporne predviđene 1900. godine, a što i je potpuno jednaka vrsta konvencije onog sasvim posve klasičnog zadanog TXLSXWorkbook uskladištenog poznatog puta spremanja. S obzirom da su oba puta suglasna oko serijskog broja, isporučena datoteka koju proizvodi pisač sa strujanjem sasvim glatko se ponovno iščitava kroz HotXLS čitač te je uz to posve na koncu uredno otvara u Excel programu uz datume koji apsolutno odgovaraju onomu što ste namjeravali dobiti, i to bez ikakve greške od jednog dana (off-by-one) ili nekakvih iznenađenja epohe između samog pisača i čitača
Redoslijed je ovdje obvezan, naprosto stoga što su bajtovi već uveliko otišli
Strujanje (streaming) kupuje svoj izgrađeni memorijski profil uz jedno jedino pravilo koje strogo morate poštovati. Izlaz se nezaustavljivo ispušta dok radite i uopće se kasnije ne može ponovno revidirati (revisited), pa zbog toga sve morate zapisati striktno onim točnim redoslijedom kojim se svaka stavka uopće pojavljuje u datoteci. Unutar istog retka, ćelije idu potpuno uzlaznim redoslijedom stupaca. Unutar istog lista, redovi se nižu posve u uzlaznom smjeru. Ovdje dakle uopće ne postoji nekakav navedeni usput dodan buffer koji pisaču prepušta sortiranje zadanih ćelija naknadno nakon izvršenog zapisivanja (after the fact), jer je redak kojeg ste eto maloprije zbilja upravo potpuno uspješno zatvorili već odavno pretvoren u sasvim obične bajtove unutar ZIP toka te više nigdje i ne može uopće biti dostupan. Prepustite mu npr. stupac s oznakom 5 a onda potom onaj pod oznakom 2 u tom istom redu i zasigurno ćete na izlazu odmah primijetiti doista oštećen izlazni redak (malformed), budući da izravni pisač naprosto emitira isključivo ono što mu vi predate i to točno onim nizom kakav ste mu i predložili
Row API posjeduje malu pogodnost za redovne klasične slučajeve. AddRow uzima indeks retka koji počinje s brojem 1, no prosljeđivanje nule 0 znači da se preuzima idući redak tik iza onog prethodnog, tako da sekvencijsko punjenje ne mora pratiti i prosljeđivati rastući brojač. Svaki poziv AddRow zatvara redak koji se nalazio prije njega, a svaki AddSheet zatvara radni list koji je bio ispred njega, tako da nikada eksplicitno ne završavate redak ili radni list. Vi samo započinjete idući a pisač umjesto vas finalizira otvorenu strukturu
Izbjegavanje (escaping) rješava se upravo tamo gdje tekst nadasve ulazi izravno u taj priloženi i izabrani XML
Svi tekstovi koje doista zapišete automatski postaju ključni odabrani dio nekog XML dokumenta, zbog čega isključivo tih jedinstvenih pet predviđenih zadanih XML oblika cjelina redovno mora biti preskočeno (escaped) ili vaš paket logično postaje posve neispravan istog trenutka kad takva posve prepoznatljiva vrijednost uopće sadrži ampersand ili kutnu zagradu. Pisač preskače &, <, >, " i ' umjesto vas, kako kod ugrađenog stringa tako i kod teksta same formule, to jest na ona dva mjesta gdje se znakovi isporučeni od pozivatelja nalaze unutar oznake (markup). Vi propuštate sirovi WideString a pisač se brine da on ostane siguran. Ime proizvoda poput Smith & Co <Ltd> ili formula koja referencira ime citiranog radnog lista izlaze kao dobro oblikovan XML bez ikakvog izbjegavanja (escaping) s vaše strane
Životni ciklus, te odgovor na pitanje zašto i samo Destroy funkcija zatvara sustav
Završavanje paketa je ono što zapravo zapisuje dio radne knjige, dio za stilove, odjeljke za vrste sadržaja i odnose, te u konačnici središnji ZIP direktorij. Taj posao dešava se unutar funkcije Close. Paket koji nikada ne biva zatvoren nepotpun je ZIP format kojeg nijedan program za proračunske tablice zapravo neće moći ispravno otvoriti, pa stoga samo zatvaranje zasigurno nije stvar proizvoljnog čišćenja, već je to ključni korak koji datoteku čini važećom (valid). Kako bi vas osigurala protiv zaboravljenog Close poziva na putanji pogreške, funkcija Destroy izvodi najbolje moguće (best-effort) zatvaranje ukoliko je paket i dalje otvoren, kako samo oslobađanje pisača ne bi propustilo (leak) pozadinski ZIP objekt čak ni onda kad sama iznimka slučajno preskoči eksplicitni poziv. Pouzdan obrazac i dalje ostaje onaj klasičan za Delphi: uvijek pišite unutar try bloka, pozovite Close, te potom sasvim uredno oslobodite unutar bloka finally
Strujanje (streaming) vrlo dugačkog radnog lista od početka pa potpuno do samog kraja
Oblik posla svodi se na sljedeće: započnite, dodajte list, izlijevajte redove, i zatvorite (close). Primjer ispod zapisuje zaglavni redak, a zatim slijedi jedan dugačak niz upisanih redaka podataka, koji spaja stringove, brojeve, formulu potpuno bez predmemoriranog (cached) rezultata, kao i sam datum. Memorija koju pri tome troši za deset redaka kao i ona koju troši za deset milijuna redaka ostaje sasvim ista, iz prostog razloga što naprosto svaka ćelija putuje prema zadanom ZIP toku uistinu odmah onog trenutka kada biva zapisana
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;
A drugi list zapravo jednostavno predstavlja tek dodatni AddSheet poziv neposredno prije no što nastavite, a pri tome sam pisač uredno zatvara onaj prvi list istovremeno kako bi odmah mogao otvoriti onaj drugi po redu. Booleove zastavice (flags) izričito koriste WriteBoolean koji uredno zapisuje isključivo jednu tipiziranu booleovu ćeliju, radije no sam tekst s oznakom "True". Ako pak doista zbilja želite uistinu i potvrditi da je ova datoteka sasvim uobičajeno sigurna i zvučna te kako glatko podržava proces zaokruženih ciklusa izmjena (round-trips), svojstvo (property) pod nazivom CellCount vrlo točno prebrojava i jasno iskazuje koliko je točno zadanih ćelija upravo bilo zapisano, pa bi shodno tome logično čitanje tog povratnog rezultata čitačem toka u suštini moralo bez greške navesti sasvim identičan ukupan zbroj
// 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]));
Zapisivanje unutar toka a ne datoteke podrazumijeva korištenje potpuno jednakog koda samo što na mjesto BeginFile stupa opcija BeginStream, i to tako ostavlja serveru otvoren prolaz kako bi radnu knjigu odmah zbilja mirno proslijedio u obliku nekog predviđenog HTTP odgovora (response) odnosno samog memorijskog toka i to sasvim bez ikakvog spremanja privremene zabilježene datoteke na disk. Priloženi tok na taj način uopće ni ne pada u stvarno vlasništvo samog pisača, pa tako jednostavno samo redovno i nadalje i dalje sasvim uobičajeno zadržavate potpunu logičnu te jasnu razinu uspostavljene cjelokupne kontrole nad njegovim uobičajenim samim predviđenim jako uobičajenim uspostavljenim isporučenim cjelokupnim vremenom njegova osiguranog trajanja (lifetime)
Kada rad podrazumijeva posve običan "server endpoint" koji radne knjige radi po zahtjevu, navedeni odabrani uzorci iz dijela o strujanju pisanja za server i batch procese (batch jobs) precizno vam u potpunosti pokazuju kako isporučiti ovo i tako pretvoriti u regularni dodatak uz osmišljeni rukovatelj zahtjeva (request handler) uz planirani zakazani redovni izvoz (scheduled export). S druge strane u pogledu onog daleko opsežnijeg širokog razmatranja širih cjelokupnih izuzetnih troškova za zbilja nadasve veoma dugačke cjelovite zabilježene jako masivne knjige, i to s pogledom na navedeno klasično sasvim redovito čitanje a ujedno zbilja i na samo izrazito opsežno navedeno zapisivanje, tema o navedenim uobičajenim pretežno izrazito jako očekivanim performansama prevelike jako uskladištene uobičajeno naravno radne predočene knjige naravno naoko uistinu Delphiju naveliko navodi upravo sva ona mjesta u koja u potpunosti vrijeme nestaje pa se doista redovno nimalo izrazito naveliko naoko naravno zacrtano utopi a nadasve i u kojima pretežno i sva pretežno nadasve uistinu usput jedinstveno posve dodana naveliko zacrtana memorija naravno zapravo nestane. Pisač uistinu naravno koji uobičajeno doista nadasve donosi doista izravno naravno strujanje naoko zadanom usput isporučuje uistinu usput naoko zadanom naravno doista iznimnom i zacrtanom se nadasve naizgled usput posve nadasve glatko naveliko i opsežno naoko usput i zadanom zacrtano nadasve kao usput uistinu zadanom dio uistinu doista nadasve zadanom doista pretežno usput naoko nadasve uskladištenom nadasve HotXLS navedenom zadanom i naoko zacrtano naravno komponente zadanom nadasve zadanom usput naravno usput i naizgled iznimnom zadanom zacrtanom doista naravno usput namijenjene zadanom naoko naravno Delphi uskladištenom nadasve naizgled naveliko naravno zadanom kao usput i naravno usput C++Builder uobičajeno naoko iznimnom zadanom uistinu naoko navedenom rješenju, zadanom zacrtanom doista opsežnoj i naravno usput skupa nadasve i naveliko s naoko usput doista usput usput uskladištenom svim usput ostalim usput zadanom naveliko i zacrtanom usput punim naoko zadanom read, usput naravno naoko naveliko edit naoko zadanom nadasve uistinu nadasve zacrtanom usput naravno doista nadasve naravno te naveliko opsežno usput zadanom naveliko usput zadanom i usput naoko naravno save zadanom doista uistinu naoko nadasve API zacrtano dodanim naveliko naizgled alatima zadanom naravno usput koji naveliko uskladišteno su zadanom nadasve usput doista naoko naoko uistinu pretežno nadasve detaljno naravno objašnjeni naoko uistinu zacrtanom naoko i naizgled zadanom zacrtano ovdje usput doista naoko i naravno naravno nadasve uobičajeno naoko usput naoko na uobičajeno nadasve naoko usput naizgled naveliko zadanom ovom zadanom nadasve naravno istom naravno usput uistinu uobičajeno naravno blogu