Урок по треку Spring и Spring Boot 7 мин

Spring Data JPA: репозитории и запросы

Репозитории, производные запросы, @Query, пагинация, проекции и аудит.

Spring Data JPA превращает интерфейс в готовый репозиторий. На собесе спрашивают, как это работает, как писать запросы, чем save отличается от persist, как устроена пагинация и проекции.

Репозиторий из интерфейса

public interface OrderRepository extends JpaRepository<Order, Long> {
    List<Order> findByStatusAndCreatedAtAfter(OrderStatus status, Instant from);
    Optional<Order> findFirstByCustomerIdOrderByCreatedAtDesc(long customerId);
    long countByStatus(OrderStatus status);
    boolean existsByNumber(String number);
}

При старте Spring создаёт для интерфейса прокси, реализация по умолчанию SimpleJpaRepository. Методы CrudRepository и JpaRepository (save, findById, findAll, deleteById) уже есть. Методы с именами по соглашению разбираются в JPQL при старте: опечатка в имени поля даёт ошибку запуска, а не запроса.

Иерархия: Repository → CrudRepository → ListCrudRepository → JpaRepository (плюс PagingAndSortingRepository). Методы SimpleJpaRepository по умолчанию транзакционные: чтение с readOnly = true, запись с обычной транзакцией.

@Query

@Query("select o from Order o join fetch o.items where o.id = :id")
Optional<Order> findWithItems(@Param("id") long id);

@Query(value = "select * from orders where created_at > now() - interval '1 day'", nativeQuery = true)
List<Order> lastDay();

@Modifying
@Query("update Order o set o.status = :status where o.id in :ids")
int updateStatus(@Param("status") OrderStatus status, @Param("ids") List<Long> ids);
  • JPQL работает с сущностями и полями, native SQL с таблицами и колонками.
  • @Modifying обязателен для UPDATE и DELETE. Такой запрос идёт мимо контекста персистентности: загруженные сущности останутся со старыми значениями (помогает clearAutomatically = true).
  • Параметры только через :name или ?1. Конкатенация строк в запрос это SQL-инъекция и в JPQL тоже.

save

save(entity) вызывает persist, если сущность новая, и merge, если нет. Новизна определяется по id: null значит новая (или через Persistable.isNew(), или поле @Version). Ловушки:

  • Если id задаётся вручную (UUID в конструкторе), Spring Data считает сущность не новой и вызывает merge: лишний SELECT перед INSERT. Решение: Persistable или @Version.
  • merge возвращает другой объект, управляемый контекстом. Работать дальше надо с результатом save, а не с исходным объектом.
  • Внутри транзакции вызывать save для загруженной сущности не нужно: изменения сохранятся при flush сами (dirty checking).

Пагинация и сортировка

Page<Order> findByStatus(OrderStatus status, Pageable pageable);
Slice<Order> findByCustomerId(long id, Pageable pageable);

repo.findByStatus(PAID, PageRequest.of(0, 20, Sort.by("createdAt").descending()));

Page делает дополнительный count-запрос ради общего числа страниц, на больших таблицах это дорого. Slice знает только, есть ли следующая страница. На глубоких страницах OFFSET медленный, тогда используют keyset-пагинацию (where id > :lastId order by id limit 20), в Spring Data 3.1+ есть ScrollPosition и Window.

join fetch вместе с пагинацией по коллекции Hibernate выполнит в памяти (предупреждение HHH90003004): загрузит все строки и обрежет сам. Так делать нельзя.

Проекции

Если нужны не все поля, интерфейсная или DTO-проекция выбирает только нужные колонки:

public record OrderSummary(long id, BigDecimal total) {}
List<OrderSummary> findByCustomerId(long customerId);

Это экономит память и не тащит сущности в контекст персистентности.

Аудит и прочее

@EnableJpaAuditing и @CreatedDate, @LastModifiedDate, @CreatedBy заполняют поля автоматически. @EntityGraph(attributePaths = "items") на методе репозитория подгружает связи без JPQL. Specification и Querydsl строят динамические фильтры.

Частые ошибки

  • findAll() и фильтрация в Java вместо запроса.
  • Ожидать, что @Modifying-запрос обновит уже загруженные сущности.
  • Работать с исходным объектом после save, а не с возвращённым.
  • Page на огромной таблице с медленным count.

Что спросят на собесе

  • Как Spring Data создаёт реализацию репозитория из интерфейса?
  • Как работают производные запросы и @Query, зачем @Modifying?
  • Что делает save и как Spring Data решает, новая ли сущность?
  • Чем Page отличается от Slice? Почему глубокий OFFSET медленный?
  • Что такое проекции и когда их использовать?

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.