บทนำเชิงสถาปัตยกรรม (Conceptual Narrative & The "Why"):
ในระบบองค์กรจริง เอกสารธุรกิจส่วนใหญ่ไม่ได้มีโครงสร้างเป็นตารางแบน (Flat 2D Table) ที่ทุกแถวมีคอลัมน์เหมือนกันหมด ลองจินตนาการถึง ใบแจ้งหนี้ (Billing Invoice) หรือ รายงานงบการเงินผู้บริหาร:
ส่วนหัวคือข้อมูลลูกค้า แต่เนื้อหาด้านในประกอบด้วยสองตารางที่ไม่เกี่ยวข้องกันเลย ตารางแรกคือ "ค่าบริการคลาวด์รายชั่วโมง" (Query จาก Server Database) และตารางที่สองคือ "ค่าลิขสิทธิ์ซอฟต์แวร์รายปี" (Query จาก License Database) หากเราพยายามบังคับให้ทั้งสองตารางนี้อยู่ในรายงานฉบับเดียวผ่าน SQL ธรรมดา เราจะต้อง FULL OUTER JOIN จนเกิดสภาวะ Cartesian Product ข้อมูลตัวเลขจะคูณซ้ำซ้อนจนยอดเงินผิดเพี้ยนทั้งหมด
นั่นคือเหตุผลที่ JasperReports จำเป็นต้องมีสถาปัตยกรรม Subreport ซึ่งทำหน้าที่เหมือนการ "แตก Sub-routine / Child Process" ในการเขียนโปรแกรม โดย Master Report จะทำหน้าที่เป็น Controller ส่งข้อมูลที่จำเป็นเข้าไปให้ Child ทำงานอย่างเป็นอิสระ แล้วรอรับผลลัพธ์สุทธิกลับมารวมยอดใหญ่
JRVerticalFiller ตัวลูกขึ้นมาภายใน JVM เพื่อรัน Process เต็มรูปแบบของเทมเพลตลูกเสมือนเป็นรายงานแยกอีกฉบับหนึ่ง
$P{REPORT_CONNECTION} เพื่อให้ลูกไปรัน SQL Query ของตัวเอง หรือส่งชุดข้อมูลย่อยในหน่วยความจำ Java ผ่าน new JRBeanCollectionDataSource($F{childList}) เพื่อตัดภาระ Database
<returnValue> เพื่อดึงค่ายอดสรุปสุทธิ (เช่น Grand Subtotal) กลับมาอัปเดตลงตัวแปรของแม่ได้อย่างแม่นยำ
เมื่อ Master Report รันมาถึง Detail Band ที่มี Subreport Element กลไกหลังบ้านของ `JRVerticalFiller` จะทำงานตามลำดับขั้นดังนี้:
วงจรการทำงานเชิงลึกภายใน JVM (5-Stage Subreport Lifecycle):
JRVerticalFiller ตัวใหม่ในหน่วยความจำ เป็นการสร้าง Context ที่มี Page Buffer, Variable Registry, และ Group State Machine แยกขาดจาก Master<parameter> และส่งข้อมูลผ่าน <dataSourceExpression> หรือ <connectionExpression><returnValue> เพื่อนำมาอัปเดตลงตัวแปร Masterกฎเหล็กท่อส่งข้อมูล (Data Pipeline Delegation Rules):
$P{REPORT_CONNECTION} เพื่อให้ Child ทำการ Query ตัวเอง แต่หากใช้ Collection ใน Java ให้ส่ง new JRBeanCollectionDataSource($F{subItems})evaluationTime="Band" หากวางใน Band เดียวกัน<element kind="subreport"> ไว้ใน Detail Band ของ Master Report ที่มี 10,000 แถว ระบบจะสร้าง Child Filler ขึ้นมาถึง 10,000 ครั้ง! หากแต่ละรอบมีการ Query ฐานข้อมูลแยก จะเกิดปัญหา Database Connection Pool แห้งเหือดทันที และหาก Child Report มีขนาดใหญ่ Heap Memory จะพุ่งสูงจนเกิด Heavy GC Stalls
$P{REPORT_DATA_SOURCE} ของรายงานแม่เข้าไปให้รายงานลูกเด็ดขาด! เพราะ Data Source แบบ Cursor เมื่อถูกอ่านจนถึงแถวสุดท้ายในลูกคนแรกแล้ว จะไม่มีทางย้อนกลับมาอ่านซ้ำได้อีก ทำให้ Subreport ในแถวถัดไปกลายเป็นตารางเปล่า 100%
การกำหนด Subreport ในเวอร์ชัน 7.0.x ได้ปรับเปลี่ยนมาใช้ Unified Element Model โดยยกเลิกการใช้แท็ก <subreport> ในระดับบนสุด และหันมาใช้ <element kind="subreport"> อย่างเป็นเอกภาพ:
<!-- Master Report: Subreport Declaration (7.0.x Syntax) -->
<element kind="subreport" x="0" y="50" width="555" height="40">
<!-- กำหนด Path ของไฟล์ .jasper ที่คอมไพล์แล้ว -->
<expression><![CDATA["subreports/items_table.jasper"]]></expression>
<!-- 1. Parameter Pipeline: ส่งค่าตัวแปรจากแม่สู่ลูก -->
<parameter name="SUB_TENANT_ID">
<expression><![CDATA[$P{TENANT_ID}]]></expression>
</parameter>
<!-- 2. Isolated Data Pipeline: สร้าง Collection ใหม่ตัดขาดจากแม่ -->
<dataSourceExpression><![CDATA[new net.sf.jasperreports.engine.data.JRBeanCollectionDataSource($F{line_items})]]></dataSourceExpression>
<!-- 3. Two-Way Synchronization: ดึงผลลัพธ์กลับมารวมยอด -->
<returnValue fromVariable="SUB_TOTAL_AMOUNT"
toVariable="MASTER_TOTAL_AMOUNT"
calculation="Sum"/>
</element>
ตารางสเปกของแท็ก <returnValue>:
| Attribute | ประเภท | บทบาทหน้าที่ |
|---|---|---|
fromVariable | String | ชื่อตัวแปรที่อยู่ใน Subreport (Child) ที่ต้องการดึงค่า |
toVariable | String | ชื่อตัวแปรใน Master Report ที่จะมารองรับค่านั้น |
calculation | Enum | สูตรการสะสมค่ากลับมา: Nothing, Sum, Highest, Lowest |
ข้อกำหนดทางเทคนิค (Full-Coverage 100%):
1. ใช้แท็กสมัยใหม่ <element kind="subreport"> ตามมาตรฐาน 7.0.x ห้ามใช้แท็กโบราณ
2. กำหนด <expression> ชี้ไปยังไฟล์ "items_detail.jasper"
3. ส่งพารามิเตอร์ TENANT_ID ไปยัง Subreport ลูกผ่านแท็ก <parameter>
4. ผูก Data Source ผ่าน <dataSourceExpression> โดยห้ามส่ง $P{REPORT_DATA_SOURCE} ตรงๆ
ข้อกำหนดทางเทคนิค (Full-Coverage 100%):
1. ประกาศตัวแปร MASTER_GRAND_TOTAL (BigDecimal) ในรายงานหลักเพื่อรอรับยอดสะสม
2. กำหนด <returnValue> ภายใน Subreport Element เพื่อดูดค่าจาก CHILD_SUBTOTAL
3. กำหนด calculation="Sum" เพื่อให้ยอดสะสมทบกันอย่างถูกต้องในกรณีที่มีหลายแถว
4. แสดงผลลัพธ์ใน Summary Band ด้วย pattern="#,##0.00"
แบบทดสอบ 15 ข้อเชิงลึก วัดความเข้าใจเรื่อง Subreport Lifecycle, Memory Allocation และ Context Synchronization: