Unlocking Full Functionality
Framework Philosophy & Prerequisites
EzQu operates on a strict "Java-First" (Code-First) architecture. In this paradigm, your Java domain model serves as the primary source of truth for the application's data structure. While EzQu provides a flexible API that can interact with unannotated POJOs for basic SQL mapping, developers must use annotations to enable full CRUD capabilities, optimistic concurrency, and complex object relationships.
The Bytecode Enhancement Mandate
To leverage advanced framework features, specifically object relationships (@One2Many, @Many2Many, etc.), inheritance strategies, and lazy loading, EzQu requires post-compilation byte code enhancement. This process must be integrated into your build pipeline using one of the following:
- Gradle Plugin: Configured via PostCompileTask in build.gradle.
- Maven Plugin:
- Ant Task: Implemented using EzquAssemblyTask within ezqu-ext.jar.
- Eclipse Plugin: Provides a custom Java builder for real-time development enhancement.
- Idea Plugin: Provides a compile time builder within the idea development project.
Architectural Note: While unannotated POJOs are sufficient for simple fluent queries where field names match column names, bytecode enhancement is the prerequisite for the framework's managed state and relationship navigation.
Depth Control (The N+1 Solution)
Architects must manage the depth of the object graph during persistence and retrieval to prevent the N+1 select problem. EzQu provides the depth parameter in Db operations:
- db.insert(entity, depth), db.update(entity, depth), db.merge(entity, depth): Controls how many levels of the relationship graph are persisted. Default - Db.FULL_DEPTH (-1): A constant used to persist/merge the entire reachable object graph.
Optimizing Data Retrieval
When working with complex, interconnected object graphs, determining when to fetch related data is critical for application performance. If an ORM retrieves an entity and automatically fetches all of its associated collections and parent objects at the same time, it can lead to severe performance bottlenecks, most notably over-fetching data and the "N+1 select problem," where a single query triggers dozens of subsequent queries. Conversely, deferring all data retrieval might result in excessive database calls later in your business logic.
EzQu solves this by providing fine-grained control over relationship loading strategies, allowing you to seamlessly dictate whether a relationship should be fetched eagerly (immediately) or lazily (deferred until accessed)
Important: Because managing these deferred method calls and immediate SQL executions requires intercepting standard Java getter methods, EzQu's lazy and eager loading capabilities rely on bytecode enhancement. You must ensure that your project is configured to run EzQu's post-compilation processing (via the Ant, Gradle, or Eclipse plugins) for these loading strategies to function. See here
Eager loading and lazy loading are strategies used to control when related entities are fetched from the database when you retrieve a parent object.
- Eager Loading: This approach fetches the associated child objects or collections immediately alongside the parent object. The main drawback of eager loading is that you risk pooling too much data from the database on a single call. If poorly configured, eager fetching can lead to the "N+1 select problem," where one query fetches the parent entities, and then N additional queries are immediately executed to fetch all associated records.
- Lazy Loading: This approach defers the fetching of the related data until that specific relationship is explicitly accessed in your code (for example, when you call a getter method on the relationship field). This saves memory and initial query execution time, fetching extra data only when you actually need it.
To optimize performance and avoid the N+1 problem, EzQu applies the following default strategies based on the relationship type:
- Default Eager: Single-valued associations, specifically
@One2Oneand@Many2One, are loaded eagerly by default. - Default Lazy: Collection-based associations, specifically
@One2Manyand@Many2Many, are lazy-loaded by default.
The developer can override these default behaviors using EzQu's annotations to fine-tune your application's performance:
- Overriding
@One2One: If you want to defer the loading of a 1:1 relationship, you can explicitly apply the@Lazyannotation to the relationship field. - Overriding
@One2Many: You can force eager loading by setting the property eagerLoad = true inside the@One2Manyannotation, though the documentation warns to use this cautiously to avoid pulling down too much data at once. @Many2ManyConstraints: For many-to-many relationships, lazy loading is strictly enforced and is always used. However, by calling the getter within the db session, you can trigger a fetch for the related objects.
Object Inheritance Mapping in EzQu
Object-oriented programming heavily relies on inheritance to share logic and structure, but mapping hierarchical object graphs onto a flat relational database schema presents a classic challenge (the Object-Relational Impedance Mismatch).
EzQu handles deep object graphs and inheritance hierarchies natively. To ensure that your inheritance contracts remain strict, safe, and easy to maintain, EzQu centralizes the hierarchy's configuration at the root class while requiring child classes only to declare their specific identity within that tree.
EzQu offers two primary strategies for inheritance mapping: Table Per Class and Discriminator (Single Table).
Hierarchy Configuration: The Root Class
To define your object tree, you must define the rules for your entire object graph. This is done by placing the @Inheritance annotation on the topmost parent class (the root) of your hierarchy.
By locking the configuration at the root level, EzQu ensures that subclasses cannot accidentally mutate the inheritance strategy mid-tree, preventing broken SQL generation and maintaining a clean architecture.
The @Inheritence annotation, used for defining the object graph hierarchy is placed only on the root @Entityor @MappedSuperclass, this annotation dictates how the entire tree is persisted. It accepts the following parameters:
The @Inheritence annotation, used for defining the object graph hierarchy is placed only on the root @Entityor @MappedSuperclass, this annotation dictates how the entire tree is persisted. It accepts the following parameters:
- inheritedType: Defines the strategy. Must be InheritedType.TABLE_PER_CLASS (default) or InheritedType.DISCRIMINATOR.
- discriminatorColumn: Used only for the Discriminator strategy. Defines the database column that holds the child identifiers. Defaults to "class". It is ignored for TABLE_PER_CLASS.
- discriminatorTableName: Used only for the Discriminator strategy. Defines the name of the single shared table. If omitted, the table name defaults to the mapped name of the root class. It is ignored for TABLE_PER_CLASS.
Table Per Class (Default)
In the Table Per Class strategy, each concrete subclass gets its own distinct table in the database. This table contains columns for all the fields inherited from the parent class, alongside the specific columns introduced by the child.
Benefits & Constraints:
- It is the easiest strategy to implement.
- It fully supports auto-incrementing primary key generators, such as IDENTITY and SEQUENCE
Polymorphic queries (querying the parent to get all children) require querying multiple tables under the hood and is not supported for entity relationships!!
How to Configure:
- Root Class: Annotate with @Inheritance. All defaults for this annotation are set up for TABLE_PER_CLASS
- Child Classes: Annotate any subclass with
@Inherited. This parameter-less annotation explicitly flags the class as participating in a multi-table inheritance tree. If the child class is also@MappedSuperClass(i.e. abstract) it should also be marked as@Inherited. Also if the class is a 'grandchild' class it still only needs@Inheritedand so does its parent.
Code Example: Table Per Class:
// 1. The Root Class
@Entity
@Inheritance
public class Animal {
@PrimaryKey(GenerationType = GenerationType.IDENTITY)
private Long id;
private String name;
}
// 2. The Child Class (Gets its own 'Dog' table with 'id', 'name', and 'breed')
@Entity
@Inherited
public class Dog extends Animal {
private String breed;
}
// 3. Another Child Class (Gets its own 'Cat' table with 'id', 'name', and 'isIndoor')
@Entity
@Inherited
public class Cat extends Animal {
private Boolean isIndoor;
}
Single Table with Discriminator
In the Discriminator strategy, all classes in the entire inheritance hierarchy share a single relational database table. This shared table contains all common inherited columns, plus all the specific columns from every child class.
To differentiate between the various object types stored within this single table, EzQu uses a single char "discriminator column".
Benefits & Constraints:
- High Performance: Because all data is stored in one table, it completely eliminates the need for complex JOIN or UNION operations when fetching polymorphic data.
- It fully supports auto-incrementing primary key generators (IDENTITY and SEQUENCE).
- If a superclass is used as the target of a relationship (like a Foreign Key, @One2Many, or @Many2Many), you must use the DISCRIMINATOR strategy so EzQu knows which table to join and how to instantiate the correct child object.
How to configure:
- Root Class: Annotate with @Inheritance(inheritedType = InheritedType.DISCRIMINATOR, discriminatorColumn="class", discriminatorTableName="tree"). DiscriminatorColumn and discriminatorTableName have defaults so it is not mandatory to define them however it is recommended. See here for more details.
- Child Classes: Annotate subclasses with @Discriminator. You must supply the discriminatorValue parameter, which is a single char that uniquely identifies this specific child class in the shared database table.
Important Note: If a child class is marked as @MappedSuperclass (abstract), use @Inherited instead of @Discriminator even when the strategy is Discriminator. This lets its fields be mapped to subclasses, but the superclass itself gets no discriminator.
Code Example: Discriminator:
// 1. The Root Class
@MappedSuperclass // (See section below on MappedSuperclasses)
@Inheritance(
inheritedType = InheritedType.DISCRIMINATOR,
discriminatorColumn = "entity_type",
discriminatorTableName = "VEHICLES"
)
public abstract class Vehicle {
@PrimaryKey(GenerationType = GenerationType.IDENTITY)
private Long id;
private String manufacturer;
}
// 2. The Child Class (Car)
@Entity
@Discriminator(discriminatorValue = 'C') // Explicitly declares its identity
public class Car extends Vehicle {
private Integer numberOfDoors;
}
// 3. Another Child Class (Motorcycle)
@Entity
@Discriminator(discriminatorValue = 'M') // Explicitly declares its identity
public class Motorcycle extends Vehicle {
private Boolean hasSidecar;
}
_Under the hood: During a CRUD operation for a Car child instance, EzQu intercepts the action and automatically injects the entitytype column into the SQL execution, assigning it the 'C' character you defined.
Abstract Parents vs. Concrete Parents
When defining your root class, and some times even a child class (which it self is a parent) you have two options depending on your domain model:
- @Entity Parent: If your root class is a concrete concept that can be instantiated and saved to the database on its own (e.g., a standard Employee class extended by a Manager class), annotate it with standard @Entity (alongside @Inheritance).
- @MappedSuperclass Parent: If your root class is strictly an abstract template designed only to share fields with its children (e.g., an abstract BaseEntity that holds id, createdAt, and updatedAt), annotate it with
@MappedSuperclass. A class annotated with@MappedSuperclasswill not be mapped to its own standalone table in the DB, or with its own discriminator, but its fields will be inherited by its children based on the configured @Inheritance strategy.
As mentioned earlier: If a @MappedSuperclass inherits from another @MappedSuperclass, you must still apply the @Inherited annotation to the child superclass so the framework understands the chain.
Custom Data Conversion
EzQu automatically handles the heavy lifting of converting standard Java types (such as String, Integer, or Date) into their corresponding SQL types across various database dialects. However, real-world business logic often requires more complex data structures. You might need to parse a raw JSON string from the database into a structured Java object, encrypt sensitive text before persisting it, or map legacy database flags into a modern Java List. Without custom intervention, EzQu defaults to serializing unrecognized complex Java objects into database BLOBs, making the data difficult to query or read externally.
To solve this, EzQu provides a seamless mechanism for developers to supply custom type conversions. By implementing the EzquConverter interface and applying the @Converter annotation directly to a field, you establish a transparent bridge between the raw database column and your Java property. During runtime, EzQu automatically intercepts the data during reads and writes, applying your custom transformation logic on the fly while keeping your entity's API clean and object-oriented.
To implement a custom converter in EzQu, you need to create a class that implements the EzquConverter<I, O> interface and then apply the @Converter annotation to the desired field in your entity.
This feature is useful when you need to apply custom logic to transform data between its raw database format and a different Java object representation, such as parsing JSON strings, applying encryption/decryption, or converting legacy database flags. Here is an example
- Implement the EzquConverter Interface.
The EzquConverter interface defines two generic types:
* I: The data type as it is stored in the database.
* O: The data type as it is represented in your Java object.[:br:][:p:]
You must implement two methods to handle the bidirectional mapping:
* fromDb(I value): Converts the value coming from the database into your Java object's type.
* toDb(O value): Converts the value from your Java object back into the database type before saving.
Example: Converting a Comma-Separated String (DB) to a List<String> (Java)
import com.centimia.orm.ezqu.util.EzquConverter;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
// I = String (Database column type)
// O = List<String> (Java object field type)
public class StringListConverter implements EzquConverter<String, List<String>> {
@Override
public List<String> fromDb(String value) {
if (value == null || value.trim().isEmpty()) {
return null;
}
// Convert the raw DB string back into a Java List
return Arrays.stream(value.split(","))
.map(String::trim)
.collect(Collectors.toList());
}
@Override
public String toDb(List<String> value) {
if (value == null || value.isEmpty()) {
return null;
}
// Convert the Java List into a comma-separated String for the DB
return String.join(",", value);
}
}
- Apply the @Converter Annotation
Once your custom converter class is built, you instruct EzQu to use it by placing the @Converter annotation directly on the target field within your @Entity class. You must provide your custom converter's class type as the annotation's value.
Important Constraint: The @Converter annotation must only be used on standard, non-relational fields. It cannot be applied to relationship fields (such as those that would use @One2Many or @Many2One).
import com.centimia.orm.ezqu.annotation.Entity;
import com.centimia.orm.ezqu.annotation.PrimaryKey;
import com.centimia.orm.ezqu.annotation.Converter;
import java.util.List;
@Entity
public class UserProfile {
@PrimaryKey
private Long id;
private String username;
// EzQu will automatically invoke StringListConverter during DB reads and writes
@Converter(StringListConverter.class)
private List<String> roles;
public UserProfile() {}
// Getters and setters...
}
By setting this up, EzQu will automatically intercept data going to and from the roles field, ensuring it is transparently stored as a String in the database but comfortably handled as a List<String> in your application's business logic.
Advanced Querying:
Aliases, Grouping, and Primary Key FiltersWhile EzQu’s type-safe fluent API handles the vast majority of SQL generation and mapping, enterprise applications often require highly specific query logic, logical groupings, or direct manipulation of underlying table aliases. EzQu provides several advanced methods within its fluent API to handle these complex scenarios.
Custom Conditions and Aliases
If you need to construct vendor-specific SQL conditions that go beyond the standard builder methods, you can inject custom SQL strings directly into your 'where' clauses using a StringFilter.
When you pass a lambda function to .where(), EzQu provides you with an instance of the ISelectTable interface. Because EzQu dynamically generates SQL aliases (e.g., T1, T2) during query construction to prevent collisions, you cannot hardcode table names in your custom strings. Instead, you use ISelectTable to dynamically discover these generated aliases.
The ISelectTable interface exposes the following metadata:
getAs(): Returns the generated alias ID given to the table within the query (e.g., returning "T1").getJoins(): Returns aMap<Object, String>containing the table identifiers and their corresponding alias names for all joined tables. If you do not have the descriptor consider usinggetOrderedJoinswhere you get the joins in the order which they were put in the query.getOrderedJoins(): Returns a Set of table IDs and their aliases, strictly preserving the order in which the joins were added to the query.getJoinType(): Returns the specific type of join (e.g., INNER, LEFT), or NONE if the table is the initial primary table in the FROM clause.
List<Employee> results = sessionFactory.getFromSession(db -> {
// 3. Injecting a custom SQL condition via StringFilter & ISelectTable
// The lambda parameter 'st' represents the ISelectTable instance.
Employee emp = new Employee();
return db.from(emp).where(st -> {
// Dynamically fetch the primary table's generated alias
String alias = st.getAs();
return alias + ".hire_date > '2020-01-01'";
})
.select();
});
Grouping Conditions
When building complex logical checks with the fluent API, you frequently need to group specific boolean conditions together, the equivalent of using parentheses () in raw SQL.
List<Employee> results = sessionFactory.getFromSession(db -> {
Employee emp = new Employee();
Employee manager = new Employee();
return db.from(emp)
// Grouping conditions using wrap() and endWrap()
.innerJoin(manager).on(emp.getManager()).is(manager)
.where(emp.getDepartmentId()).is(100L)
.and().wrap()
.where(emp.getStatus()).is("ACTIVE")
.or(emp.getStatus()).is("ON_LEAVE")
.endWrap().select();
});
Filtering by Relationship Keys
Often, your entity holds a relationship to another @Entity (such as a O2O foreign key field). If you want to filter a query based strictly on that foreign key's value, instantiating a dummy object just to populate that relationship field is cumbersome.
To solve this, EzQu provides the Db.asPrimaryKey() utility method. It returns a GenericMask that tells the query builder to evaluate your condition directly against the related entity's primary key column, rather than comparing it against a full entity object.
List<Employee> results = sessionFactory.getFromSession(db -> {
Employee emp = new Employee();
return db.from(emp)
.where(emp.getDepartmentId()).is(100L)
// Querying a relationship field using Db.asPrimaryKey()
// Assuming 'emp.getManager()' maps to a Manager @Entity.
// We check the manager's PK without needing a Manager object instance.
.and(Db.asPrimaryKey(emp.getManager(), Long.class)).is(50L).select()
});
Pagination, Limits, and Ordering
When querying large datasets, controlling the size of the result set and the order in which rows are returned is essential for both performance and user experience. EzQu provides built-in mechanisms to handle sorting, limiting, and pagination, seamlessly translating these operations into dialect-specific SQL.
To sort results when using the fluent query builder, you can append ordering directives directly to your where clause before invoking .select(). EzQu provides two primary methods for this: orderBy() for ascending order, and orderByDesc() for descending order.
Null Handling Across Dialects: Sorting nullable columns can yield inconsistent results depending on the database vendor. EzQu standardizes this behavior using its internal OrderExpression. By default, EzQu attempts to explicitly append NULLS FIRST (for descending sorts) or NULLS LAST (for ascending sorts). If the configured database dialect does not natively support these clauses, EzQu automatically emulates the behavior by prepending a CASE WHEN [field] IS NULL THEN 0 ELSE 1 END statement to the generated SQL.
List<Employee> results = sessionFactory.getFromSession(db -> {
Employee emp = new Employee();
// 4. ordering
return db.from(emp)
.where(emp.getDepartmentId()).is(100L)
// Sorts the results by hireDate in descending order (newest first)
.orderByDesc(emp.getHireDate())
.select();
});
Ordering Relationship Collections: When dealing with entity relationships, you often want the child records to be automatically sorted when they are retrieved. EzQu allows you to define default sorting rules directly within your relationship annotations. Both the @One2Many and @Many2Many annotations accept two optional attributes to govern sorting:
orderByField: The name of the field in the target/child entity that determines the order of the response.direction: The direction of the sort. The default is "ASC", but it can be set to "DESC".
When EzQu lazy-loads or eagerly fetches these relationships from the database, it automatically generates the SQL to sort them based on these parameters. If the collection is already loaded into memory and needs to be re-sorted, EzQu utilizes an internal FieldComperator to maintain the correct order in the Java collection.
@Entity
public class Department {
@PrimaryKey
private Long id;
// Automatically sorts the lazy-loaded employees by their 'lastName' field in ascending order
@One2Many(childType = Employee.class, orderByField = "lastName", direction = "ASC")
private List<Employee> employees;
}
Limits and Pagination: To restrict the number of rows returned by a query, or to implement pagination, EzQu's underlying engine utilizes LimitToken and OffsetToken objects. (Tokens are EzQu internal command operators which create the underlying SQL).
Because different relational databases use drastically different syntax for limiting results (e.g., LIMIT / OFFSET in PostgreSQL and MySQL, FETCH NEXT in newer Oracle versions, or complex sub-queries in older SQL Server versions), EzQu delegates the actual SQL generation to the configured Dialect.
List<Employee> topTenEmployees = sessionFactory.getFromSession(db -> {
Employee emp = new Employee();
return db.from(emp)
.where(emp.getStatus()).is("ACTIVE")
// Sort newest first
.orderByDesc(emp.getHireDate())
// Restrict the query to return only 10 results
.limit(10)
.select();
});
List<Employee> secondPageEmployees = sessionFactory.getFromSession(db -> {
Employee emp = new Employee();
int pageSize = 10;
int pageNumber = 2; // Assuming 1-indexed pages for this example
int offsetValue = (pageNumber - 1) * pageSize; // Skips the first 10 records
return db.from(emp)
.where(emp.getStatus()).is("ACTIVE")
// Explicit ordering is critical for consistent pagination
.orderByDesc(emp.getHireDate())
// Restrict to 10 results per page
.limit(pageSize)
// Skip the previous pages
.offset(offsetValue)
.select();
});
Note: When paginating, it is highly recommended to always include an orderBy or orderByDesc clause. Without explicit ordering, relational databases do not guarantee a consistent row order, which can lead to missing or duplicated records across pages.
Executing Native SQL and Callable Statements
While EzQu's fluent API and Object-Relational Mapping are designed to handle most database interactions, there are inevitably situations where you must write complex, highly-optimized native SQL or invoke vendor-specific database functions and stored procedures.
To bridge this gap, the Db session object provides methods that allow you to execute raw SQL and Callable Statements directly against the database, while still leveraging EzQu's ability to map the results back into your Java POJOs.
Executing Native SQL Queries (executeQuery)
To run a raw SELECT query, you can use the executeQuery methods. EzQu handles the underlying PreparedStatement generation and result mapping for you.
Mapping Directly to a Class: The simplest approach is to pass your raw SQL string, the target Class you want the results mapped to, and any parameters. EzQu will automatically map the resulting columns to the fields of the provided class.
List<Employee> activeEmployees = sessionFactory.getFromSession(db -> {
String sql = "SELECT * FROM EMPLOYEES WHERE status = ? AND department_id = ?";
// Executes the query and maps the ResultSet into a List of Employee objects
return db.executeQuery(sql, Employee.class, "ACTIVE", 100L);
});
Custom Result Processing (IResultProcessor): If your native query returns complex aggregated data that doesn't cleanly map to a single entity, you can use the IResultProcessor interface to manually process the ResultSet.
List<DepartmentStats> stats = sessionFactory.getFromSession(db -> {
String sql = "SELECT department_id, COUNT(*) as emp_count FROM EMPLOYEES GROUP BY department_id";
return db.executeQuery(sql, rs -> {
List<DepartmentStats> resultList = new ArrayList<>();
while (rs.next()) {
DepartmentStats stat = new DepartmentStats();
stat.setDeptId(rs.getLong("department_id"));
stat.setCount(rs.getInt("emp_count"));
resultList.add(stat);
}
return resultList;
});
});
You can also execute pre-configured PreparedStatement objects using db.executeQuery(PreparedStatement stmnt, Class clazz), but be aware that EzQu does not automatically close the statement for you in this scenario.
Executing Native Updates (executeUpdate)
For raw INSERT, UPDATE, or DELETE operations, use the executeUpdate method.
sessionFactory.runInSession(db -> {
String sql = "UPDATE EMPLOYEES SET status = ? WHERE last_login < ?";
// Returns the number of affected rows
int updatedRows = db.executeUpdate(sql, "INACTIVE", someDateLimit);
});
Invoking Stored Procedures (executeCallable)
To trigger database stored procedures, use the executeCallable method. The SQL string format must follow the standard JDBC callable syntax: {call procedure_name(?, ?)}.
Similar to raw queries, EzQu can process the results of the callable statement and return them as a mapped List of objects.
List<ArchivedRecord> records = sessionFactory.getFromSession(db -> {
// Calling a stored procedure that takes two parameters
String callableSql = "{call archive_old_records(?, ?)}";
// Executes the procedure and maps the returned data to ArchivedRecord objects
return db.executeCallable(callableSql, ArchivedRecord.class, "2023-01-01", true);
});
Like prepared statements, you can also pass a pre-configured CallableStatement directly, but you must manage its closure.
Native Executions and Cache Clearing
As discussed in the caching mechanics section, executing raw SQL or callable statements automatically clears EzQu's multi-call cache.
Because native updates and procedures execute without object-graph awareness and directly affect the state of the database, EzQu can no longer guarantee that the objects it previously held in memory are still valid or consistent with the database.
Whenever you invoke executeUpdate(), executeCallable(), or executeQuery(), EzQu purges the cache for the current session to ensure safety. Consequently, if you re-fetch an entity after running a native command within the same session, it will be a completely new Java object instance, not the one you were working with previously.
Locking
In a relational database, locking is used to stop data from changing between the time it is read and the time it is acted upon. (Or at least acknowledge the fact it has and not allow the action).
There are two main approaches:
- Optimistic locking - assumes transactions won't interfere with one another. Data isn't locked when read; instead, before a transaction commits it checks whether the data it read has been altered by another transaction. If a conflict is detected, the transaction is rolled back.
- Pessimistic locking - assumes concurrent transactions will conflict. Data is locked as soon as it is read and remains locked until the transaction finishes using it, preventing other transactions from modifying it in the meantime.
EzQu supports only optimistic locking and does not provide pessimistic locking at this time.
To enable optimistic locking, a developer marks a field with the @Version annotation. Currently @Version is supported only for Long and Integer types, and it can be applied to a single field per entity; multiple fields for optimistic locking are not allowed.
If an @Version field annotation is present, EzQu compares the entity's version with the value stored in the database before executing the update. When the numbers differ, a concurrency exception is raised and the transaction is rolled back. If they match, the update proceeds and EzQu automatically increments the version field on commit.
Here is an example of a version declaration:
@Entity
public class Person {
@PrimaryKey(GeneratorType=GeneratorType.IDENTITY)
private Long id;
@Column
private String name;
@Version
private Long version;
//Getters and setters are omitted for brevity
}
Caching Mechanics
To optimize performance and minimize unnecessary database trips, EzQu implements internal session-level caching. It is important to understand how this works to avoid accidental data inconsistencies, especially when performing partial updates.
Unlike many heavy enterprise ORMs, EzQu intentionally does not support second-level caching. This architectural decision eliminates the complexity of cross-session data staleness. Instead, EzQu relies on two scoped caching mechanisms: a query cache and a per-session "multi-call" cache (managed internally via multiCallCache and reEntrantCache).
How the Multi-Call Cache Works
The primary goal of the multi-call cache is to ensure that multiple queries retrieving the same database record within a single Db session return the exact same Java object instance.
For example, if you fetch entity A which holds a relationship to entity C, and later in the same session you fetch entity B which also relates to C, EzQu will use the cached instance of C for both. If a change is applied to C, that change instantly reflects across all objects holding that relationship in your current session.
The Pitfall: Partial-Depth Updates and Cache Desynchronization
Because the multi-call cache blindly trusts the objects it has already processed during the current connection, performing CRUD operations with a depth less than Db.FULL_DEPTH can lead to severe state inconsistencies.
Consider a scenario where ObjectA holds ObjectB, which in turn holds ObjectC:
sessionFactory.runInSession(db -> {
// 1. Fetch the full graph from the DB
ObjectA aFromDb = new ObjectA();
aFromDb = db.from(aFromDb).primaryKey().is(1L).selectFirst();
// The multiCallCache now holds the DB state for A, B, and C.
// 2. Modify the entire graph in Java
aFromDb.setName("New A");
aFromDb.getB().setName("New B");
aFromDb.getB().getC().setName("New C");
// 3. Update the DB, but artificially limit the depth to 1!
// This updates A and B in the DB, but leaves C untouched.
db.update(aFromDb, 1);
// 4. Re-fetch the object using a new query
ObjectA a2 = new ObjectA();
a2 = db.from(a2).primaryKey().is(1L).selectFirst();
// PITFALL: a2.getB().getC().getName() will return "New C"
// instead of the actual database value!
});
Why did this happen? When the fluent API queried the database for a2, it recognized that A, B, and C were already in the multi-call cache. It pulled C directly from the cache, which still holds the un-persisted "New C" state from step 2, completely ignoring the fact that the actual database row for C was never updated.
How to Manage the Cache Safely
If you frequently run into scenarios where you mix partial-depth updates with subsequent read queries in the exact same business transaction, you must manually manage the cache state:
- Close and Reopen the Session: The safest way to clear the multi-call cache is to simply close the current connection and open a new one.
- Execute Raw SQL: If you execute raw SQL updates (
executeUpdate()) or callable statements (executeCallable()), EzQu automatically clears the multi-call cache for you. Because raw SQL modifies the database state outside of EzQu's object graph awareness, the framework assumes all cached objects are potentially invalid and purges them to guarantee safety. - Use the
@ImmutableAnnotation: If you have read-only lookup entities (like a Country or Status table), you can annotate them with@Immutable. This tells EzQu that it doesn't matter if multiple distinct instances of the same database record exist in memory, preventing the framework from throwing concurrency exceptions when it encounters variations in the cache.