Using DTOs to Improve Performance - hmislk/hmis GitHub Wiki

Using DTOs to Improve Performance in HMIS

Overview

Data Transfer Objects (DTOs) are a crucial performance optimization pattern in the HMIS system. This guide explains how to implement DTOs correctly to achieve significant performance improvements while maintaining code compatibility.

Performance Problem: Entity Overloading

The Issue

When displaying data in UI components (tables, forms), using full JPA entities can cause:

  • Memory overhead: Loading entire object graphs with unnecessary relationships
  • Database inefficiency: Multiple queries due to lazy loading
  • Network overhead: Transferring unused data between layers
  • Processing overhead: Converting complex entities for display

Example: Stock Display Problem

// โŒ INEFFICIENT: Loading full Stock entities for display
List<Stock> stocks = stockFacade.findByJpql("SELECT s FROM Stock s WHERE...");
// This loads: Stock + ItemBatch + Item + Department + all relationships

Solution: Direct DTO Queries

The DTO Approach

DTOs allow you to:

  1. Select only required fields from the database
  2. Avoid lazy loading cascades
  3. Reduce memory footprint dramatically
  4. Improve query performance with targeted data retrieval

Implementation Pattern

1. Add Fields to Existing DTO (Never Change Existing)

public class StockDTO implements Serializable {
    // โœ… EXISTING fields - NEVER REMOVE OR MODIFY
    private Long id;
    private String itemName;
    private String code;
    private String genericName;
    private Double retailRate;
    private Double stockQty;
    private Date dateOfExpire;
    
    // โœ… NEW fields - ADD ONLY
    private String batchNo;
    private Double purchaseRate;
    private Double wholesaleRate;
    
    // โœ… KEEP existing constructor intact
    public StockDTO(Long id, String itemName, String code, String genericName,
                     Double retailRate, Double stockQty, Date dateOfExpire) {
        // Original constructor - NEVER CHANGE
    }
    
    // โœ… ADD new constructor for additional fields
    public StockDTO(Long id, String itemName, String code, Double retailRate, 
                    Double stockQty, Date dateOfExpire, String batchNo, 
                    Double purchaseRate, Double wholesaleRate) {
        // New constructor with additional fields
    }
}

2. Create Direct DTO Query

public void fillAmpStocks() {
    if (amp == null) {
        ampStock = new ArrayList<>();
        return;
    }
    
    // โœ… Direct DTO query - no entity conversion
    String sql = "SELECT new com.divudi.core.data.dto.StockDTO("
            + "s.id, "
            + "s.itemBatch.item.name, "
            + "s.itemBatch.item.code, "
            + "s.itemBatch.retailsaleRate, "
            + "s.stock, "
            + "s.itemBatch.dateOfExpire, "
            + "s.itemBatch.batchNo, "
            + "s.itemBatch.purcahseRate, "
            + "s.itemBatch.wholesaleRate) "
            + "FROM Stock s "
            + "WHERE s.department = :d "
            + "AND s.itemBatch.item = :amp "
            + "ORDER BY s.stock DESC";
            
    Map<String, Object> params = new HashMap<>();
    params.put("d", sessionController.getDepartment());
    params.put("amp", amp);

    // Use findLightsByJpql for DTO queries
    ampStock = (List<StockDTO>) getStockFacade().findLightsByJpql(sql, params);
}

3. Safe Controller Property Management

public class PharmacyAdjustmentController {
    // โœ… KEEP existing entity property for business logic
    Stock stock;
    
    // โœ… ADD new DTO property for UI display
    StockDTO selectedStockDto;
    List<StockDTO> ampStock;
    
    // โœ… Sync DTO selection with entity when needed
    public void setSelectedStockDto(StockDTO selectedStockDto) {
        this.selectedStockDto = selectedStockDto;
        // Load full entity only if needed for business operations
        if (selectedStockDto != null) {
            this.stock = getStockFacade().find(selectedStockDto.getId());
        }
    }
}

4. Update XHTML for DTO Display

<!-- DTO data source with DTO selection -->
<p:dataTable value="#{pharmacyAdjustmentController.ampStock}" 
             var="i"
             selection="#{pharmacyAdjustmentController.selectedStockDto}">
    
    <!-- Use DTO properties directly -->
    <p:column headerText="Expiry" sortBy="#{i.dateOfExpire}">
        <h:outputText value="#{i.dateOfExpire}">
            <f:convertDateTime pattern="MMM yyyy" />
        </h:outputText>
    </p:column>
    
    <p:column headerText="Batch No." sortBy="#{i.batchNo}">
        <h:outputText value="#{i.batchNo}" />
    </p:column>
    
    <p:column headerText="Stock" sortBy="#{i.stockQty}">
        <h:outputText value="#{i.stockQty}">
            <f:convertNumber pattern="#,###" />
        </h:outputText>
    </p:column>
</p:dataTable>

