JavaRush /Курсы /Модуль 5. Spring /Использование JPQL и кастомные запросы в репозиториях

Использование JPQL и кастомные запросы в репозиториях

Модуль 5. Spring
5 уровень , 8 лекция
Открыта

Сегодня мы углубимся в создание кастомных запросов с использованием JPQL (Java Persistence Query Language). Это позволит нам писать более сложные запросы, которые выходят за рамки предустановленных методов репозиториев. Ведь универсальные методы репозиториев — это здорово, но иногда нам нужна гибкость.


1. Знакомство с JPQL

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

Пример простого SQL-запроса:


SELECT * FROM employees WHERE department = 'IT';

Пример аналогичного JPQL-запроса:


SELECT e FROM Employee e WHERE e.department = 'IT'

Обратите внимание:

  • Мы работаем с классами и их полями (Employee и department), а не с именами таблиц и колонок (employees и department).
  • JPQL чувствителен к имени класса и его полей. Ошибка в названии приведет к QuerySyntaxException.

Зачем использовать JPQL?

Иногда стандартных методов репозитория, таких как findById, save или deleteAll, недостаточно для получения сложных данных. JPQL позволяет:

  • Выполнять агрегации (например, AVG, SUM, COUNT).
  • Писать более сложные условия (например, джойны и фильтры).
  • Использовать более точный контроль над выборкой данных, например, применять сортировки и ограничения.

2. Написание кастомных запросов

Создадим пример, чтобы показать, как писать и использовать JPQL. Представьте, что у нас есть система управления сотрудниками, где есть сущность Employee с такими полями:

  • id (уникальный идентификатор сотрудника).
  • name (имя сотрудника).
  • department (отдел сотрудника).
  • salary (зарплата сотрудника).

Шаг 1: Создание сущности

Класс Employee выглядит так:


import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Column;

@Entity
public class Employee {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String name;

    @Column(nullable = false)
    private String department;

    @Column(nullable = false)
    private Double salary;

    // Геттеры и сеттеры
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getDepartment() {
        return department;
    }

    public void setDepartment(String department) {
        this.department = department;
    }

    public Double getSalary() {
        return salary;
    }

    public void setSalary(Double salary) {
        this.salary = salary;
    }
}

Шаг 2: Создание репозитория с кастомным запросом

Добавим интерфейс репозитория EmployeeRepository. Он будет наследоваться от JpaRepository, чтобы мы могли использовать стандартные методы. Кроме того, мы определим собственный метод с использованием JPQL.


import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.List;

public interface EmployeeRepository extends JpaRepository<Employee, Long> {
    // Кастомный запрос на получение сотрудников заданного департамента
    @Query("SELECT e FROM Employee e WHERE e.department = :department")
    List<Employee> findByDepartment(@Param("department") String department);

    // Кастомный запрос на получение сотрудников с зарплатой выше заданной
    @Query("SELECT e FROM Employee e WHERE e.salary > :salary")
    List<Employee> findEmployeesWithSalaryAbove(@Param("salary") Double salary);

    // Кастомный запрос с агрегацией
    @Query("SELECT AVG(e.salary) FROM Employee e WHERE e.department = :department")
    Double findAverageSalaryByDepartment(@Param("department") String department);
}

Обратите внимание:

  1. Используется аннотация @Query, чтобы написать кастомный JPQL-запрос.
  2. Мы используем параметры (:department или :salary) для передачи значений в запрос.
  3. Аннотация @Param связывает параметры метода с параметрами запроса.

Шаг 3: Использование репозитория

Давайте вызовем наши методы через сервисный слой или контроллер.

Пример сервиса:


import org.springframework.stereotype.Service;
import java.util.List;

@Service
public class EmployeeService {

    private final EmployeeRepository employeeRepository;

    public EmployeeService(EmployeeRepository employeeRepository) {
        this.employeeRepository = employeeRepository;
    }

    public List<Employee> getEmployeesByDepartment(String department) {
        return employeeRepository.findByDepartment(department);
    }

    public List<Employee> getEmployeesWithHighSalary(Double salary) {
        return employeeRepository.findEmployeesWithSalaryAbove(salary);
    }

    public Double getAverageSalary(String department) {
        return employeeRepository.findAverageSalaryByDepartment(department);
    }
}

Пример контроллера:


import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/employees")
public class EmployeeController {

    private final EmployeeService employeeService;

    public EmployeeController(EmployeeService employeeService) {
        this.employeeService = employeeService;
    }

    @GetMapping("/by-department")
    public List<Employee> getEmployeesByDepartment(@RequestParam String department) {
        return employeeService.getEmployeesByDepartment(department);
    }

    @GetMapping("/high-salary")
    public List<Employee> getEmployeesWithHighSalary(@RequestParam Double salary) {
        return employeeService.getEmployeesWithHighSalary(salary);
    }

    @GetMapping("/average-salary")
    public Double getAverageSalary(@RequestParam String department) {
        return employeeService.getAverageSalary(department);
    }
}

Шаг 4: Тестирование запросов

При запросе GET /employees/by-department?department=IT мы получим всех сотрудников из отдела IT.

При запросе GET /employees/high-salary?salary=50000 — всех сотрудников с зарплатой больше 50,000.

И при запросе GET /employees/average-salary?department=HR — среднюю зарплату сотрудников в отделе HR.


3. Примеры посложнее

Если вам кажется, что всё это слишком просто, давайте добавим сложности.

Запрос с сортировкой

Мы можем добавить упорядочивание, используя ORDER BY в JPQL:


@Query("SELECT e FROM Employee e ORDER BY e.salary DESC")
List<Employee> findAllEmployeesBySalaryDesc();

Запрос с LIKE

Мы можем искать сотрудников по части имени:


@Query("SELECT e FROM Employee e WHERE e.name LIKE %:keyword%")
List<Employee> findByNameContaining(@Param("keyword") String keyword);

Запрос с джойном

Если у нас есть сущность Department, связанная с Employee, мы можем использовать JOIN:


@Query("SELECT e FROM Employee e JOIN e.department d WHERE d.name = :departmentName")
List<Employee> findByDepartmentName(@Param("departmentName") String departmentName);

4. Обработка типичных ошибок

Чаще всего ошибок две: ошибки в синтаксисе запроса и ошибки в параметрах. Например:

  1. Если в запросе SELECT e FROM Employee e WHERE e.departments = :department вы написали departments, а у класса поле называется department, Spring выдаст исключение IllegalArgumentException с описанием ошибки.
  2. Если вы забыли передать параметр в @Param, Spring тоже не останется довольным.

Совет: всегда тестируйте запросы на ранних этапах, чтобы избежать неожиданных сюрпризов.


5. Когда использовать JPQL

Использование JPQL оправдано в следующих случаях:

  • Вам требуется сложная выборка данных (например, агрегирование или джойны).
  • Стандартных методов репозитория недостаточно.
  • Вы хотите написать запрос, основанный на бизнес-логике, но не хотите сильно закапываться в SQL.

Если же запросы становятся слишком сложными, возможно, стоит рассмотреть использование нативных SQL-запросов или даже вынос логики в хранимые процедуры.


@Query(value = "SELECT * FROM employees WHERE salary > :salary", nativeQuery = true)
List<Employee> findHighSalaryEmployees(@Param("salary") Double salary);

Надеюсь, теперь вы чувствуете себя уверенно, словно только что окончательно победили баг, который мучил вас три недели. Настало время строить действительно мощные запросы в ваших приложениях!

Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