TQPRO EntityFacade Guide¶
Version: 1.0 Last Updated: 2025-11-19 Author: TourLinq Development Team
Table of Contents¶
- Introduction
- The Facade Pattern in TQPRO
- EntityFacade Architecture
- Using EntityFacade
- Creating Custom Facades
- Facade Patterns and Styles
- Advanced Topics
- Best Practices
- Complete Examples
- Troubleshooting
- Reference
Introduction¶
The EntityFacade is a core component of the TQPRO entity management framework. It provides a simplified, high-level API for performing CRUD (Create, Read, Update, Delete) operations on entities while hiding the complexity of service factories, configuration management, and entity transformation.
What is a Facade?¶
The Facade Pattern is a structural design pattern that provides a simplified interface to a complex subsystem. In TQPRO:
- Without Facade: Developers would need to manually handle service factories, entity transformers, configuration lookups, and error handling
- With Facade: Developers get a simple API like
save(),load(),search()that handles all the complexity
Why Use EntityFacade?¶
Benefits: - Simplified API: Hide complexity behind simple method calls - Session Management: Automatic token handling - Entity Lifecycle: Manage entity state (new, loaded, modified) - Business Logic: Centralize entity-specific business rules - Type Safety: Work with strongly-typed entity objects - Testability: Easy to mock and test
The Facade Pattern in TQPRO¶
Architectural Position¶
┌─────────────────────────────────────────────────────────────┐
│ Application Layer │
│ (Controllers, Business Services) │
│ │
│ Example: REST Controller │
│ - Receives HTTP requests │
│ - Calls facade methods │
│ - Returns responses │
└─────────────────────────────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ FACADE LAYER │
│ (CustomerFacade, ProductFacade, etc.) │
│ │
│ Responsibilities: │
│ - Provide simple CRUD API (save, load, search, delete) │
│ - Hold current entity state │
│ - Manage entity lifecycle │
│ - Coordinate business logic │
│ - Handle validation │
│ - Manage relationships │
└─────────────────────────────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ ENTITY LAYER │
│ (CCustomer, CProduct, etc.) │
│ │
│ - Pure data objects (POJOs) │
│ - Field getters and setters │
│ - No business logic │
└─────────────────────────────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ BASE FACADE LAYER │
│ (EntityFacade) │
│ │
│ - Generic CRUD operations │
│ - Service factory coordination │
│ - Entity transformation │
│ - Configuration lookup │
│ - Criteria conversion │
└─────────────────────────────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ SERVICE FACTORY LAYER │
│ (OdooServiceFactory, AmadeusServiceFactory) │
│ │
│ - Backend-specific implementations │
│ - Service creation │
│ - API calls │
│ - Entity mapping │
└─────────────────────────────────────────────────────────────┘
Facade Types in TQPRO¶
1. Base Facade: EntityFacade - Generic operations for any entity - Used directly for simple cases - Base class for custom facades
2. Custom Entity Facades - Entity-specific facades (CustomerFacade, ProductFacade, etc.) - Add business logic and specialized methods - Extend EntityFacade
EntityFacade Architecture¶
Class Hierarchy¶
EntityFacade (base - generic operations)
↑
├── CustomerFacade (stateful - holds current customer)
├── ProductFacade (stateless - search and retrieval)
├── QuotationFacade (stateful - holds current quote)
├── InvoiceFacade (stateful - holds current invoice)
└── ... (other entity-specific facades)
EntityFacade Core Components¶
1. Session Management¶
All operations use the session token for authentication and authorization.
2. CRUD Operations¶
Search
Read
Write (Create/Update)
Delete
public Object deleteEntity(TlinqEntity entity)
public Object deleteEntity(String factoryName, String entityName, Object idValue)
3. Custom Service Execution¶
4. Criteria Conversion¶
Converts abstract entity field names to native field names:
public SelectCriteriaList convertSearchCriteria(
String factoryName,
String entityName,
SelectCriteriaList criteriaList)
5. Entity Transformation¶
Converts between native and canonical entities:
private List<TlinqEntity> convertToCanonical(
String factoryName,
String canonicalClassName,
List nativeResultList,
EntityConfig entityConfig)
Using EntityFacade¶
Direct Usage (Generic Operations)¶
For simple CRUD operations without custom logic:
// Create facade
EntityFacade facade = new EntityFacade(securityToken);
// Search for entities
SelectCriteriaList criteria = new SelectCriteriaList();
criteria.addCriterion("email", "=", "john@example.com");
List<TlinqEntity> results = facade.search("Customer", criteria);
// Read entity by ID
List<TlinqEntity> entities = facade.read(null, "Customer", 123);
CCustomer customer = (CCustomer) entities.get(0);
// Modify and save
customer.setEmail("newemail@example.com");
facade.write(customer);
// Delete entity
facade.deleteEntity(customer);
Using Custom Facades¶
Custom facades provide entity-specific methods and simplified APIs:
// Create customer facade
CustomerFacade facade = new CustomerFacade(securityToken);
// Load customer
facade.loadCustomer(123);
CCustomer customer = facade.getCustomer();
// Modify using facade methods
facade.setEmail("newemail@example.com");
facade.setPhone("+1-555-0100");
// Save changes
facade.save();
Search Criteria¶
Building search criteria:
SelectCriteriaList criteria = new SelectCriteriaList();
// Simple equality
criteria.addCriterion("customerId", "=", 123);
// String matching
criteria.addCriterion("custName", "like", "John%");
// Boolean values
criteria.addCriterion("isActive", "=", Boolean.TRUE);
// Numeric ranges
criteria.addCriterion("totalAmount", ">", 100.0);
// Multiple criteria (AND logic)
criteria.addCriterion("country", "=", "US");
criteria.addCriterion("status", "=", "active");
Factory Selection¶
Specify which backend to use:
// Use default factory
List results = facade.search("Product", criteria);
// Use specific factory
List results = facade.search("RaynaB2BServiceFactory", "Product", criteria);
Creating Custom Facades¶
Step-by-Step Guide¶
Step 1: Decide on Facade Style¶
Stateful Facade (holds current entity): - Use for: Entities with complex lifecycle (Customer, Quote, Invoice) - Provides: Convenient property accessors, simplified save/load
Stateless Facade (search and utility methods): - Use for: Reference data, lookup operations (Products, Categories) - Provides: Specialized search methods, data retrieval
Step 2: Create Facade Class¶
Package: com.perun.tlinq.entity.[domain]
Example: Stateful Facade (CustomerFacade)
package com.perun.tlinq.entity.customer;
import com.perun.tlinq.config.SelectCriteriaList;
import com.perun.tlinq.entity.EntityFacade;
import com.perun.tlinq.util.TlinqClientException;
public class CustomerFacade extends EntityFacade {
// Hold current entity
private CCustomer thisCustomer = null;
// Entity name constant
private static final String CUST_ENTITY = "Customer";
// Constructor: Empty facade
public CustomerFacade(String token) {
super(token);
}
// Constructor: New entity
public CustomerFacade(String token, boolean initializeNew) {
super(token);
if (initializeNew) {
thisCustomer = new CCustomer();
}
}
// Constructor: Load existing
public CustomerFacade(String token, Integer customerId)
throws TlinqClientException {
super(token);
thisCustomer = findById(customerId);
}
// Constructor: Use existing entity
public CustomerFacade(String token, CCustomer customer) {
super(token);
thisCustomer = customer;
}
// CRUD Methods
public void save() throws TlinqClientException {
if (thisCustomer == null) {
throw new TlinqClientException("Customer not initialized");
}
if (thisCustomer.getCustomerId() != null) {
// Update existing
thisCustomer = updateCustomer(thisCustomer);
} else {
// Create new
thisCustomer = createCustomer(thisCustomer);
}
}
public void loadCustomer(Integer id) throws TlinqClientException {
thisCustomer = findById(id);
}
public void delete() throws TlinqClientException {
if (thisCustomer == null || thisCustomer.getCustomerId() == null) {
throw new TlinqClientException("Cannot delete unsaved customer");
}
deleteEntity(thisCustomer);
thisCustomer = null;
}
// Entity Access
public CCustomer getCustomer() {
return thisCustomer;
}
public void setCustomer(CCustomer customer) {
thisCustomer = customer;
}
// Property Accessors (convenience methods)
public String getEmail() {
return thisCustomer != null ? thisCustomer.getEmail() : null;
}
public void setEmail(String email) {
if (thisCustomer != null) {
thisCustomer.setEmail(email);
}
}
public String getCustName() {
return thisCustomer != null ? thisCustomer.getCustName() : null;
}
public void setCustName(String name) {
if (thisCustomer != null) {
thisCustomer.setCustName(name);
}
}
// Business Logic Methods
public boolean isCompany() {
return thisCustomer != null && thisCustomer.isCompany();
}
public boolean hasEmail() {
return thisCustomer != null &&
thisCustomer.getEmail() != null &&
!thisCustomer.getEmail().isEmpty();
}
public List<CCustomer> getContacts() throws TlinqClientException {
if (thisCustomer == null || thisCustomer.getCustomerId() == null) {
return Collections.emptyList();
}
return loadCustomerContacts(thisCustomer.getCustomerId());
}
// Helper Methods
private CCustomer findById(Integer id) throws TlinqClientException {
List<TlinqEntity> results = read(null, CUST_ENTITY, id);
if (results.isEmpty()) {
throw new TlinqClientException("Customer not found: " + id);
}
return (CCustomer) results.get(0);
}
private CCustomer createCustomer(CCustomer customer)
throws TlinqClientException {
return (CCustomer) write(customer);
}
private CCustomer updateCustomer(CCustomer customer)
throws TlinqClientException {
return (CCustomer) write(customer);
}
private List<CCustomer> loadCustomerContacts(Integer customerId)
throws TlinqClientException {
SelectCriteriaList criteria = new SelectCriteriaList();
criteria.addCriterion("parentId", "=", customerId);
List<TlinqEntity> results = search(CUST_ENTITY, criteria);
return results.stream()
.map(e -> (CCustomer) e)
.collect(Collectors.toList());
}
// Search Methods
public List<CCustomer> searchByEmail(String email)
throws TlinqClientException {
SelectCriteriaList criteria = new SelectCriteriaList();
criteria.addCriterion("email", "=", email);
List<TlinqEntity> results = search(CUST_ENTITY, criteria);
return results.stream()
.map(e -> (CCustomer) e)
.collect(Collectors.toList());
}
public List<CCustomer> searchByName(String namePart)
throws TlinqClientException {
SelectCriteriaList criteria = new SelectCriteriaList();
criteria.addCriterion("custName", "like", namePart + "%");
List<TlinqEntity> results = search(CUST_ENTITY, criteria);
return results.stream()
.map(e -> (CCustomer) e)
.collect(Collectors.toList());
}
}
Example: Stateless Facade (ProductFacade)
package com.perun.tlinq.entity.product;
import com.perun.tlinq.config.SelectCriteriaList;
import com.perun.tlinq.entity.EntityFacade;
import com.perun.tlinq.util.TlinqClientException;
public class ProductFacade extends EntityFacade {
private static final String PRODUCT_ENTITY = "Product";
private static final String CATEGORY_ENTITY = "ProductCategory";
public ProductFacade(String token) {
super(token);
}
// Search Methods
public List<CProduct> getProducts(SelectCriteriaList criteria)
throws TlinqClientException {
List<TlinqEntity> results = search(PRODUCT_ENTITY, criteria);
return results.stream()
.map(e -> (CProduct) e)
.collect(Collectors.toList());
}
public CProduct getProduct(Integer productId)
throws TlinqClientException {
List<TlinqEntity> results = read(null, PRODUCT_ENTITY, productId);
return results.isEmpty() ? null : (CProduct) results.get(0);
}
public List<CProduct> getCategoryProducts(Integer categoryId)
throws TlinqClientException {
SelectCriteriaList criteria = new SelectCriteriaList();
criteria.addCriterion("public_categ_ids", "=", categoryId);
criteria.addCriterion("availableOnWeb", "=", Boolean.TRUE);
return getProducts(criteria);
}
public List<CProduct> searchByName(String namePart)
throws TlinqClientException {
SelectCriteriaList criteria = new SelectCriteriaList();
criteria.addCriterion("prodName", "like", namePart + "%");
criteria.addCriterion("availableOnWeb", "=", Boolean.TRUE);
return getProducts(criteria);
}
// Category Methods
public List<CProductCategory> getChildCategories(Integer parentId)
throws TlinqClientException {
SelectCriteriaList criteria = new SelectCriteriaList();
criteria.addCriterion("parent_id", "=", parentId);
List<TlinqEntity> results = search(CATEGORY_ENTITY, criteria);
return results.stream()
.map(e -> (CProductCategory) e)
.collect(Collectors.toList());
}
// Business Logic
public CProduct calculatePrice(CProduct product, Double marginPercent) {
if (product == null || product.getProductPrice() == null) {
return product;
}
Double basePrice = product.getProductPrice();
Double margin = marginPercent != null ? marginPercent : 0.0;
Double finalPrice = basePrice * (1 + margin / 100.0);
product.setCalculatedPrice(finalPrice);
return product;
}
public boolean isAvailable(CProduct product) {
return product != null &&
Boolean.TRUE.equals(product.getAvailableOnWeb()) &&
product.getProductPrice() != null &&
product.getProductPrice() > 0;
}
}
Step 3: Add Business Logic¶
Custom facades are ideal for centralizing business rules:
public class QuotationFacade extends EntityFacade {
private CQuote currentQuote;
// Business logic: Calculate quote total
public Double calculateTotal() {
if (currentQuote == null || currentQuote.getItems() == null) {
return 0.0;
}
return currentQuote.getItems().stream()
.filter(item -> item.getAmount() != null)
.mapToDouble(CQuoteItem::getAmount)
.sum();
}
// Business logic: Apply discount
public void applyDiscount(Double discountPercent) {
if (currentQuote == null || discountPercent == null) {
return;
}
Double total = calculateTotal();
Double discount = total * (discountPercent / 100.0);
currentQuote.setDiscountAmount(discount);
currentQuote.setFinalAmount(total - discount);
}
// Business logic: Validate quote
public boolean isValid() {
if (currentQuote == null) {
return false;
}
// Must have customer
if (currentQuote.getCustomerId() == null) {
return false;
}
// Must have at least one item
if (currentQuote.getItems() == null || currentQuote.getItems().isEmpty()) {
return false;
}
// All items must have price
return currentQuote.getItems().stream()
.allMatch(item -> item.getAmount() != null && item.getAmount() > 0);
}
// Business logic: Submit quote
public void submit() throws TlinqClientException {
if (!isValid()) {
throw new TlinqClientException("Quote is not valid for submission");
}
if (currentQuote.getQuoteId() == null) {
throw new TlinqClientException("Quote must be saved before submission");
}
// Calculate final amounts
currentQuote.setTotal(calculateTotal());
// Set submission date
currentQuote.setSubmissionDate(LocalDate.now());
// Update status
currentQuote.setStatus("submitted");
// Save
write(currentQuote);
}
}
Step 4: Add Validation¶
public class CustomerFacade extends EntityFacade {
public void save() throws TlinqClientException {
// Validate before saving
validate();
// Perform save
if (thisCustomer.getCustomerId() != null) {
thisCustomer = updateCustomer(thisCustomer);
} else {
thisCustomer = createCustomer(thisCustomer);
}
}
private void validate() throws TlinqClientException {
if (thisCustomer == null) {
throw new TlinqClientException("Customer not initialized");
}
List<String> errors = new ArrayList<>();
// Required fields
if (TypeUtil.isEmptyString(thisCustomer.getCustName())) {
errors.add("Customer name is required");
}
if (TypeUtil.isEmptyString(thisCustomer.getEmail())) {
errors.add("Email is required");
} else if (!isValidEmail(thisCustomer.getEmail())) {
errors.add("Email format is invalid");
}
// Business rules
if (thisCustomer.isCompany() &&
TypeUtil.isEmptyString(thisCustomer.getTaxNumber())) {
errors.add("Tax number is required for companies");
}
if (!errors.isEmpty()) {
throw new TlinqClientException("Validation failed: " +
String.join(", ", errors));
}
}
private boolean isValidEmail(String email) {
return email != null && email.contains("@");
}
}
Facade Patterns and Styles¶
Pattern 1: Stateful Facade (Active Record Style)¶
Use Case: Entities with complex lifecycle, multiple operations
Characteristics: - Holds current entity instance - Provides property accessors - Centralizes state management
Example:
CustomerFacade facade = new CustomerFacade(token, customerId);
facade.setEmail("new@example.com");
facade.setPhone("+1-555-0100");
facade.save(); // Saves current customer
Pros: - Convenient for UI binding - Clear entity lifecycle - Simplified API
Cons: - Stateful (not thread-safe by default — do not share across threads) - One entity at a time
Pattern 2: Stateless Facade (Repository Style)¶
Use Case: Search operations, reference data, bulk operations
Characteristics: - No state (thread-safe) - Returns entities - Focus on data retrieval
Example:
ProductFacade facade = new ProductFacade(token);
List<CProduct> products = facade.getCategoryProducts(categoryId);
CProduct product = facade.getProduct(productId);
Pros: - Thread-safe - Can work with multiple entities - Good for search operations
Cons: - No lifecycle management - Need to handle entities separately
Pattern 3: Hybrid Facade¶
Use Case: Complex entities needing both styles
Characteristics: - Can hold current entity (optional) - Also provides stateless methods - Flexibility in usage
Example:
QuotationFacade facade = new QuotationFacade(token);
// Stateless usage
List<CQuote> quotes = facade.searchQuotes(criteria);
// Stateful usage
facade.loadQuote(quoteId);
facade.addItem(item);
facade.calculateTotal();
facade.save();
Pattern 4: Domain Facade¶
Use Case: Multiple related entities, complex workflows
Characteristics: - Works with multiple entity types - Orchestrates business processes - May manage transactions
Example:
public class OrderProcessingFacade extends EntityFacade {
public CInvoice processOrder(CQuote quote) throws TlinqClientException {
// Validate quote
if (!isQuoteValid(quote)) {
throw new TlinqClientException("Invalid quote");
}
// Create invoice from quote
CInvoice invoice = convertQuoteToInvoice(quote);
// Save invoice
write(invoice);
// Update quote status
quote.setStatus("invoiced");
write(quote);
// Create payment record
CPayment payment = createPaymentRecord(invoice);
write(payment);
return invoice;
}
private CInvoice convertQuoteToInvoice(CQuote quote) {
// Conversion logic
}
private CPayment createPaymentRecord(CInvoice invoice) {
// Payment creation logic
}
}
Advanced Topics¶
Custom Service Actions¶
Execute custom backend actions beyond CRUD:
public class BookingFacade extends EntityFacade {
public CBooking confirmBooking(Integer bookingId)
throws TlinqClientException {
// Load booking
CBooking booking = getBooking(bookingId);
// Call custom service action
List<Object> params = Arrays.asList(bookingId);
Object result = callCustomService(
booking,
"confirm", // Custom action
params
);
return (CBooking) result;
}
public CBooking cancelBooking(Integer bookingId, String reason)
throws TlinqClientException {
CBooking booking = getBooking(bookingId);
List<Object> params = Arrays.asList(bookingId, reason);
Object result = callCustomService(
booking,
"cancel",
params
);
return (CBooking) result;
}
}
Transaction Coordination¶
Handle multi-entity transactions:
public class TransactionFacade extends EntityFacade {
public void processPayment(Integer invoiceId, CPayment payment)
throws TlinqClientException {
try {
// Get invoice
CInvoice invoice = getInvoice(invoiceId);
// Validate payment amount
if (payment.getAmount() > invoice.getAmountDue()) {
throw new TlinqClientException("Payment exceeds amount due");
}
// Create payment record
write(payment);
// Update invoice paid amount
invoice.setAmountPaid(
invoice.getAmountPaid() + payment.getAmount()
);
invoice.setAmountDue(
invoice.getTotalAmount() - invoice.getAmountPaid()
);
// Update invoice status
if (invoice.getAmountDue() <= 0) {
invoice.setStatus("paid");
} else {
invoice.setStatus("partial");
}
write(invoice);
} catch (Exception e) {
// Rollback if needed
throw new TlinqClientException("Payment processing failed", e);
}
}
}
Caching Integration¶
Integrate with caching systems:
public class ProductFacade extends EntityFacade {
private ProductCache cache = ProductCache.instance();
@Override
public CProduct getProduct(Integer productId)
throws TlinqClientException {
// Check cache first
CProduct cached = cache.get(productId);
if (cached != null) {
return cached;
}
// Load from backend
List<TlinqEntity> results = read(null, "Product", productId);
if (results.isEmpty()) {
return null;
}
CProduct product = (CProduct) results.get(0);
// Add to cache
cache.put(productId, product);
return product;
}
public void invalidateCache(Integer productId) {
cache.remove(productId);
}
@Override
public CProduct save(CProduct product) throws TlinqClientException {
// Save to backend
CProduct saved = (CProduct) write(product);
// Update cache
if (saved.getProductId() != null) {
cache.put(saved.getProductId(), saved);
}
return saved;
}
}
Multi-Factory Operations¶
Work with multiple backends:
public class ProductFacade extends EntityFacade {
public CProduct getProductWithVendorData(Integer productId)
throws TlinqClientException {
// Get product from default factory (Odoo)
CProduct product = getProduct(productId);
// If product has vendor, get additional data
if (product.getSupplierId() != null) {
String vendorFactory = determineVendorFactory(product.getSupplierId());
// Get vendor product data
List<TlinqEntity> vendorResults = read(
vendorFactory,
"Product",
product.getVendorProductId()
);
if (!vendorResults.isEmpty()) {
CProduct vendorProduct = (CProduct) vendorResults.get(0);
// Merge vendor data
product.setProductInfo(vendorProduct.getProductInfo());
product.setProductTerms(vendorProduct.getProductTerms());
}
}
return product;
}
private String determineVendorFactory(Integer supplierId) {
// Logic to determine which factory based on supplier
return "RaynaB2BServiceFactory";
}
}
Best Practices¶
Facade Design¶
1. Single Responsibility - One facade per entity (or logical entity group) - Don't mix unrelated entities in one facade - Keep facades focused
2. Consistent Naming
- Use EntityNameFacade pattern
- Method names should be descriptive and consistent
- Follow existing patterns in codebase
3. Constructor Patterns
// Standard constructors
public CustomerFacade(String token) // Empty facade
public CustomerFacade(String token, boolean init) // New entity
public CustomerFacade(String token, Integer id) // Load existing
public CustomerFacade(String token, CCustomer entity) // Use existing
4. Error Handling
public void save() throws TlinqClientException {
if (entity == null) {
throw new TlinqClientException("Entity not initialized");
}
try {
// Save logic
} catch (Exception e) {
logger.log(Level.SEVERE, "Failed to save entity", e);
throw new TlinqClientException("Save failed: " + e.getMessage(), e);
}
}
State Management¶
1. Null Safety
public String getEmail() {
return thisCustomer != null ? thisCustomer.getEmail() : null;
}
public void setEmail(String email) {
if (thisCustomer != null) {
thisCustomer.setEmail(email);
}
}
2. Immutable After Save
public void save() throws TlinqClientException {
// Validate
validate();
// Save
CCustomer saved = (CCustomer) write(thisCustomer);
// Replace current instance with saved version
thisCustomer = saved;
}
3. Clear State
public void delete() throws TlinqClientException {
deleteEntity(thisCustomer);
thisCustomer = null; // Clear state after delete
}
Search Operations¶
1. Use Specific Methods
// Good: Specific, documented method
public List<CCustomer> searchByEmail(String email) {
SelectCriteriaList criteria = new SelectCriteriaList();
criteria.addCriterion("email", "=", email);
return search("Customer", criteria);
}
// Avoid: Generic method requiring criteria knowledge
public List<CCustomer> search(SelectCriteriaList criteria) {
return search("Customer", criteria);
}
2. Type Safety
// Good: Return typed list
public List<CProduct> getProducts(SelectCriteriaList criteria) {
List<TlinqEntity> results = search("Product", criteria);
return results.stream()
.map(e -> (CProduct) e)
.collect(Collectors.toList());
}
// Avoid: Return raw list
public List getProducts(SelectCriteriaList criteria) {
return search("Product", criteria);
}
Business Logic¶
1. Centralize in Facade
// Good: Business logic in facade
public class QuotationFacade extends EntityFacade {
public void applyDiscount(Double percent) {
Double total = calculateTotal();
currentQuote.setDiscount(total * percent / 100);
}
}
// Avoid: Business logic in entity
public class CQuote extends TlinqEntity {
public void applyDiscount(Double percent) {
// Business logic here - harder to test and maintain
}
}
2. Validation
// Validate before operations
private void validate() throws TlinqClientException {
List<String> errors = new ArrayList<>();
if (entity == null) {
throw new TlinqClientException("Entity not initialized");
}
// Field validation
if (TypeUtil.isEmptyString(entity.getName())) {
errors.add("Name is required");
}
// Business rules
if (entity.getTotal() < 0) {
errors.add("Total cannot be negative");
}
if (!errors.isEmpty()) {
throw new TlinqClientException(
"Validation failed: " + String.join(", ", errors)
);
}
}
Testing¶
1. Mock Dependencies
@Test
public void testSaveCustomer() throws TlinqClientException {
// Create mock token
String token = "test-token";
// Create facade
CustomerFacade facade = new CustomerFacade(token, true);
// Set properties
facade.setCustName("Test Customer");
facade.setEmail("test@example.com");
// Save
facade.save();
// Verify
assertNotNull(facade.getCustomer().getCustomerId());
}
2. Test Business Logic
@Test
public void testCalculateTotal() {
QuotationFacade facade = new QuotationFacade(token);
CQuote quote = new CQuote();
CQuoteItem item1 = new CQuoteItem();
item1.setAmount(100.0);
CQuoteItem item2 = new CQuoteItem();
item2.setAmount(50.0);
quote.setItems(Arrays.asList(item1, item2));
facade.setQuote(quote);
Double total = facade.calculateTotal();
assertEquals(150.0, total, 0.01);
}
Complete Examples¶
Example 1: Customer Management¶
/**
* Complete customer management example
*/
public class CustomerManagementExample {
private String securityToken;
public void demonstrateCustomerOperations() throws TlinqClientException {
// 1. Create new customer
CustomerFacade facade = new CustomerFacade(securityToken, true);
facade.setCustName("Acme Corporation");
facade.setEmail("contact@acme.com");
facade.setPhone("+1-555-0100");
facade.setCompany(true);
facade.setTaxNumber("123456789");
facade.save();
Integer customerId = facade.getCustomer().getCustomerId();
System.out.println("Created customer: " + customerId);
// 2. Load and update
CustomerFacade facade2 = new CustomerFacade(securityToken, customerId);
facade2.setEmail("newemail@acme.com");
facade2.save();
// 3. Search for customers
CustomerFacade searchFacade = new CustomerFacade(securityToken);
List<CCustomer> customers = searchFacade.searchByName("Acme%");
System.out.println("Found " + customers.size() + " customers");
// 4. Delete customer
facade2.delete();
}
}
Example 2: Product Catalog¶
/**
* Product catalog operations
*/
public class ProductCatalogExample {
public void demonstrateProductOperations(String token)
throws TlinqClientException {
ProductFacade facade = new ProductFacade(token);
// 1. Get products by category
List<CProduct> products = facade.getCategoryProducts(10);
// 2. Search products by name
List<CProduct> searchResults = facade.searchByName("Tour");
// 3. Get specific product
CProduct product = facade.getProduct(123);
// 4. Calculate price with margin
CProduct priced = facade.calculatePrice(product, 15.0);
// 5. Check availability
boolean available = facade.isAvailable(product);
// 6. Get product categories
List<CProductCategory> categories = facade.getChildCategories(1);
}
}
Example 3: Quote Management¶
/**
* Quote creation and management
*/
public class QuotationExample {
public CQuote createQuote(String token, Integer customerId)
throws TlinqClientException {
// Create facade with new quote
QuotationFacade facade = new QuotationFacade(token, true);
// Set quote properties
CQuote quote = facade.getQuote();
quote.setCustomerId(customerId);
quote.setQuoteName("Q-2025-001");
quote.setQuoteDate(LocalDate.now());
// Add quote items
CQuoteItem item1 = new CQuoteItem();
item1.setDescription("Hotel Package");
item1.setQuantity(2);
item1.setUnitPrice(500.0);
item1.setAmount(1000.0);
CQuoteItem item2 = new CQuoteItem();
item2.setDescription("Flight Tickets");
item2.setQuantity(2);
item2.setUnitPrice(300.0);
item2.setAmount(600.0);
quote.setItems(Arrays.asList(item1, item2));
// Calculate total
Double total = facade.calculateTotal(); // 1600.0
// Apply discount
facade.applyDiscount(10.0); // 10% discount
// Validate
if (!facade.isValid()) {
throw new TlinqClientException("Quote is not valid");
}
// Save
facade.save();
return facade.getQuote();
}
public void submitQuote(String token, Integer quoteId)
throws TlinqClientException {
QuotationFacade facade = new QuotationFacade(token, quoteId);
facade.submit(); // Custom business logic
}
}
Example 4: Multi-Entity Workflow¶
/**
* Complex workflow involving multiple entities
*/
public class OrderProcessingExample {
public CInvoice processOrder(String token, Integer quoteId)
throws TlinqClientException {
// Load quote
QuotationFacade quoteFacade = new QuotationFacade(token, quoteId);
CQuote quote = quoteFacade.getQuote();
// Validate quote
if (!quoteFacade.isValid()) {
throw new TlinqClientException("Quote is not valid");
}
// Create invoice
InvoiceFacade invoiceFacade = new InvoiceFacade(token, true);
CInvoice invoice = invoiceFacade.getInvoice();
// Copy quote data to invoice
invoice.setCustomerId(quote.getCustomerId());
invoice.setQuoteId(quote.getQuoteId());
invoice.setInvoiceDate(LocalDate.now());
invoice.setDueDate(LocalDate.now().plusDays(30));
// Convert quote items to invoice items
List<CInvoiceItem> invoiceItems = new ArrayList<>();
for (CQuoteItem quoteItem : quote.getItems()) {
CInvoiceItem invItem = new CInvoiceItem();
invItem.setDescription(quoteItem.getDescription());
invItem.setQuantity(quoteItem.getQuantity());
invItem.setUnitPrice(quoteItem.getUnitPrice());
invItem.setAmount(quoteItem.getAmount());
invoiceItems.add(invItem);
}
invoice.setItems(invoiceItems);
// Set amounts
invoice.setSubtotal(quote.getTotal());
invoice.setDiscount(quote.getDiscountAmount());
invoice.setTotalAmount(quote.getFinalAmount());
invoice.setAmountPaid(0.0);
invoice.setAmountDue(quote.getFinalAmount());
// Save invoice
invoiceFacade.save();
// Update quote status
quote.setStatus("invoiced");
quoteFacade.save();
return invoice;
}
}
Example 5: REST Controller Integration¶
/**
* Using facades in REST controllers
*/
@RestController
@RequestMapping("/api/customers")
public class CustomerController {
@PostMapping
public ResponseEntity<CCustomer> createCustomer(
@RequestHeader("Authorization") String token,
@RequestBody CustomerRequest request) {
try {
CustomerFacade facade = new CustomerFacade(token, true);
facade.setCustName(request.getName());
facade.setEmail(request.getEmail());
facade.setPhone(request.getPhone());
facade.setCompany(request.isCompany());
facade.save();
return ResponseEntity.ok(facade.getCustomer());
} catch (TlinqClientException e) {
return ResponseEntity.badRequest().build();
}
}
@GetMapping("/{id}")
public ResponseEntity<CCustomer> getCustomer(
@RequestHeader("Authorization") String token,
@PathVariable Integer id) {
try {
CustomerFacade facade = new CustomerFacade(token, id);
return ResponseEntity.ok(facade.getCustomer());
} catch (TlinqClientException e) {
return ResponseEntity.notFound().build();
}
}
@PutMapping("/{id}")
public ResponseEntity<CCustomer> updateCustomer(
@RequestHeader("Authorization") String token,
@PathVariable Integer id,
@RequestBody CustomerRequest request) {
try {
CustomerFacade facade = new CustomerFacade(token, id);
facade.setEmail(request.getEmail());
facade.setPhone(request.getPhone());
facade.save();
return ResponseEntity.ok(facade.getCustomer());
} catch (TlinqClientException e) {
return ResponseEntity.badRequest().build();
}
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteCustomer(
@RequestHeader("Authorization") String token,
@PathVariable Integer id) {
try {
CustomerFacade facade = new CustomerFacade(token, id);
facade.delete();
return ResponseEntity.noContent().build();
} catch (TlinqClientException e) {
return ResponseEntity.badRequest().build();
}
}
@GetMapping("/search")
public ResponseEntity<List<CCustomer>> searchCustomers(
@RequestHeader("Authorization") String token,
@RequestParam String name) {
try {
CustomerFacade facade = new CustomerFacade(token);
List<CCustomer> results = facade.searchByName(name);
return ResponseEntity.ok(results);
} catch (TlinqClientException e) {
return ResponseEntity.badRequest().build();
}
}
}
Troubleshooting¶
Common Issues¶
1. Null Entity¶
Error: NullPointerException when accessing entity
Cause: Entity not initialized or loaded
Solution:
// Check before access
if (facade.getCustomer() == null) {
throw new TlinqClientException("Customer not loaded");
}
// Or use null-safe methods
public String getEmail() {
return thisCustomer != null ? thisCustomer.getEmail() : null;
}
2. Session Token Invalid¶
Error: Authentication failed or Unauthorized
Cause: Invalid or expired session token
Solution:
// Verify token before creating facade
if (token == null || token.isEmpty()) {
throw new TlinqClientException("Invalid session token");
}
// Refresh token if needed
String freshToken = authService.refreshToken(oldToken);
facade = new CustomerFacade(freshToken);
3. Entity Not Found¶
Error: Entity not found: 123
Cause: ID doesn't exist in backend
Solution:
// Handle not found gracefully
try {
facade.loadCustomer(id);
} catch (TlinqClientException e) {
if (e.getMessage().contains("not found")) {
// Handle not found case
return null;
}
throw e;
}
4. Validation Errors¶
Error: Validation failed: Email is required
Cause: Missing required fields
Solution:
// Validate before operations
try {
facade.save();
} catch (TlinqClientException e) {
if (e.getMessage().contains("Validation failed")) {
// Show validation errors to user
displayErrors(e.getMessage());
}
}
5. Concurrent Modification¶
Error: Entity was modified by another user
Cause: Optimistic locking conflict
Solution:
// Reload and retry
try {
facade.save();
} catch (TlinqClientException e) {
if (e.getMessage().contains("modified")) {
// Reload fresh version
facade.loadCustomer(customerId);
// Reapply changes
facade.setEmail(newEmail);
facade.save();
}
}
Debugging Tips¶
1. Enable Logging
2. Inspect Facade State
System.out.println("Entity: " + facade.getCustomer());
System.out.println("Token: " + facade.getToken());
3. Test Criteria
SelectCriteriaList criteria = new SelectCriteriaList();
// ... add criteria
System.out.println("Criteria: " + criteria.toString());
4. Verify Configuration
EntityConfig config = ClientConfig.instance().getEntityConfig("Customer");
System.out.println("Entity class: " + config.getEntityClass());
System.out.println("ID field: " + config.getIdField());
Reference¶
EntityFacade Public Methods¶
| Method | Description | Returns |
|---|---|---|
search(String entityName, SelectCriteriaList) |
Search entities by criteria | List<TlinqEntity> |
search(String factory, String entity, SelectCriteriaList) |
Search with specific factory | List<TlinqEntity> |
read(String factory, String entity, Object... ids) |
Read entities by ID | List<TlinqEntity> |
write(TlinqEntity) |
Create or update entity | Object (ID) |
deleteEntity(TlinqEntity) |
Delete entity | Object (ID) |
deleteEntity(String factory, String entity, Object id) |
Delete by ID | Object (ID) |
callCustomService(TlinqEntity, String action, List params) |
Execute custom action | Object |
convertSearchCriteria(String factory, String entity, SelectCriteriaList) |
Convert criteria | SelectCriteriaList |
getServerDefault(String factory, String setting) |
Get factory property | String |
Common Facade Patterns¶
| Pattern | When to Use | Example |
|---|---|---|
| Stateful | Complex entity lifecycle | CustomerFacade, QuoteFacade |
| Stateless | Search and retrieval | ProductFacade, CategoryFacade |
| Hybrid | Flexible usage | OrderFacade |
| Domain | Multiple entities, workflows | OrderProcessingFacade |
SelectCriteriaList Operations¶
| Operator | Description | Example |
|---|---|---|
= |
Equals | criteria.addCriterion("id", "=", 123) |
!= |
Not equals | criteria.addCriterion("status", "!=", "cancelled") |
> |
Greater than | criteria.addCriterion("amount", ">", 100.0) |
< |
Less than | criteria.addCriterion("amount", "<", 1000.0) |
>= |
Greater or equal | criteria.addCriterion("date", ">=", startDate) |
<= |
Less or equal | criteria.addCriterion("date", "<=", endDate) |
like |
Pattern match | criteria.addCriterion("name", "like", "John%") |
in |
In list | criteria.addCriterion("status", "in", statusList) |
Facade Checklist¶
Creating a new facade:
- [ ] Extend EntityFacade
- [ ] Add entity constant
- [ ] Implement constructors (empty, new, load, existing)
- [ ] Add CRUD methods (save, load, delete)
- [ ] Add search methods
- [ ] Add business logic methods
- [ ] Add validation
- [ ] Add JavaDoc comments
- [ ] Write unit tests
- [ ] Document usage examples
Known Issues and Fixes (TQ-52)¶
The following issues in EntityFacade were identified and fixed during the TQ-52 code quality review:
Action String Comparison: == vs .equals()¶
Issue: EntityFacade internally compared action strings (e.g., "create", "update", "search") using == instead of .equals(). Since action strings may originate from XML configuration parsing, they may be distinct String instances with the same value. Using == performs reference comparison, which could fail.
Fix: All action string comparisons now use .equals() for correct value-based comparison.
Lesson: Always use .equals() for String comparison in Java, never ==.
Immutable List from Arrays.asList()¶
Issue: The callCustomService() method wrapped service parameters with Arrays.asList(), which returns a fixed-size list. Downstream code that attempted to modify this list (e.g., calling add() or remove()) would throw UnsupportedOperationException.
Fix: Changed to new ArrayList<>(Arrays.asList(...)) to produce a mutable list.
Lesson: When building parameter lists that may be modified downstream, use new ArrayList<>() rather than Arrays.asList().
Conclusion¶
EntityFacade is a powerful pattern in TQPRO that simplifies entity management and provides a clean API for application developers. By understanding the different facade patterns and following best practices, you can create maintainable, testable, and robust entity operations.
Key Takeaways¶
- Use the Right Pattern: Choose stateful, stateless, or hybrid based on use case
- Centralize Logic: Put business rules in facades, not entities
- Validate Early: Validate before operations to catch errors
- Handle Errors: Provide meaningful error messages
- Type Safety: Return typed lists, not raw collections
- Test Thoroughly: Write unit tests for all facade methods
Further Reading¶
- Entity Management Guide:
docs/ENTITY_MANAGEMENT_GUIDE.md - Entity Mapping Documentation:
config/entities/docs/README.md - Service Factory Guide:
docs/SERVICE_FACTORY_GUIDE.md - TQPRO Architecture:
docs/ARCHITECTURE.md
Document Version: 1.1 Last Updated: 2026-02-19 Maintained by: TourLinq Development Team
Revision History: - v1.1 (2026-02-19): Added "Known Issues and Fixes" section documenting TQ-52 bug fixes (action string comparison, immutable list) - v1.0 (2025-11-19): Initial version
For questions or issues, please contact the development team or file an issue in the project repository.