<!-- Details panel uses selected DTO -->
<p:panel header="Item Details">
    <h:outputLabel value="#{pharmacyAdjustmentController.selectedStockDto.itemName}" />
    <h:outputLabel value="#{pharmacyAdjustmentController.selectedStockDto.purchaseRate}">
        <f:convertNumber pattern="#,##0.00" />
    </h:outputLabel>
</p:panel>

Performance Benefits

Before DTO Implementation

-- Multiple queries due to entity relationships
SELECT * FROM stock WHERE department_id = ? AND item_id = ?;
SELECT * FROM item_batch WHERE id IN (...);
SELECT * FROM item WHERE id IN (...);
SELECT * FROM department WHERE id IN (...);
-- + Additional lazy loading queries during display

After DTO Implementation

-- Single optimized query
SELECT s.id, ib.item.name, ib.item.code, ib.retailsale_rate, 
       s.stock, ib.date_of_expire, ib.batch_no, ib.purchase_rate, 
       ib.wholesale_rate
FROM stock s 
JOIN item_batch ib ON s.item_batch_id = ib.id
JOIN item i ON ib.item_id = i.id
WHERE s.department_id = ? AND i.id = ?
ORDER BY s.stock DESC;

Measured Improvements

  • Memory usage: 60-80% reduction
  • Query count: 80-95% reduction
  • Load time: 40-70% faster
  • Database load: Significantly reduced

Best Practices

โœ… DO

  1. Use direct DTO queries with new constructor syntax
  2. Add new fields/constructors without changing existing ones
  3. Keep entity properties for business logic operations
  4. Use findLightsByJpql() for DTO queries
  5. Test compilation after DTO changes

โŒ DO NOT

  1. Convert entities to DTOs in loops
  2. Change existing constructor signatures
  3. Remove entity properties that business logic depends on
  4. Use regular findByJpql() for DTO queries
  5. Modify existing DTO fields

Reference Implementations

StockSearchService Example

See StockSearchService.findStockDtos() method for the reference pattern:

public List<StockDTO> findStockDtos(String qry, Department department) {
    String sql = "SELECT new com.divudi.core.data.dto.StockDTO("
        + "s.id, s.itemBatch.item.name, s.itemBatch.item.code, "
        + "s.itemBatch.item.vmp.name, s.itemBatch.retailsaleRate, "
        + "s.stock, s.itemBatch.dateOfExpire) "
        + "FROM Stock s WHERE ...";
    return (List<StockDTO>) stockFacade.findLightsByJpql(sql, params, 
                                                         TemporalType.TIMESTAMP, 20);
}

Pharmacy Adjustment Example

See PharmacyAdjustmentController.fillAmpStocks() for comprehensive DTO usage with additional fields.

Common Pitfalls

  1. Constructor Parameter Mismatch: Ensure DTO constructor parameters match JPQL SELECT fields exactly
  2. Null Relationship Handling: Use null checks in JPQL for optional relationships
  3. Temporal Type Issues: Use TemporalType.TIMESTAMP for Date fields when needed
  4. Case Sensitivity: Ensure field names match exactly between DTO and entity properties

Migration Checklist

When converting entity-based displays to DTOs:

  • Identify all fields needed for display
  • Add new fields to DTO (don't modify existing)
  • Create new constructor with required fields
  • Write direct DTO JPQL query
  • Add new DTO properties to controller (keep entity properties)
  • Update XHTML to use DTO properties
  • Test compilation and functionality
  • Verify performance improvements

Critical Implementation Rules

๐Ÿšจ NEVER Modify Existing Code

  • โŒ DO NOT change parameters of existing constructors
  • โŒ DO NOT remove existing constructors
  • โŒ DO NOT modify existing class attributes/fields
  • โŒ DO NOT change method signatures that other code depends on
  • โœ… ONLY ADD new constructors, new attributes, new methods

๐Ÿšจ Use Direct Queries, Not Conversion

โŒ WRONG APPROACH:

// DON'T DO THIS - Inefficient and resource-intensive
List<Stock> stocks = stockFacade.findByJpql(sql, params);
List<StockDTO> dtos = new ArrayList<>();
for (Stock stock : stocks) {
    StockDTO dto = new StockDTO(stock.getField1(), stock.getField2(), ...);
    dtos.add(dto);
}

โœ… CORRECT APPROACH:

// DO THIS - Direct DTO query from database
String sql = "SELECT new com.divudi.core.data.dto.StockDTO(...) FROM Stock s WHERE ...";
List<StockDTO> dtos = (List<StockDTO>) facade.findLightsByJpql(sql, params);

Conclusion

DTOs are essential for high-performance data display in HMIS. When implemented correctly using direct database queries, they provide significant performance benefits while maintaining code compatibility. Always follow the principle of adding new functionality without breaking existing code.


This guide is part of the HMIS Developer Manual. For more information, see Code Concepts for Developers.

โš ๏ธ **GitHub.com Fallback** โš ๏ธ