Ласкаво просимо в одну з найкорисніших і найпопулярніших тем 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()для керування станом persistence context
Приклад пагінації:
// Отримати перших 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у сервісні методи, які змінюють дані. - Відсутність конструктора без параметрів у сутності: переконайся, що у класів-сутностей є публічний конструктор без параметрів.
- Проблема з lazy-завантаженням (LazyInitializationException): будь обережний з lazy-завантаженням і намагайся використовувати
FetchType.LAZYтільки там, де це потрібно.
Навіщо все це потрібно?
Репозиторії — це класний спосіб зосередитися на логіці застосунку, не витрачаючи час на рутину. Працювати з репозиторіями швидше, безпечніше і набагато простіше, ніж писати SQL-запити вручну.
У реальній роботі ти будеш постійно використовувати репозиторії, хай то в простому CRUD-застосунку, хай то в складній мікросервісній системі з десятками баз даних.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