Level 4: Export Specialization • Chapter 4.1 Pixel-Perfect Excel Sanitization (JRXlsxExporter)
คำถาม: จากบทที่ 3.4 เราใช้ JRDefaultScriptlet ในการคำนวณ State สะสมยอดรวมภาษีแบบ In-memory ได้อย่างสมบูรณ์แบบ แต่หากฝ่ายบัญชีต้องการนำข้อมูลยอดรวมนี้ไปเปิดบน Microsoft Excel แล้วเขียนสูตรต่อยอด เช่น =SUM(D2:D500) หรือทำ Pivot Table วิเคราะห์ข้อมูลต่อยอด หากเราเพียงแค่นำตัวเลขที่จัดรูปแบบเป็นสตริง "1,250,000.00 THB" ไปลงใน Excel เกิดหายนะทางธุรกิจใดขึ้น และ JRXlsxExporter ของ JasperReports จัดการกับปัญหานี้อย่างไรในระดับสถาปัตยกรรม?
=SUM() จะได้ผลลัพธ์เป็น 0 ทันที) และไม่สามารถสร้าง Pivot Table ได้ นอกจากนี้ หาก Layout มีระยะ Margin เยื้องแม้แต่ 1 พิกเซล Exporter จะสร้าง Merged Cells (เซลล์ผสาน) ทันที ซึ่งทำลายความสามารถในการ Filter/Sort ข้อมูล ทางแก้คือการใช้ Data Typing Preservation (ส่งออก BigDecimal/Double ดิบพร้อมผูก Excel Format Mask เช่น #,##0.00) ควบคู่กับ Virtual Grid Alignment และคุณสมบัติ net.sf.jasperreports.export.xls.detect.cell.type=true
The "Why" & Real-world Problem: The Merged Cell Nightmare & Broken Financial Pipelines
รายงานที่ออกแบบมาเพื่อการพิมพ์แบบ PDF มักเน้นความสวยงามทางสายตา มีขอบกระดาษ (Page Margins), มีส่วนหัว (Header/Footer), และมีการวางตำแหน่งกล่องข้อความแบบ Relative Coordinate ซึ่งเป็นสวรรค์ของงานพิมพ์ แต่เป็น "นรกของผู้ใช้งาน Excel":
new DecimalFormat().format(...) แปลงตัวเลขเป็นข้อความตั้งแต่ใน Expression ทำให้ Excel ถือว่าเป็น Label Text ไม่สามารถเขียนสูตร SUM หรือคำนวณทางคณิตศาสตร์ต่อได้JRXlsxExporter ทำงานโดยตัดพื้นที่ 2 มิติออกเป็น Virtual Grid Lines (แกน X และ Y) ทุกองค์ประกอบต้องจัดพิกัดให้ Snap พอดีกับเส้นกริด ($X_n = X_{n-1} + W_{n-1}$) เพื่อให้แต่ละ Element ตกเป็น 1 Excel Cell เดี่ยวๆ 100% ปราศจาก Merged Cells
คง Type ดิบของข้อมูลไว้เป็น java.math.BigDecimal หรือ java.time.LocalDate และใช้แอตทริบิวต์ pattern="#,##0.00" เพื่อให้ Excel บันทึกเป็น Native Numeric Cell ควบคู่กับ Built-in Excel Number Format
แทนที่จะแสดงผลรวมแบบ Hardcoded จาก Java Memory ให้ฉีดสูตร Excel แท้จริงผ่าน Property net.sf.jasperreports.export.xls.formula เช่น "SUM(D2:D" + ($V{REPORT_COUNT} + 1) + ")" เพื่อให้สูตรคำนวณใหม่ได้ทันทีเมื่อผู้ใช้แก้ข้อมูล
สำหรับข้อมูลขนาดใหญ่ระดับ 500,000 แถว ต้องเปิดใช้งาน Apache POI SXSSF (Streaming Usermodel API) ร่วมกับ SimpleXlsxReportConfiguration เพื่อ Flush แถวลง Temp Disk ป้องกัน JVM Heap OOM
| Failure Mode | Engine & POI Mechanics | Architectural Defense (Standard 7.0.x) |
|---|---|---|
| Merged Cell Disruption | พิกัด X หรือ Width ของ Title, Header, Detail ไม่ตรงกัน ทำให้ Exporter ต้องสร้าง Sub-columns เพื่อครอบองค์ประกอบ | กำจัด Margin ทั้งหมด (`leftMargin="0"`), จัด Typographic Grid ให้คอลัมน์ทุก Band มี $X$ และ $Width$ เท่ากันทุกพิกเซล |
| Pagination Noise in Sheet | Exporter ทำ Page Breaks ตามระยะความสูง A4 สร้างช่องว่างและทำซ้ำ Column Header กลางแผ่นงาน | เปิดใช้ Property net.sf.jasperreports.export.xls.one.page.per.sheet=false และ remove.empty.space.between.rows=true |
| Numeric Treated as String | Expression ทำ String Formatting มาก่อน หรือไม่ได้เปิดใช้งาน Auto Cell Type Detection | กำหนด detect.cell.type=true และคง Field Type เป็น BigDecimal พร้อมผูก pattern="#,##0.00" |
| POI DOM Heap OOM | Apache POI Standard XSSF เก็บ DOM XML ของทุกเซลล์ไว้ใน RAM หากเกิน 50,000 แถวจะเกิด Heap Exhaustion | เปิดใช้งาน Apache POI SXSSF Streaming Mode (Row Windowing) และจำกัดขนาด In-Memory Cache |
สลับแท็บเพื่อศึกษาและทดสอบทั้ง 2 ส่วน: Lab 4.1.1 (Sanitized JRXML 7.0.x Template) และ Lab 4.1.2 (Spring Boot 3.x JRXlsxExporter Configuration)
| # | A (TXN_REF) | B (SECURITY_NAME) | C (VOLUME) | D (SETTLED_VALUE) |
|---|---|---|---|---|
| 1 | TXN-202609-01 | PTT Treasury Bond Series A | 10,000 | 1,250,000.00 |
| 2 | TXN-202609-02 | KBANK Short-term Note 26 | 5,000 | 520,000.00 |
| 3 | TXN-202609-03 | CPALL Debenture Tier-2 | 12,500 | 1,312,500.00 |
| 4 | TXN-202609-04 | SCG Sustainability Bond | 8,000 | 840,000.00 |
| 5 | TOTAL PORTFOLIO (NATIVE FORMULA) | =SUM(D1:D4) → 3,922,500.00 | ||
ทดสอบความเข้าใจเชิงลึกเกี่ยวกับ JRXlsxExporter, Virtual Grid Line Snapping, Native Formula Injection, และ Memory Flushing
# 1. กำจัด Merged Cells และช่องว่างที่ไม่ต้องการ
net.sf.jasperreports.export.xls.remove.empty.space.between.rows=true
net.sf.jasperreports.export.xls.remove.empty.space.between.columns=true
net.sf.jasperreports.export.xls.collapse.row.span=true
# 2. ปิด Pagination เพื่อให้ข้อมูลไหลต่อเนื่องใน Sheet เดียว
net.sf.jasperreports.export.xls.one.page.per.sheet=false
net.sf.jasperreports.export.xls.exclude.origin.band.1=pageHeader
net.sf.jasperreports.export.xls.exclude.origin.band.2=pageFooter
# 3. รักษา Data Type ดิบ และเปิดใช้งาน Auto-sizing คอลัมน์
net.sf.jasperreports.export.xls.detect.cell.type=true
net.sf.jasperreports.export.xls.auto.fit.column=true
net.sf.jasperreports.export.xls.white.page.background=false