Skip to content

TQPRO EntityFacade Guide

Version: 1.0 Last Updated: 2025-11-19 Author: TourLinq Development Team


Table of Contents

  1. Introduction
  2. The Facade Pattern in TQPRO
  3. EntityFacade Architecture
  4. Using EntityFacade
  5. Creating Custom Facades
  6. Facade Patterns and Styles
  7. Advanced Topics
  8. Best Practices
  9. Complete Examples
  10. Troubleshooting
  11. 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

protected String sessionToken = null;

public EntityFacade(String token) {
    sessionToken = token;
}

All operations use the session token for authentication and authorization.

2. CRUD Operations

Search

public List<TlinqEntity> search(String entityName, SelectCriteriaList criteria)

Read

public List<TlinqEntity> read(String factoryName, String entityName, Object... ids)

Write (Create/Update)

public Object write(TlinqEntity entity)

Delete

public Object deleteEntity(TlinqEntity entity)
public Object deleteEntity(String factoryName, String entityName, Object idValue)

3. Custom Service Execution

public Object callCustomService(TlinqEntity entity, String serviceAction, List serviceParams)

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

Logger.getLogger("com.perun.tlinq.entity").setLevel(Level.FINE);

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

  1. Use the Right Pattern: Choose stateful, stateless, or hybrid based on use case
  2. Centralize Logic: Put business rules in facades, not entities
  3. Validate Early: Validate before operations to catch errors
  4. Handle Errors: Provide meaningful error messages
  5. Type Safety: Return typed lists, not raw collections
  6. 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.