Добро пожаловать в одну из самых полезных и популярных тем Spring Data — работу с репозиториями! В этой лекции мы разберем, что такое репозитории, зачем они нужны, чем отличаются CrudRepository и JpaRepository, как они упрощают нашу жизнь, и, конечно же, внедрим их в наше приложение. Все это будет полезно не только в ваших учебных проектах, но и в реальной работе, ведь репозитории — это основа взаимодействия с базой данных в Spring.
Что такое репозиторий?
Репозиторий (Repository) в данном контексте — это слой приложения, который отвечает за взаимодействие с базой данных. Он абстрагирует базовые операции вроде создания, чтения, обновления и удаления данных (так называемые CRUD-операции).
Если раньше взаимодействие с базой данных требовало ручного написания SQL-запросов, то Spring Data упрощает этот процесс в разы. Вам больше не нужно "пачкать руки" SQL'ем (хотя никто не запрещает, если хочется). Вместо этого репозитории позволяют фокусироваться на бизнес-логике, доверив рутинные задачи Spring.
CrudRepository и JpaRepository: отличия
Spring Data предоставляет несколько интерфейсов для работы с базой данных, но два из них используются наиболее часто:
CrudRepository
CrudRepository — это базовый интерфейс для выполнения CRUD-операций. Вот его основные методы:
public interface CrudRepository<T, ID> {
<S extends T> S save(S entity); // Сохранение (или обновление) сущности
Optional<T> findById(ID id); // Поиск сущности по идентификатору
boolean existsById(ID id); // Проверка на существование записи
Iterable<T> findAll(); // Получение всех сущностей
void deleteById(ID id); // Удаление по идентификатору
void delete(T entity); // Удаление переданной сущности
}
Когда использовать: если вам нужно реализовать только базовые CRUD-операции.
JpaRepository
JpaRepository расширяет CrudRepository и добавляет новые возможности:
- Пагинация (разбивка большого списка данных на страницы, как в Google: 10 результатов на странице)
- Сортировка данных
- JPA-специфичные методы вроде
flush()для управления состоянием базы
Пример пагинации:
// Получить первые 20 пользователей, отсортированных по имени
Page<User> users = repository.findAll(PageRequest.of(0, 20, Sort.by("name")));
Примеры методов из JpaRepository:
public interface JpaRepository<T, ID> extends CrudRepository<T, ID> {
List<T> findAll(Sort sort); // Сортировка результатов
Page<T> findAll(Pageable pageable); // Пагинация результатов
void flush(); // Сохранение изменений в базу данных
<S extends T> S saveAndFlush(S entity); // Сохранить и сразу сбросить контекст
}
Когда использовать: если вам нужны дополнительные возможности DAO-уровня, такие как сортировка или пагинация.
Использование репозиториев на практике
Теперь, когда теория стала немного понятнее, перейдем к практике! Мы будем работать с приложением, где уже есть сущность User.
1. Сущность User
Наш класс User — это простейшая сущность, которая будет хранить данные о пользователе:
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
public User() {}
public User(String name, String email) {
this.name = name;
this.email = email;
}
// Геттеры и сеттеры
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 getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
2. Создание репозитория
Теперь создадим интерфейс репозитория. Spring Data сделает всё остальное за нас:
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<User, Long> {
// Здесь можно добавлять кастомные методы
}
Вот и всё! Репозиторий готов. Теперь у нас есть доступ ко всем CRUD-операциям, которые предоставляет JpaRepository.
Выполнение CRUD-операций
Попробуем использовать наш репозиторий для базовых операций.
Сохранение пользователя
@Service
public class UserService {
private final UserRepository userRepository;
@Autowired
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User createUser(String name, String email) {
User user = new User(name, email);
return userRepository.save(user);
}
}
Получение пользователя по ID
public Optional<User> getUserById(Long id) {
return userRepository.findById(id);
}
Обновление данных пользователя
public User updateUser(Long id, String name, String email) {
User user = userRepository.findById(id).orElseThrow(() -> new RuntimeException("User not found"));
user.setName(name);
user.setEmail(email);
return userRepository.save(user);
}
Удаление пользователя
public void deleteUser(Long id) {
userRepository.deleteById(id);
}
Использование в REST-контроллере Создадим контроллер, который будет использовать наш UserService:
@RestController
@RequestMapping("/users")
public class UserController {
private final UserService userService;
@Autowired
public UserController(UserService userService) {
this.userService = userService;
}
@PostMapping
public ResponseEntity<User> createUser(@RequestBody User user) {
User savedUser = userService.createUser(user.getName(), user.getEmail());
return new ResponseEntity<>(savedUser, HttpStatus.CREATED);
}
@GetMapping("/{id}")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
return userService.getUserById(id)
.map(user -> new ResponseEntity<>(user, HttpStatus.OK))
.orElse(new ResponseEntity<>(HttpStatus.NOT_FOUND));
}
@PutMapping("/{id}")
public ResponseEntity<User> updateUser(@PathVariable Long id, @RequestBody User user) {
User updatedUser = userService.updateUser(id, user.getName(), user.getEmail());
return new ResponseEntity<>(updatedUser, HttpStatus.OK);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
}
Кастомные методы репозиториев
Иногда стандартных операций недостаточно, и нужно написать что-то своё. Например, поиск пользователя по имени.
Реализация с использованием имени метода
List<User> findByName(String name);
Spring Data "магически" понимает, что вы хотите найти пользователей по имени, благодаря соглашениям об именовании методов.
Реализация с использованием @Query
@Query("SELECT u FROM User u WHERE u.email = :email")
Optional<User> findByEmail(@Param("email") String email);
Пагинация Если у вас есть много данных, вы можете возвращать их небольшими частями:
Page<User> findAll(Pageable pageable);
Использование:
Pageable pageable = PageRequest.of(0, 10, Sort.by("name"));
Page<User> page = userRepository.findAll(pageable);
Возможные ошибки и как их избежать
Вот несколько распространённых ошибок, о которых следует помнить:
- Ошибка "No EntityManager with actual transaction": не забудьте добавить аннотацию
@Transactionalв сервисные методы, которые изменяют данные. - Отсутствие конструктора без параметров в сущности: убедитесь, что у классов-сущностей есть публичный конструктор без параметров.
- Ошибка с ленивой загрузкой данных (LazyInitializationException): будьте осторожны с ленивой загрузкой и старайтесь использовать
FetchType.LAZYтолько там, где это необходимо.
Зачем всё это нужно?
Репозитории — это великолепный способ сосредоточиться на логике приложения, не тратя время на рутину. Работать с репозиториями быстрее, безопаснее и намного проще, чем писать SQL-запросы вручную.
В реальной работе вы будете постоянно использовать репозитории, хоть в простом CRUD-приложении, хоть в сложной микросервисной системе с десятками баз данных.