🚨 These rules MUST be followed when working with DTOs:
- NEVER modify existing constructors - only add new ones
- Use direct DTO queries - avoid entity-to-DTO conversion loops
- JPQL PERSISTED FIELDS ONLY: NEVER use derived/calculated properties like
nameWithTitle,age,displayNamein JPQL - only persisted database fields work
When implementing DTOs to replace entity objects in UI/display components, follow these strict rules to prevent compilation errors and maintain backward compatibility:
- ❌ 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
When replacing entities with DTOs in controllers:
❌ 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 jpql = "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 ...";
// 🚨 CRITICAL: Use findLightsByJpql() with cast for DTO constructor queries
List<StockDTO> dtos = (List<StockDTO>) facade.findLightsByJpql(jpql, params, TemporalType.TIMESTAMP);🚨 CRITICAL PATTERN for Navigation Support:
When DTOs need to support navigation (e.g., clicking on a row to view details), use IDs and String names instead of full entity references.
❌ WRONG - Including entity objects in DTOs:
public class OpdSaleSummaryDTO {
private Category category; // Don't do this - defeats DTO purpose
private Item item; // Don't do this - loads entity graph
private String itemName;
private Double total;
}✅ CORRECT - Use IDs and names for navigation:
public class OpdSaleSummaryDTO {
private Long categoryId; // For navigation
private String categoryName; // For display
private Long itemId; // For navigation
private String itemName; // For display
private Double total;
// Constructor for JPQL query
public OpdSaleSummaryDTO(Long categoryId, String categoryName,
Long itemId, String itemName, Double total) {
this.categoryId = categoryId;
this.categoryName = categoryName;
this.itemId = itemId;
this.itemName = itemName;
this.total = total;
}
}JPQL Query Pattern:
String jpql = "SELECT new com.divudi.core.data.dto.OpdSaleSummaryDTO("
+ "bi.item.category.id, " // Category ID for navigation
+ "bi.item.category.name, " // Category name for display
+ "bi.item.id, " // Item ID for navigation
+ "bi.item.name, " // Item name for display
+ "sum(bi.netValue)) " // Aggregated data
+ "FROM BillItem bi "
+ "WHERE ... "
+ "GROUP BY bi.item.category.id, bi.item.category.name, bi.item.id, bi.item.name";
List<OpdSaleSummaryDTO> dtos = (List<OpdSaleSummaryDTO>) facade.findLightsByJpql(jpql, params, TemporalType.TIMESTAMP);Navigation Controller Pattern:
// In controller - load full entity only when navigating
public String navigateToDetails(OpdSaleSummaryDTO dto) {
// Load full entities only when needed for detail page
if (dto.getCategoryId() != null) {
this.category = categoryFacade.find(dto.getCategoryId());
}
if (dto.getItemId() != null) {
this.item = itemFacade.find(dto.getItemId());
}
return "/detail_page?faces-redirect=true";
}Benefits:
- ✅ DTOs remain lightweight (no entity graph loading)
- ✅ Navigation still works (using IDs to load entities on demand)
- ✅ Display names available without entity access
- ✅ Database aggregation stays efficient
- ✅ Memory footprint minimized
When changing controller properties from entities to DTOs:
❌ WRONG - Breaking existing functionality:
// This breaks other code that depends on the Stock entity
Stock stock; // Changed to StockDTO - BREAKS OTHER CODE!✅ CORRECT - Add new property, keep existing:
Stock stock; // Keep existing for business logic
StockDTO selectedStockDto; // Add new for UI displayWhen updating XHTML to use DTOs:
For dataTable with DTO data source:
<p:dataTable value="#{controller.stockDtoList}" var="i"
selection="#{controller.selectedStockDto}">
<p:column headerText="Name">
<h:outputText value="#{i.itemName}" />
</p:column>
</p:dataTable>Sync DTO selection with entity if needed:
public void setSelectedStockDto(StockDTO dto) {
this.selectedStockDto = dto;
// Load full entity only if needed for business operations
if (dto != null) {
this.stock = stockFacade.find(dto.getId());
}
}When adding new DTO constructors:
// ✅ 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 constructors for additional use cases
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
}Follow this pattern for efficient DTO implementation:
- Identify the display use case - what data does the UI actually need?
- Add new fields to DTO (never remove existing ones)
- Add new constructor with required fields for the use case
- Create direct JPQL query using the new constructor
- Add new controller properties for DTO selection (keep existing entity properties)
- Update XHTML to use DTO properties for display
- Maintain entity properties for business logic operations
Direct DTO queries provide:
- Memory efficiency: Only loads required display fields
- Database efficiency: Single optimized query instead of entity + conversion
- Network efficiency: Reduced data transfer
- Compilation safety: No breaking changes to existing code
See StockSearchService.findStockDtos() method for the correct pattern of direct DTO querying.
🚨 ALWAYS use findLightsByJpql() with explicit cast for DTO constructor queries:
// ✅ CORRECT - DTO constructor query
String jpql = "SELECT new com.divudi.core.data.dto.PharmacySaleByBillTypeDTO("
+ "i.bill.billTypeAtomic.label, "
+ "sum(i.pharmaceuticalBillItem.qty)) "
+ "FROM BillItem i "
+ "WHERE ... "
+ "GROUP BY i.bill.billTypeAtomic.label";
// MUST use findLightsByJpql() with cast
salesByBillType = (List<PharmacySaleByBillTypeDTO>) getBillItemFacade().findLightsByJpql(jpql, m, TemporalType.TIMESTAMP);❌ WRONG facade methods for DTO constructor queries:
// DON'T USE THESE for DTO constructor queries:
facade.findByJpql(jpql, params) // Wrong return type
facade.findAggregates(jpql, params) // For Object[] results only
facade.findLightsByJpql(jpql, params) // Missing TemporalType when using Date parametersWhy findLightsByJpql() is required:
- Optimized for lightweight objects (DTOs)
- Handles constructor queries correctly
- Supports temporal parameters for Date/Timestamp filtering
- Returns properly typed collections
- Changing existing constructor signatures → Compilation errors in dependent code
- Converting entities to DTOs in loops → Performance degradation
- Removing entity properties used by business logic → Runtime failures
- Using wrong facade method for DTO queries →
findByJpql()instead offindLightsByJpql() - Missing explicit cast → Type safety issues with DTO constructor queries
- Accessing properties through null relationships → Silent query failures (most common issue!)
- Including cancellation details in list DTOs → Unnecessary complexity and performance issues
- Using derived/calculated properties in JPQL → JPQL only supports persisted fields, not getter methods (see below)
Always use wrapper types (Boolean, Integer, Long) in DTO constructor parameters for consistency and null safety:
// ✅ RECOMMENDED - Use Boolean wrapper type
public PharmacyPurchaseOrderDTO(
Long billId,
String deptId,
Boolean cancelled, // ✅ Wrapper type - handles nulls gracefully
Boolean billClosed, // ✅ Wrapper type
Boolean fullyIssued) { // ✅ Wrapper type
this.cancelled = cancelled != null ? cancelled : false;
this.billClosed = billClosed != null ? billClosed : false;
this.fullyIssued = fullyIssued != null ? fullyIssued : false;
}| Entity Type | DTO Constructor Parameter | Result |
|---|---|---|
boolean (primitive) |
Boolean (wrapper) |
✅ Works |
Boolean (wrapper) |
Boolean (wrapper) |
✅ Works |
int (primitive) |
Integer (wrapper) |
✅ Works |
Long |
Long |
✅ Works |
String |
String |
✅ Works |
Note: Primitive-to-wrapper auto-boxing works correctly in EclipseLink JPQL. The more common issue is null relationship access (see next section).
When COUNT returns results but DTO query returns 0:
- Check for null relationship access - This is the #1 cause!
b.cancelledBill.createdAtfails ifcancelledBillis null - Test with minimal constructor - Create a 4-param constructor with just basic fields (id, deptId, createdAt, netTotal). If it works, the issue is with additional fields
- Verify parameter count - Must match exactly (11 params in query = 11 in constructor)
- Check parameter order - Must match query SELECT order exactly
- Check constructor parameter types - Use wrapper types (
Boolean, notboolean) - Remove relationship traversals one by one - Identify which nullable relationship is causing the failure
This is the most common cause of "DTO query returns 0 results" issues.
When accessing properties through a nullable relationship in a JPQL DTO constructor expression, the entire query fails silently if the relationship is null - returning 0 results with no exception.
❌ WRONG - Direct access through nullable relationship:
String jpql = "SELECT new DTO("
+ "b.id, "
+ "b.cancelledBill.createdAt, " // ❌ FAILS SILENTLY if cancelledBill is null
+ "b.cancelledBill.creater.webUserPerson.name) " // ❌ FAILS SILENTLY
+ "FROM Bill b WHERE ...";What happens:
- If ANY row has
cancelledBill = null, the ENTIRE query returns 0 results - No exception is thrown
- COUNT query on same data returns correct count (e.g., 1)
- This is JPQL behavior, not a bug
// Simply don't include cancelledBill fields in the DTO query
String jpql = "SELECT new DTO("
+ "b.id, "
+ "b.deptId, "
+ "b.cancelled) " // Just the boolean flag, not the relationship details
+ "FROM Bill b WHERE ...";String jpql = "SELECT new DTO("
+ "cb.createdAt, " // Safe - cb can be null from LEFT JOIN
+ "COALESCE(cancellerPerson.name, '')) " // Safe - COALESCE handles null
+ "FROM Bill b "
+ "LEFT JOIN b.cancelledBill cb "
+ "LEFT JOIN cb.creater cancellerCreater "
+ "LEFT JOIN cancellerCreater.webUserPerson cancellerPerson "
+ "WHERE ...";Note: Even with LEFT JOIN, you must join EACH level of the relationship chain separately.
For list/table displays, AVOID including cancellation relationship details:
cancelledBill.createdAt(cancellation date)cancelledBill.creater.name(canceller name)cancelledBill.comments(cancellation reason)
Why:
- Performance: These require LEFT JOINs through multiple tables
- Complexity: Nullable relationships cause silent query failures
- UX: Users can click through to view full bill details including cancellation info
- Simplicity: A boolean
cancelledflag is sufficient for list filtering/display
✅ Recommended Pattern for List DTOs:
public class PurchaseOrderListDTO {
private Long billId;
private String deptId;
private Date createdAt;
private Double netTotal;
private Boolean cancelled; // ✅ Simple boolean flag for display/filtering
private Boolean billClosed;
// ❌ Don't include: cancelledAt, cancellerName, cancellationReason
}If user needs cancellation details: Provide a "View Details" action that navigates to the full bill view where all cancellation information is available.
- Always use wrapper types (
Boolean,Integer,Long) for DTO constructor parameters - Avoid nullable relationship traversal - accessing
b.cancelledBill.createdAtfails silently ifcancelledBillis null - Use LEFT JOIN with explicit aliases if you must access nullable relationships
- Use COALESCE for nullable String fields to provide default values
- Test COUNT separately to verify data exists before troubleshooting DTO construction
- Add debug logging when implementing new DTO queries to catch silent failures early
- Match parameter types exactly - don't rely on implicit conversions with
Object - Avoid cancellation details in list DTOs - use boolean flags, let users navigate to details for full info
- Test with minimal constructor first - if a 4-param constructor works but 11-param fails, the issue is with the additional fields
- Only use persisted fields in JPQL - derived properties like
nameWithTitleare not valid (see below)
JPQL can only access persisted database fields, not derived properties (getter methods that compute values).
❌ WRONG - Using derived property:
// Person entity has getNameWithTitle() method that combines title + name
// But 'nameWithTitle' is NOT a persisted column in the database!
String jpql = "SELECT new DTO("
+ "p.nameWithTitle) " // ❌ ERROR: 'nameWithTitle' cannot be resolved to a valid type
+ "FROM Person p";✅ CORRECT - Use only persisted fields:
String jpql = "SELECT new DTO("
+ "p.name) " // ✅ 'name' is a persisted field
+ "FROM Person p";
// If you need the title, select it separately:
String jpql = "SELECT new DTO("
+ "p.title, " // ✅ Persisted field
+ "p.name) " // ✅ Persisted field
+ "FROM Person p";| Entity | Non-Persisted Property | Use Instead |
|---|---|---|
Person |
nameWithTitle |
name (or title, name separately) |
Person |
age |
dob (calculate age in Java) |
Bill |
netTotal (if calculated) |
Sum the actual persisted fee fields |
Item |
displayName |
name |
- Check if the property has
@Columnor@JoinColumnannotation → Persisted - Check if the property is only a getter method with no backing field → NOT Persisted
- Check if the getter computes/derives a value from other fields → NOT Persisted
Example from Person entity:
// This is a derived property - NO @Column annotation, just a getter
public String getNameWithTitle() {
return (title != null ? title + " " : "") + name;
}
// This IS a persisted field - has @Column annotation
@Column(name = "name")
private String name;When implementing dual DTO/Entity approach:
- Separate navigation entries for Entity and DTO versions
- Example: "Transfer Reports (Entity)" and "Transfer Reports (DTO)"
- Double the navigation buttons, but clearer separation of concerns
- Each page has single purpose - either Entity OR DTO, not both
- Simple Fill button on each page - no switching within page
- Entity page: Contains only entity-based Fill button and entity-specific actions
- DTO page: Contains only DTO-based Fill button and DTO-specific actions
- No cross-navigation buttons within pages - navigation choice made at menu level
- Clear page headers indicating which approach (Entity vs DTO)
- Original page:
feature_name.xhtml(Entity-based for backward compatibility) - DTO page:
feature_name_dto.xhtml(DTO-based, optimized) - Navigation labels: "Feature Name" and "Feature Name (DTO - Recommended)"
- DTO approach should be the default where applicable
- Label DTO version clearly in navigation to indicate it's the recommended approach
- Maintain entity version for backward compatibility and business logic needs
Navigation Configuration (pharmacy_analytics.xhtml):
<!-- Entity Version - Traditional -->
<p:commandButton rendered="#{configOptionApplicationController.getBooleanValueByKey('Pharmacy Analytics - Show Transfer Issue by Bill')}"
value="Transfer Issue by Bill (Entity)"
action="#{reportsTransfer.navigateToTransferIssueByBill}"
ajax="false"
icon="fa fa-file-export"
class="w-100"/>
<!-- DTO Version - Recommended (defaults to enabled) -->
<p:commandButton rendered="#{configOptionApplicationController.getBooleanValueByKey('Pharmacy Analytics - Show Transfer Issue by Bill (DTO)', true)}"
value="Transfer Issue by Bill (DTO - Fast)"
action="/pharmacy/reports/disbursement_reports/pharmacy_report_transfer_issue_bill_dto?faces-redirect=true"
ajax="false"
icon="fa fa-rocket"
class="w-100 ui-button-success"
title="High-performance DTO-based report - Recommended"/>Configuration Key Pattern:
- Entity version:
'Feature Name'(existing configuration) - DTO version:
'Feature Name (DTO)'withtrueas default value - This allows administrators to disable DTO versions if needed while defaulting to enabled
Resulting Navigation Menu Structure:
Pharmacy Analytics → Disbursement Reports
├── Transfer Issue by Bill (Entity) → pharmacy_report_transfer_issue_bill.xhtml
└── Transfer Issue by Bill (DTO - Fast) → pharmacy_report_transfer_issue_bill_dto.xhtml
Entity Page Content:
- Single "Fill" button →
fillDepartmentTransfersIssueByBillEntity() - Excel/Print buttons specific to entity data
- Uses
#{reportsTransfer.transferBills}for data binding
DTO Page Content:
- Single "Fill" button →
fillDepartmentTransfersIssueByBillDto() - Excel/Print buttons specific to DTO data
- Uses
#{reportsTransfer.transferIssueDtos}for data binding
Controller Structure:
// Keep both properties for backward compatibility
private List<Bill> transferBills; // For entity approach
private List<PharmacyTransferIssueDTO> transferIssueDtos; // For DTO approach
// Separate methods for each approach
public void fillDepartmentTransfersIssueByBillEntity() { ... }
public void fillDepartmentTransfersIssueByBillDto() { ... }
// Navigation control method
public boolean isTransferIssueDtoEnabled() { return true; }Benefits of Navigation-Level Selection:
- Clear user choice before entering report
- No switching confusion within pages
- Easy configuration control via controller methods
- Gradual migration path - can disable DTO option if needed
- Performance awareness - users can choose fast DTO version consciously
Before committing DTO changes:
- Compile the entire project to check for breaking changes
- Test the specific feature that uses the new DTOs
- Verify existing functionality still works (entities for business logic)
- Check performance improvements compared to entity approach
- Test both DTO and Entity versions if dual approach is implemented