Using DTOs to Improve Performance - hmislk/hmis GitHub Wiki
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.
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
// โ 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
DTOs allow you to:
- Select only required fields from the database
- Avoid lazy loading cascades
- Reduce memory footprint dramatically
- Improve query performance with targeted data retrieval
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
}
}
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);
}
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());
}
}
}
<!-- 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>
-- 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
-- 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;
- Memory usage: 60-80% reduction
- Query count: 80-95% reduction
- Load time: 40-70% faster
- Database load: Significantly reduced
-
Use direct DTO queries with
new
constructor syntax - Add new fields/constructors without changing existing ones
- Keep entity properties for business logic operations
-
Use
findLightsByJpql()
for DTO queries - Test compilation after DTO changes
- Convert entities to DTOs in loops
- Change existing constructor signatures
- Remove entity properties that business logic depends on
-
Use regular
findByJpql()
for DTO queries - Modify existing DTO fields
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);
}
See PharmacyAdjustmentController.fillAmpStocks()
for comprehensive DTO usage with additional fields.
- Constructor Parameter Mismatch: Ensure DTO constructor parameters match JPQL SELECT fields exactly
- Null Relationship Handling: Use null checks in JPQL for optional relationships
-
Temporal Type Issues: Use
TemporalType.TIMESTAMP
for Date fields when needed - Case Sensitivity: Ensure field names match exactly between DTO and entity properties
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
- โ 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
โ 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);
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.