Какими способами можно определить собственный (кастомный) запрос в JPA?

«Какими способами можно определить собственный (кастомный) запрос в JPA?» — вопрос из категории Hibernate, который задают на 10% собеседований Java Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

JPA предоставляет несколько механизмов для определения запросов, выходящих за рамки стандартных CRUD-операций.

1. Query Methods в Spring Data JPA (Наиболее простой)

Spring Data JPA может автоматически генерировать запросы на основе имени метода в репозитории.

public interface UserRepository extends JpaRepository<User, Long> {
    // SELECT * FROM user WHERE email = ?
    List<User> findByEmail(String email);

    // SELECT * FROM user WHERE name LIKE ? AND age > ?
    List<User> findByNameContainingAndAgeGreaterThan(String namePart, int age);
}

2. Аннотация @Query (JPQL или Native SQL)

Позволяет явно указать запрос на JPQL или нативном SQL.

public interface UserRepository extends JpaRepository<User, Long> {
    // JPQL запрос (работает с сущностями)
    @Query("SELECT u FROM User u WHERE u.active = true AND u.role = :role")
    List<User> findActiveUsersByRole(@Param("role") UserRole role);

    // Нативный SQL запрос (работает с таблицами)
    @Query(value = "SELECT * FROM users u WHERE u.registration_date > :date",
           nativeQuery = true)
    List<User> findUsersRegisteredAfter(@Param("date") LocalDate date);
}

3. Именованные запросы (@NamedQuery / @NamedNativeQuery)

Запросы определяются статически в аннотациях к классу сущности, а затем вызываются по имени.

@Entity
@NamedQuery(
    name = "User.findByStatus",
    query = "SELECT u FROM User u WHERE u.status = :status"
)
public class User { ... }

// В репозитории или EntityManager
List<User> users = entityManager.createNamedQuery("User.findByStatus", User.class)
                                .setParameter("status", "ACTIVE")
                                .getResultList();

4. Criteria API (Типобезопасный программный API)

Используется для динамического построения сложных запросов в runtime.

CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<User> query = cb.createQuery(User.class);
Root<User> user = query.from(User.class);

// Построение предикатов
Predicate namePredicate = cb.like(user.get("name"), "%John%");
Predicate agePredicate = cb.gt(user.get("age"), 21);

query.select(user).where(cb.and(namePredicate, agePredicate));

List<User> result = entityManager.createQuery(query).getResultList();

5. Использование EntityManager для JPQL/Criteria/Native Query

Низкоуровневый доступ для прямого выполнения запросов.

// JPQL
TypedQuery<User> jpqlQuery = entityManager.createQuery(
    "SELECT u FROM User u JOIN u.orders o WHERE o.total > :minTotal", User.class
).setParameter("minTotal", 100.0);

// Нативный SQL
Query nativeQuery = entityManager.createNativeQuery(
    "SELECT u.* FROM users u WHERE u.id IN (:ids)", User.class
).setParameter("ids", idList);

Рекомендации по выбору:

  • Для простых запросов — Query Methods.
  • Для сложных, но статических запросов — @Query с JPQL.
  • Для максимальной производительности или использования специфичных функций СУБД — @Query с nativeQuery=true (осторожно, теряется переносимость).
  • Для динамических запросов с переменным набором условий — Criteria API или QueryDSL.
  • Всегда используйте параметризованные запросы (:param или ?) для предотвращения SQL-инъекций.