Сьогодні ми заглибимось у створення кастомних запитів з використанням 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);
Запит з JOIN
Якщо в нас є сутність 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);
Сподіваюся, тепер ви почуваєтеся впевненіше, ніби щойно остаточно перемогли баг, який мучив вас три тижні. Час будувати по-справжньому потужні запити в ваших застосунках!
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