Level 2: Complex Structures

Sub-topic 2.2: Subreport Isolation, Parameter Pipelines & Return Values

สถาปัตยกรรม Nested Report Engine, Isolated Memory Lifecycles, Data Pipeline Delegation และ Two-Way Return Value Synchronization (JasperReports 7.0.3+)

บทนำเชิงสถาปัตยกรรม (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 ทำงานอย่างเป็นอิสระ แล้วรอรับผลลัพธ์สุทธิกลับมารวมยอดใหญ่

ผ่าความหมาย 4 เสาหลักทางสถาปัตยกรรม (Core Pillars Breakdown)

🧩 1. Nested Report Engine
ระบบไม่ใช้วิธีนำ Text หรือตารางมาต่อกันดื้อๆ แต่ใช้วิธี ซ้อน Engine ลงไปใน Engine โดย Master Engine จะ Spawn อินสแตนซ์ของ JRVerticalFiller ตัวลูกขึ้นมาภายใน JVM เพื่อรัน Process เต็มรูปแบบของเทมเพลตลูกเสมือนเป็นรายงานแยกอีกฉบับหนึ่ง
🛡️ 2. Isolated Memory Lifecycles
การตัดขาดขอบเขตหน่วยความจำ ตัวแปรสะสม ยอดรวม (Subtotal) หรือ Group State ภายใน Child Report จะ ไม่รั่วไหล (Zero Leakage) ไปปนกับตัวแปรของแม่ และเมื่อลูกประมวลผลเสร็จ JVM จะ Dereference และคืนหน่วยความจำให้ Garbage Collection ทันที
🚰 3. Data Pipeline Delegation
การกระจายท่อข้อมูลอย่างมีเอกภาพ โดยแม่สามารถเลือกได้ว่าจะส่ง $P{REPORT_CONNECTION} เพื่อให้ลูกไปรัน SQL Query ของตัวเอง หรือส่งชุดข้อมูลย่อยในหน่วยความจำ Java ผ่าน new JRBeanCollectionDataSource($F{childList}) เพื่อตัดภาระ Database
🔄 4. Return Value Synchronization
ท่อสะพานเชื่อมสองทิศทาง (Two-way Bridge) ก่อนที่ Context ของลูกจะถูกทำลายทิ้ง แม่จะเข้าถึง Variable Registry ของลูกผ่านแท็ก <returnValue> เพื่อดึงค่ายอดสรุปสุทธิ (เช่น Grand Subtotal) กลับมาอัปเดตลงตัวแปรของแม่ได้อย่างแม่นยำ

Step 1: Deep Dive (Subreport Engine Internals & Memory Architecture)

เมื่อ Master Report รันมาถึง Detail Band ที่มี Subreport Element กลไกหลังบ้านของ `JRVerticalFiller` จะทำงานตามลำดับขั้นดังนี้:

Subreport Execution Flowchart
รูปที่ 1: สถาปัตยกรรม Subreport Engine Isolation, Variable Delegation และ Return Value Pipeline

วงจรการทำงานเชิงลึกภายใน JVM (5-Stage Subreport Lifecycle):

  1. Child Engine Instantiation: Master Filler สร้างออบเจกต์ JRVerticalFiller ตัวใหม่ในหน่วยความจำ เป็นการสร้าง Context ที่มี Page Buffer, Variable Registry, และ Group State Machine แยกขาดจาก Master
  2. Pipeline Ingestion: แม่ส่งพารามิเตอร์ผ่าน <parameter> และส่งข้อมูลผ่าน <dataSourceExpression> หรือ <connectionExpression>
  3. Isolated Filling Execution: Child Engine รันกระบวนการอ่านข้อมูลและคำนวณ Band ทุกขั้นตอนจนครบถ้วน ผลลัพธ์ Print Elements จะถูกนำมาผสานลงในตำแหน่งปัจจุบันของ Master Band
  4. Return Value Harvest: ทันทีที่ Subreport ทำงานจบ Master Engine จะไปดักอ่านค่าจาก Child Variables ตามที่ผูกไว้ใน <returnValue> เพื่อนำมาอัปเดตลงตัวแปร Master
  5. Context Dereference: ปลดการอ้างอิง Child Filler เพื่อเปิดโอกาสให้ JVM Garbage Collector เก็บกวาดหน่วยความจำ

กฎเหล็กท่อส่งข้อมูล (Data Pipeline Delegation Rules):

  • Data Source Isolation: หากใช้ JDBC ให้ส่ง $P{REPORT_CONNECTION} เพื่อให้ Child ทำการ Query ตัวเอง แต่หากใช้ Collection ใน Java ให้ส่ง new JRBeanCollectionDataSource($F{subItems})
  • Return Value Timing: การแสดงผลค่า Return Value ใน Master Report จะต้องวางใน Band ที่ประมวลผล หลังจาก Subreport ทำงานเสร็จแล้วเท่านั้น เช่น Summary Band หรือใช้ evaluationTime="Band" หากวางใน Band เดียวกัน
จุดตายบน Production (The N+1 Memory Allocation Hazard):
หากคุณวาง <element kind="subreport"> ไว้ใน Detail Band ของ Master Report ที่มี 10,000 แถว ระบบจะสร้าง Child Filler ขึ้นมาถึง 10,000 ครั้ง! หากแต่ละรอบมีการ Query ฐานข้อมูลแยก จะเกิดปัญหา Database Connection Pool แห้งเหือดทันที และหาก Child Report มีขนาดใหญ่ Heap Memory จะพุ่งสูงจนเกิด Heavy GC Stalls
ข้อควรระวัง: Data Source Exhaustion (Cursor หมดอายุ):
ห้ามส่ง $P{REPORT_DATA_SOURCE} ของรายงานแม่เข้าไปให้รายงานลูกเด็ดขาด! เพราะ Data Source แบบ Cursor เมื่อถูกอ่านจนถึงแถวสุดท้ายในลูกคนแรกแล้ว จะไม่มีทางย้อนกลับมาอ่านซ้ำได้อีก ทำให้ Subreport ในแถวถัดไปกลายเป็นตารางเปล่า 100%

Step 2: Real-world Implementation & Architectural Schema (7.0.x)

การกำหนด Subreport ในเวอร์ชัน 7.0.x ได้ปรับเปลี่ยนมาใช้ Unified Element Model โดยยกเลิกการใช้แท็ก <subreport> ในระดับบนสุด และหันมาใช้ <element kind="subreport"> อย่างเป็นเอกภาพ:

Master Detail Production Invoice
รูปที่ 2: Real-world Final Production Output ของรายงานใบแจ้งหนี้แบบ Master-Detail พร้อมระบบ Isolated Pipeline
<!-- 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ประเภทบทบาทหน้าที่
fromVariableStringชื่อตัวแปรที่อยู่ใน Subreport (Child) ที่ต้องการดึงค่า
toVariableStringชื่อตัวแปรใน Master Report ที่จะมารองรับค่านั้น
calculationEnumสูตรการสะสมค่ากลับมา: Nothing, Sum, Highest, Lowest

Hands-on Engineering Lab 2.2.1: Master-Detail Parameter Delegation

🛠️ Lab 2.2.1: Subreport Element Configuration & Parameter Passing

ข้อกำหนดทางเทคนิค (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} ตรงๆ

Hands-on Engineering Lab 2.2.2: Cross-Report Variable Synchronization

🛠️ Lab 2.2.2: Subreport Return Values & Variable Accumulation

ข้อกำหนดทางเทคนิค (Full-Coverage 100%):
1. ประกาศตัวแปร MASTER_GRAND_TOTAL (BigDecimal) ในรายงานหลักเพื่อรอรับยอดสะสม
2. กำหนด <returnValue> ภายใน Subreport Element เพื่อดูดค่าจาก CHILD_SUBTOTAL
3. กำหนด calculation="Sum" เพื่อให้ยอดสะสมทบกันอย่างถูกต้องในกรณีที่มีหลายแถว
4. แสดงผลลัพธ์ใน Summary Band ด้วย pattern="#,##0.00"

Step 3: Interactive Checkpoint Quiz (Deep-Level Internal Mechanics)

แบบทดสอบ 15 ข้อเชิงลึก วัดความเข้าใจเรื่อง Subreport Lifecycle, Memory Allocation และ Context Synchronization:

⚡ Real-time Score Tracker (Level 2.2)
คะแนน: 0 / 15 (ตอบแล้ว: 0 / 15)