Artikel Teknis

Mengekspor Hasil Database Delphi ke Laporan Excel dengan HotXLS

Mengubah hasil sebuah query menjadi laporan Excel sebenarnya adalah tiga masalah yang mengenakan satu jubah. Setiap tipe field Delphi harus mendarat di sebuah cell sebagai tipe Excel yang tepat, baris header harus terbaca seperti sebuah laporan, bukan dump skema, dan angka, tanggal, serta nilai uang harus membawa format yang bertahan melewati perjalanan itu. Lewatkan salah satu saja dari ketiganya, dan file itu tetap terbuka, tetap terlihat masuk akal, dan tetap gagal begitu seorang pengguna keuangan memilih sebuah kolom dan menunggu sebuah jumlah yang tidak pernah muncul. Nilai-nilainya ditulis sebagai teks, Excel memperlakukannya sebagai label, dan tidak ada exception yang pernah muncul untuk memperingatkan Anda

HotXLS adalah sebuah library spreadsheet Object Pascal native yang menulis file XLS dan XLSX langsung dari Delphi dan C++Builder, tanpa Excel automation yang terlibat. Ia menawarkan dua jalur dari sebuah TDataset menuju sebuah workbook: komponen TDataToXLS yang siap pakai, dan sebuah loop tulisan tangan terhadap workbook API. Keduanya tidak bisa saling dipertukarkan begitu saja. Komponen ini adalah warga VCL sepenuhnya yang dibangun di atas facade XLS, sehingga pilihan yang tepat bergantung pada di mana kode itu berjalan dan format file mana yang diharapkan konsumen. Berikut adalah kedua jalur tersebut, batas tempat komponen ini berhenti menjadi alat yang tepat, dan cara menjaga tipe field tetap utuh apa pun yang Anda pilih

Diagram dua rute ekspor HotXLS dari TDataset Delphi: komponen VCL TDataToXLS yang menulis berkas BIFF8 dan loop TXLSXWorkbook tulisan-tangan untuk XLSX
TDataToXLS adalah jalur satu-panggilan bagi alat desktop VCL yang menulis .xls, sementara loop TXLSXWorkbook yang ditulis tangan melayani pekerjaan tanpa pengawasan dan .xlsx native

Tipe field adalah kontrak ekspor yang sesungguhnya

Sebelum pemanggilan API apa pun, tentukan lebih dulu bagaimana setiap tipe field Delphi mendarat di sebuah cell. Sebuah cell yang menerima sebuah string Delphi tetap menjadi sebuah string. HotXLS tidak menebak bahwa '1,234.50' dimaksudkan sebagai sebuah angka, dan memang seharusnya tidak, karena reparsing yang bergantung pada locale persis seperti itulah cara sebuah koma desimal Jerman berubah menjadi pemisah ribuan pada sebuah server berbahasa Inggris. Pola yang bisa diandalkan adalah menetapkan nilai lewat accessor bertipe: AsFloat atau AsCurrency untuk field numerik, AsDateTime untuk tanggal sehingga cell-nya menyimpan sebuah date serial Excel yang sesungguhnya, bukan sebuah string berformat, dan AsString hanya untuk field yang memang benar-benar berupa teks

Penanganan null layak mendapat sebuah keputusan eksplisit, bukan sekadar sebuah default. Mengonversi nilai sebuah field dengan VarToStr mengubah SQL NULL menjadi sebuah string kosong, yang merupakan sebuah cell teks, sementara melewatkan penetapan nilai meninggalkan cell itu benar-benar kosong, yang justru diharapkan oleh AVERAGE, COUNT, dan konsumen pivot-table. Untuk kolom uang, tentukan sebelum loop-nya ditulis apakah NULL berarti nol atau tidak diketahui. Keduanya dirender identik begitu seseorang memformat kolom itu, dan perbedaannya mengubah setiap agregat yang dihitung di hilir

Jalur komponen: TDataToXLS dalam aplikasi VCL

Untuk sebuah aplikasi VCL klasik dengan sebuah query yang sudah terhubung ke sebuah data module, TDataToXLS adalah jalur satu-pemanggilan. Ia menelusuri turunan TDataset apa pun, baik FireDAC, ADO, IBX, atau apa pun lain yang mengimplementasikan antarmuka dataset abstrak, dan menghasilkan sebuah worksheet berstyle dengan caption header, font, border, subtotal grup opsional, dan pemisahan sheet otomatis untuk hasil query berukuran besar

var
  Exporter: TDataToXLS;
begin
  Exporter := TDataToXLS.Create(nil);
  try
    Exporter.Dataset := OrdersQuery;          // turunan TDataset apa pun
    Exporter.WorksheetName := 'Orders';
    Exporter.HeaderSource := hsDisplayLabel;  // caption, bukan nama kolom mentah
    Exporter.GroupFields.Add('CustomerID');   // blok subtotal per customer
    Exporter.RowsPerSheet := 50000;           // tetap di bawah batas atas baris BIFF8
    Exporter.VisibleFieldsOnly := True;             // menghormati Field.Visible
    Exporter.SaveDatasetAs('orders.xls');
  finally
    Exporter.Free;
  end;
end;

Dua properti menanggung sebagian besar bobot produksi di sini. HeaderSource := hsDisplayLabel menulis DisplayLabel milik setiap field, bukan nama kolom SQL mentah, sehingga workbook itu menuliskan "Customer Name" alih-alih CUST_NM. RowsPerSheet ada karena komponen ini menulis BIFF8, yang grid-nya berhenti pada 65.536 baris kali 256 kolom; menyetelnya menjadi 50.000 memecah sebuah hasil query besar ke beberapa sheet sebelum batas atas format itu memotongnya. Tampilan ditangani oleh properti HeaderFont, DetailFont, GroupColor, dan gaya border, dan kumpulan DisableFormat mematikan seluruh kategori formatting ketika konsumen menginginkan cell polos. Untuk hal-hal khusus, event AfterCell dan AfterRow menyerahkan kepada Anda range yang baru saja ditulis untuk pemrosesan lanjutan

Di mana komponen ini berhenti

Tiga batasan memang dirancang ke dalam TDataToXLS, dan mengetahuinya lebih dulu menghindarkan sebuah desain ulang yang canggung dua sprint kemudian

Diagram yang memetakan accessor bidang dataset Delphi ke tipe sel Excel dengan HotXLS, mengontraskan penanganan NULL VarToStr dengan sel kosong sungguhan
Kontrak ekspor adalah tipe field: aksesor bertipe mendaratkan angka dan tanggal sebagai nilai Excel sesungguhnya, sementara VarToStr diam-diam mengubah SQL NULL menjadi sel teks
  • Ini adalah komponen VCL dalam arti penuh. Unit-nya menarik masuk Forms, Controls, dan Dialogs, sehingga menautkannya ke dalam sebuah job console atau sebuah Windows service ikut menyeret VCL ke dalam binary. Unit workbook inti tidak punya ketergantungan semacam itu. Mereka hanya butuh Windows, Classes, SysUtils, dan Variants, itulah sebabnya kode di sisi server sebaiknya menggunakan loop yang ditunjukkan di bawah ini
  • Ini dibangun di atas facade XLS. Komponen ini mengisi sebuah IXLSWorkbook dan menulis .xls (BIFF8). Tidak ada properti yang mengalihkannya ke output OOXML
  • Event-nya berbicara dalam dialek XLS. Parameter Cell: IXLSRange dalam AfterCell adalah milik model objek XLS, sehingga kustomisasi per-cell yang ditulis di sana adalah kode bergaya XLS bahkan jika file itu dikonversi menjadi .xlsx setelahnya

Menghasilkan .xlsx dari output komponen ini

Ketika konsumen bersikeras menginginkan .xlsx tetapi logika ekspornya sudah terlanjur berada di TDataToXLS, fungsi jembatan dalam unit lxXlsxExport mengonversi workbook yang sudah terisi itu dalam satu pemanggilan:

uses lxXlsxExport;

Exporter.SaveDatasetAs('orders.xls');
// komponen ini mengekspos IXLSWorkbook yang telah diisinya
SaveXLSWorkbookAsXLSX(Exporter.Workbook, 'orders.xlsx');

Perlakukan jembatan ini sebagai pembawa data tabular, bukan sebuah konverter fidelitas-penuh. Ia menyalin nilai, formula, format angka, warna fill, atribut font, lebar kolom, dan pengaturan tampilan. Ia dengan sengaja tidak menyalin border, range gabungan, comment, chart, atau conditional format. Untuk sebuah grid datar berisi header plus baris, itu sudah persis cukup. Untuk sebuah laporan berstyle, itu tidak cukup, dan perbaikan yang jujur adalah menghasilkan XLSX secara langsung, bukan menambal file hasil konversi

Diagram yang mengontraskan unit VCL yang ditarik TDataToXLS ke biner Delphi dengan empat unit RTL yang dibutuhkan kode workbook inti HotXLS
Menautkan TDataToXLS ke dalam service menyeret Forms, Controls, dan Dialogs, sementara unit workbook inti hanya butuh Windows, Classes, SysUtils, dan Variants

Loop tulisan tangan untuk service dan batch job

