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);

Запит з 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. Обробка типових помилок

Найчастіше помилок дві: помилки в синтаксисі запиту та помилки в параметрах. Наприклад:

  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);

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

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