1. JPQL: entities instead of tables
If SQL is the language for talking to a database directly (“tables, columns, join on FK”), then JPQL is the language for talking to your object model (“entities, fields, relationships”). It looks like SQL in form, so our brain does not cry immediately, but in meaning it is closer to Java: you work with class names and their fields. You could say JPQL is SQL that took a course in “how to speak in camelCase and not be shy.”
The most important rule here sounds boring, but it saves hours of life: in JPQL we write the entity name — Product and CustomerOrder, not product and customer_order. By default, it matches the class name if you did not override it with @Entity(name = ...). And we write orderNumber, not order_number. Why? Because JPQL does not know what your table is called until Hibernate looks at the mapping. JPQL only knows what you described as entities and relationships.
Let us pin down the difference with one small example. Tables are often named in snake_case, while fields in Java use camelCase. And that is normal: databases like underscores, Java likes humps.
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
@Entity // This is a JPA entity: in JPQL we will refer to it by the entity name CustomerOrder
@Table(name = "customer_order") // Mapping to a table in the DB, but this name is NOT used in JPQL
public class CustomerOrder {
@Column(name = "order_number", nullable = false, unique = true) // Column order_number, but the field in JPQL is orderNumber
private String orderNumber;
}
If you now write a query, in JPQL you refer to the field orderNumber, not the column order_number. Hibernate will then “translate” it into SQL.
To make it easier for your brain to switch modes, here is a small cheat-sheet table (not to memorize, just to see the logic):
| What you write | SQL (about tables) | JPQL (about entities) |
|---|---|---|
| “Where we read from” | from product p | from Product p |
| “What we filter by” | where o.order_number = ... | where o.orderNumber = ... |
| “How we join” | join category c on p.category_id = c.id | join p.category c |
| “Where we reach for FK” | p.category_id | p.category.id or |
Yes, JPQL looks like SQL, but if you start writing table/column names there, Hibernate will answer with an error roughly like “I do not know the entity product.” It would be right: the entity is Product, and product is the table.
Below I will again show short local snippets of repositories: it is easier to catch the JPQL language itself and not get lost in the full interface file.
2. The root entity in from
When we read data, we usually intuitively understand “what exactly we want to get”: a list of products, a specific order, order items, categories. In JPQL, this intuitive desire must be formalized with one decision: which entity will be the root, that is, which one will appear in from. The root entity is like the main character in a movie: you can show other characters too, but the camera still starts with that one.
For example, if you want to get products, the root entity becomes Product. If you want order items, the root becomes OrderItem. And this decision affects everything: select, joins, and even how the repository method looks.
The most basic JPQL query “skeleton” looks like this:
select <what to return> -- Usually this is the entity alias: p, o, oi, etc.
from <root entity> <alias> -- Root entity (entity name), not a table
where <condition> -- Conditions on entity fields and relationship paths
order by <sorting> -- Sorting by entity fields
In our shop-data-jpa project, for “read the list of products” it makes sense to start with Product:
import com.example.shopdatajpa.catalog.entity.Product;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import java.util.List;
public interface ProductRepository extends JpaRepository<Product, Long> {
@Query("select p from Product p order by p.id") // Product is the entity name; p is the alias; we sort by the id field
List<Product> findAllProductsJpql();
}
Notice two details. First, the query uses Product (the entity name), not the table product. Second, p is the alias, and we need it later so we do not have to write “Product.” every time.
And here a good practical question appears: “Can I put any entity in from and then reach the one I need?” Technically, you can do a lot of things, but mentally — better not. If you are reading products, let Product be the root. If you are reading order items, let OrderItem be the root. It makes the query simpler and more readable: you immediately understand what the repository method returns.
3. Aliases (alias)
An alias in JPQL is basically a variable. Like in Java: you do not write new BigDecimal("10.00") every time; you put the value into a variable and work with the name. In a query, the alias is a short name for an entity so you can refer to its fields and relationships. It is not a “magic letter” but an ordinary readability tool (and yes, in JPQL without it you will very quickly start suffering).
Most often, aliases are short but meaningful: p for Product, c for Category, o for CustomerOrder, oi for OrderItem. Not because the commandments of ORM say so, but because it is convenient and feels like team conversation. If a query has three entities, aliases become especially important: otherwise you will not understand whose fields you are referring to.
Here is a small orientation table for our project (this is not a rule, just a habit that saves nerves):
| Entity | Typical alias |
|---|---|
| Product | p |
| Category | c |
| CustomerOrder | o |
| OrderItem | oi |
Now let us see how an alias makes a query readable. Without an alias, you would not be able to properly write order by, where, and relationship traversals.
import com.example.shopdatajpa.ordering.entity.CustomerOrder;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import java.util.List;
public interface CustomerOrderRepository extends JpaRepository<CustomerOrder, Long> {
@Query("select o from CustomerOrder o where o.totalAmount > 0 order by o.id") // o is the order alias; filtering by the totalAmount field
List<CustomerOrder> findNonZeroOrders();
}
Here, o is just a short name. No mysticism. But if you forget the alias and try to write where totalAmount > 0, JPQL will not understand you: it needs to know which “object” owns that field.
4. Entity fields in JPQL
The most common beginner mistake in JPQL is to think: “this is almost SQL, so columns are almost columns too.” No. In JPQL you refer to entity fields, meaning the names you see in the Java class. Even if the database column is called differently, JPQL stays on the Java side of the world.
For example, if the Product field is called sku, then in JPQL it is called the same way: p.sku. If in the database the column is called product_sku, and you wrote @Column(name = "product_sku"), then in JPQL it is still p.sku. JPQL is basically saying: “I work with objects. What does the object call this? Then call it that too.”
From a thinking perspective, this is useful: you read the query and immediately map it to the entity code. There is no need to jump back and forth between SQL/DDL and a Java class to understand what you are filtering.
Against that background, it is very useful to treat p.name as a “path to a field,” almost like writing Java p.getName() (except without a method call, because this is a query).
By the way, the same applies to @Embeddable (we already used it with DeliveryAddress). If an order has a field deliveryAddress, and it has a field city, then the path looks like o.deliveryAddress.city. This is very “object-oriented” and very intentional: we read through the model, not through tables.
5. Navigating relationships
When relationships appear (ManyToOne, OneToMany), JPQL becomes especially pleasant: you start writing conditions as if you are truly walking through objects. That is, you do not think “where is the FK column?”; you think “a product has a category, and a category has a code.”
In the shop-data-jpa project, Product has a category reference. So in JPQL you can write:
import com.example.shopdatajpa.catalog.entity.Product;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import java.util.List;
public interface ProductRepository extends JpaRepository<Product, Long> {
@Query("select p from Product p where p.category.active = true") // Traverse the category relationship and filter by the active field
List<Product> findProductsFromActiveCategories();
}
Here is what matters to feel: p.category.active looks like ordinary object navigation. But under the hood Hibernate understands that it needs data from category, and it creates a SQL join. In other words, you express the thought in the object model, and the ORM translates that thought into relational reality.
To make this less abstract, here is a simplified relationship diagram from our domain, because sometimes the brain needs a picture rather than another line of text:
classDiagram
class Category {
+Long id
+String code
+boolean active
}
class Product {
+Long id
+String sku
+String name
+Category category
}
class CustomerOrder {
+Long id
+String orderNumber
+String customerEmail
+BigDecimal totalAmount
}
class OrderItem {
+Long id
+CustomerOrder order
+Product product
+int quantity
}
Category "1" <-- "*" Product : category
CustomerOrder "1" <-- "*" OrderItem : order
Product "1" <-- "*" OrderItem : product
Now an example of traversing OrderItem -> order in a query. We need to get order items for a specific customer (for now, let us leave the email as a literal string — purely as a query-form demonstration):
import com.example.shopdatajpa.ordering.entity.OrderItem;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import java.util.List;
public interface OrderItemRepository extends JpaRepository<OrderItem, Long> {
@Query("""
select oi
from OrderItem oi
join oi.order o
where o.customerEmail = 'customer@example.com'
order by oi.id
""") // Text block is convenient for multiline JPQL; the join goes through the oi.order relationship; the email is hardcoded only for demo
List<OrderItem> findItemsOfCustomerDemo();
}
Two important things are visible here.
First: join oi.order o means we are not joining table to table, but a relationship between entities. JPQL does not require you to write on oi.order_id = o.id, because that is already “baked into” the @ManyToOne mapping.
Second: in where we use the customerEmail field of the CustomerOrder entity, not the customer_email column. So again — the object model.
You might ask: “Why do people sometimes write p.category.code without join, and sometimes write join oi.order o?” Because JPQL allows “relationship traversal” directly in expressions, and the provider will create the needed joins on its own. But when a query becomes a bit more complex, or when you want the reader to clearly see which entities are involved, an explicit join makes the query clearer. This is not religion; it is just a matter of readability.
6. How JPQL turns into SQL
JPQL is not an “alternative database.” By itself, it sends nothing to PostgreSQL. It goes through Hibernate, which parses the query, understands your entity model, and generates ordinary SQL. Only then does the SQL go to PostgreSQL. If you keep this in mind, the feeling of “I wrote some string, and magic happened” disappears.
Here is the diagram of that path, literally like an assembly line:
flowchart TD
A[Repository method] --> B[JPQL string]
B --> C["Hibernate / JPA provider"]
C --> D[SQL for PostgreSQL]
D --> E[(PostgreSQL)]
E --> D
D --> F[Result set]
F --> G[Entities via mapping]
Let us take a simple JPQL query and imagine what it turns into. For example:
select p -- In JPQL, people often select the entity alias (that is, they return the entity as a whole)
from Product p
where p.category.code = 'phones' -- Traversing the relationship: Product -> Category -> code
The pseudo-SQL (approximately) may look like this:
select p.* -- In real SQL, these are rows of the product table (or specific columns)
from product p
join category c on p.category_id = c.id -- What was p.category... in JPQL is expressed here via JOIN on the FK
where c.code = 'phones'
Why “pseudo”? Because the exact form depends on the dialect, settings, table names, column names, and how exactly Hibernate decides to build the SQL. But the meaning is the same: p.category.code does not exist as a “column” on product, so filtering by the category code requires a join.
In the end, a useful habit is this: when you write JPQL, you keep two pictures in your head at once. The first is object-based: “Product -> Category -> code.” The second is relational: “product join category by FK, filter by category.code.” And this bridge is what gives you mature ORM thinking without mysticism.
7. Typical mistakes in JPQL thinking
Mistakes in JPQL are almost never “syntactic” — they are “worldview” mistakes. That is, you write the query in SQL style, while JPQL expects entity style. And if you catch that idea now, everything later becomes much easier: you stop fighting the tool and start using it.
Mistake #1: writing table and column names in JPQL.
A very typical start: your hand naturally reaches for from product or where order_number = .... But JPQL does not live in the world of DDL; it lives in the world of Java classes. So the “truth” for JPQL is Product, CustomerOrder, and orderNumber. If you feel like writing snake_case, you are probably still mentally in SQL, not in the entity model.
Mistake #2: trying to access a foreign key as category_id.
In the product table you really do have category_id. But in the Product entity you have a category reference. And JPQL expects you to go through the relationship: p.category.id or p.category.code. When you write p.category_id, it is as if you tried to access a field in Java that does not exist. Hibernate will answer something like: “I do not know such a property.”
Mistake #3: choosing the wrong root entity and then being surprised by the query complexity.
If you want a list of OrderItems, but put CustomerOrder in from and then try to “dig” your way to items, the query becomes longer and less readable. The root of the query is, in essence, “what you return to the calling code.” If you return products, start with Product. If you return items, start with OrderItem.
Mistake #4: forgetting aliases or mixing up aliases between entities.
JPQL quickly becomes unreadable if you are careless with aliases. When p suddenly means both Product and Price (a joke, but every joke...), and o suddenly becomes either an order or “order by” in your head, you start making mistakes out of nowhere. Aliases are discipline, not “just some letter.”
Mistake #5: treating join as “joining tables,” not as “traversing a relationship.”
In SQL, you join tables because otherwise you simply cannot. In JPQL, you join entities through the relationship you already described with annotations. Therefore, a correct join looks like join oi.order o, not like an attempt to manually simulate on. When you think in “tables,” you start arguing with the mapping. When you think in “relationships,” queries become shorter.
GO TO FULL VERSION