Kode di sisi server sebaiknya menyasar TXLSXWorkbook secara langsung. Perhatikan perbedaan masa hidup antara kedua facade sebelum menyalin contoh kode apa pun. TXLSWorkbook di sisi XLS dipegang lewat sebuah interface reference-counted dan tidak boleh dibebaskan secara manual, sementara TXLSXWorkbook adalah sebuah class biasa yang membutuhkan try..finally Free. Mencampur kedua konvensi itu adalah cara yang ampuh untuk menciptakan sebuah leak atau sebuah double-free

procedure ExportOrders(Q: TDataSet; const FileName: string);
var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
  Row: Integer;
begin
  Book := TXLSXWorkbook.Create;
  try
    Sheet := Book.Sheets.Add('Orders');
    Sheet.Cells[1, 1].Value := 'Order No';
    Sheet.Cells[1, 2].Value := 'Customer';
    Sheet.Cells[1, 3].Value := 'Ordered';
    Sheet.Cells[1, 4].Value := 'Amount';

    Row := 2;
    Q.First;
    while not Q.Eof do
    begin
      Sheet.Cells[Row, 1].Value := Q.FieldByName('OrderNo').AsInteger;
      Sheet.Cells[Row, 2].Value := Q.FieldByName('Customer').AsString;
      if not Q.FieldByName('Ordered').IsNull then
        Sheet.Cells[Row, 3].Value := Q.FieldByName('Ordered').AsDateTime;
      Sheet.Cells[Row, 4].Value := Q.FieldByName('Amount').AsFloat;
      Inc(Row);
      Q.Next;
    end;

    Book.StreamingWrite := True;  // alirkan XML sheet langsung ke dalam zip
    Book.SaveAs(FileName);
  finally
    Book.Free;
  end;
end;

Baris-baris yang benar-benar penting adalah penetapan bertipe dan penjaga IsNull. Tanggal datang sebagai date serial, jumlah uang datang sebagai double, dan tanggal order yang NULL tetap benar-benar kosong alih-alih menjadi string kosong. StreamingWrite := True hanya mengubah jalur penyimpanan: XML worksheet mengalir langsung ke dalam kontainer zip alih-alih dirakit sebagai satu string besar lebih dulu, yang meratakan lonjakan memori saat SaveAs untuk jumlah baris enam digit. Setiap method penyimpanan juga punya sebuah overload TStream, sehingga workbook itu bisa langsung masuk ke sebuah response HTTP tanpa menyentuh disk. Artikel tentang streaming write dan batch job membahas pola deployment itu, dan artikel tentang performa workbook besar membahas apa yang harus dilakukan ketika jumlah baris terus bertambah

Loop ini juga jalur yang bisa berskala lintas thread. Kedua engine adalah writer Object Pascal native, stream rekaman BIFF8 di satu sisi dan zip plus XML OOXML di sisi lain, sehingga tidak ada bagian mana pun dari sebuah ekspor yang menyentuh COM automation atau membutuhkan lisensi Excel di server. Yang Anda dapatkan dari itu adalah paralelisme tanpa bottleneck instance-tunggal, selama setiap thread membangun workbook-nya sendiri. Objek workbook tidak thread-safe untuk pemakaian bersama, sehingga aturannya adalah satu instance per ekspor, tidak pernah sebuah instance bersama yang dijaga oleh sebuah lock

Ada satu batas yang layak diketahui sebelum Anda mendesain sesuatu di sekitarnya. Grid XLSX berhenti pada 1.048.576 baris kali 16.384 kolom, sehingga pemisahan sheet yang ditangani RowsPerSheet di sisi XLS jarang dibutuhkan di sini. Sebuah workbook sejuta baris juga jarang menjadi yang diinginkan seorang konsumen manusia. Ketika hasil query benar-benar sebesar itu, sebuah file berpembatas biasanya menjadi kontrak yang lebih baik, dan artikel tentang ekspor CSV dan TSV membahas delimiter, perilaku BOM, dan catatan penting soal evaluasi formula yang berlaku di sana

Memilih titik awal

Jika ekspornya berada dalam sebuah tool desktop VCL dan output .xls masih bisa diterima, mulai dengan TDataToXLS dan dukungan grouping-nya. Itu adalah kode paling sedikit, dan jembatan lewat SaveXLSWorkbookAsXLSX sudah tersedia ketika seseorang belakangan meminta .xlsx, selama Anda menerima batas fidelitas yang sudah dijelaskan. Jika kode itu berjalan tanpa pengawasan, atau konsumen mensyaratkan .xlsx sejak awal, tulis loop-nya. Kedua jalur ini dilengkapi proyek demo yang berfungsi dan menjadi bagian dari paket HotXLS Delphi Component