Получить доступ

Spring Data JPA: репозитории и Hibernate

Абстракция репозитория, derived queries и @Query, persistence context и dirty checking, lazy/N+1 и почему DTO, а не entity.

Абстракция репозитория

Достаточно объявить интерфейс-наследник JpaRepository<T, ID> — Spring Data на старте генерирует его реализацию (прокси) с CRUD, пагинацией и сортировкой. Своего кода доступа писать не нужно.

Иерархия абстракций: Repository (маркер) → CrudRepository (CRUD) → PagingAndSortingRepository → JpaRepository (плюс JPA-специфика: flush, batch, saveAll). Выбирают самый узкий, достаточный для задачи.

interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByEmail(String email);
}

Derived queries и @Query

Простые запросы выводятся из имени метода: findByEmail, findByAgeGreaterThan, existsByLogin, countByStatus — Spring парсит имя и строит JPQL. Удобно, но длинные имена-«простыни» — сигнал, что пора на @Query.

@Query задаёт запрос явно (JPQL или, с nativeQuery=true, нативный SQL); параметры связывают @Param; для изменяющих запросов нужен @Modifying (и транзакция). Пагинацию/сортировку передают параметром Pageable/Sort.

Persistence context и dirty checking

За репозиториями стоит JPA/Hibernate с EntityManager и его кэшем первого уровня — persistence context. В пределах транзакции загруженная сущность становится managed: Hibernate отслеживает изменения её полей и на flush сам выгружает UPDATE — без явного вызова save (это dirty checking).

Отсюда практическое следствие: внутри транзакции достаточно загрузить сущность и поменять поля. save() нужен в основном для НОВЫХ сущностей. Вне транзакции/сессии сущность становится detached, и её изменения не отслеживаются.

@Transactional
void rename(long id, String name) {
    User u = repo.findById(id).orElseThrow();
    u.setName(name); // dirty checking сам сделает UPDATE на flush — save() не нужен
}

Lazy-связи и N+1

Коллекции (@OneToMany) по умолчанию lazy и подгружаются прокси при первом обращении; @ManyToOne по умолчанию eager. Обход lazy-коллекции в цикле порождает проблему N+1: 1 запрос за списком + по запросу на связь каждого элемента.

Лечат тем же набором, что в разделе баз данных: JOIN FETCH, @EntityGraph, batch fetching (hibernate.default_batch_fetch_size), DTO-проекции. Отдельная боль — LazyInitializationException при обращении к lazy-связи уже вне транзакции; «лечить» это включением Open-Session-In-View — антипаттерн (он маскирует N+1 и держит соединение дольше).

DTO против entity

Сущности JPA не стоит возвращать прямо из контроллера: при сериализации Hibernate-прокси и lazy-связей легко получить лишние запросы, LazyInitializationException или бесконечную рекурсию по двусторонним связям. Плюс это завязывает API на модель хранения.

Поэтому наружу отдают DTO/проекции (отдельные record-классы или интерфейсные проекции Spring Data), а entity остаётся внутренней моделью персистентности.

Как отвечать на собеседовании

Объясните генерируемые репозитории и иерархию, derived queries vs @Query/@Modifying, и обязательно persistence context + dirty checking (managed-сущность сохраняется без save, save — для новых; detached вне сессии). Разберите lazy/eager и N+1 с лечением (JOIN FETCH/EntityGraph/batch), LazyInitializationException и почему Open-Session-In-View — антипаттерн. Завершите «DTO наружу, entity внутри».

Вопросы по теме

Читайте также

Закрытая бета

Знать тему — половина дела

На живом собеседовании sotto слышит вопрос интервьюера и показывает короткий ответ и план. Оставьте контакт — пришлём доступ и промокод.

  • Промокод на 2 часа — сверх пробных 30 минут
  • Программа для Windows — скачать сразу, войти — через Яндекс ID или VK ID
  • Прямая связь с разработчиком: что улучшить — сделаем