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 distinct where 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.