Вопрос 26 · Раздел 16

Что такое JPQL и чем он отличается от SQL

JPQL (Java Persistence Query Language) — объектно-ориентированный язык запросов, который работает с сущностями и их полями, а не с таблицами и колонками. Понимание различий межд...

Версии по языкам: English Russian Ukrainian

Обзор

JPQL (Java Persistence Query Language) — объектно-ориентированный язык запросов, который работает с сущностями и их полями, а не с таблицами и колонками. Понимание различий между JPQL и SQL критически важно для эффективной работы с JPA.


🟢 Junior Level

Что такое JPQL

JPQL — язык запросов, который работает с сущностями и полями, а не с таблицами и колонками.

// JPQL — работает с entity классами
List<User> users = entityManager.createQuery(
    "SELECT u FROM User u WHERE u.age > :age", User.class)
    .setParameter("age", 18)
    .getResultList();

// SQL — работает с таблицами
// SELECT * FROM users WHERE age > 18;

Основные различия

JPQL SQL
User (entity class) users (table name)
u.age (field name) age (column name)
u.userProfile.city (navigation) JOIN user_profiles ON ...
Database-agnostic (абстрагирует структуру запросов, но не все dialect differences — например, строковые и дата-функции могут отличаться) Database-specific
Автоматически генерирует SQL Прямой SQL

Примеры JPQL

// Простой SELECT
SELECT u FROM User u WHERE u.email = :email

// JOIN
SELECT o FROM Order o JOIN o.user u WHERE u.name = :name

// Aggregation
SELECT u, COUNT(o) FROM User u LEFT JOIN u.orders o GROUP BY u

// UPDATE
UPDATE User u SET u.status = :status WHERE u.id = :id

// DELETE
DELETE FROM Order o WHERE o.status = 'cancelled'

Параметризация

// ✅ Named parameters (рекомендуется)
@Query("SELECT u FROM User u WHERE u.name = :name AND u.age > :age")
List<User> find(@Param("name") String name, @Param("age") int age);

// ❌ String concatenation (SQL injection!)
"SELECT u FROM User u WHERE u.name = '" + name + "'"  // ❌

🟡 Middle Level

JPQL vs SQL — детальное сравнение

JPQL:
- Работает с entities и полями
- Database-agnostic (работает с любой БД)
- Автоматически генерирует SQL
- Поддерживает полиморфные запросы
- Работает с наследованием

SQL:
- Работает с таблицами и колонками
- Database-specific (синтаксис зависит от БД)
- Полный контроль над запросом
- Нет автоматической маппинга
- Нет полиморфизма

JPQL примеры

// SELECT с конструктором (DTO projection)
@Query("SELECT new com.example.UserDto(u.name, u.email) FROM User u")
List<UserDto> findUserDtos();

// JOIN FETCH
@Query("SELECT DISTINCT o FROM Order o JOIN FETCH o.items WHERE o.id = :id")
Order findByIdWithItems(@Param("id") Long id);

// Subquery
@Query("SELECT u FROM User u WHERE u.id IN (SELECT o.user.id FROM Order o WHERE o.total > :amount)")
List<User> findUsersWithOrdersOver(@Param("amount") BigDecimal amount);

// GROUP BY с HAVING
@Query("SELECT u, COUNT(o) FROM User u JOIN u.orders o GROUP BY u HAVING COUNT(o) > :minOrders")
List<Object[]> findActiveUsers(@Param("minOrders") int minOrders);

// CASE WHEN
@Query("SELECT u, CASE WHEN COUNT(o) > 0 THEN 'active' ELSE 'inactive' END FROM User u LEFT JOIN u.orders o GROUP BY u")
List<Object[]> findUserStatus();

Native SQL — когда нужен

// Database-specific функции
List<User> users = entityManager.createNativeQuery(
    "SELECT * FROM users WHERE created_at > NOW() - INTERVAL '30 days'",  -- PostgreSQL-specific! В MySQL: NOW() - INTERVAL 30 DAY
    User.class
).getResultList();

// CTE (Common Table Expressions)
List<User> users = entityManager.createNativeQuery("""
    WITH recent_orders AS (
        SELECT user_id, MAX(created_at) as last_order
        FROM orders GROUP BY user_id
    )
    SELECT u.* FROM users u JOIN recent_orders ro ON u.id = ro.user_id
    WHERE ro.last_order > :date
    """, User.class).setParameter("date", date).getResultList();

Типичные ошибки

// ❌ Table name вместо entity
entityManager.createQuery("SELECT u FROM users u", User.class);  // ❌
// ✅ Entity name
entityManager.createQuery("SELECT u FROM User u", User.class);  // ✅

