Что такое JPQL и чем он отличается от SQL
JPQL (Java Persistence Query Language) — объектно-ориентированный язык запросов, который работает с сущностями и их полями, а не с таблицами и колонками. Понимание различий межд...
Обзор
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]]