Un job de raportare rulează bine timp de un an. Construiește un registru de lucru, umple o foaie cu ce returnează interogarea și îl salvează. Apoi un client cu cinci ani de istoric solicită un export complet, numărul de rânduri depășește un milion, iar procesul se prăbușește cu o eroare de memorie insuficientă mult înainte ca fișierul să ajungă pe disc. Codul nu avea nicio problemă. Reținea întregul registru de lucru în RAM pentru a-l serializa la final, iar memoria de care avea nevoie creștea pas cu pas cu numărul de rânduri pe care era solicitat să le scrie
Soluția nu este o mașină mai mare. Este un model de scriere diferit. Scriitorul direct în flux din HotXLS emite pachetul OOXML incremental pe măsură ce rândurile sosesc, deci memoria pe care o folosește nu depinde de câte rânduri scrieți. Este contrapartea de scriere a cititorului în flux: acolo unde cititorul parcurge o foaie uriașă fără a construi un arbore de celule, scriitorul produce una fără a construi un arbore de celule nici el
De ce calea normală de salvare crește odată cu datele
Calea obișnuită TXLSXWorkbook construiește mai întâi un model de obiecte complet. Fiecare celulă, cu valoarea, tipul și referința de stil, trăiește ca un obiect în memorie până când apelați salvarea, moment în care întregul arbore este serializat în pachet. Acel model este cel potrivit atunci când doriți să citiți o foaie, să o editați, să o recalculați și să o scrieți înapoi, deoarece accesul aleator la orice celulă este exact ceea ce are nevoie editarea. Este cel greșit atunci când turnați rânduri într-o singură direcție și nu vă uitați niciodată înapoi, deoarece plătiți pentru a menține fiecare rând rezident fără niciun beneficiu. Un milion de rânduri de obiecte este un milion de rânduri de obiecte indiferent dacă le mai revizitați sau nu
Scriitorul în flux elimină arborele. De îndată ce o celulă este scrisă, devine octeți în partea foii de lucru, iar acei octeți sunt predați ieșirii zip. Fluxul foii de lucru este singurul buffer care crește, și crește pe partea de ieșire, nu ca obiecte Delphi active pe heap. Ceea ce rămâne rezident este o cantitate fixă de evidențe: numele foilor, câteva indicatori, numărul rândului curent, un contor de celule. Acel set nu se schimbă între rândul unu și rândul zece milioane
Tabela de șiruri partajate este capcana, iar șirurile inline sunt calea de ieșire
Cei mai mulți scriitori XLSX în flux se descurcă bine până întâlnesc text. Formatul OOXML stochează în mod normal șirurile într-o tabelă de șiruri partajate: fiecare șir distinct este scris o singură dată într-o parte separată, iar fiecare celulă care conține acel șir poartă un index în tabelă în loc de text. Este o bună optimizare de spațiu pentru fișierele pline de etichete repetate, și este implicita pe care o folosește calea de salvare standard. Problema pentru un scriitor în flux este brutală. Pentru a deduplica, tabela trebuie să rămână rezidentă pe toată durata jobului, deoarece orice rând care urmează să vină ar putea repeta un șir dintr-un rând deja scris, și numai o hartă completă în memorie a șirurilor văzute poate atribui indexul corect. Deci singura structură pe care un scriitor în flux nu o poate transmite în flux este tocmai structura care ar trebui să facă fișierul mic. Datele bogate în text anulează fluxul pentru care ați venit
Scriitorul direct ocolește tabela complet. Șirurile sunt scrise inline, ca celule t="inlineStr" al căror text se află direct în interiorul celulei cu un element <is><t>. Nu există nicio tabelă de acumulat și nicio hartă a șirurilor văzute de reținut, deci coloanele text nu costă mai multă memorie decât cele numerice. Compromisul este explicit și merită spus clar. Șirurile inline repetă același text oriunde apare, deci un fișier cu multe etichete identice este mai mare pe disc decât echivalentul cu șiruri partajate. Cheltuiți dimensiunea fișierului pentru a cumpăra memorie constantă. Pentru un export într-o singură trecere, aceasta este partea corectă a compromisului, iar compresia zip absoarbe mare parte din repetiție la ieșire oricum
Tabela de stiluri sosește la final, cu un singur format de dată
Stilurile prezintă aceeași tensiune ca șirurile. Un registru de lucru își referențiază formatarea printr-o parte de stiluri, iar un scriitor în flux nu poate menține o paletă crescătoare de stiluri în pas cu celulele pe care le-a deja descărcat. Scriitorul direct răspunde la aceasta menținând tabela de stiluri mică și fixă, și emițând-o la închidere, nu în avans. Un format de celulă implicit acoperă celulele obișnuite. Un format de număr pentru date acoperă datele, înregistrat cu un cod de format yyyy-mm-dd la o poziție cunoscută în lista de formate de celule
Acel format de dată este motivul pentru care WriteDateTime există ca apel propriu. Excel nu are un tip nativ de dată; o dată este un număr purtând un format de dată. WriteDateTime scrie valoarea ca un număr serial simplu și etichetează celula cu singurul stil de dată, astfel încât foaia de calcul o redă ca dată în loc de un număr întreg de cinci cifre. Serialul pe care îl scrie contează pentru retur la triplă. Stochează valoarea TDateTime direct sub sistemul de date 1900, care este aceeași convenție pe care o folosește calea de salvare obișnuită TXLSXWorkbook. Deoarece ambele căi sunt de acord cu serialul, un fișier pe care îl produce scriitorul în flux se citește înapoi prin cititorul HotXLS și se deschide în Excel cu date care corespund intenției dvs., fără surprize de decalaj sau epocă între scriitor și cititor
Ordinea este obligatorie, deoarece octeții sunt deja plecați
Fluxul cumpără profilul de memorie cu o regulă pe care trebuie să o respectați. Ieșirea este emisă pe parcurs și nu poate fi revizitată, deci totul trebuie scris în ordinea în care apare în fișier. În interiorul unui rând, celulele merg în ordine crescătoare a coloanelor. În interiorul unei foi, rândurile merg în ordine crescătoare. Nu există niciun buffer care să permită scriitorului să sorteze celulele dvs. după fapt, deoarece rândul pe care l-ați închis acum un moment este deja octeți în fluxul zip și nu mai este accesibil. Dați-i coloana 5 și apoi coloana 2 în același rând și ieșirea este malformată, deoarece scriitorul emite pur și simplu ce îi dați în secvența în care i le dați
API-ul de rând are o mică facilitate pentru cazul obișnuit. AddRow ia un index de rând bazat pe 1, dar trecerea lui 0 înseamnă luați rândul următor după cel anterior, deci o umplere secvențială nu trebuie să urmărească și să treacă un contor în creștere. Fiecare AddRow închide rândul dinaintea lui, iar fiecare AddSheet închide foaia dinaintea lui, deci nu terminați niciodată explicit un rând sau o foaie. Porniți pe cel următor și scriitorul finalizează structura deschisă pentru dvs
Escaparea este gestionată acolo unde textul intră în XML
Orice text pe care îl scrieți devine parte dintr-un document XML, deci cele cinci entități XML predefinite trebuie escapate sau pachetul este invalid în momentul în care o valoare conține un ampersand sau un parantez unghiular. Scriitorul escapează &, <, >, " și ' pentru dvs., atât în textul șirurilor inline cât și în textul formulelor, cele două locuri unde caracterele furnizate de apelant aterizează în interiorul marcajului. Treceți un WideString brut și scriitorul îl face sigur. Un nume de produs precum Smith & Co <Ltd> sau o formulă care referențiază un nume de foaie citat iese ca XML bine format fără nicio escapare din partea dvs
Ciclul de viață și de ce Destroy încă mai închide
Finalizarea pachetului este ceea ce scrie partea registrului de lucru, partea de stiluri, părțile content-types și relationship, și în final directorul central al zip-ului. Acea muncă se întâmplă în Close. Un pachet care nu este niciodată închis este un zip incomplet pe care niciun program de foi de calcul nu îl va deschide, deci închiderea nu este o curățare opțională, este pasul care face fișierul valid. Pentru a proteja împotriva unui Close uitat într-o cale de eroare, Destroy efectuează o închidere de tip best-effort dacă pachetul este încă deschis, deci eliberarea scriitorului nu scurgere obiectul zip subiacent nici când o excepție a sărit apelul explicit. Modelul fiabil este totuși cel obișnuit Delphi: scrieți în interiorul unui try, apelați Close, și eliberați în finally
Transmiterea în flux a unei foi mari de la cap la coadă
Forma jobului este: începeți, adăugați o foaie, turnați rânduri, închideți. Exemplul de mai jos scrie un rând de antet și apoi o serie lungă de rânduri de date tipizate, amestecând șiruri, numere, o formulă fără rezultat memorat în cache, și o dată. Memoria pe care o folosește pentru zece rânduri și pentru zece milioane de rânduri este aceeași, deoarece fiecare celulă pleacă la fluxul zip imediat ce este scrisă
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;
O a doua foaie este pur și simplu un alt AddSheet înainte de a continua, iar scriitorul închide prima foaie pe măsură ce deschide a doua. Indicatorii booleeni folosesc WriteBoolean, care scrie o celulă booleană tipizată în loc de textul "True". Dacă doriți să confirmați că fișierul este corect și că se poate face tur-retur, proprietatea CellCount raportează câte celule au fost scrise, iar citirea rezultatului înapoi cu cititorul în flux ar trebui să raporteze același total
// 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]));
Scrierea într-un flux în loc de un fișier este același cod cu BeginStream în locul lui BeginFile, ceea ce permite unui server să trimită registrul de lucru la un răspuns HTTP sau un flux de memorie fără un fișier temporar pe disc. Scriitorul nu deține fluxul pe care îl treceți, deci dvs. mențineți controlul asupra duratei sale de viață
Când munca este un endpoint de server care construiește registre de lucru la cerere, modelele din scrierile în flux pentru joburi de server și batch arată cum să conectați aceasta la un handler de cereri și un export programat. Când întrebarea este costul mai larg al registrelor de lucru foarte mari, atât la citire cât și la scriere, performanța registrelor de lucru mari în Delphi acoperă unde se duc de fapt timpul și memoria. Scriitorul direct în flux este livrat ca parte a componentei HotXLS Component pentru Delphi și C++Builder, alături de API-urile complete de citire, editare și salvare acoperite în altă parte pe acest blog