// ❌ Column name вместо field
"SELECT u FROM User u WHERE u.email_address = :email"  // ❌
// ✅ Field name
"SELECT u FROM User u WHERE u.email = :email"  // ✅

// ❌ Concat для параметров (SQL injection!)
"SELECT u FROM User u WHERE u.name = '" + name + "'"  // ❌
// ✅ Parameters
"SELECT u FROM User u WHERE u.name = :name"  // ✅

🔴 Senior Level

JPQL и наследование

// Полиморфный запрос — включает все подклассы
SELECT p FROM Payment p  // CardPayment, CashPayment, CryptoPayment
// Полиморфный запрос включает все подклассы. На SINGLE_TABLE — быстрый (одна таблица).
// На JOINED — генерирует множество JOIN (медленно). На TABLE_PER_CLASS — UNION ALL (ещё медленнее).

// С TYPE — фильтр по подклассу
SELECT p FROM Payment p WHERE TYPE(p) = CardPayment

// С TREAT — cast к подклассу
SELECT p FROM Payment p JOIN TREAT(p AS CardPayment) cp WHERE cp.cardNumber = :number

Функции JPQL

String: CONCAT, SUBSTRING, TRIM, LOWER, UPPER, LENGTH, LOCATE
Math: ABS, SQRT, MOD, SIZE
Date: CURRENT_DATE, CURRENT_TIME, CURRENT_TIMESTAMP
Collection: SIZE, INDEX, MEMBER OF
Null: NULLIF, COALESCE
Conditional: CASE WHEN, NULLIF

Performance considerations

JPQL overhead:
- Parsing JPQL → генерация SQL
- Parameter binding
- Entity mapping

Native SQL overhead:
- Нет parsing (прямой SQL)
- Parameter binding
- Entity mapping (если указан entity class)

Разница минимальна для простых запросов.
Для сложных — native SQL может быть быстрее.

Когда использовать JPQL vs SQL

JPQL:
✅ Database-agnostic запросы
✅ Простые и средние запросы
✅ Когда нужна полиморфность
✅ Когда работаете с entities

Native SQL:
✅ Database-specific функции (CTE, window functions)
✅ Сложные аналитические запросы
✅ Bulk operations (UPDATE/DELETE тысяч записей)
✅ Когда JPQL не поддерживает синтаксис

Best Practices

✅ JPQL для database-agnostic запросов
✅ Native SQL для database-specific запросов
✅ Parameterized queries (не concat!)
✅ Индексы для полей в WHERE
✅ DTO projection для read-only
✅ JOIN FETCH для связанных данных

❌ Concat для параметров (SQL injection!)
❌ JPQL для сложных database-specific запросов
❌ Без индексов на полях в WHERE
❌ Загрузка полных entities для read-only

🎯 Шпаргалка для интервью

Обязательно знать:

  • JPQL работает с сущностями и полями, SQL — с таблицами и колонками
  • JPQL database-agnostic (работает с любой БД), SQL — database-specific
  • JPQL автоматически генерирует SQL, поддерживает полиморфные запросы и наследование
  • Параметризация обязательна: :name (named parameters), НЕ string concat (SQL injection!)
  • Native SQL нужен для: CTE, window functions, bulk operations, database-specific функций
  • DTO projection в JPQL через конструктор: SELECT new com.example.Dto(…)

Частые уточняющие вопросы:

  • Когда native SQL вместо JPQL? CTE, window functions, сложные аналитические запросы, bulk UPDATE/DELETE
  • JPQL и наследование — как работает? Полиморфный запрос SELECT FROM Parent включает все подклассы; TYPE для фильтра, TREAT для cast
  • Почему concat опасен? SQL injection: “WHERE name = ‘” + userInput + “’ — уязвимость
  • JOIN FETCH в JPQL зачем? Загрузка связанных сущностей в одном запросе (решение N+1)

Красные флаги (НЕ говорить):

  • «Использую table name вместо entity name в JPQL» — PersistenceException
  • «String concat для параметров» — SQL injection уязвимость
  • «JPQL для CTE и window functions» — JPQL не поддерживает, нужен native SQL
  • «Загружаю полные entities для read-only отчётов» — DTO projection эффективнее

Связанные темы:

  • [[27. Что такое Criteria API и когда его использовать]]
  • [[28. Как использовать JOIN FETCH для решения проблемы N+1]]
  • [[29. Что такое projection в JPA]]
  • [[30. Какие типы наследования поддерживает JPA]]