Advanced Topics
This section covers more advanced features and usage patterns in PojoQuery.
Inheritance Mapping
PojoQuery supports table-per-subclass inheritance using the @SubClasses annotation. Each subclass has its own table that extends the base table through a shared primary key.
-
Annotate the base class with
@Tablepointing to the base table. -
Annotate the base class with
@SubClasses, listing all subclass types. -
Each subclass extends the base class and has its own
@Tableannotation. -
The subclass table uses the same primary key as the base table (foreign key relationship).
Database Schema:
private static void createTables(DataSource db) {
DB.executeDDL(db, """
CREATE TABLE person (
id BIGINT IDENTITY PRIMARY KEY,
name VARCHAR(255)
)
""");
DB.executeDDL(db, """
CREATE TABLE employee (
id BIGINT PRIMARY KEY REFERENCES person(id),
department VARCHAR(100),
salary DECIMAL(10,2)
)
""");
DB.executeDDL(db, """
CREATE TABLE customer (
id BIGINT PRIMARY KEY REFERENCES person(id),
loyalty_points INT
)
""");
}
POJOs:
// Base class mapped to the 'person' table
@Table("person")
@SubClasses({Employee.class, Customer.class})
public static class Person {
@Id Long id;
String name;
public Long getId() { return id; }
public String getName() { return name; }
}
// Employee subclass - has its own 'employee' table
@Table("employee")
public static class Employee extends Person {
String department;
BigDecimal salary;
public String getDepartment() { return department; }
public BigDecimal getSalary() { return salary; }
}
// Customer subclass - has its own 'customer' table
@Table("customer")
public static class Customer extends Person {
Integer loyaltyPoints;
public Integer getLoyaltyPoints() { return loyaltyPoints; }
}
Querying:
// Querying for the base class fetches instances of the correct subclass
List<Person> persons = PojoQuery.build(Person.class).execute(db);
for (Person p : persons) {
if (p instanceof Employee emp) {
System.out.println("Employee: " + emp.getName() + ", Dept: " + emp.getDepartment());
} else if (p instanceof Customer cust) {
System.out.println("Customer: " + cust.getName() + ", Points: " + cust.getLoyaltyPoints());
}
}
You can also query directly for a subclass:
// You can also query directly for a subclass
List<Employee> employees = PojoQuery.build(Employee.class)
.addWhere("employee.department = ?", "Sales")
.execute(db);
for (Employee emp : employees) {
System.out.println("Sales employee: " + emp.getName());
}
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)orDB.insert(…) -
Handles single-table and multi-table inheritance inserts.
-
Populates the
@Idfield if it’s auto-generated. -
Update:
PojoQuery.update(dataSource, entity)orDB.update(…) -
Updates based on the
@Idfield(s). -
Handles multi-table inheritance updates.
-
Respects
@NoUpdate. -
Can optionally handle optimistic locking if the entity implements
HasVersion. -
Delete:
PojoQuery.delete(dataSource, entity)orPojoQuery.deleteById(dataSource, Class, id) -
Deletes based on the
@Idfield(s). -
Handles multi-table inheritance deletes (deletes from all mapped tables).
Insert:
// --- Insert ---
User newUser = new User();
newUser.setFirstName("Jane");
newUser.setLastName("Doe");
newUser.setEmail("jane.doe@example.com");
PojoQuery.insert(dataSource, newUser);
// newUser.getId() is now populated if auto-generated
System.out.println("Inserted user with ID: " + newUser.getId());
Update:
// --- Update ---
if (existingUser != null) {
existingUser.setEmail("jane.d@example.com");
int updatedRows = PojoQuery.update(dataSource, existingUser);
System.out.println("Updated rows: " + updatedRows);
}
Delete:
// --- Delete ---
// Delete by entity instance
PojoQuery.delete(dataSource, existingUser);
// Or delete by ID directly
// PojoQuery.deleteById(dataSource, User.class, existingUser.getId());
Transactions
PojoQuery provides transaction support through DB.runInTransaction(), which handles commit/rollback automatically.
Basic Usage
import java.sql.Connection;
import org.pojoquery.DB;
DB.runInTransaction(dataSource, (Connection connection) -> {
// All operations use the same connection = same transaction
User user = new User();
user.setFirstName("Alice");
PojoQuery.insert(connection, user);
Account account = new Account();
account.setUserId(user.getId());
account.setBalance(BigDecimal.ZERO);
PojoQuery.insert(connection, account);
return true; // Return value from the transaction
});
How It Works
-
Auto-commit disabled: The connection’s auto-commit is set to
false -
Commit on success: If the lambda completes without exception, the transaction commits
-
Rollback on failure: If any exception is thrown (checked or unchecked), the transaction rolls back
-
Return values: The lambda can return a value of any type
Method Signatures
There are two overloads available:
// Using a DataSource - connection is obtained and closed automatically
public static <T> T runInTransaction(DataSource dataSource, Transaction<T> transaction);
// Using an existing Connection - you manage the connection lifecycle
public static <T> T runInTransaction(Connection connection, Transaction<T> transaction);
Returning Values
Use the return value to get results from your transaction:
Order createdOrder = DB.runInTransaction(dataSource, connection -> {
Order order = new Order();
order.setCustomerId(customerId);
order.setOrderDate(LocalDate.now());
PojoQuery.insert(connection, order);
for (CartItem item : cartItems) {
OrderLine line = new OrderLine();
line.setOrderId(order.getId());
line.setProductId(item.getProductId());
line.setQuantity(item.getQuantity());
PojoQuery.insert(connection, line);
}
return order; // Return the created order with its generated ID
});
Exception Handling
When an exception occurs, the transaction rolls back and the exception propagates:
try {
DB.runInTransaction(dataSource, connection -> {
PojoQuery.insert(connection, order);
PojoQuery.insert(connection, invalidOrderLine); // This fails
return null;
});
} catch (DB.DatabaseException e) {
// Transaction was rolled back
// The cause contains the original SQLException
System.err.println("Transaction failed: " + e.getCause().getMessage());
}
Both SQLException and RuntimeException trigger a rollback. SQLExceptions are wrapped in DB.DatabaseException.
|
Void Transactions
If you don’t need a return value, simply return null or any placeholder:
DB.runInTransaction(dataSource, connection -> {
PojoQuery.delete(connection, oldRecord);
PojoQuery.insert(connection, newRecord);
return null; // No meaningful return value
});
Nested Transactions
PojoQuery does not support true nested transactions. If you call runInTransaction() with an existing connection that’s already in a transaction, both blocks share the same transaction:
DB.runInTransaction(dataSource, outerConn -> {
PojoQuery.insert(outerConn, record1);
// This uses the same transaction - NOT a nested transaction
DB.runInTransaction(outerConn, innerConn -> {
PojoQuery.insert(innerConn, record2);
return null;
});
return null;
});
// Both inserts commit or rollback together
For complex transaction scenarios requiring savepoints or isolation level control, obtain a Connection directly from your DataSource and use standard JDBC transaction APIs.
|
Optimistic Locking
PojoQuery supports optimistic locking to prevent lost updates in concurrent environments. This is implemented through the HasVersion interface.
How It Works
-
Your entity implements
HasVersion, providing aversionfield -
On each update, PojoQuery checks that the version in the database matches the version in your entity
-
If versions match, the update proceeds and the version is incremented
-
If versions don’t match (another transaction modified the row), a
StaleObjectExceptionis 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
}
}
}