Advanced Topics

This section covers more advanced features and usage patterns in PojoQuery.

Inheritance Mapping

PojoQuery supports single-table inheritance using the @SubClasses and @SubClass annotations.

  1. Annotate the base class with @Table pointing to the table containing data for all subclasses.

  2. Annotate the base class with @SubClasses, specifying the discriminator column name and an array of @SubClass definitions.

  3. Each @SubClass annotation links a specific subclass (value) to its corresponding discriminatorValue.

  4. Define the subclasses extending the base class. Fields specific to subclasses must correspond to columns present in the single base table.

// Base class mapped to the 'person' table
@Table("person")
@SubClasses(
    discriminator = "person_type", // Discriminator column in 'person' table
    value = {
        @SubClass(value = Employee.class, discriminatorValue = "EMP"), // 'EMP' identifies Employee rows
        @SubClass(value = Customer.class, discriminatorValue = "CUST") // 'CUST' identifies Customer rows
    }
)
public abstract class Person {
    @Id Long id;
    String name;
    @FieldName("person_type") String type; // Field mapping to the discriminator column

    // Getters and setters omitted for brevity
}

// Employee subclass - fields map to columns in 'person' table
public class Employee extends Person {
    String department;
    BigDecimal salary;

    // Getters and setters omitted for brevity
}

// Customer subclass - fields map to columns in 'person' table
public class Customer extends Person {
    Integer loyaltyPoints;

    // Getters and setters omitted for brevity
}

// --- Querying ---
// Querying for the base class fetches instances of the correct subclass
List<Person> persons = PojoQuery.build(Person.class).execute(dataSource);

for (Person p : persons) {
    if (p instanceof Employee) {
        Employee emp = (Employee) p;
        System.out.println("Employee: " + emp.getName() + ", Dept: " + emp.getDepartment());
    } else if (p instanceof Customer) {
        Customer cust = (Customer) p;
        System.out.println("Customer: " + cust.getName() + ", Points: " + cust.getLoyaltyPoints());
    }
}

// You can also query directly for a subclass
List<Employee> employees = PojoQuery.build(Employee.class)
    .addWhere("employee.department = ?", "Sales") // Note: Alias might be 'employee' or 'person' depending on context
    .execute(dataSource);

Streaming Large Result Sets

For queries returning many rows, use executeStreaming() to process results row-by-row, avoiding OutOfMemoryError.

PojoQuery<LogEntry> query = PojoQuery.build(LogEntry.class)
    .addWhere("log.timestamp > ?", oneHourAgo);

// Process stream using DataSource
try (Stream<LogEntry> stream = query.executeStreaming(dataSource).stream()) {
    long errorCount = stream
        .filter(entry -> "ERROR".equals(entry.level))
        .count();
    System.out.println("Found " + errorCount + " errors.");
}

// Alternative using DB helper for raw SQL streaming
DB.queryRowsStreaming(dataSource,
    SqlExpression.sql("SELECT id, message FROM huge_log WHERE level = ?", "WARN"),
    rowMap -> {
        // Process each row as a Map<String, Object>
        System.out.println("Warning ID: " + rowMap.get("id"));
    });

Custom Joins and Conditions

Use @Join or addJoin() for joins not covered by convention-based relationship mapping. Use curly braces {} in addWhere() for alias resolution when referencing explicitly joined tables.

@Table("article")
@Join(type = JoinType.INNER, tableName = "category", alias = "cat", joinCondition = "article.category_id = cat.id")
public class ArticleWithCategory {
    @Id Long id;
    String title;
    // Map category fields explicitly since we're using @Join
    @Select("cat.name") String categoryName;
}

// Querying with condition on joined table
List<ArticleWithCategory> techArticles = PojoQuery.build(ArticleWithCategory.class)
    .addWhere("{cat.name} = ?", "Technology") // Use alias from @Join
    .execute(dataSource);

CRUD Operations

PojoQuery provides static helpers for basic Insert, Update, Delete.

  • Insert: PojoQuery.insert(dataSource, entity) or DB.insert(…​)

  • Handles single-table and multi-table inheritance inserts.

  • Populates the @Id field if it’s auto-generated.

  • Update: PojoQuery.update(dataSource, entity) or DB.update(…​)

  • Updates based on the @Id field(s).

  • Handles multi-table inheritance updates.

  • Respects @NoUpdate.

  • Can optionally handle optimistic locking if the entity implements HasVersion.

  • Delete: PojoQuery.delete(dataSource, entity) or PojoQuery.deleteById(dataSource, Class, id)

  • Deletes based on the @Id field(s).

  • Handles multi-table inheritance deletes (deletes from all mapped tables).

