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.
-
Annotate the base class with
@Tablepointing to the table containing data for all subclasses. -
Annotate the base class with
@SubClasses, specifying thediscriminatorcolumn name and an array of@SubClassdefinitions. -
Each
@SubClassannotation links a specific subclass (value) to its correspondingdiscriminatorValue. -
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)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 ---
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
-
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
}
}
}