Level 2: Complex Structures

Sub-topic 2.1: Multi-Level Grouping Engine & Group Lifecycle

สถาปัตยกรรม Group State Machine, Lifecycle Hooks, Orphan Header Defense และการควบคุม Reset Scope ของ Variables ใน 7.0.x

Step 1: Deep Dive (Group State Machine & In-Memory Pipeline)

ในกระบวนการ Fill Stage ตัวประมวลผล JRVerticalFiller จัดการการจัดกลุ่มเป็นเสมือน State Machine ที่ตรวจจับความเปลี่ยนแปลงของออบเจกต์ใน <expression> ของแต่ละ Record แบบ Sequential Stream

Group Lifecycle State Machine Diagram
รูปที่ 1: แผนผัง Group Break State Machine & Sequence การประมวลผล Group Hooks

ลำดับการประมวลผลเมื่อเกิด Group Break Event:

[Record N Ingestion] -> Evaluate Group Expression
   │
   ├─ If Changed (!Objects.equals(old, new)):
   │    1. Trigger  (Inner to Outer)
   │    2. Reset Variables (resetType="Group")
   │    3. Evaluate Pagination Constraints (minHeight)
   │    4. Trigger  (Outer to Inner)
   │    5. Render Detail Band
   └─ If Unchanged:
        1. Render Detail Band
        2. Accumulate Variables

กฎเหล็กระดับ Production (Key Production Attributes):

  • minHeightToStartNewPage: สกัดกั้น Orphan Group Header หากพื้นที่ท้ายกระดาษเหลือน้อยกว่าค่าที่กำหนด จะตัดขึ้นหน้าใหม่ทันทีก่อนวาด Header
  • reprintHeaderOnEachPage="true": พิมพ์ Group Header ซ้ำอัตโนมัติบนยอดหน้าถัดไปเมื่อเนื้อหาในกลุ่มไหลข้ามหน้า
  • keepTogether="true": ล็อก Header + Detail ทั้งหมด + Footer ให้อยู่หน้าเดียวกันหากเป็นไปได้
  • resetType="Group" + resetGroup="...": บังคับล้างผลรวม Subtotal กลับเป็น 0 เมื่อจบคอลเลกชันของกลุ่มนั้น
Data Source Ordering Contract (จุดตายอันดับหนึ่งบน Production):
JasperReports ไม่มี In-memory Sorter ในตัว Engine อ่าน Stream แถวข้อมูลไปข้างหน้าอย่างเดียว หาก DataSource ไม่ถูก ORDER BY เรียงลำดับตาม Group Expression ล่วงหน้า ข้อมูลที่สลับไปมาจะทำให้เกิด Group Break ซ้ำซ้อน ส่งผลให้รายงานสร้างกลุ่มย่อยกระจัดกระจายและยอด Subtotal ผิดพลาด 100%

Step 2: Real-world 7.0.x Architecture Example

Grouped Ledger Production Mockup
รูปที่ 2: Real-world Final Production Output ของรายงานบัญชีแยกตามแผนกและประเภทสินค้า
<!-- JRXML 7.0.x: Nested Group Architecture -->
<jasperReport xmlns="http://jasperreports.sourceforge.net/jasperreports"
              name="GroupedLedger" pageWidth="595" pageHeight="842"
              columnWidth="555" leftMargin="20" rightMargin="20"
              topMargin="20" bottomMargin="20"
              whenNoDataType="AllSectionsNoDetail">

    <field name="department" class="java.lang.String"/>
    <field name="category" class="java.lang.String"/>
    <field name="amount" class="java.math.BigDecimal"/>

    <!-- Variable: Isolated Category Subtotal -->
    <variable name="CAT_SUBTOTAL" calculation="Sum"
              resetType="Group" resetGroup="CategoryGroup"
              class="java.math.BigDecimal">
        <expression><![CDATA[$F{amount}]]></expression>
        <initialValueExpression><![CDATA[BigDecimal.ZERO]]></initialValueExpression>
    </variable>

    <!-- Outer Group: Department -->
    <group name="DeptGroup" isStartNewPage="true">
        <expression><![CDATA[$F{department}]]></expression>
        <groupHeader>
            <band height="30">
                <element kind="textField" x="0" y="5" width="555" height="20">
                    <expression><![CDATA["DEPT: " + $F{department}]]></expression>
                </element>
            </band>
        </groupHeader>
    </group>

    <!-- Inner Group: Category -->
    <group name="CategoryGroup" minHeightToStartNewPage="120" reprintHeaderOnEachPage="true">
        <expression><![CDATA[$F{category}]]></expression>
        <groupHeader>
            <band height="25">
                <element kind="textField" x="20" y="2" width="535" height="20">
                    <expression><![CDATA["> CATEGORY: " + $F{category}]]></expression>
                </element>
            </band>
        </groupHeader>
        <groupFooter>
            <band height="25">
                <element kind="textField" x="355" y="2" width="200" height="20" pattern="#,##0.00">
                    <expression><![CDATA[$V{CAT_SUBTOTAL}]]></expression>
                </element>
            </band>
        </groupFooter>
    </group>

    <detail>
        <band height="20" splitType="Prevent">
            <element kind="textField" x="40" y="0" width="315" height="18">
                <expression><![CDATA[$F{amount}]]></expression>
            </element>
        </band>
    </detail>
</jasperReport>

Hands-on Engineering Lab 2.1.1: Departmental Segmentation & Orphan Defense

🛠️ Lab 2.1.1: Outer Group Pagination & Orphan Protection

ข้อกำหนดทางเทคนิค (Full-Coverage 100%):
1. กำหนด <group name="DeptGroup"> พร้อมตั้งค่า isStartNewPage="true" เพื่อตัดหน้าใหม่ทุกแผนก
2. กำหนด minHeightToStartNewPage="140" เพื่อสกัดกั้น Orphan Header ก่อนเริ่มวาดเนื้อหา
3. กำหนด reprintHeaderOnEachPage="true" เพื่อพิมพ์หัวกลุ่มซ้ำเมื่อเนื้อหาไหลข้ามหน้า
4. ใช้ Unified Element Model 7.0.x (<element kind="textField">) ห้ามใช้แท็กโบราณ

Hands-on Engineering Lab 2.1.2: Multi-Tier Subtotal Isolation & Continuous Flow

🛠️ Lab 2.1.2: Subtotal Scopes & Variable Reset Isolation

ข้อกำหนดทางเทคนิค (Full-Coverage 100%):
1. กำหนดตัวแปร CATEGORY_TOTAL พร้อม calculation="Sum", resetType="Group" และ resetGroup="CategoryGroup"
2. กำหนดตัวแปร DIVISION_TOTAL พร้อม calculation="Sum", resetType="Group" และ resetGroup="DivisionGroup"
3. สร้างโครงสร้าง Inner Group CategoryGroup ภายใต้ Outer Group DivisionGroup
4. กำหนดการจัดรูปแบบตัวเลข pattern="#,##0.00" บน TextFields แสดงผลลัพธ์ทางการเงินทั้งหมด

Step 3: Interactive Checkpoint Quiz (100% In-Content Guarantee)

แบบทดสอบ 15 ข้อเชิงลึก อ้างอิงจากหลักการ Group State Machine, Sorting Contract, Lifecycle Hooks และ Production Attributes:

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