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 медленный? - Что такое проекции и когда их использовать?