Сегодня мы углубимся в создание кастомных запросов с использованием 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);
}
Обратите внимание:
- Используется аннотация
@Query, чтобы написать кастомный JPQL-запрос. - Мы используем параметры (
:departmentили:salary) для передачи значений в запрос. - Аннотация
@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. Обработка типичных ошибок
Чаще всего ошибок две: ошибки в синтаксисе запроса и ошибки в параметрах. Например:
- Если в запросе
SELECT e FROM Employee e WHERE e.departments = :departmentвы написалиdepartments, а у класса поле называетсяdepartment, Spring выдаст исключениеIllegalArgumentExceptionс описанием ошибки. - Если вы забыли передать параметр в
@Param, Spring тоже не останется довольным.
Совет: всегда тестируйте запросы на ранних этапах, чтобы избежать неожиданных сюрпризов.
5. Когда использовать JPQL
Использование JPQL оправдано в следующих случаях:
- Вам требуется сложная выборка данных (например, агрегирование или джойны).
- Стандартных методов репозитория недостаточно.
- Вы хотите написать запрос, основанный на бизнес-логике, но не хотите сильно закапываться в SQL.
Если же запросы становятся слишком сложными, возможно, стоит рассмотреть использование нативных SQL-запросов или даже вынос логики в хранимые процедуры.
@Query(value = "SELECT * FROM employees WHERE salary > :salary", nativeQuery = true)
List<Employee> findHighSalaryEmployees(@Param("salary") Double salary);
Надеюсь, теперь вы чувствуете себя уверенно, словно только что окончательно победили баг, который мучил вас три недели. Настало время строить действительно мощные запросы в ваших приложениях!
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