JPQL, Criteria API, and Native Queries: Choosing the Right Query Approach
Objective
JPA gives you three ways to define a query: JPQL, the Criteria API, and native SQL. They look like three ways to do the same thing, but they don't: each one trades off portability, type safety, and access to raw database features differently, and Hibernate (or any JPA provider) can only translate what it understands. JPQL and the Criteria API expose the same feature set (JPQL as a string-based query language, Criteria API as a Java object API over that same model), and both are intentionally less powerful than SQL. Native queries remove that ceiling entirely.
Use Cases
- Writing a query against your domain model (entity classes and their mapped associations) instead of table names and foreign key columns, so the query stays valid if the underlying schema is refactored: use JPQL.
- Supporting multiple databases with one codebase, where a hand-written SQL statement would need a different dialect per vendor: use JPQL or the Criteria API, since Hibernate generates the vendor-specific SQL for you.
- Building a query whose joins, filters, or selected columns depend on runtime input (a search form with a dozen optional fields), rather than a query fixed at compile time: use the Criteria API.
- Needing a database-specific feature, a complex function, or fine-tuned SQL that JPQL simply cannot express: use a native query.
Deep Dive
JPQL: string-based, but on the domain model
javaTypedQuery<Book> query = em.createQuery(
"SELECT b FROM Book b WHERE b.title LIKE :title", Book.class);
query.setParameter("title", "%Hibernate%");
List<Book> books = query.getResultList();SELECT b FROM Book b refers to the Book entity, not a table the database understands. Hibernate translates this into the SQL statement that actually runs, resolving Book to its mapped table and columns. The named bind parameter (:title, set with setParameter) is what protects this query from SQL injection; concatenating the search string directly into the query string would not.
You can also declare a JPQL query once, next to the entity, as a named query:
java@Entity
@NamedQuery(
name = "Book.findByTitle",
query = "SELECT b FROM Book b WHERE b.title LIKE :title"
)
public class Book { /* ... */ }javaTypedQuery<Book> query = em.createNamedQuery("Book.findByTitle", Book.class);
query.setParameter("title", "%Hibernate%");Criteria API: the same feature set, as a Java object graph
javaCriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Book> cq = cb.createQuery(Book.class);
Root<Book> book = cq.from(Book.class);
Join<Book, Author> author = book.join(Book_.authors);
ParameterExpression<String> firstName = cb.parameter(String.class);
ParameterExpression<String> lastName = cb.parameter(String.class);
cq.where(cb.and(
cb.equal(author.get(Author_.firstName), firstName),
cb.equal(author.get(Author_.lastName), lastName)
));
TypedQuery<Book> query = em.createQuery(cq);
query.setParameter(firstName, "Joshua");
query.setParameter(lastName, "Bloch");
List<Book> books = query.getResultList();Book_ and Author_ are static metamodel classes generated by the JPA annotation processor, one per entity, mirroring its mapped attributes as static fields. Referencing Book_.authors instead of the string "authors" is what gives the Criteria API its main advantages over JPQL: a typo becomes a compile error, and renaming an entity attribute is a safe refactor instead of a silent runtime failure in a query string.
The tree of and/equal/join calls is what makes the Criteria API noisy to read compared to the equivalent JPQL. That verbosity buys the ability to build the query conditionally: whether to add the join, which attribute to compare, and how many and clauses to chain can all be decided by code, based on which fields the caller actually provided, something a fixed JPQL string cannot do.
Native queries: the abstraction removed entirely
javaQuery query = em.createNativeQuery(
"SELECT * FROM book b WHERE b.title LIKE :title", Book.class);
query.setParameter("title", "%Hibernate%");
List<Book> books = query.getResultList();This time book is the table, not the entity, and * can include columns the entity doesn't even map. Hibernate does not parse this statement at all, it hands it to the database as-is. That means you get access to every feature your specific database supports (vendor functions, hints, complex constructs JPQL has no syntax for), but the statement is no longer portable: the same native query can need a different dialect on Oracle than on PostgreSQL.
Bind parameters: the actual SQL-injection defense
All three approaches are equally safe from SQL injection, but only if bind parameters are used consistently; string concatenation defeats the protection no matter which API builds the query. The JPQL and native query examples above use setParameter with a named placeholder; the Criteria API achieves the same thing with ParameterExpression.
Trade-offs
JPQL and the Criteria API are only as powerful as what Hibernate can translate to SQL. Their feature set is intentionally a subset of full SQL, so a query that needs a database-specific function or construct simply cannot be written in either, and falls back to a native query.
The Criteria API's added safety and flexibility come with real verbosity. A query with several
and/orconditions turns into deeply nested builder calls that are hard to read at a glance.javacq.where(cb.and( cb.equal(author.get(Author_.firstName), firstName), cb.equal(author.get(Author_.lastName), lastName) ));Native queries are the least portable of the three. A statement using PostgreSQL-specific syntax will not run unchanged against Oracle or SQL Server; multi-database support pushes you back toward JPQL or the Criteria API.
Selecting the entity, rather than specific columns, fetches every mapped column.
SELECT b FROM Book bloads every columnBookmaps, even when only the title is needed; selecting individual attributes (SELECT b.title FROM Book b) returns a narrower projection instead.javaList<Object[]> rows = em.createQuery( "SELECT b.id, b.title FROM Book b", Object[].class).getResultList();The Criteria API's own performance cost is in building the query, not running it. Once translated, the generated SQL for an equivalent JPQL and Criteria query is identical, so there is no runtime penalty from choosing one over the other for the same query; the choice should be driven by whether dynamic construction is actually needed.