// --- Insert ---
User newUser = new User();
newUser.setFirstName("Jane");
newUser.setLastName("Doe");
newUser.setEmail("jane.doe@example.com");
PojoQuery.insert(dataSource, newUser); // newUser.getId() will be populated if auto-generated
System.out.println("Inserted user with ID: " + newUser.getId());

// --- Update ---
User existingUser = repo.findById(newUser.getId()); // Fetch first
if (existingUser != null) {
    existingUser.setEmail("jane.d@example.com");
    int updatedRows = PojoQuery.update(dataSource, existingUser);
    System.out.println("Updated rows: " + updatedRows);
}

// --- Delete ---
PojoQuery.delete(dataSource, existingUser); // Delete by entity instance
// OR
PojoQuery.deleteById(dataSource, User.class, existingUser.getId()); // Delete by ID

Transactions

Use DB.runInTransaction() for simple transaction management.

import java.sql.Connection;

DB.runInTransaction(dataSource, (Connection connection) -> {
    // Operations within this lambda run in a single transaction
    User user = new User();
    user.setFirstName("Temp");
    PojoQuery.insert(connection, user); // Use the provided connection

    Account account = new Account();
    account.setUserId(user.getId());
    account.setBalance(BigDecimal.ZERO);
    PojoQuery.insert(connection, account); // Use the provided connection

    // If no exception is thrown, the transaction commits automatically.
    // If an exception occurs, it rolls back automatically.
    return true; // Return value from the transaction lambda
});

Optimistic Locking

PojoQuery supports optimistic locking to prevent lost updates in concurrent environments. This is implemented through the HasVersion interface.

How It Works

  1. Your entity implements HasVersion, providing a version field

  2. On each update, PojoQuery checks that the version in the database matches the version in your entity

  3. If versions match, the update proceeds and the version is incremented

  4. If versions don’t match (another transaction modified the row), a StaleObjectException is thrown

Implementation

import org.pojoquery.HasVersion;

@Table("product")
public class Product implements HasVersion {
    @Id Long id;
    String name;
    BigDecimal price;
    Long version; // Version column in database

    @Override
    public Long getVersion() {
        return version;
    }

    @Override
    public void setVersion(Long version) {
        this.version = version;
    }

    // Other getters and setters omitted for brevity
}

Database Schema

Your table needs a version column (or whatever name you use):

CREATE TABLE product (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(255),
    price DECIMAL(10,2),
    version BIGINT DEFAULT 0
);

Usage Example

import org.pojoquery.StaleObjectException;

// User A fetches the product
Product productA = PojoQuery.build(Product.class)
    .addWhere("product.id = ?", 123L)
    .execute(dataSource)
    .get(0);
// productA.getVersion() = 1

// User B fetches the same product
Product productB = PojoQuery.build(Product.class)
    .addWhere("product.id = ?", 123L)
    .execute(dataSource)
    .get(0);
// productB.getVersion() = 1

// User A updates the product
productA.setPrice(new BigDecimal("29.99"));
PojoQuery.update(dataSource, productA); // Success! version becomes 2

// User B tries to update the same product
productB.setPrice(new BigDecimal("24.99"));
try {
    PojoQuery.update(dataSource, productB); // Fails! version mismatch
} catch (StaleObjectException e) {
    // Handle the conflict - typically refresh and retry, or notify user
    System.out.println("Product was modified by another user. Please refresh.");
}

Handling Stale Object Exceptions

public void updateProductPrice(Long productId, BigDecimal newPrice) {
    int maxRetries = 3;
    for (int attempt = 0; attempt < maxRetries; attempt++) {
        try {
            Product product = PojoQuery.build(Product.class)
                .addWhere("product.id = ?", productId)
                .execute(dataSource)
                .stream().findFirst()
                .orElseThrow(() -> new RuntimeException("Product not found"));

            product.setPrice(newPrice);
            PojoQuery.update(dataSource, product);
            return; // Success

        } catch (StaleObjectException e) {
            if (attempt == maxRetries - 1) {
                throw new RuntimeException("Failed to update after " + maxRetries + " attempts", e);
            }
            // Retry with fresh data
        }
    }
}