Этап 3 - Repositories: JpaRepository и базовый доступ к данным
Repository Pattern дает service интерфейс доступа к данным и скрывает повторяющиеся persistence operations. Service должен выражать бизнес-операцию, а не повторять SQL или вызовы EntityManager для каждого базового действия. Repository дает service понятный интерфейс для сохранения, загрузки, подсчета и удаления entities.
Что предоставляет JpaRepository
Spring Data JPA создает реализации repository во время выполнения. Разработчик объявляет interface, наследуется от JpaRepository и указывает тип entity и тип id. Для типовых операций отдельный class-implementation писать не нужно.
public interface UserRepository extends JpaRepository<User, Long> {
}
User - это entity. Long - тип ее primary key. Из такого маленького interface Spring Data дает методы save, findById, findAll, delete, deleteById, count, pagination methods, sorting methods и создание запросов по именам методов.
Repository обычно inject-ится в service. Это оставляет controllers тонкими и не дает database access code расползаться по web layer. Service решает, что означает операция; repository выполняет persistence.
Основные методы
save() сохраняет entity. Для новой entity Hibernate создает insert. Для существующей managed или merged entity он может создать update. Название метода широкое намеренно: JPA решает, новый объект или нет, на основе identifier и persistence state.
findById() ищет по primary key и возвращает Optional<T>. Метод не возвращает null, потому что отсутствие записи - ожидаемый результат, а не техническая авария. Вызывающий код обязан обработать оба случая: entity есть или entity нет.
findAll() возвращает все строки таблицы entity. Это удобно для демо и маленьких справочников, но опасно для больших production tables. Для реальных экранов и API лучше использовать pagination через Pageable.
delete() удаляет конкретную entity. deleteById() удаляет по primary key. count() возвращает количество строк. Методы простые, но они все равно выполняют SQL, поэтому важно помнить о transaction boundaries и размере данных.
| Repository call в service code | Что service должен решить до вызова | Какая SQL-работа скрыта за методом |
|---|---|---|
save(entity) | Валиден ли объект, и это create или update сценарий? | Hibernate выбирает insert для новой entity или update для существующей. |
findById(id) | Что делать, если такого id нет? | Выполняется select ... where id = ?, а результат заворачивается в Optional. |
findAll() | Достаточно ли маленькая таблица, чтобы грузить ее целиком? | Выполняется full table query; без pagination это может быть опасно. |
delete(entity) | Разрешено ли удаление по business rules? | Hibernate планирует delete по primary key entity. |
count() | Нужен ли точный count для этого экрана или правила? | База выполняет select count(*), что на больших данных тоже может быть дорого. |
Optional и отсутствующие данные
Optional - это контейнер, в котором значение может быть, а может отсутствовать. findById возвращает Optional, потому что запрошенный id может не существовать. Это заставляет разработчика принять решение: вернуть 404, выбросить domain exception или использовать fallback.
public User getUser(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new EntityNotFoundException("User not found: " + id));
}
Не стоит вызывать .get() у Optional без проверки. Это просто переносит ошибку в менее понятное место. Service method должен преобразовать отсутствие данных в осмысленное поведение приложения.
Практика
Создай UserRepository extends JpaRepository<User, Long> и ProductRepository extends JpaRepository<Product, Long>. Напиши service method для создания пользователя, метод загрузки по id и метод получения paginated list товаров. Включи SQL logging и сравни repository-вызовы со сгенерированным SQL. Repository убирает boilerplate, но не отменяет необходимости понимать, какой запрос выполняется.
Чек-лист понимания
- Могу объяснить, зачем repositories нужны вместо прямого database code в controllers.
- Понимаю, что означает
JpaRepository<User, Long>. - Могу описать
save,findById,findAll,deleteиcount. - Понимаю, почему
findByIdвозвращаетOptional. - Знаю, когда
findAllрискован и почему нужна pagination.