CodeGym /Cursos /Spring Data JPA /JPQL según el modelo de entidades

JPQL según el modelo de entidades

Spring Data JPA
Nivel 11, Lección 1
Disponible

1. JPQL: entidades en lugar de tablas

Si SQL es el lenguaje de comunicación directa con la base de datos (“tablas, columnas, join por FK”), entonces JPQL es el lenguaje de comunicación con su modelo de objetos (“entidades, campos, relaciones”). Se parece a SQL en forma para que nuestro cerebro no llore de inmediato, pero en significado está más cerca de Java: usted opera con nombres de clases y sus campos. Se podría decir que JPQL es SQL que ha tomado un curso de «cómo hablar en camelCase y no avergonzarse».

La regla más importante aquí suena aburrida pero ahorra horas de vida: en JPQL escribimos el nombre de la entidad — Product y CustomerOrder, no product y customer_order. Por defecto coincide con el nombre de la clase, a menos que lo haya sobrescrito con @Entity(name = ...). Y escribimos orderNumber, no order_number. ¿Por qué? Porque JPQL no sabe cómo se llama su tabla hasta que Hibernate mire el mapeo. JPQL solo conoce lo que usted ha descrito como entidades y relaciones.

Fijemos la diferencia con un pequeño ejemplo. A menudo las tablas se nombran en snake_case, y los campos en Java — en camelCase. Y es normal: la base de datos ama los guiones bajos, Java ama las jorobas.

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;

@Entity // Esto es una entidad JPA: en JPQL nos referiremos por el nombre de la entidad CustomerOrder
@Table(name = "customer_order") // Mapeo a la tabla en BD, pero en JPQL este nombre NO se usa
public class CustomerOrder {

    @Column(name = "order_number", nullable = false, unique = true) // Columna order_number, pero el campo en JPQL es orderNumber
    private String orderNumber;
}

Si ahora escribe una consulta, en JPQL se refiere al campo orderNumber, no a la columna order_number. Hibernate luego “traduce” eso a SQL.

Para que al cerebro le sea más fácil cambiar, tenga una pequeña chuleta (no para memorizar, solo para ver la lógica):

Qué escribe SQL (sobre tablas) JPQL (sobre entidades)
“De dónde leemos” from product p from Product p
“Por qué filtramos” where o.order_number = ... where o.orderNumber = ...
“Cómo unimos” join category c on p.category_id = c.id join p.category c
“A dónde vamos por la FK” p.category_id p.category.id o
p.category.code

Sí, JPQL se parece a SQL, pero si empieza a escribir allí nombres de tablas/columnas — Hibernate le responderá con un error más o menos como “No conozco la entidad product”. Tendrá razón: la entidad es Product, y product es la tabla.

A continuación volveré a mostrar fragmentos locales cortos de repositorios: así es más fácil captar el lenguaje JPQL y no perderse en el archivo completo de la interfaz.

2. Entidad raíz en from

Cuando leemos datos, generalmente entendemos intuitivamente “qué queremos obtener exactamente”: lista de productos, un pedido concreto, posiciones de pedido, categorías. En JPQL este deseo intuitivo debe formalizarse con una decisión: qué entidad será la raíz, es decir, la que irá en from. La entidad raíz es como el protagonista de una película: se pueden mostrar otros personajes, pero la cámara arranca con él.

Por ejemplo, si quiere obtener productos, la entidad raíz es Product. Si quiere obtener posiciones de pedido, la raíz será OrderItem. Y esta decisión lo afecta todo: el select, los join e incluso cómo se verá el método del repositorio.

El “esqueleto” más básico de una consulta JPQL se ve así:

select <qué devolver>            -- Normalmente es el alias de la entidad: p, o, oi, etc.
from <entidad raíz> <alias> -- Entidad raíz (nombre de la entidad), no tabla
where <condición>                 -- Condiciones sobre campos de entidades y rutas por relaciones
order by <orden>           -- Ordenación por campos de entidades

En nuestro proyecto shop-data-jpa para «leer lista de productos» es lógico empezar con 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 — nombre de la entidad; p — alias; ordenamos por el campo id
    List<Product> findAllProductsJpql();
}

Observe dos detalles. Primero, en la consulta aparece Product (nombre de la entidad), no la tabla product. Segundo, p es un alias, lo necesitaremos luego para no escribir “Product.” cada vez.

Y aquí surge una buena pregunta práctica: “¿Se puede poner en from cualquier entidad y luego llegar a la deseada?” Técnicamente se puede hacer mucho, pero mentalmente es mejor no hacerlo. Si lee productos, que la raíz sea Product. Si lee posiciones, que sea OrderItem. Esto hace la consulta más simple y legible: entiende inmediatamente qué devuelve el método del repositorio.

3. Alias (alias)

