Вступление: Два брата-акробата или кто здесь главный?
Представьте, что вы строите дом. У вас есть базовый набор инструментов: молоток, отвертка, пила. Это ваш CrudRepository — надежный, простой и понятный. А теперь представьте, что к этому набору вам дают еще и профессиональный электроинструмент, лазерный уровень и автоматический шуруповерт. Это JpaRepository — мощный, но требующий осторожности.
На Stack Overflow вопрос "What is difference between CrudRepository and JpaRepository interfaces in Spring Data JPA?" собирает тысячи просмотров. И это не случайно. Разработчики (и новички, и опытные) постоянно путают эти два интерфейса, выбирают не тот и получают неожиданные баги в продакшене.
В этой статье мы не просто перечислим сухие отличия. Мы разберем топ ошибок, которые совершают Java-разработчики при работе с CrudRepository и JpaRepository, и покажем, как их избежать. Готовы? Поехали!
Что такое CrudRepository и JpaRepository?
Оба интерфейса — часть фреймворка Spring Data JPA. Они предоставляют готовые методы для работы с базой данных, избавляя нас от написания шаблонного кода. Но их возможности и назначение различаются.
CrudRepository: Базовый уровень
CrudRepository — это самый фундаментальный интерфейс. Он предоставляет стандартные операции CRUD (Create, Read, Update, Delete). Вот его основные методы:
save(S entity)— сохранить сущностьfindById(ID id)— найти по IDfindAll()— получить все записиdeleteById(ID id)— удалить по IDcount()— количество записей
Это как базовый набор функций для любой таблицы. Он универсален и работает с любым типом хранилища (не только JPA).
JpaRepository: Расширенный арсенал
JpaRepository наследует CrudRepository и PagingAndSortingRepository. Он добавляет специфические для JPA возможности:
flush()— принудительная синхронизация с БДsaveAndFlush(S entity)— сохранить и сразу сбросить в БДdeleteInBatch(Iterable— массовое удаление (одним SQL-запросом)entities) getOne(ID id)— ленивая загрузка сущности (возвращает прокси)findAll(Sort sort)— сортировкаfindAll(Pageable pageable)— пагинация
Это профессиональный инструмент для работы именно с JPA и Hibernate.
Топ-5 ошибок при выборе и использовании
Теперь перейдем к самому интересному — к реальным граблям, на которые наступают разработчики.
Ошибка 1: Использование CrudRepository, когда нужна пагинация
Ситуация: Вы пишете API для списка пользователей. Данных много — нужна постраничная выдача. Вы используете CrudRepository и начинаете вручную реализовывать пагинацию через findAll() и subList(). Это ужасно, медленно и небезопасно.
Как избежать: Если вам нужна пагинация или сортировка, используйте JpaRepository (или PagingAndSortingRepository).
// ❌ Плохо: CrudRepository + ручная пагинация
public interface UserRepository extends CrudRepository<User, Long> {
// Нет методов для пагинации
}
// ✅ Хорошо: JpaRepository + встроенная пагинация
public interface UserRepository extends JpaRepository<User, Long> {
Page<User> findAll(Pageable pageable);
}
// Использование
Page<User> users = userRepository.findAll(PageRequest.of(0, 10, Sort.by("name")));
Ошибка 2: Забываем про flush() и получаем странные баги
Ситуация: Вы сохраняете сущность через save(), а затем сразу пытаетесь найти ее через findById(). Вроде бы все логично, но в некоторых случаях данные могут еще не попасть в базу данных (из-за кэша первого уровня Hibernate).
Как избежать: Если вам критично, чтобы данные были немедленно записаны в БД (например, перед выполнением другого запроса, который зависит от этих данных), используйте saveAndFlush() из JpaRepository.
// ❌ Плохо: save() без гарантии записи
userRepository.save(newUser);
User user = userRepository.findById(newUser.getId()).orElse(null); // Может вернуть null!
// ✅ Хорошо: saveAndFlush() гарантирует запись
User savedUser = userRepository.saveAndFlush(newUser);
User user = userRepository.findById(savedUser.getId()).orElse(null); // Всегда найдет
Ошибка 3: Использование getOne() без понимания ленивой загрузки
Ситуация: Вы используете getOne(id) из JpaRepository, думая, что это аналог findById(). Но getOne() возвращает прокси-объект (ленивая загрузка). Если вы попытаетесь обратиться к полям этого объекта вне контекста транзакции (например, в контроллере), получите LazyInitializationException.
Как избежать: Используйте findById() (который есть в CrudRepository), если вам нужна реальная сущность. getOne() используйте только для установки связей (например, при создании связанных сущностей) и строго внутри транзакции.
// ❌ Плохо: getOne() вне транзакции
public User getUser(Long id) {
return userRepository.getOne(id); // Прокси! Проблемы при обращении к полям
}
// ✅ Хорошо: findById() для получения данных
public User getUser(Long id) {
return userRepository.findById(id).orElseThrow(() -> new RuntimeException("User not found"));
}
// ✅ Хорошо: getOne() только для установки связи
@Transactional
public void assignRole(Long userId, Long roleId) {
User user = userRepository.getOne(userId); // Прокси, но это ок
Role role = roleRepository.getOne(roleId);
user.setRole(role);
}
Ошибка 4: Массовое удаление через CrudRepository — убийца производительности
Ситуация: Вам нужно удалить 1000 записей. Вы используете deleteAll() из CrudRepository. Spring Data JPA сгенерирует 1000 отдельных DELETE-запросов (по одному на каждую сущность). Это катастрофически медленно.
Как избежать: Использу