Hibernate HQL and Criteria API Interview Questions and Answers
Master Hibernate querying with production-ready interview questions covering HQL, JPQL, Criteria API, joins, named parameters, projections, pagination, bulk updates, dynamic filters, performance optimization, and senior-level best practices.
Introduction
Hibernate provides several ways to query relational data:
- Hibernate Query Language
- Jakarta Persistence Query Language
- Criteria API
- Native SQL
- Named Queries
- DTO Projections
- Spring Data repository queries
Choosing the correct querying technique affects:
- Code readability
- Type safety
- Dynamic filtering
- Database portability
- SQL performance
- Maintainability
- Testability
- Pagination behavior
- Persistence-context consistency
HQL and JPQL use SQL-like query strings, but they query entities and entity properties rather than database tables and columns.
The Criteria API builds queries programmatically using Java objects. It is particularly useful when query conditions must be assembled dynamically.
A senior developer should understand not only query syntax, but also:
- Generated SQL
- Fetch joins
- Parameter binding
- DTO projections
- Pagination
- Bulk update behavior
- N+1 query prevention
- Query plan stability
- Database indexing
This guide covers the 15 most frequently asked Hibernate HQL and Criteria API interview questions with code examples, Mermaid diagrams, production scenarios, common mistakes, and senior-level recommendations.
Q1. What is HQL?
Answer
HQL stands for Hibernate Query Language.
It is an object-oriented query language used to query Hibernate entities and their relationships.
Unlike SQL, HQL refers to:
- Entity class names
- Entity field names
- Entity relationships
It does not normally use physical table and column names.
Entity Example
@Entity
@Table(name = "customers")
public class Customer {
@Id
private Long id;
@Column(name = "customer_name")
private String name;
private String email;
private boolean active;
}
HQL Query
List<Customer> customers =
session.createQuery(
"""
from Customer c
where c.active = true
order by c.name
""",
Customer.class
)
.getResultList();
Generated SQL Concept
SELECT
c.id,
c.customer_name,
c.email,
c.active
FROM customers c
WHERE c.active = true
ORDER BY c.customer_name;
Query Translation
flowchart TD
A[HQL Query] --> B[Hibernate Query Parser]
B --> C[Entity Mapping Metadata]
C --> D[Database-Specific SQL]
D --> E[JDBC]
E --> F[Relational Database]
F --> G[Rows]
G --> H[Hibernate Entities]
Why Interviewers Ask This
Interviewers want to confirm that you understand the difference between querying the object model and querying the relational schema directly.
Production Example
Retrieving all active customers without coupling application code to physical table or column names.
Common Mistake
Writing database table names in an HQL query.
Incorrect:
from customers
Correct:
from Customer
Senior Follow-up
HQL supports Hibernate-specific capabilities beyond standardized JPQL, so provider portability should be considered when those features are used.
Q2. What is the difference between HQL and SQL?
Answer
HQL queries entities and their mapped properties.
SQL queries database tables and columns.
Comparison
| HQL | SQL |
|---|---|
| Queries entities | Queries tables |
| Uses Java property names | Uses column names |
| Understands relationships | Requires explicit join conditions |
| Supports polymorphism | Works with relational structures |
| Converted to SQL by Hibernate | Executed directly by database |
| More database-portable | May use database-specific features |
| Returns entities or projections | Returns rows or scalar values |
HQL Example
select o
from Order o
where o.customer.email = :email
Equivalent SQL Concept
SELECT
o.*
FROM orders o
JOIN customers c
ON c.id = o.customer_id
WHERE c.email = ?;
Relationship Navigation
flowchart LR
A[Order Entity] --> B[customer Property]
B --> C[email Property]
C --> D[Hibernate Determines Required SQL Join]
HQL Benefit
The query navigates through:
o.customer.email
Hibernate uses mapping metadata to determine the required foreign-key join.
Common Mistake
Assuming HQL prevents every database-specific performance issue.
Senior Follow-up
Developers must still understand generated SQL, indexes, cardinality, joins, execution plans, and database-specific limitations.
Q3. What is JPQL?
Answer
JPQL stands for Jakarta Persistence Query Language.
JPQL is the standardized query language defined by Jakarta Persistence.
Like HQL, it queries:
- Entity names
- Entity properties
- Entity relationships
JPQL Example
TypedQuery<Customer> query =
entityManager.createQuery(
"""
select c
from Customer c
where c.active = true
order by c.name
""",
Customer.class
);
List<Customer> customers =
query.getResultList();
Architecture
flowchart TD
A[Application] --> B[JPQL]
B --> C[JPA Provider]
C --> D[Hibernate]
D --> E[SQL]
E --> F[Database]
Important Characteristics
- Standardized by Jakarta Persistence
- Provider-portable
- Object-oriented
- Supports joins, aggregates, grouping, subqueries, and updates
- Uses string-based query definitions
Production Example
Using JPQL through EntityManager so persistence code remains mostly independent of the Hibernate-specific API.
Common Mistake
Calling every entity query HQL even when using only standardized JPQL syntax.
Senior Follow-up
JPQL provides portability, while HQL may provide additional Hibernate-specific query capabilities.
Q4. What is the difference between HQL and JPQL?
Answer
HQL and JPQL are closely related object-oriented query languages.
JPQL is standardized.
HQL is Hibernate-specific and generally offers a broader feature set.
Comparison
| JPQL | HQL |
|---|---|
| Jakarta Persistence standard | Hibernate-specific |
| Provider-portable | Tied to Hibernate features |
Used with EntityManager |
Commonly used with Session |
| Standard syntax | Standard syntax plus Hibernate extensions |
| Best for portability | Best when Hibernate-specific power is needed |
JPQL Example
entityManager.createQuery(
"""
select e
from Employee e
where e.department.id = :departmentId
""",
Employee.class
);
Native Hibernate API Example
session.createQuery(
"""
from Employee e
where e.department.id = :departmentId
""",
Employee.class
);
Decision Flow
flowchart TD
A[Need Entity Query] --> B{Need Hibernate-specific feature?}
B -->|No| C[Prefer JPQL-compatible query]
B -->|Yes| D[Use HQL extension]
C --> E[Improved provider portability]
D --> F[Document provider dependency]
Common Mistake
Assuming every valid HQL query is portable across all JPA providers.
Senior Follow-up
Prefer standardized syntax unless a Hibernate extension provides a clear production benefit.
Q5. How do named parameters work in HQL and JPQL?
Answer
Named parameters represent values using a descriptive parameter name prefixed with :.
Example
List<Customer> customers =
entityManager.createQuery(
"""
select c
from Customer c
where c.active = :active
and c.createdAt >= :createdAfter
order by c.name
""",
Customer.class
)
.setParameter("active", true)
.setParameter(
"createdAfter",
LocalDateTime.now().minusDays(30)
)
.getResultList();
Benefits
- Protects against query injection
- Improves readability
- Avoids manual string formatting
- Allows correct type conversion
- Helps query-plan reuse
- Makes queries easier to maintain
Parameter Flow
sequenceDiagram
participant App
participant Hibernate
participant JDBC
participant DB
App->>Hibernate: JPQL with named parameters
App->>Hibernate: Bind active and date values
Hibernate->>JDBC: Prepared SQL and typed values
JDBC->>DB: Execute prepared statement
DB-->>JDBC: Results
JDBC-->>Hibernate: Rows
Hibernate-->>App: Entities
Collection Parameter
List<Order> orders =
entityManager.createQuery(
"""
select o
from Order o
where o.status in :statuses
""",
Order.class
)
.setParameter(
"statuses",
List.of(
OrderStatus.NEW,
OrderStatus.PROCESSING
)
)
.getResultList();
Common Mistake
Concatenating user input into the query.
Incorrect:
String query =
"from Customer c where c.email = '"
+ email
+ "'";
Senior Follow-up
Parameters can represent values, but they generally cannot replace entity names, property names, keywords, or sort directions. Dynamic query structure must be validated and constructed separately.
Q6. What are Named Queries?
Answer
Named Queries are predefined queries identified by a reusable name.
They can be declared using annotations or XML configuration.
Named Query Example
@Entity
@NamedQuery(
name = "Customer.findActive",
query = """
select c
from Customer c
where c.active = true
order by c.name
"""
)
public class Customer {
@Id
private Long id;
private String name;
private boolean active;
}
Query Execution
List<Customer> customers =
entityManager
.createNamedQuery(
"Customer.findActive",
Customer.class
)
.getResultList();
Benefits
- Centralized query definition
- Reusable query names
- Earlier validation by some providers
- Less repeated query text
- Useful for stable frequently used queries
Drawbacks
- Queries may become separated from their use cases
- Large entities can accumulate many declarations
- Dynamic conditions remain difficult
- Refactoring string property references remains limited
Named Query Lifecycle
flowchart TD
A[Application Startup] --> B[Read Named Query Definitions]
B --> C[Validate Query Metadata]
C --> D[Application Calls Query by Name]
D --> E[Bind Parameters]
E --> F[Execute Generated SQL]
Production Example
A stable query used by several repositories to retrieve all active product categories.
Common Mistake
Using Named Queries for highly dynamic search screens.
Senior Follow-up
Named Queries are useful for stable operations, while dynamic search requirements are often better served by Criteria, Specifications, or QueryDSL.
Q7. What is the Criteria API?
Answer
The Criteria API is a programmatic API for constructing queries using Java objects instead of query strings.
It is especially useful when query conditions must be added dynamically.
Basic Example
CriteriaBuilder builder =
entityManager.getCriteriaBuilder();
CriteriaQuery<Customer> query =
builder.createQuery(Customer.class);
Root<Customer> customer =
query.from(Customer.class);
query.select(customer)
.where(
builder.isTrue(
customer.get("active")
)
)
.orderBy(
builder.asc(
customer.get("name")
)
);
List<Customer> customers =
entityManager
.createQuery(query)
.getResultList();
Criteria Construction
flowchart TD
A[CriteriaBuilder] --> B[CriteriaQuery]
B --> C[Root Entity]
C --> D[Predicates]
C --> E[Joins]
C --> F[Order Expressions]
D --> G[Construct Final Query]
E --> G
F --> G
G --> H[Hibernate Generates SQL]
Main Components
| Component | Purpose |
|---|---|
CriteriaBuilder |
Creates query expressions |
CriteriaQuery |
Represents the complete query |
Root |
Represents the query root entity |
Predicate |
Represents a condition |
Join |
Represents an entity association join |
Path |
Represents an attribute path |
Order |
Represents sorting |
Subquery |
Represents a nested query |
Why Interviewers Ask This
The Criteria API demonstrates whether you can design type-aware dynamic queries without unsafe string concatenation.
Common Mistake
Using Criteria for every static query and making simple code unnecessarily verbose.
Senior Follow-up
Use Criteria when query structure changes dynamically; use JPQL for stable readable queries.
Q8. Criteria API vs HQL or JPQL?
Answer
HQL and JPQL are usually easier to read for static queries.
Criteria is more flexible for dynamically composed queries.
Comparison
| HQL / JPQL | Criteria API |
|---|---|
| String-based | Object-based |
| Concise | Verbose |
| Easy for static queries | Strong for dynamic queries |
| Property references are strings | Can use generated metamodel |
| Easy to read like SQL | More difficult to read |
| Dynamic composition can become messy | Predicates can be assembled safely |
| Query syntax errors may appear later | More compile-time assistance with metamodel |
Decision Diagram
flowchart TD
A[Query Requirement] --> B{Are filters dynamic?}
B -->|No| C[Use JPQL or HQL]
B -->|Yes| D{How complex is composition?}
D -->|Moderate| E[Criteria or Specification]
D -->|Highly complex| F[Consider QueryDSL or dedicated query layer]
Static JPQL Example
select o
from Order o
where o.status = :status
order by o.createdAt desc
Dynamic Criteria Example
List<Predicate> predicates =
new ArrayList<>();
if (filter.status() != null) {
predicates.add(
builder.equal(
order.get("status"),
filter.status()
)
);
}
if (filter.createdAfter() != null) {
predicates.add(
builder.greaterThanOrEqualTo(
order.get("createdAt"),
filter.createdAfter()
)
);
}
if (filter.customerEmail() != null) {
predicates.add(
builder.equal(
order.get("customer")
.get("email"),
filter.customerEmail()
)
);
}
Common Mistake
Building dynamic JPQL through uncontrolled string concatenation.
Senior Follow-up
Choose the simplest query technology that remains readable, safe, testable, and performant.
Q9. How do you build dynamic queries using Criteria API?
Answer
Dynamic Criteria queries build a collection of predicates based on optional search parameters.
Filter Model
public record CustomerSearchFilter(
String name,
String email,
Boolean active,
LocalDateTime createdAfter
) {
}
Dynamic Query
public List<Customer> searchCustomers(
CustomerSearchFilter filter
) {
CriteriaBuilder builder =
entityManager.getCriteriaBuilder();
CriteriaQuery<Customer> query =
builder.createQuery(Customer.class);
Root<Customer> customer =
query.from(Customer.class);
List<Predicate> predicates =
new ArrayList<>();
if (
filter.name() != null
&& !filter.name().isBlank()
) {
predicates.add(
builder.like(
builder.lower(
customer.get("name")
),
"%"
+ filter.name()
.toLowerCase(Locale.ROOT)
+ "%"
)
);
}
if (
filter.email() != null
&& !filter.email().isBlank()
) {
predicates.add(
builder.equal(
builder.lower(
customer.get("email")
),
filter.email()
.toLowerCase(Locale.ROOT)
)
);
}
if (filter.active() != null) {
predicates.add(
builder.equal(
customer.get("active"),
filter.active()
)
);
}
if (filter.createdAfter() != null) {
predicates.add(
builder.greaterThanOrEqualTo(
customer.get("createdAt"),
filter.createdAfter()
)
);
}
query.select(customer)
.where(
predicates.toArray(
Predicate[]::new
)
)
.orderBy(
builder.asc(
customer.get("name")
)
);
return entityManager
.createQuery(query)
.getResultList();
}
Dynamic Query Flow
flowchart TD
A[Receive Search Filter] --> B[Create Empty Predicate List]
B --> C{Name supplied?}
C -->|Yes| D[Add name predicate]
C -->|No| E[Skip name predicate]
D --> F{Email supplied?}
E --> F
F -->|Yes| G[Add email predicate]
F -->|No| H[Skip email predicate]
G --> I[Combine Predicates]
H --> I
I --> J[Execute Criteria Query]
Benefits
- Optional filters
- Safe value binding
- Composable conditions
- No uncontrolled query concatenation
- Reusable filtering logic
Common Mistake
Adding joins and predicates without checking whether they duplicate rows.
Senior Follow-up
Dynamic search endpoints should enforce maximum page sizes, allowed sort fields, query timeouts, and index-aware filters.
Q10. How do joins work in HQL and Criteria API?
Answer
Joins connect entities through mapped relationships.
Entity Relationship
@Entity
public class Order {
@Id
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
private Customer customer;
@OneToMany(mappedBy = "order")
private List<OrderItem> items;
}
JPQL Inner Join
List<Order> orders =
entityManager.createQuery(
"""
select o
from Order o
join o.customer c
where c.email = :email
""",
Order.class
)
.setParameter("email", email)
.getResultList();
JPQL Left Join
select c
from Customer c
left join c.orders o
where o.id is null
Criteria Join
CriteriaBuilder builder =
entityManager.getCriteriaBuilder();
CriteriaQuery<Order> query =
builder.createQuery(Order.class);
Root<Order> order =
query.from(Order.class);
Join<Order, Customer> customer =
order.join(
"customer",
JoinType.INNER
);
query.where(
builder.equal(
customer.get("email"),
email
)
);
Join Types
| Join | Behavior |
|---|---|
| Inner Join | Returns matching records |
| Left Join | Returns all left records and matching right records |
| Fetch Join | Loads association into entity graph |
| Cross Join | Produces combinations of rows |
Relationship Join Flow
flowchart LR
A[Order] -->|ManyToOne| B[Customer]
B --> C[Hibernate Mapping]
C --> D[SQL JOIN using customer_id]
Join vs Fetch Join
join o.customer c
allows filtering through the relationship.
join fetch o.customer
also initializes the association in the returned entity graph.
Common Mistake
Assuming a regular join automatically initializes a lazy association.
Senior Follow-up
Use normal joins for filtering and fetch joins only when the associated data is required in the returned graph.
Q11. What are DTO projections?
Answer
DTO projections select only the data required by the use case rather than loading complete entities.
DTO Record
public record OrderSummary(
Long orderId,
String customerName,
OrderStatus status,
BigDecimal total
) {
}
Constructor Projection
List<OrderSummary> summaries =
entityManager.createQuery(
"""
select new com.codewithvenu.order.OrderSummary(
o.id,
o.customer.name,
o.status,
o.total
)
from Order o
where o.status = :status
order by o.createdAt desc
""",
OrderSummary.class
)
.setParameter(
"status",
OrderStatus.COMPLETED
)
.getResultList();
Projection Flow
flowchart TD
A[API Requests Order Summary] --> B[Projection Query]
B --> C[Select Four Required Columns]
C --> D[Create OrderSummary DTO]
D --> E[Return Lightweight Response]
Benefits
- Selects fewer columns
- Avoids unnecessary entity graphs
- Prevents lazy-loading problems
- Reduces persistence-context memory
- Avoids dirty-checking overhead
- Creates clear API contracts
- Works well for dashboards and reports
Limitations
- DTO is not managed
- No automatic dirty checking
- Constructor expressions can be sensitive to signature changes
- Separate query models may be needed
Criteria DTO Projection
CriteriaQuery<OrderSummary> query =
builder.createQuery(OrderSummary.class);
Root<Order> order =
query.from(Order.class);
Join<Order, Customer> customer =
order.join("customer");
query.select(
builder.construct(
OrderSummary.class,
order.get("id"),
customer.get("name"),
order.get("status"),
order.get("total")
)
);
Common Mistake
Loading complete entities and converting them into DTOs after over-fetching large relationships.
Senior Follow-up
Use projections for read models and managed entities for transactional write models.
Q12. How do pagination and sorting work?
Answer
Pagination limits the number of query results returned at one time.
JPA Pagination
List<Order> orders =
entityManager.createQuery(
"""
select o
from Order o
where o.status = :status
order by o.createdAt desc, o.id desc
""",
Order.class
)
.setParameter(
"status",
OrderStatus.COMPLETED
)
.setFirstResult(0)
.setMaxResults(50)
.getResultList();
Pagination Calculation
int offset =
pageNumber * pageSize;
Stable Sorting
Always include deterministic ordering.
Better:
ORDER BY created_at DESC, id DESC
Risky:
ORDER BY created_at DESC
When several rows have the same timestamp, pages may contain duplicates or missing entries.
Offset Pagination Flow
flowchart TD
A[Request Page 3 Size 50] --> B[Calculate Offset 150]
B --> C[Execute Ordered Query]
C --> D[Database Skips 150 Rows]
D --> E[Return Next 50 Rows]
Offset Pagination Risks
- Large offsets become expensive
- Concurrent inserts can shift pages
- Collection fetch joins can break page accuracy
- Count queries may be expensive
Keyset Pagination
Instead of offset, use the last observed sort values.
select o
from Order o
where
o.createdAt < :lastCreatedAt
or (
o.createdAt = :lastCreatedAt
and o.id < :lastId
)
order by o.createdAt desc, o.id desc
Keyset Flow
flowchart LR
A[Previous Page Last Row] --> B[Use createdAt and ID as Cursor]
B --> C[Query Rows After Cursor]
C --> D[Return Next Page]
Common Mistake
Using collection fetch joins with direct pagination.
Senior Follow-up
Use offset pagination for simple administrative screens and keyset pagination for large continuously changing data sets.
Q13. What are bulk update and delete queries?
Answer
Bulk DML queries modify multiple database rows without loading each entity into the persistence context.
Bulk Update
int updated =
entityManager.createQuery(
"""
update Customer c
set c.active = false
where c.lastLoginAt < :cutoff
"""
)
.setParameter(
"cutoff",
LocalDateTime.now().minusYears(2)
)
.executeUpdate();
Bulk Delete
int deleted =
entityManager.createQuery(
"""
delete from Notification n
where n.createdAt < :cutoff
"""
)
.setParameter(
"cutoff",
LocalDateTime.now().minusMonths(6)
)
.executeUpdate();
Benefit
Instead of:
Load 100,000 Entities
Modify Each Entity
Dirty Check Each Entity
Execute Many Updates
Hibernate can execute:
One Bulk SQL Statement
Bulk DML Flow
flowchart TD
A[Execute Bulk HQL Update] --> B[Hibernate Generates Direct SQL]
B --> C[Database Updates Matching Rows]
C --> D[Persistence Context Is Not Automatically Synchronized]
Critical Persistence-Context Risk
Suppose this entity is already managed:
Customer customer =
entityManager.find(
Customer.class,
customerId
);
Then a bulk update modifies that row directly.
The managed Java object may still contain the old value.
Safer Pattern
int updated =
entityManager.createQuery(
"""
update Customer c
set c.active = false
where c.lastLoginAt < :cutoff
"""
)
.setParameter("cutoff", cutoff)
.executeUpdate();
entityManager.clear();
Limitations
Bulk DML may bypass:
- Dirty checking
- Entity lifecycle callbacks
- Per-entity cascades
- In-memory managed-state synchronization
- Some auditing mechanisms
- Automatic version handling, unless included explicitly
Common Mistake
Executing a bulk update and continuing to use stale managed entities.
Senior Follow-up
Flush before bulk DML when pending changes matter, then clear or refresh affected entities afterward.
Q14. What are common HQL and Criteria interview mistakes?
Answer
Common mistakes include:
- Using table names instead of entity names in HQL.
- Using column names instead of entity properties.
- Concatenating user input into queries.
- Using positional parameters without clear need.
- Confusing HQL with SQL.
- Confusing HQL with standardized JPQL.
- Using Criteria for simple static queries.
- Building dynamic JPQL through unsafe string concatenation.
- Fetch joining multiple large collections.
- Applying pagination to collection fetch joins.
- Loading complete entities for summary screens.
- Ignoring duplicate results after joins.
- Forgetting
distinctwhere root duplication matters. - Executing bulk updates without clearing stale managed entities.
- Using leading-wildcard searches without considering indexes.
- Allowing arbitrary unvalidated sort fields.
- Ignoring generated SQL.
- Ignoring query execution plans.
- Selecting unbounded result sets.
- Assuming ORM-generated SQL is automatically optimal.
Unsafe Sort Construction
String jpql =
"select c from Customer c order by c."
+ userSortField;
This is unsafe and may allow invalid query structures.
Safer Sort Validation
private static final Map<String, String>
ALLOWED_SORT_FIELDS =
Map.of(
"name",
"name",
"createdAt",
"createdAt",
"email",
"email"
);
Validate requested fields against an allowlist.
Interview Tip
A strong candidate explains query correctness and production performance.
Q15. What are senior-level HQL and Criteria best practices?
Answer
Senior developers should design queries around use cases, data volume, indexes, and transaction behavior.
Best Practices
- Use JPQL-compatible syntax when portability matters.
- Use HQL extensions only when they provide clear value.
- Bind all values through parameters.
- Prefer named parameters.
- Use JPQL for stable readable queries.
- Use Criteria or Specifications for dynamic filters.
- Use DTO projections for read-only APIs.
- Avoid unbounded queries.
- Paginate large result sets.
- Use deterministic sorting.
- Consider keyset pagination for large data.
- Avoid multiple collection fetch joins.
- Avoid collection fetch joins with pagination.
- Validate dynamic sort fields.
- Review generated SQL.
- Test query plans with realistic data.
- Add indexes based on filtering and sorting patterns.
- Clear the persistence context after bulk DML.
- Use query timeouts where appropriate.
- Monitor slow-query metrics in production.
Query Decision Flow
flowchart TD
A[New Query Requirement] --> B{Static or dynamic?}
B -->|Static| C[JPQL or HQL]
B -->|Dynamic| D[Criteria or Specification]
C --> E{Need full entity?}
D --> E
E -->|Yes| F[Entity Query with explicit fetch plan]
E -->|No| G[DTO Projection]
F --> H{Large result set?}
G --> H
H -->|Yes| I[Pagination or Streaming]
H -->|No| J[Execute Bounded Query]
I --> K[Review SQL and Indexes]
J --> K
Production Query Checklist
Define Required Output
│
▼
Select Query Technology
│
▼
Bind Parameters
│
▼
Choose Entity or DTO Result
│
▼
Design Joins and Fetch Plan
│
▼
Add Pagination and Stable Sorting
│
▼
Review Generated SQL
│
▼
Analyze Execution Plan
│
▼
Load Test
│
▼
Monitor Production
Senior Follow-up
The query API is only one layer. Database schema design, indexes, statistics, cardinality, transaction boundaries, and workload patterns determine real production performance.
Hibernate Query Architecture
flowchart TD
A[Application Service] --> B{Query Method}
B --> C[HQL or JPQL]
B --> D[Criteria API]
B --> E[Native SQL]
B --> F[Named Query]
C --> G[Hibernate Query Engine]
D --> G
E --> H[JDBC SQL]
F --> G
G --> I[SQL Generation]
I --> H
H --> J[Database]
J --> K[Rows]
K --> L{Result Type}
L --> M[Managed Entities]
L --> N[DTO Projection]
L --> O[Scalar Values]
HQL Query Lifecycle
sequenceDiagram
participant Service
participant Hibernate
participant JDBC
participant Database
Service->>Hibernate: Create HQL query
Service->>Hibernate: Bind parameters
Hibernate->>Hibernate: Parse entity query
Hibernate->>Hibernate: Resolve mappings
Hibernate->>JDBC: Generated SQL
JDBC->>Database: Execute prepared statement
Database-->>JDBC: Result rows
JDBC-->>Hibernate: Rows
Hibernate->>Hibernate: Hydrate entities or DTOs
Hibernate-->>Service: Query results
Criteria API Architecture
flowchart TD
A[CriteriaBuilder] --> B[CriteriaQuery]
B --> C[Root]
B --> D[Selection]
B --> E[Predicates]
B --> F[Joins]
B --> G[Grouping]
B --> H[Ordering]
C --> I[Final Query Model]
D --> I
E --> I
F --> I
G --> I
H --> I
I --> J[Hibernate SQL Generation]
J --> K[Database]
HQL vs Criteria Decision Matrix
| Requirement | Recommended Option |
|---|---|
| Simple static query | JPQL or HQL |
| Complex but stable query | JPQL or HQL |
| Optional dynamic filters | Criteria API |
| Reusable Spring filters | Specifications |
| Type-safe fluent query layer | QueryDSL |
| Read-only summary | DTO projection |
| Database-specific feature | Native SQL or HQL extension |
| Bulk update | JPQL or HQL bulk DML |
| Full-text search | Dedicated search technology |
| Complex analytical report | Native SQL or dedicated read model |
Entity Query Example
@Transactional(readOnly = true)
public List<Customer> findActiveCustomers() {
return entityManager.createQuery(
"""
select c
from Customer c
where c.active = true
order by c.name, c.id
""",
Customer.class
)
.setMaxResults(100)
.getResultList();
}
Relationship Query Example
@Transactional(readOnly = true)
public List<Order> findOrdersByCustomerEmail(
String email
) {
return entityManager.createQuery(
"""
select o
from Order o
join o.customer c
where lower(c.email) = lower(:email)
order by o.createdAt desc
""",
Order.class
)
.setParameter("email", email)
.getResultList();
}
Aggregate Query Example
public record CustomerOrderStatistics(
Long customerId,
String customerName,
long orderCount,
BigDecimal totalAmount
) {
}
@Transactional(readOnly = true)
public List<CustomerOrderStatistics>
loadCustomerStatistics() {
return entityManager.createQuery(
"""
select new com.codewithvenu.customer.CustomerOrderStatistics(
c.id,
c.name,
count(o.id),
coalesce(sum(o.total), 0)
)
from Customer c
left join c.orders o
group by c.id, c.name
order by count(o.id) desc
""",
CustomerOrderStatistics.class
)
.getResultList();
}
Group By Query Flow
flowchart LR
A[Customers] --> B[Join Orders]
B --> C[Group by Customer]
C --> D[Count Orders]
C --> E[Sum Order Totals]
D --> F[Statistics DTO]
E --> F
Criteria Dynamic Search Example
@Repository
public class OrderSearchRepository {
@PersistenceContext
private EntityManager entityManager;
public List<OrderSummary> search(
OrderSearchFilter filter,
int pageSize
) {
CriteriaBuilder builder =
entityManager.getCriteriaBuilder();
CriteriaQuery<OrderSummary> query =
builder.createQuery(
OrderSummary.class
);
Root<Order> order =
query.from(Order.class);
Join<Order, Customer> customer =
order.join(
"customer",
JoinType.INNER
);
List<Predicate> predicates =
new ArrayList<>();
if (filter.status() != null) {
predicates.add(
builder.equal(
order.get("status"),
filter.status()
)
);
}
if (filter.customerEmail() != null) {
predicates.add(
builder.equal(
builder.lower(
customer.get("email")
),
filter.customerEmail()
.toLowerCase(Locale.ROOT)
)
);
}
if (filter.minimumTotal() != null) {
predicates.add(
builder.greaterThanOrEqualTo(
order.get("total"),
filter.minimumTotal()
)
);
}
if (filter.createdAfter() != null) {
predicates.add(
builder.greaterThanOrEqualTo(
order.get("createdAt"),
filter.createdAfter()
)
);
}
query.select(
builder.construct(
OrderSummary.class,
order.get("id"),
customer.get("name"),
order.get("status"),
order.get("total")
)
);
query.where(
predicates.toArray(
Predicate[]::new
)
);
query.orderBy(
builder.desc(
order.get("createdAt")
),
builder.desc(
order.get("id")
)
);
return entityManager
.createQuery(query)
.setMaxResults(
Math.min(pageSize, 100)
)
.getResultList();
}
}
Static Metamodel Example
Instead of:
customer.get("email")
a generated metamodel may allow:
customer.get(Customer_.email)
Benefit
A metamodel reduces raw string attribute references and improves refactoring safety.
Concept
flowchart LR
A[Entity Attribute] --> B[Generated Metamodel Field]
B --> C[Criteria Query]
C --> D[Compile-Time Assistance]
Fetch Join Example
@Transactional(readOnly = true)
public Optional<Order> findDetailedOrder(
Long orderId
) {
return entityManager.createQuery(
"""
select distinct o
from Order o
join fetch o.customer
left join fetch o.items
where o.id = :orderId
""",
Order.class
)
.setParameter(
"orderId",
orderId
)
.getResultStream()
.findFirst();
}
Use fetch joins only when the fetched graph is required and bounded.
Pagination with Count Query
public PageResult<OrderSummary> findOrders(
OrderStatus status,
int page,
int size
) {
List<OrderSummary> content =
entityManager.createQuery(
"""
select new com.codewithvenu.order.OrderSummary(
o.id,
o.customer.name,
o.status,
o.total
)
from Order o
where o.status = :status
order by o.createdAt desc, o.id desc
""",
OrderSummary.class
)
.setParameter("status", status)
.setFirstResult(page * size)
.setMaxResults(size)
.getResultList();
long total =
entityManager.createQuery(
"""
select count(o)
from Order o
where o.status = :status
""",
Long.class
)
.setParameter("status", status)
.getSingleResult();
return new PageResult<>(
content,
total,
page,
size
);
}
Bulk Update Safe Pattern
@Transactional
public int deactivateInactiveCustomers(
LocalDateTime cutoff
) {
entityManager.flush();
int updated =
entityManager.createQuery(
"""
update Customer c
set c.active = false
where c.lastLoginAt < :cutoff
"""
)
.setParameter("cutoff", cutoff)
.executeUpdate();
entityManager.clear();
return updated;
}
Query Performance Flow
flowchart TD
A[JPQL or Criteria Query] --> B[Generated SQL]
B --> C[Database Optimizer]
C --> D[Execution Plan]
D --> E{Suitable Index?}
E -->|Yes| F[Efficient Lookup]
E -->|No| G[Table Scan or Expensive Join]
F --> H[Fast Response]
G --> I[High Latency]
Query Optimization Checklist
| Area | Question |
|---|---|
| Selection | Are only required columns selected? |
| Filtering | Are predicates index-friendly? |
| Joining | Are joins necessary and bounded? |
| Fetching | Could the query cause N+1? |
| Pagination | Is ordering deterministic? |
| Result size | Is the query bounded? |
| Indexing | Do indexes support filters and sorting? |
| Parameters | Are all values parameterized? |
| Bulk DML | Will managed state become stale? |
| Monitoring | Is query latency measured? |
Common Query Performance Problems
Leading Wildcard
where lower(c.name) like '%venu%'
A normal B-tree index may not efficiently support a leading wildcard.
Function on Indexed Column
where lower(c.email) = :email
This may require:
- A functional index
- A normalized persisted column
- Database-specific indexing support
Unbounded Result
select o from Order o
This can load millions of entities into memory.
Excessive Fetch Join
select distinct c
from Customer c
left join fetch c.orders
left join fetch c.addresses
left join fetch c.payments
This can create a Cartesian product.
Query Error Handling
No Result
Optional<Customer> customer =
entityManager.createQuery(
"""
select c
from Customer c
where c.email = :email
""",
Customer.class
)
.setParameter("email", email)
.getResultStream()
.findFirst();
Using getResultStream().findFirst() can avoid handling NoResultException for optional single results.
Multiple Results
A query expected to return one row should have an enforced database uniqueness constraint.
UNIQUE (email)
Application assumptions should be supported by schema constraints.
Interview Quick Revision
| Concept | Purpose |
|---|---|
| HQL | Hibernate entity query language |
| JPQL | Standard Jakarta Persistence query language |
| SQL | Relational database query language |
| Named Parameter | Safe value binding |
| Named Query | Reusable predefined query |
| Criteria API | Programmatic query construction |
CriteriaBuilder |
Builds query expressions |
CriteriaQuery |
Represents query structure |
Root |
Query root entity |
Predicate |
Query condition |
| Join | Navigates relationships |
| Fetch Join | Loads association with entity |
| DTO Projection | Selects required fields |
| Pagination | Limits result size |
| Bulk DML | Updates or deletes rows directly |
Key Takeaways
- HQL is Hibernate’s object-oriented query language and queries entities rather than database tables.
- JPQL is the standardized Jakarta Persistence query language.
- JPQL-compatible queries improve provider portability, while HQL may offer additional Hibernate features.
- HQL and JPQL use entity names, properties, and mapped relationships.
- Named parameters improve readability, security, type handling, and query-plan reuse.
- Named Queries are useful for stable reusable queries but are not ideal for highly dynamic searches.
- The Criteria API constructs queries programmatically and is particularly useful for optional dynamic filters.
- JPQL is generally more readable for static queries, while Criteria is more composable for dynamic queries.
- Regular joins support filtering, while fetch joins also initialize returned associations.
- DTO projections reduce selected columns, memory usage, lazy-loading risks, and dirty-checking overhead.
- Large result sets should use pagination, streaming, or dedicated batch-processing strategies.
- Stable pagination requires deterministic ordering.
- Keyset pagination can outperform offset pagination for large, continuously changing data sets.
- Bulk updates and deletes bypass normal entity dirty checking and can leave the persistence context stale.
- Generated SQL, database indexes, execution plans, row volume, and production metrics must be reviewed regardless of the query API used.