El alias en JPQL es, esencialmente, una variable. Como en Java: no escribe cada vez new BigDecimal("10.00"), sino que asigna el valor a una variable y luego trabaja con el nombre. En la consulta, el alias es un nombre corto para la entidad, para acceder a sus campos y relaciones. No es una “letra mágica”, sino una herramienta de legibilidad (y sí, en JPQL sin él enseguida empezará a sufrir).

La mayoría de las veces los alias se ponen cortos pero significativos: p para Product, c para Category, o para CustomerOrder, oi para OrderItem. No porque esté escrito en los mandamientos del ORM, sino porque es cómodo y se parece a la conversación del equipo. Si en la consulta hay tres entidades, los alias se vuelven especialmente importantes: de otro modo no entendería a qué campos se refiere.

Aquí hay una pequeña tabla de referencia para nuestro proyecto (no es una regla, sino un hábito que ahorra nervios):

Entidad Alias típico
Product p
Category c
CustomerOrder o
OrderItem oi

Ahora veamos cómo el alias hace legible la consulta. Sin alias no podría escribir normalmente order by, where y las transiciones por relaciones.

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 — alias del pedido; filtrado por el campo totalAmount
    List<CustomerOrder> findNonZeroOrders();
}

Aquí o es solo un nombre corto. Sin misterios. Pero si olvida el alias e intenta escribir where totalAmount > 0, JPQL no le entenderá: necesita saber de qué “objeto” es ese campo.

4. Campos de entidad en JPQL

El error más común de un principiante en JPQL es pensar que “esto es casi SQL, entonces las columnas también son casi columnas”. No. En JPQL se accede a los campos de la entidad, es decir, a los nombres que ve en la clase Java. Aunque en la base de datos la columna se llame de otra forma, JPQL permanece en el lado del mundo Java.

Por ejemplo, si en Product el campo se llama sku, entonces en JPQL se llama igual: p.sku. Si en la base de datos la columna se llama product_sku, y usted ha puesto @Column(name = "product_sku"), igualmente en JPQL será p.sku. JPQL dice: “Trabajo con objetos. ¿Cómo lo llama el objeto? Pues así llámalo”.

Desde el punto de vista del pensamiento, esto es útil: lee la consulta y la asocia inmediatamente con el código de la entidad. No necesita saltar entre SQL-DDL y la clase Java para entender qué está filtrando.

En este contexto, es muy útil percibir la expresión p.name como “ruta al campo”, casi como si en Java escribiera p.getName() (solo que sin llamada a método, porque es una consulta).

Por cierto, lo mismo aplica para @Embeddable (ya lo usamos en DeliveryAddress). Si un pedido tiene el campo deliveryAddress, y este a su vez tiene el campo city, entonces la ruta será o.deliveryAddress.city. Esto es muy “orientado a objetos” y muy intencionado: leemos según el modelo, no según las tablas.

5. Navegación por relaciones

Cuando aparecen relaciones (ManyToOne, OneToMany), JPQL se vuelve especialmente agradable: empieza a escribir condiciones como si realmente caminara sobre objetos. Es decir, no piensa “¿dónde está la columna FK?”, piensa “el producto tiene una categoría, la categoría tiene un código”.

En el proyecto shop-data-jpa, Product tiene una referencia category. Por lo tanto, en JPQL puede escribir:

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") // Transitamos por la relación category y filtramos por el campo active
    List<Product> findProductsFromActiveCategories();
}

Lo importante aquí es sentir: p.category.active se ve como navegación normal por objeto. Pero bajo el capó, Hibernate entenderá que necesita datos de category y hará un SQL-join. Es decir, usted expresa la idea “según el modelo de objetos”, y el ORM traduce la idea “a la realidad relacional”.

Para que no sea una abstracción, aquí está el esquema de relaciones de nuestro dominio (simplificado), porque a veces el cerebro necesita un dibujo, no otra línea de texto:

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

Ahora un ejemplo con transición a través de OrderItem -> order en una consulta. Necesitamos tomar las posiciones de pedido de un cliente concreto (de momento dejamos el email directamente como cadena — solo como demostración de la forma de la consulta):

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 es cómodo para JPQL multilínea; join va por la relación oi.order; email hardcodeado solo para demo
    List<OrderItem> findItemsOfCustomerDemo();
}

Aquí se ven dos puntos importantes.

Primero: join oi.order o — hacemos join no de tabla con tabla, sino de relación con entidad. JPQL no requiere escribir on oi.order_id = o.id porque eso ya está “incrustado” en el mapeo @ManyToOne.

Segundo: en where usamos el campo customerEmail de la entidad CustomerOrder, no la columna customer_email. Otra vez — modelo de objetos.

Se puede preguntar: “¿Y por qué a veces escriben p.category.code sin join, y otras veces hacen join oi.order o?” Porque JPQL permite “transitar por relaciones” directamente en expresiones, y el proveedor creará los join necesarios. Pero cuando la consulta se vuelve un poco más compleja o cuando quiere que el lector vea explícitamente qué entidades participan — el join explícito hace la consulta más clara. No es religión, es solo cuestión de legibilidad.

