CrudRepository vs JpaRepository: Топ ошибок и как их избежать

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Вступление: Два брата-акробата или кто здесь главный?

Представьте, что вы строите дом. У вас есть базовый набор инструментов: молоток, отвертка, пила. Это ваш 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) — найти по ID
  • findAll() — получить все записи
  • deleteById(ID id) — удалить по ID
  • count() — количество записей

Это как базовый набор функций для любой таблицы. Он универсален и работает с любым типом хранилища (не только JPA).

JpaRepository: Расширенный арсенал

JpaRepository наследует CrudRepository и PagingAndSortingRepository. Он добавляет специфические для JPA возможности:

  • flush() — принудительная синхронизация с БД
  • saveAndFlush(S entity) — сохранить и сразу сбросить в БД
  • deleteInBatch(Iterable entities) — массовое удаление (одним SQL-запросом)
  • 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-запросов (по одному на каждую сущность). Это катастрофически медленно.

Как избежать: Использу

Блоги

Рекомендаций по книгам