6. Cómo JPQL se convierte en SQL

JPQL no es una “base de datos alternativa”. Por sí mismo no envía nada a PostgreSQL. Pasa por Hibernate, que analiza la consulta, entiende su modelo de entidades y genera SQL normal. Y solo entonces el SQL viaja a PostgreSQL. Si tiene esto en mente, desaparece la sensación de “escribí una línea y ocurrió magia”.

Aquí está el esquema de este camino, literalmente como una cinta transportadora:

flowchart TD
    A[Método del repositorio] --> B[Cadena JPQL]
    B --> C["Hibernate / JPA provider"]
    C --> D[SQL para PostgreSQL]
    D --> E[(PostgreSQL)]
    E --> D
    D --> F[Conjunto de resultados]
    F --> G[Entidades vía mapeo]

Tomemos un JPQL simple e imaginemos en qué se convierte. Por ejemplo:

select p -- En JPQL a menudo se selecciona el alias de la entidad (es decir, se devuelve la entidad completa)
from Product p
where p.category.code = 'phones' -- Navegación por relación: Product -> Category -> code

El pseudo-SQL (aproximadamente) podría verse así:

select p.* -- En SQL real ya son filas de la tabla product (o columnas concretas)
from product p
join category c on p.category_id = c.id -- Lo que en JPQL era p.category..., aquí se expresa mediante JOIN por FK
where c.code = 'phones'

¿Por qué “pseudo”? Porque la forma exacta depende del dialecto, la configuración, los nombres de tablas, columnas y de cómo exactamente Hibernate decida construir el SQL. Pero el sentido es el mismo: p.category.code no existe como “columna” de product, por lo tanto para filtrar por el código de la categoría se necesita un join.

En conclusión, un hábito útil: cuando escribe JPQL, mantiene simultáneamente dos imágenes en la cabeza. La primera — objetual: “Product -> Category -> code”. La segunda — relacional: “product join category by FK, filter by category.code”. Y precisamente este puente le proporciona un pensamiento ORM maduro sin misticismo.

7. Errores típicos en el pensamiento JPQL

Los errores en JPQL casi nunca son “sintácticos”, sino “mundovisionales”. Es decir, escribe la consulta al estilo SQL, pero JPQL espera un estilo según las entidades. Y si capta esta idea ahora, luego será mucho más fácil: dejará de luchar contra la herramienta y empezará a usarla.

Error n.º 1: escribir en JPQL nombres de tablas y columnas.
Un comienzo muy típico: la mano tiende a escribir from product o where order_number = .... Pero JPQL no vive en el mundo del DDL, sino en el mundo de las clases Java. Por lo tanto, la “verdad” para JPQL es Product, CustomerOrder y orderNumber. Si quiere escribir en snake_case, probablemente todavía está mentalmente en SQL, no en el modelo de entidades.

Error n.º 2: intentar acceder a la clave foránea como category_id.
En la tabla product realmente tiene category_id. Pero en la entidad Product tiene una referencia category. Y JPQL espera que recorra la relación: p.category.id o p.category.code. Cuando escribe p.category_id, es como si en Java intentara acceder a un campo que no existe. Hibernate le responderá más o menos: “no conozco esa propiedad”.

Error n.º 3: elegir una entidad raíz incorrecta y luego sorprenderse de la complejidad de la consulta.
Si quiere obtener una lista de OrderItem, pero pone en from CustomerOrder y luego intenta “excavar” hasta las posiciones, la consulta se hará más larga y menos legible. La raíz de la consulta es, esencialmente, “qué devuelve al código llamante”. Si devuelve productos, comience con Product. Si devuelve posiciones, comience con OrderItem.

Error n.º 4: olvidar el alias o confundir alias entre entidades.
JPQL rápidamente se vuelve ilegible si no es cuidadoso con los alias. Cuando p de repente significa tanto Product como Price (broma, pero en toda broma...), y o de repente se convierte en pedido o en “order by” en su cabeza — empieza a equivocarse sin motivo. Los alias son disciplina, no “una letra cualquiera”.

Error n.º 5: percibir join como “unión de tablas” en lugar de “transición por relación”.
En SQL une tablas porque de otra manera no puede. En JPQL une entidades por la relación que ya describió con anotaciones. Por lo tanto, un join correcto se ve como join oi.order o, no como un intento de simular manualmente un on. Cuando piensa en “tablas”, empieza a discutir con el mapeo. Cuando piensa en “relaciones”, las consultas se vuelven más cortas.

1
Tarea
Spring Data JPA, nivel 11, lección 1
Bloqueada
JPQL por nombre de entidad y campos de relacion
JPQL por nombre de entidad y campos de relacion
1
Tarea
Spring Data JPA, nivel 11, lección 1
Bloqueada
Join explícito por relación en JPQL
Join explícito por relación en JPQL
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION